code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

use DateTimeImmutable;
use DateTimeZone;

class DateTime
{
    protected $timezone = 'UTC';

    public function setTimezone(string $timezone) : void
    {
        $this->timezone = $timezone;
    }

    public function getTimezone() : string
    {
        return $this->timezone;
    }

    public function local(string $utcTime, string $format) : string
    {
        $date = new DateTimeImmutable($utcTime);
        $date = $date->setTimezone(new DateTimeZone($this->timezone));
        return $date->format($format);
    }

    public function utc() : string
    {
        $date = new DateTimeImmutable($this->now(), new DateTimeZone('UTC'));
        return $date->format('Y-m-d H:i:s e');
    }

    public function month(string $ym) : string
    {
        return $this->local("{$ym}-01 12:00:00", 'F Y');
    }

    public function ymd() : string
    {
        return $this->local($this->utc(), 'Y/m/d');
    }

    protected function now() : string
    {
        return 'now';
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

use Argo\Domain\Config\Config;
use Argo\Domain\Config\ConfigGateway;
use Capsule\Di\Container;
use Capsule\Di\Definitions;

class DomainContainerFactory
{
    static public function new() : Container
    {
        $def = static::define();

        $def->object(Config::CLASS)
            ->factory(function (Container $container) {
                $configGateway = $container->get(ConfigGateway::CLASS);
                return $configGateway->getConfig();
            });

        return $def->newContainer();
    }

    static protected function define() : Definitions
    {
        return new Definitions();
    }
}

repo_name: tkkonline/php

license: mit


code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

class Exception extends \Exception
{
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain\Exception;

use Argo\Domain\Exception;

class Invalid extends Exception
{
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain\Exception;

class InvalidData extends Invalid
{
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain\Exception;

class InvalidJson extends Invalid
{
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

use Argo\Domain\Exception;
use Seld\JsonLint\JsonParser;
use JsonException;

class Json
{
    public static function encode(/* array|object */ $data) : string
    {
        return json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
    }

    public static function decode(
        string $text,
        bool $assoc = false,
        int $depth = 512
    ) /* : array|object */
    {
        try {
            return json_decode($text, $assoc, $depth, JSON_THROW_ON_ERROR);
        } catch (JsonException $e) {
            $lint = (new JsonParser())->lint($text);
            throw new Exception\InvalidJson($lint->getMessage());
        }
    }

    public static function recode(
        /* array|object */ $data,
        bool $assoc = false,
        int $depth = 512
    ) /* : array|object */
    {
        $recode = self::decode(self::encode($data), $assoc, $depth);
        return is_array($recode) && empty($recode) && ! $assoc
            ? (object) $recode
            : $recode;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

interface Log
{
    public function emergency($message, array $context = []);

    public function alert($message, array $context = []);

    public function critical($message, array $context = []);

    public function error($message, array $context = []);

    public function warning($message, array $context = []);

    public function notice($message, array $context = []);

    public function info($message, array $context = []);

    public function debug($message, array $context = []);

    public function echo($message, array $context = []);
}


code:
<?php
declare(strict_types=1);

namespace Argo\Domain;

interface Storage
{
    public function path(string $id = '') : string;

    public function glob(string $pattern) : array;

    public function read(string $id) : ?string;

    public function write(string $id, string $text) : void;

    public function forceDir(string $id) : string;

    public function move(string $sourceId, string $targetId) : bool;

    public function trash(string $sourceId) : bool;

    public function exists(string $id) : bool;

    public function copy(string $sourceDir, string $targetId) : void;
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http;

use Argo\App\Payload;
use Argo\App\UseCase;
use SapiRequest;
use SapiResponse;

abstract class Action
{
    protected $request;

    protected $responder;

    protected $domain;

    public function __construct(
        SapiRequest $request,
        Responder $responder,
        ?UseCase $domain = null
    ) {
        $this->request = $request;
        $this->responder = $responder;
        $this->domain = $domain;
    }

    protected function domain(...$args) : Payload
    {
        return $this->domain->__invoke(...$args);
    }

    protected function response(
        SapiRequest $request,
        Payload $payload = null
    ) : SapiResponse
    {
        if ($payload === null) {
            $payload = Payload::found();
        }

        return $this->responder->respond($request, $payload);
    }

    protected function implode(array $parts) : string
    {
        return trim(implode('/', $parts), '/');
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Build;

use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetBuild extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder
    ) {
        parent::__construct($request, $responder);
    }

    public function __invoke() : SapiResponse
    {
        return $this->response($this->request);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Build\Process;

use Argo\App\Site\BuildSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostBuildProcess extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        BuildSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Config;

use Argo\App\Config\FetchConfig;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetConfig extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchConfig $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $name) : SapiResponse
    {
        $payload = $this->domain($name);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Config;

use Argo\App\Config\SaveConfig;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostConfig extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SaveConfig $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $name) : SapiResponse
    {
        $payload = $this->domain($name, $this->request->input['text'] ?? '');
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Draft\Add;

use Argo\App\Content\Draft\AddDraft;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostDraftAdd extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddDraft $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->input['title'] ?? 'Untitled');
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Draft;

use Argo\App\Content\Draft\TrashDraft;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class DeleteDraft extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        TrashDraft $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain($relId);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Draft;

use Argo\App\Content\Draft\FetchDraft;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetDraft extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchDraft $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain($relId);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Draft;

use Argo\App\Content\Draft\SaveDraft;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

/**
 * this should probably be `PATCH /draft/$relId`
 */
class PostDraft extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SaveDraft $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain(
            $relId,
            [
                'title' => $this->request->input['title'] ?? null,
                'author' => $this->request->input['author'] ?? null,
                'tags' => $this->request->input['tags'] ?? [],
                'markup' => $this->request->input['markup'] ?? 'markdown',
            ],
            $this->request->input['body'] ?? '',
        );
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Draft\Publish;

use Argo\App\Content\Draft\PublishDraft;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostDraftPublish extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        PublishDraft $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain(
            $relId,
            [
                'title' => $this->request->input['title'] ?? null,
                'author' => $this->request->input['author'] ?? null,
                'tags' => $this->request->input['tags'] ?? [],
            ],
            $this->request->input['body'] ?? ''
        );
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Folder;

use Argo\App\OpenFolder;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetFolder extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        OpenFolder $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$idParts) : SapiResponse
    {
        $payload = $this->domain($this->implode($idParts));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action;

use Argo\App\Dashboard;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class Get extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        Dashboard $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action;

use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class Head extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder
    ) {
        parent::__construct($request, $responder);
    }

    public function __invoke() : SapiResponse
    {
        return $this->response($this->request);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Import;

use Argo\App\Payload;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetImport extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder
    ) {
        parent::__construct($request, $responder);
    }

    public function __invoke() : SapiResponse
    {
        return $this->response($this->request);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Import;

use Argo\App\Import\Import;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostImport extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        Import $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->uploads['wpxml'] ?? null);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Page\Add;

use Argo\App\Content\Page\AddPage;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostPageAdd extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddPage $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->input['id'] ?? '');
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Page;

use Argo\App\Content\Page\TrashPage;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class DeletePage extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        TrashPage $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$idParts) : SapiResponse
    {
        $payload = $this->domain($this->implode($idParts));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Page;

use Argo\App\Content\Page\FetchPage;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetPage extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchPage $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$idParts) : SapiResponse
    {
        $payload = $this->domain($this->implode($idParts));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Page;

use Argo\App\Content\Page\SavePage;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostPage extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SavePage $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$idParts) : SapiResponse
    {
        $payload = $this->domain(
            $this->implode($idParts),
            [
                'title' => $this->request->input['title'] ?? null,
                'author' => $this->request->input['author'] ?? null,
                'tags' => $this->request->input['tags'] ?? null,
                'markup' => $this->request->input['markup'] ?? 'markdown',
            ],
            $this->request->input['body'] ?? '',
        );

        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Pages;

use Argo\App\Content\Page\FetchPages;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetPages extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchPages $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Post;

use Argo\App\Content\Post\TrashPost;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class DeletePost extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        TrashPost $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$relId) : SapiResponse
    {
        $payload = $this->domain($this->implode($relId));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Post;

use Argo\App\Content\Post\FetchPost;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetPost extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchPost $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$relId) : SapiResponse
    {
        $payload = $this->domain($this->implode($relId));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Post;

use Argo\App\Content\Post\SavePost;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostPost extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SavePost $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$relId) : SapiResponse
    {

        $payload = $this->domain(
            $this->implode($relId),
            [
                'title' => $this->request->input['title'] ?? null,
                'author' => $this->request->input['author'] ?? null,
                'tags' => $this->request->input['tags'] ?? [],
                'markup' => $this->request->input['markup'] ?? 'markdown',
            ],
            $this->request->input['body'] ?? ''
        );

        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Posts;

use Argo\App\Content\Post\FetchPosts;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetPosts extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchPosts $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(int $page = 1) : SapiResponse
    {
        $payload = $this->domain($page);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Setup;

use Argo\App\Site\AddSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetSetup extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Setup;

use Argo\App\Site\AddSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostSetup extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->input);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Site;

use Argo\App\Site\SwapSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetSite extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SwapSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $name) : SapiResponse
    {
        $payload = $this->domain($name);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Site;

use Argo\App\Site\AddSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostSite extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->input);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Sites;

use Argo\App\Site\FetchSites;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetSites extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchSites $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Sync;

use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetSync extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder
    ) {
        parent::__construct($request, $responder);
    }

    public function __invoke() : SapiResponse
    {
        return $this->response($this->request);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Sync\Process;

use Argo\App\Site\SyncSite;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostSyncProcess extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SyncSite $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Tag\Add;

use Argo\App\Content\Tag\AddTag;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostTagAdd extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        AddTag $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain($this->request->input['relId'] ?? '');
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Tag;

use Argo\App\Content\Tag\TrashTag;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class DeleteTag extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        TrashTag $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string ...$relId) : SapiResponse
    {
        $payload = $this->domain($this->implode($relId));
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Tag;

use Argo\App\Content\Tag\FetchTag;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetTag extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchTag $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain($relId);
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Tag;

use Argo\App\Content\Tag\SaveTag;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class PostTag extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        SaveTag $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke(string $relId) : SapiResponse
    {
        $payload = $this->domain(
            $relId,
            [
                'title' => $this->request->input['title'],
                'markup' => $this->request->input['markup'] ?? 'markdown',
            ],
            $this->request->input['body'] ?? ''
        );
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http\Action\Tags;

use Argo\App\Content\Tag\FetchTags;
use Argo\Http\Action;
use Argo\Http\Responder;
use SapiRequest;
use SapiResponse;

class GetTags extends Action
{
    public function __construct(
        SapiRequest $request,
        Responder $responder,
        FetchTags $domain
    ) {
        parent::__construct($request, $responder, $domain);
    }

    public function __invoke() : SapiResponse
    {
        $payload = $this->domain();
        return $this->response($this->request, $payload);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http;

use Argo\App\AppContainerFactory;
use AutoRoute\AutoRoute;
use AutoRoute\Generator;
use AutoRoute\Router;
use Capsule\Di\Container;
use Capsule\Di\Definitions;
use SapiRequest;

class HttpContainerFactory extends AppContainerFactory
{
    static protected function define() : Definitions
    {
        $def = parent::define();

        $def->object(SapiRequest::CLASS)
            ->argument('globals', [
                '_GET' => $_GET,
                '_POST' => $_POST,
                '_FILES' => $_FILES,
                '_COOKIE' => $_COOKIE,
                '_SERVER' => $_SERVER,
            ]);

        $def->object(AutoRoute::CLASS)
            ->factory(function (Container $container) {
                $autoRoute = new AutoRoute(
                    'Argo\\Http\\Action',
                    __DIR__ . '/Action'
                );
                return $autoRoute;
            });

        $def->object(Router::CLASS)
            ->factory(function (Container $container) {
                return $container->get(AutoRoute::CLASS)->newRouter();
            });

        $def->object(Generator::CLASS)
            ->factory(function (Container $container) {
                $generator = $container->get(AutoRoute::CLASS)->newGenerator();
                return $generator;
            });

        return $def;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Http;

use Argo\Domain\Storage;
use Argo\App\Payload;
use Argo\App\Status;
use Argo\View\ViewFactory;
use SapiRequest;
use SapiResponse;

class Responder
{
    protected $viewFactory;

    protected $storage;

    public function __construct(
        ViewFactory $viewFactory,
        Storage $storage
    ) {
        $this->viewFactory = $viewFactory;
        $this->storage = $storage;
    }

    public function respond(
        SapiRequest $request,
        Payload $payload
    ) : SapiResponse
    {
        $response = new SapiResponse();
        $response->setHeader('Access-Control-Allow-Origin', '*');

        $status = $payload->getStatus();
        $result = (object) $payload->getResult();
        $forward = "/";

        switch ($status) {
            case Status::FOUND:
                $this->renderIntoResponse(
                    $response,
                    $payload,
                    $this->getViewTemplate($request),
                    'layout'
                );
                break;

            case Status::NOT_FOUND:
                $response->setCode(404);
                $this->renderIntoResponse(
                    $response,
                    $payload,
                    'not-found',
                    'layout'
                );
                break;

            case Status::CREATED:
                if ($result->item->type !== 'post') {
                    // newly-created posts go back to the dashboard;
                    // everything else goes to its own editing page
                    $forward .= "{$result->item->type}/{$result->item->relId}/";
                }

                $response->setCode(201);
                $response->setHeader('X-Argo-Forward', $forward);
                break;

            case Status::UPDATED:
                $forward .= "{$result->item->type}/{$result->item->relId}/";
                $response->setHeader('X-Argo-Forward', $forward);
                break;

            case Status::DELETED:
                $forward .= in_array($result->item->type, ['draft', 'post'])
                    ? ''
                    : "{$result->item->type}s/";
                $response->setHeader('X-Argo-Forward', $forward);
                break;

            case Status::ERROR:
                $response->setCode(500);
                $this->renderIntoResponse($response, $payload, 'error');
                break;

            case Status::INVALID:
                $response->setCode(422);
                $this->renderIntoResponse($response, $payload, 'invalid');
                break;

            case Status::SUCCESS:
                $label = $request->method === 'GET'
                    ? 'Location'
                    : 'X-Argo-Forward';

                $response->setHeader($label, $forward);
                break;

            case Status::ACCEPTED:
                $response->setCode(202);
                break;

            case Status::PROCESSING:
                $response->setHeader('Content-Type', 'text/plain');
                $response->setCode(200);
                $response->setContent($result->callable);
                break;
        }

        return $response;
    }

    protected function getViewTemplate(SapiRequest $request) : string
    {
        if ($request->method === 'HEAD') {
            return '';
        }

        $path = trim($request->url['path'], '/');
        if (trim($path) === '') {
            return 'dashboard';
        }
        $path = explode('/', $path);
        return trim(array_shift($path));
    }

    protected function renderIntoResponse(
        SapiResponse $response,
        Payload $payload,
        string $viewTemplate,
        ?string $layoutTemplate = null
    ) : void
    {
        if ($viewTemplate === '') {
            return;
        }

        $response->setHeader('Content-Type', 'text/html');
        $view = $this->viewFactory->new([
            dirname(__DIR__, 2) . '/resources/admin',
        ]);
        $view->setData($payload->getResult());
        $view->addData(['docroot' => $this->storage->path()]);
        $view->setView($viewTemplate);
        $view->setLayout($layoutTemplate);
        $content = $view();
        $response->setContent($content);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Config\Config;
use Argo\Domain\Config\ConfigGateway;
use Argo\Domain\Content\Draft\Draft;
use Argo\Domain\Content\Folio;
use Argo\Domain\Content\Month;
use Argo\Domain\Content\Page\Page;
use Argo\Domain\Content\Post\Post;
use Argo\Domain\Content\PostIndex;
use Argo\Domain\Content\Tag\Tag;
use Argo\Domain\Log;
use Argo\Domain\Storage;
use Argo\View\ViewFactory;
use Throwable;

class Build
{
    protected $storage;

    protected $config;

    protected $content;

    protected $log;

    protected $level;

    protected $view;

    protected $viewFactory;

    protected $folio;

    public function __construct(
        Storage $storage,
        Config $config,
        ConfigGateway $configGateway,
        Log $log,
        string $level,
        ViewFactory $viewFactory,
        Folio $folio
    ) {
        $this->storage = $storage;
        $this->config = $config;
        $this->configGateway = $configGateway;
        $this->log = $log;
        $this->level = $level;
        $this->viewFactory = $viewFactory;
        $this->folio = $folio;
    }

    public function all() : void
    {
        $start = microtime(true);
        $this->theme();
        $this->menuShtml();
        $this->blogrollShtml();
        $this->featuredShtml();
        $this->index();
        $this->postIndexes();
        $this->posts();
        $this->months();
        $this->tags();
        $this->pages();
        $time = number_format(microtime(true) - $start, 2);

        $message = 'Collected folio ('
            . count($this->folio->posts) . ' posts and '
            . count($this->folio->pages) . ' pages) in '
            . $this->folio->time . ' seconds.';

        $this->log($message);
        $this->log("Built site in {$time} seconds.");
        $this->log("Done!");

        $this->config->admin->lastBuild = $this->folio->utc;
        $this->configGateway->saveValues($this->config->admin);
    }

    public function onePost(Post $post, bool $new = true) : void
    {
        $post = $this->folio->posts[$post->id];

        // for each tag on this post, rebuild the tag listing
        foreach ($post->tags as $tag) {
            $this->tag($tag);
        }

        // and the shared tags html
        $this->tagsShtml();

        // for the post month, rebuild the month listing
        $ym = substr($post->created, 0, 7);
        $month = $this->folio->months[$ym];
        $this->month($month);

        // and the shared months html
        $this->monthsShtml();

        // if there is a previous month, rebuild its listing
        if ($month->prev !== null) {
            $this->month($month->prev);
        }

        // build this post
        $this->post($post);

        // build the previous post for a "next" link etc.
        if ($post->prev !== null) {
            $this->post($post->prev);
        }

        // build the next post in case the title changed
        if ($post->next !== null) {
            $this->post($post->next);
        }

        // a newly published post has to build all indexes
        if ($new) {
            $this->index();
            $this->postIndexes();
            return;
        }

        // an existing post should rebuild only its own index, including
        // the home index if it is the first one
        $i = $post->postIndexKey;
        if ($i === 0) {
            $this->index();
        }

        $postIndex = $this->folio->postIndexes[$i];
        $pageNum = $i + 1;
        $this->postIndex($postIndex, "Posts ({$pageNum})", $pageNum === 1);
    }

    public function trashedPost(Post $post) : void
    {
        // for each tag on this post, rebuild the tag listing
        foreach ($post->tags as $tag) {
            $this->tag($this->folio->tags[Tag::absId($tag)]);
        }

        // and the shared tags html
        $this->tagsShtml();

        // for the post month, rebuild the month listing ...
        $ym = substr($post->created, 0, 7);
        $month = $this->folio->months[$ym] ?? null;
        if ($month !== null)  {
            $this->month($month);
        } else {
            // ... unless it was the only remaining post in that month;
            // delete the month listing, and rebuild the listing of
            // all months.
            $sourceId = "posts/month/" . str_replace($ym, '-', '/');
            $this->storage->trash($sourceId);
            $this->months();
        }

        // and the share months html
        $this->monthsShtml();

        // @todo a deleted post only needs to rebuild its own index
        // and all others after that
        $this->index();
        $this->postIndexes();
    }

    public function onePage(Page $page) : void
    {
        $page = $this->folio->pages[$page->id];
        $this->page($page);
    }

    protected function postIndexes() : void
    {
        foreach ($this->folio->postIndexes as $i => $postIndex) {
            $pageNum = $i + 1;
            $this->postIndex($postIndex, "Posts ({$pageNum})", $pageNum === 1);
        }
    }

    protected function postIndex(PostIndex $postIndex, string $title, bool $hasAtom) : void
    {
        $postIndex->setTitle($title);

        if ($postIndex->prev !== null) {
            $postIndex->prev->setTitle('Newer Posts');
        }

        if ($postIndex->next !== null) {
            $postIndex->next->setTitle('Older Posts');
        }

        $this->write("{$postIndex->href}/index.html", 'posts/index.html', [
            'item' => $postIndex,
            'postIndex' => $postIndex,
            'hasAtom' => $hasAtom,
            'hasJson' => true,
        ]);

        if ($hasAtom) {
            $this->write('/posts/atom.xml', 'atom.xml', [
                'postIndex' => $postIndex,
            ]);
        }
    }

    public function theme() : void
    {
        $theme = trim($this->config->general->theme);

        $dirs = [
            $this->storage->path("_theme/vendor/{$theme}/public") => "theme/{$theme}",
            $this->storage->path("_theme/custom/{$theme}/public") => "theme/{$theme}",
        ];

        foreach ($dirs as $sourceDir => $targetId) {

            $this->log("Copy {$sourceDir} to {$targetId}");

            if (! is_dir($sourceDir)) {
                $this->log("... {$sourceDir} does not exist.");
                continue;
            }

            $this->storage->copy($sourceDir, $targetId);
        }

        $this->write("/theme/{$theme}/style.css", "theme/style.css");
    }

    public function blogrollShtml() : void
    {
        $this->write('/blogroll.shtml', 'blogroll.shtml');
    }

    public function menuShtml() : void
    {
        $this->write('/menu.shtml', 'menu.shtml');
    }

    public function featuredShtml() : void
    {
        $this->write('/featured.shtml', 'featured.shtml');
    }

    public function monthsShtml() : void
    {
        $this->write('/posts/months/index.shtml', 'posts/months/index.shtml', [
            'months' => $this->folio->months,
        ]);
    }

    public function tagsShtml() : void
    {
        $this->write('/tags/index.shtml', 'tags/index.shtml', [
            'tags' => $this->folio->tags,
        ]);
    }

    protected function index() : void
    {
        $postIndex = new PostIndex($this->folio->postIndexes[0]->posts ?? [], '/');
        $postIndex->setTitle('Home');

        if (isset($this->folio->postIndexes[1])) {
            $postIndex->setNext($this->folio->postIndexes[1]);
            $postIndex->next->setTitle('Older Posts');
        }

        $this->write('/index.html', 'index.html', [
            'item' => $postIndex,
            'postIndex' => $postIndex,
            'hasAtom' => true,
            'hasJson' => true,
        ]);

        $this->write('/atom.xml', 'atom.xml', [
            'postIndex' => $postIndex,
        ]);
    }

    protected function posts() : void
    {
        foreach ($this->folio->posts as $post) {
            $this->post($post);
        }
    }

    protected function post(Post $post) : void
    {
        $this->write("{$post->href}/index.html", 'post/index.html', [
            'post' => $post,
            'item' => $post,
            'hasJson' => true,
        ]);

        $this->write("{$post->href}/index.json", 'post/index.json', [
            'post' => $post,
            'item' => $post
        ]);
    }

    public function tags() : void
    {
        foreach ($this->folio->tags as $tag) {
            $this->tag($tag);
        }

        $this->write('/tags/index.html', 'tags/index.html', [
            'tags' => $this->folio->tags,
            'hasJson' => true,
        ]);

        $this->write('/tags/index.json', 'tags/index.json', [
            'tags' => $this->folio->tags,
        ]);

        $this->tagsShtml();
    }

    public function trashedTag(Tag $tag, array $posts) : void
    {
        $this->tags();

        if (empty($posts)) {
            return;
        }

        foreach ($posts as $post) {
            $this->post($post);
        }

        $this->index();
        $this->postIndexes();
    }

    protected function tag(Tag $tag) : void
    {
        $this->write("{$tag->href}/index.html", 'tag/index.html', [
            'tag' => $tag,
            'item' => $tag,
            'hasAtom' => true,
            'hasJson' => true,
        ]);

        $this->write("{$tag->href}/index.json", 'tag/index.json', [
            'tag' => $tag,
            'item' => $tag
        ]);

        $this->write("{$tag->href}/atom.xml", 'tag/atom.xml', [
            'tag' => $tag,
            'item' => $tag
        ]);
    }

    protected function months() : void
    {
        foreach ($this->folio->months as $month) {
            $this->month($month);
        }

        $this->write('/posts/months/index.html', 'posts/months/index.html', [
            'months' => $this->folio->months,
            'hasJson' => true,
        ]);

        $this->write('/posts/months/index.json', 'posts/months/index.json', [
            'months' => $this->folio->months,
        ]);

        $this->monthsShtml();
    }

    protected function month(Month $month) : void
    {
        $this->write("{$month->href}/index.html", 'posts/month/index.html', [
            'month' => $month,
            'item' => $month,
            'hasJson' => true,
        ]);

        $this->write("{$month->href}/index.json", 'posts/month/index.json', [
            'month' => $month,
            'item' => $month
        ]);
    }

    protected function pages() : void
    {
        foreach ($this->folio->pages as $page) {
            $this->page($page);
        }
    }

    protected function page(Page $page) : void
    {
        $this->write("{$page->href}/index.html", 'page.html', [
            'page' => $page,
            'item' => $page
        ]);
    }

    protected function drafts() : void
    {
        foreach ($this->folio->drafts as $draft) {
            $this->draft($draft);
        }
    }

    protected function draft(Draft $draft) : void
    {
        $this->write("{$draft->href}/index.html", 'post/index.html', [
            'post' => $draft,
            'item' => $draft
        ]);
    }

    protected function write(string $id, string $template, $data = []) : void
    {
        $data += [
            'config' => $this->config,
            'hasAtom' => false,
            'hasJson' => false,
            'penders' => $this->folio->penders,
        ];

        $id = str_replace('//', '/', $id);
        $this->log($id);

        $theme = trim($this->config->general->theme);

        $view = $this->viewFactory->new([
            $this->storage->path("_theme/custom/{$theme}/templates"),
            $this->storage->path("_theme/vendor/{$theme}/templates"),
        ]);

        $view->setData($data);
        $view->setView($template);

        $isHtml = strrchr($id, '.') === '.html';

        if ($isHtml) {
            $view->setLayout('layout/html');
        }

        // php seems to dump the entire buffer to output if there
        // is an exception thrown within the view. so, this captures
        // the buffer around the view, and closes all of its own
        // buffers regardless of exceptions.
        try {
            $obLevel = ob_get_level();
            ob_start();
            $text = $view();
        } finally {
            while (ob_get_level() > $obLevel) {
                ob_end_clean();
            }
        }

        $this->storage->write($id, $text);
    }

    protected function log(/* string|array */ $message) : void
    {
        $level = $this->level;
        $this->log->$level($message);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Config\Config;
use Argo\Domain\Config\ConfigGateway;
use Argo\Domain\Content\ContentLocator;
use Argo\Domain\Content\Folio;
use Argo\Domain\Content\Month;
use Argo\Domain\DateTime;
use Argo\Domain\Log;
use Argo\Domain\Storage;
use Argo\View\ViewFactory;

class BuildFactory
{
    protected $dateTime;

    protected $storage;

    protected $config;

    protected $content;

    protected $log;

    protected $view;

    protected $viewFactory;

    public function __construct(
        DateTime $dateTime,
        Storage $storage,
        Config $config,
        ConfigGateway $configGateway,
        ContentLocator $content,
        Log $log,
        ViewFactory $viewFactory
    ) {
        $this->dateTime = $dateTime;
        $this->storage = $storage;
        $this->config = $config;
        $this->configGateway = $configGateway;
        $this->content = $content;
        $this->log = $log;
        $this->viewFactory = $viewFactory;
    }

    public function new(string $level = 'info') : Build
    {
        return new Build(
            $this->storage,
            $this->config,
            $this->configGateway,
            $this->log,
            $level,
            $this->viewFactory,
            Folio::new($this->storage, $this->config, $this->content, $this->dateTime)
        );
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\DateTime;
use Argo\Domain\Storage;
use RuntimeException;

class Fsio implements Storage
{
    protected $system;

    protected $dateTime;

    protected $docroot;

    public function __construct(
        System $system,
        DateTime $dateTime,
        string $docroot
    ) {
        $this->system = $system;
        $this->dateTime = $dateTime;
        $this->docroot = rtrim($docroot, '/') . '/';
    }

    public function path(string $id = '') : string
    {
        if (strpos($id, '..') !== false) {
            throw new RuntimeException("Double-dots not allowed in IDs: {$id}");
        }

        return $this->docroot . ltrim($id, '/');
    }

    public function glob(string $pattern) : array
    {
        return glob($this->path($pattern));
    }

    public function read(string $id) : ?string
    {
        $file = $this->path($id);
        return file_exists($file)
            ? file_get_contents($file)
            : null;
    }

    public function write(string $id, string $text) : void
    {
        $file = $this->path($id);
        $dir = dirname($file);
        if (! is_dir($dir)) {
            $this->system->mkdir($dir);
        }

        $text = strtr($text, [
            "\r\n" => "\n",
            "\r" => "\n"
        ]);

        file_put_contents($file, trim($text) . "\n");

        chmod($file, 0755);
    }

    public function forceDir(string $id) : string
    {
        $dir = $this->path($id);
        if (! is_dir($dir)) {
            $this->system->mkdir($dir);
        }
        return $dir;
    }

    public function move(string $sourceId, string $targetId) : bool
    {
        $targetDir = $this->path($targetId);

        if (is_dir($targetDir)) {
            throw new RuntimeException("Directory already exists: {$targetDir}");
        }

        $this->system->mkdir($targetDir);
        $sourceDir = $this->path($sourceId);

        return rename($sourceDir, $targetDir);
    }

    public function trash(string $sourceId) : bool
    {
        $time = $this->dateTime->local(
            $this->dateTime->utc(),
            'Ymd\THis'
        );
        $targetId = "_trash/{$time}/{$sourceId}";
        return $this->move($sourceId, $targetId);
    }

    public function exists(string $id) : bool
    {
        return file_exists($this->path($id));
    }

    public function copy(string $sourceDir, string $targetId) : void
    {
        $targetDir = $this->forceDir($targetId);
        $this->system->exec("cp -rf {$sourceDir}/* {$targetDir}/");
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra\Import;

use Argo\Domain\Content\ContentLocator;
use Argo\Domain\Log;
use Argo\Domain\Storage;
use DomDocument;
use DomXpath;

class ImportWordpress
{
    protected $content;

    protected $log;

    protected $urls = [];

    public function __construct(
        Storage $storage,
        ContentLocator $content,
        Log $log
    ) {
        $this->storage = $storage;
        $this->content = $content;
        $this->log = $log;
    }

    public function __invoke(string $file) : void
    {
        $xml = simplexml_load_file($file);
        $wp = $this->getNamespaceChildren($xml->channel, 'wp');
        $this->urls = array_unique([
            (string) $xml->channel->link,
            (string) $wp->base_site_url,
            (string) $wp->base_blog_url,
        ]);

        foreach ($xml->channel->item as $wpItem) {
            $this->importItem($wpItem);
        }
    }

    protected function importItem($wpItem)
    {
        $wp = $this->getNamespaceChildren($wpItem, 'wp');
        $dc = $this->getNamespaceChildren($wpItem, 'dc');
        $content = $this->getNamespaceChildren($wpItem, 'content');
        $excerpt = $this->getNamespaceChildren($wpItem, 'excerpt');

        list($type, $id) = $this->listTypeId($wp);
        if ($type === null) {
            return;
        }

        if ($this->storage->exists($id)) {
            $this->log->echo("SKIP {$id} (already exists)");
            return;
        }

        $uctype = ucfirst($type);
        $class = "Argo\Domain\Content\\{$uctype}\\{$uctype}";
        $item = new $class($id);
        $data = [
            'title' => (string) $wpItem->title,
            'author' => (string) $dc->creator,
            'markup' => 'wordpress',
            'tags' => [],
        ];

        $tags = [];

        foreach ($wpItem->category as $category) {
            // covers all category domains, incl "category" and "post_tag"
            $tags[] = (string) $category['nicename'];
        }

        $data['tags'] = array_unique($data['tags']);

        $item->fill($data);

        $item->data->created = $wp->post_date_gmt . ' UTC';
        $item->data->updated = [$wp->post_date_gmt . ' UTC'];

        $body = (string) $content->encoded;

        $this->log->echo("save {$id}");
        $repo = "{$type}s";
        $this->content->$repo->save($item, $body);

        $this->importItemImages($body);
    }

    protected function listTypeId(object $wp) : array
    {
        $type = (string) $wp->post_type;

        if ($type !== 'page' && $type !== 'post') {
            return [null, null];
        }

        $name = (string) $wp->post_name;

        if (empty($name)) {
            return [null, null];
        }

        $id = str_replace('_', '-', $name);

        if ($type === 'post') {
            list($date, $time) = explode(' ', (string) $wp->post_date);
            $id = "post/"
                . str_replace('-', '/', $date)
                . "/{$id}";
        }

        return [$type, $id];
    }

    protected function getNamespaceChildren($xml, string $namespace)
    {
        $ns = $xml->getNamespaces(true);
        return $xml->children($ns[$namespace]);
    }

    protected function importItemImages(string $body)
    {
        $doc = new DomDocument();
        $doc->formatOutput = true;
        $doc->loadHtml(
            mb_convert_encoding($body, 'HTML-ENTITIES', 'UTF-8'),
            LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING
        );

        $xpath = new DomXpath($doc);
        $query = '//img';
        $imgNodes = $xpath->query($query);

        foreach ($imgNodes as $imgNode) {
            $src = $imgNode->attributes->getNamedItem('src');
            $this->importItemImage($src);
        }
    }

    protected function importItemImage($src) : void
    {
        $remote = $src->nodeValue;

        foreach ($this->urls as $url) {
            if (strpos($remote, '://') === false) {
                $remote = $url . $remote;
            }

            if (strpos($remote, $url) === false) {
                continue;
            }

            $data = $this->fileGetContents($remote);

            if ($data === false) {
                $this->log->echo("FAIL image {$remote}");
                continue;
            }

            $local = substr($remote, strlen($url));
            $local = ltrim($local, '/');

            $this->storage->forceDir(dirname($local));
            $file = $this->storage->path($local);

            file_put_contents($file, $data);
            $this->log->echo('save image ' . $local);
            return;
        }
    }

    protected function fileGetContents($file)
    {
        return @file_get_contents($file);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\DomainContainerFactory;
use Argo\Domain\Log;
use Argo\Domain\Storage;
use Capsule\Di\Container;
use Capsule\Di\Definitions;
use Capsule\Di\Lazy;

class InfraContainerFactory extends DomainContainerFactory
{
    static protected function define() : Definitions
    {
        $def = parent::define();

        $def->object(Log::CLASS, Stdlog::CLASS);

        $def->object(Storage::CLASS, Fsio::CLASS)
            ->argument(
                'docroot',
                Lazy::getCall(System::CLASS, 'docroot')
            );

        return $def;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Config\Config;
use Argo\Domain\Content\ContentLocator;
use Argo\Domain\Content\Post\Post;
use Argo\Domain\DateTime;
use Argo\Domain\Storage;
use Argo\Infra\BuildFactory;

class Initialize
{
    public function __construct(
        DateTime $dateTime,
        Config $config,
        ContentLocator $content,
        Storage $storage,
        BuildFactory $buildFactory
    ) {
        $this->dateTime = $dateTime;
        $this->config = $config;
        $this->content = $content;
        $this->storage = $storage;
        $this->buildFactory = $buildFactory;
    }

    public function __invoke()
    {
        $text = [
            'Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"',
            'Header set Expires "0"',
            'Header set Pragma "no-cache"',
        ];

        $this->storage->write('.htaccess', implode("\n", $text));

        $date = $this->dateTime->ymd();
        $relId = "{$date}/sample-post";
        $post = new Post(
            Post::absId($relId),
            [
                'title' => 'Sample Post',
                'author' => $this->config->general->author,
                'tags' => ['general'],
            ]
        );
        $this->content->posts->save($post, 'Sample post body.');
        $this->buildFactory->new()->all();
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Content\ContentLocator;
use Argo\Domain\Content\Post\Post;
use Argo\Domain\Config\Config;
use Argo\Domain\Config\ConfigGateway;
use Argo\Domain\DateTime;
use Argo\Domain\Json;
use Argo\Domain\Storage;
use RuntimeException;

class Preflight
{
    protected $system;

    protected $dateTime;

    protected $storage;

    protected $config;

    protected $configGateway;

    protected $initialize;

    protected $server;

    public function __construct(
        System $system,
        DateTime $dateTime,
        Storage $storage,
        Config $config,
        ConfigGateway $configGateway,
        Initialize $initialize,
        Server $server,
        ContentLocator $content,
        BuildFactory $buildFactory
    ) {
        $this->system = $system;
        $this->dateTime = $dateTime;
        $this->storage = $storage;
        $this->config = $config;
        $this->configGateway = $configGateway;
        $this->initialize = $initialize;
        $this->server = $server;
        $this->content = $content;
        $this->buildFactory = $buildFactory;
    }

    public function __invoke(string $path) : ?string
    {
        $this->extensions();
        $sites = $this->system->sites();

        if (empty($sites)) {
            if (trim($path, '/') === 'setup') {
                return null;
            }

            return '/setup/';
        }

        $docroot = $this->system->docroot();

        if ($docroot === '' || ! is_dir($docroot)) {
            $docroot = reset($sites);
            $this->server->stop($docroot);
            return '/';
        }

        $this->storage->forceDir('_trash');
        $this->storage->forceDir('_theme');

        if ($this->config->admin->initialize ?? false) {
            $this->initialize();
        } else {
            $this->upgrade();
        }

        $this->dateTime->setTimezone($this->config->general->timezone);

        $this->server->start();

        return null;
    }

    protected function extensions() : void
    {
        $missing = ['date', 'dom', 'json'];

        foreach ($missing as $key => $val) {
            if (extension_loaded($val)) {
                unset($missing[$key]);
            }
        }

        if ($missing) {
            $message = "Please install the following PHP extensions: "
                . implode(', ', $missing);
            throw new RuntimeException($message);
        }
    }

    protected function configs() : void
    {
        $this->config('admin', '_argo/admin', [
            'lastBuild' => null,
            'lastSync' => null,
            'version' => null,
        ]);

        $this->config('blogroll', '_argo/blogroll', []);

        $this->config('featured', '_argo/featured', []);

        $this->config('general', '_argo/general', [
            'title' => '',
            'tagline' => '',
            'author' => '',
            'url' => '',
            'timezone' => $this->system->timezone(),
            'perPage' => 10,
            'theme' => 'argo/bootstrap4'
        ]);

        $this->config('menu', '_argo/menu', []);

        $this->config('sync', '_argo/sync', [
            'type' => 'git',
            'host' => '',
            'user' => '',
            'path' => '',
        ]);

        // load up the config for the theme as default values.
        // when the time comes for composer-based themes,
        // change to _theme/vendor/{$theme}/config/theme.json.
        // and: how to look for local themes?
        $theme = $this->config->general->theme;
        $file = $this->system->docroot("_theme/vendor/{$theme}/config/theme.json");
        $json = file_exists($file) ? file_get_contents($file) : '{}';
        $default = Json::decode($json, true);
        $this->config('theme', "_argo/theme/{$theme}", $default);
    }

    protected function config(string $name, string $id, array $default) : void
    {
        $old = isset($this->config->$name)
            ? $this->config->$name->getData()
            : null;

        if ($old === null) {
            // no existing config, create from default
            $this->config->$name = $this->configGateway->newValues(
                $id,
                Json::recode($default)
            );
            $this->configGateway->saveValues($this->config->$name);
            return;
        }

        $old = Json::recode($old, true);
        $new = array_replace_recursive($default, $old);
        if ($new !== $old) {
            // pre-existing config, write the changed values
            $this->config->$name->setData(Json::recode((object) $new));
            $this->configGateway->saveValues($this->config->$name);
        }
    }

    protected function initialize()
    {
        $this->initializeComposerThemes();

        $this->configs();

        $text = [
            'Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"',
            'Header set Expires "0"',
            'Header set Pragma "no-cache"',
        ];

        $this->storage->write('.htaccess', implode("\n", $text));

        $date = $this->dateTime->ymd();
        $relId = "{$date}/sample-post";
        $post = new Post(
            Post::absId($relId),
            [
                'title' => 'Sample Post',
                'author' => $this->config->general->author,
                'tags' => ['general'],
            ]
        );
        $this->content->posts->save($post, 'Sample post body.');
        $this->buildFactory->new()->all();

        unset($this->config->admin->initialize);
        $this->configGateway->saveValues($this->config->admin);
    }

    protected function upgrade() : void
    {
        $this->configs();
        $version = $this->config->admin->version ?? '1.0.0';
        $method = 'upgradeFrom_' . str_replace('.', '_', $version);
        if (method_exists($this, $method)) {
            $this->$method();
        }
    }

    protected function composer(string $command) : void
    {
        $composer = $this->system->approot('bin/composer.phar');
        $docroot = $this->system->docroot('_theme');
        $command = "cd $docroot; php $composer $command";
        $this->system->exec($command);
    }

    protected function initializeComposerThemes() : void
    {
        $source = $this->system->approot('resources/theme');
        $target = $this->system->supportDir();
        $command = "cp -rf '$source' '$target'";
        $this->system->exec($command);

        $this->storage->write('_theme/composer.json', Json::encode([
            'name' => 'argo/themes',
            'description' => 'Argo themes for this site.',
            'license' => 'proprietary',
            'repositories' => [
                [
                    'type' => 'path',
                    'url' => $this->system->supportDir() . '/theme/default/',
                    'options' => [
                        'symlink' => true,
                    ],
                ],
                [
                    'type' => 'path',
                    'url' => $this->system->supportDir() . '/theme/bootstrap4/',
                    'options' => [
                        'symlink' => true,
                    ],
                ],
            ],
            'require' => [
                'argo/bootstrap4' => 'dev-master',
                'argo/original' => 'dev-master',
            ],
        ]));

        $this->composer('install');
    }

    protected function upgradeFrom_1_0_0() : void
    {
        // get the current theme name, and move it from _theme to _general
        $theme = $this->config->theme->name ?? 'argo/original';
        unset($this->config->theme->name);
        $this->config->general->theme = $theme;
        $this->configGateway->saveValues($this->config->general);
        $this->configGateway->saveValues($this->config->theme);

        // copy the _argo/theme.json file to _argo/{$theme}.json;
        // e.g., from _argo/theme.json to _argo/theme/argo/original.json
        $this->storage->forceDir("_argo/theme/{$theme}");
        $text = $this->storage->read('_argo/theme.json');
        $this->storage->write("_argo/theme/{$theme}.json", $text);

        // remove the old _argo/theme.json file
        $file = $this->storage->path('_argo/theme.json');
        $this->system->exec("rm {$file}");

        // use composer for themes
        $this->initializeComposerThemes();

        // done
        $this->config->admin->version = '1.2.0';
        $this->configGateway->saveValues($this->config->admin);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Log;

class Server
{
    protected $system;

    protected $log;

    public function __construct(
        System $system,
        Log $log
    ) {
        $this->system = $system;
        $this->log = $log;
    }

    public function start() : void
    {
        $pid = $this->pid();
        if ($pid === null) {
            $docroot = require $this->system->supportDir() . "/docroot.php";
            $cmd = "php -S 127.0.0.1:8081 -t {$docroot} &"; // echo $! > $support/pid
            $this->log->info($cmd);
            popen($cmd, 'r'); // DO NOT CLOSE
        }
    }

    public function stop(string $docroot = null) : void
    {
        $pid = $this->pid();

        if ($pid !== null) {
            $cmd = "kill {$pid}";
            $this->system->exec($cmd, $this->log, 'info');
        }

        if ($docroot !== null) {
            $this->system->putDocroot($docroot);
        }
    }

    protected function pid() : ?int
    {
        exec(
            'ps -o command=COMMAND---------------,pid | grep "^php -S 127.0.0.1:8081"',
            $output
        );

        if (empty($output)) {
            return null;
        }

        $process = trim(array_shift($output));
        preg_match('/(\d+)$/', $process, $matches);
        return (int) $matches[1];
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Log;
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;

class Stdlog extends AbstractLogger implements Log
{
    protected $stdout;

    protected $stderr;

    protected $stderrLevels = [
        LogLevel::EMERGENCY,
        LogLevel::ALERT,
        LogLevel::CRITICAL,
        LogLevel::ERROR,
    ];

    public static function console(string $message, ...$vars)
    {
        $log = new static();
        $log->debug($message);
        foreach ($vars as $var) {
            $log->debug(print_r($var, true));
        }
    }

    public function __construct($stdout = null, $stderr = null)
    {
        $this->stdout = $stdout ?? fopen('php://stdout', 'w');
        $this->stderr = $stderr ?? fopen('php://stderr', 'w');
    }

    public function echo($message, array $context = [])
    {
        echo $this->interpolate($message, $context);
        flush();
    }

    public function log($level, $message, array $context = [])
    {
        $handle = $this->stdout;

        if (in_array($level, $this->stderrLevels)) {
            $handle = $this->stderr;
        }

        fwrite($handle, $this->interpolate($message, $context));
    }

    protected function interpolate($message, array $context) : string
    {
        if (is_array($message)) {
            $message = implode(PHP_EOL, $message);
        }

        $replace = [];
        foreach ($context as $key => $val) {
            $replace['{' . $key . '}'] = $val;
        }

        return strtr($message, $replace) . PHP_EOL;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Config\Config;
use Argo\Domain\Config\ConfigGateway;
use Argo\Domain\Config\Values;
use Argo\Domain\Content\ContentLocator;
use Argo\Domain\DateTime;
use Argo\Domain\Log;
use Argo\Domain\Storage;
use Argo\Infra\System;

class Sync
{
    protected $config;

    protected $dateTime;

    protected $configGateway;

    protected $log;

    protected $storage;

    protected $system;

    public function __construct(
        System $system,
        Storage $storage,
        Config $config,
        ConfigGateway $configGateway,
        DateTime $dateTime,
        Log $log
    ) {
        $this->system = $system;
        $this->storage = $storage;
        $this->config = $config;
        $this->configGateway = $configGateway;
        $this->dateTime = $dateTime;
        $this->log = $log;
    }

    public function __invoke() : void
    {
        $sync = $this->config->sync;

        if (
            ! isset($sync->type)
            || ! is_string($sync->type)
            || trim($sync->type) === ''
        ) {
            $this->log->error('No sync type specified in config.');
            return;
        }

        $type = trim($sync->type);

        if (! method_exists($this, $type)) {
            $this->log->error("Sync type '$type' not recognized.");
            return;
        }

        $cmd = $this->$type($sync);
        $this->system->exec("{$cmd} 2>&1", $this->log, 'echo');

        $this->config->admin->lastSync = $this->dateTime->utc();
        $this->configGateway->saveValues($this->config->admin);

        $this->log->echo('Done!');
    }

    protected function rsync(Values $sync) : string
    {
        return implode(" \\" . PHP_EOL . "  ", [
            "rsync",
            "-avz",
            "-e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'",
            "--delete",
            "--delete-excluded",
            "--delete-after",
            "--progress",
            "--exclude='_*'",
            "--exclude='.DS_Store'",
            "--exclude='.git'",
            "--exclude='.gitignore'",
            "--chmod=ugo+rx",
            $this->storage->path('.'),
            "{$sync->user}@{$sync->host}:{$sync->path}",
        ]);
    }

    protected function git(Values $sync) : string
    {
        return implode(" \\" . PHP_EOL, [
            "cd " . $this->storage->path(),
            "&& git add .",
            "&& git commit -a --message='sync site'",
            "&& git push -u origin `git branch --show-current`"
        ]);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\Infra;

use Argo\Domain\Log;
use Argo\Exception;
use DateTimeImmutable;
use RuntimeException;

class System
{
    protected $os;

    public function __construct(string $os = PHP_OS_FAMILY)
    {
        $this->os = $os;
    }

    public function exec(string $cmd, Log $log = null, string $level = null) : void
    {
        if ($log !== null && $level !== null) {
            $log->$level($cmd);
        }

        $handle = popen($cmd, 'r');

        if ($log !== null && $level !== null) {
            while (false !== $text = fgets($handle)) {
                $log->$level(rtrim($text));
            }
        }

        pclose($handle);
    }

    public function open(string $file) : void
    {
        $cmd = $this->call(__FUNCTION__, $file);
        $this->exec($cmd);
    }

    public function timezone() : string
    {
        $time = shell_exec('export LC_ALL=en;date');
        $date = new DateTimeImmutable($time);
        $zone = $date->getTimeZone()->getName();
        switch ($zone) {
            case 'EDT':
                return 'EST5EDT';
            case 'CDT':
                return 'CST6CDT';
            case 'MDT':
                return 'MST7MDT';
            case 'PDT':
                return 'PST8PDT';
            default:
                return $zone;
        }
    }

    public function approot(string $subpath = '') : string
    {
        $path = dirname(__DIR__, 2);

        if ($subpath !== '') {
            $path .= "/{$subpath}";
        }

        return $path;
    }

    public function docroot(string $subpath = '') : string
    {
        $file = $this->supportDir() . '/docroot.php';

        if (! file_exists($file)) {
            $docroot = '';
            $sites = $this->sites();

            if (! empty($sites)) {
                $docroot = array_shift($sites);
            }

            $this->putDocroot($docroot);
        }

        $path = require $file;

        if ($subpath !== '') {
            $path .= "/{$subpath}";
        }

        return $path;
    }

    public function putDocroot(string $docroot) : void
    {
        $file = $this->supportDir() . '/docroot.php';
        file_put_contents($file, "<?php return '{$docroot}';");
    }

    public function sites() : array
    {
        $pattern = $this->sitesDir() . '/*/_argo';

        $dirs = glob($pattern, GLOB_ONLYDIR);
        $sites = [];

        foreach ($dirs as $dir) {
            $sites[basename(dirname($dir))] = dirname($dir) . '/';
        }

        return $sites;
    }

    public function homeDir() : string
    {
        return $this->call(__FUNCTION__);
    }

    public function sitesDir() : string
    {
        $dir = $this->call(__FUNCTION__);
        if (! is_dir($dir)) {
            $this->mkdir($dir);
        }
        return $dir;
    }

    public function supportDir() : string
    {
        $dir = $this->call(__FUNCTION__);
        if (! is_dir($dir)) {
            $this->mkdir($dir);
        }
        return $dir;
    }

    public function mkdir(string $dir) : void
    {
        $level = error_reporting(0);
        $result = mkdir($dir, 0755, true);
        error_reporting($level);

        if ($result !== false) {
            return;
        }

        $error = error_get_last();
        throw new RuntimeException($error['message'] . ": {$dir}");
    }

    public function whoami() : string
    {
        return trim(shell_exec('whoami'));
    }

    protected function call($func, ...$args)
    {
        $func = $this->os . ucfirst($func);
        return $this->$func(...$args);
    }

    protected function darwinOpen(string $file) : string
    {
        return "open {$file}";
    }

    protected function linuxOpen(string $file) : string
    {
        return "xdg-open {$file}";
    }

    protected function darwinHomeDir() : string
    {
        $user = $this->whoami();
        return "/Users/{$user}";
    }

    protected function linuxHomeDir() : string
    {
        $user = $this->whoami();
        return "/home/{$user}";
    }

    protected function darwinSitesDir() : string
    {
        return $this->homeDir() . '/Sites';
    }

    protected function linuxSitesDir() : string
    {
        return $this->homeDir() . '/Sites';
    }

    protected function darwinSupportDir() : string
    {
        return $this->homeDir() . '/Library/Application Support/Argo';
    }

    protected function linuxSupportDir() : string
    {
        return $this->homeDir() . '/.config/Argo';
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Aura\Html\Escaper;
use Aura\Html\Helper\Anchor;

class AnchorLocal
{
    protected $anchor;

    public function __construct(
        Escaper $escaper,
        Anchor $anchor
    ) {
        $this->escaper = $escaper;
        $this->anchor = $anchor;
    }

    public function __invoke(string $href, string $text, array $attr = [])
    {
        $href = ltrim($href, '/');
        return ($this->anchor)("http://127.0.0.1:8081/{$href}", $text, $attr);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Aura\Html\Escaper;
use Aura\Html\Helper\Anchor;

class AnchorOpenFolder
{
    protected $anchor;

    public function __construct(
        Escaper $escaper,
        Anchor $anchor
    ) {
        $this->escaper = $escaper;
        $this->anchor = $anchor;
    }

    public function __invoke(string $id, string $text = 'Open Folder') : string
    {
        $id = $this->escaper->js($id);
        return ($this->anchor)('', $text, [
            'onclick' => "return openFolder('{$id}');",
        ]);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Argo\Domain\Content\Item;
use Argo\Domain\Storage;
use DomDocument;
use DomXpath;

class Body
{
    protected $storage;

    public function __construct(Storage $storage)
    {
        $this->storage = $storage;
    }

    public function __invoke(Item $item) : string
    {
        $body = $this->storage->read($item->getBodyFile());

        $body = trim($body);
        if ($body === '') {
            return $body;
        }

        $markup = $item->data->markup ?? 'markdown';
        $class = 'Argo\View\Helper\Body\\' . ucfirst($markup);
        $convert = new $class();
        $html = $convert->toHtml($body);
        $html = $this->expandImgSrc($item, $html);
        $html = $this->retargetAnchor($item, $html);
        $html = $this->moreOrLess($item, $html);
        return $html;
    }

    protected function moreOrLess(Item $item, string $html) : string
    {
        $found = preg_match('/(.*)\n+\s*\<\!--\s*more\s*--\>\s*\n+(.*)/ms', $html, $matches);
        if ($found) {
            $html = $matches[1] . "\n\n<a id=\"more\"\n\n" . $matches[2];
        }
        return $html;
    }

    protected function retargetAnchor(Item $item, string $html) : string
    {
        return $html;
    }

    protected function expandImgSrc(Item $item, string $html) : string
    {
        $doc = new DomDocument();
        $doc->formatOutput = true;
        $doc->loadHtml(
            mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'),
            LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING
        );

        $xpath = new DomXpath($doc);
        $query = '//img';
        $imgNodes = $xpath->query($query);

        foreach ($imgNodes as $imgNode) {
            $src = $imgNode->getAttribute('src');

            if (strpos($src, '://') !== false) {
                continue;
            }

            if (substr($src, 0, 2) === './') {
                $imgNode->setAttribute('src', $this->href($item->href . substr($src, 2)));
                continue;
            }

            if (substr($src, 0, 1) === '/') {
                $imgNode->setAttribute('src', $this->href($src));
                continue;
            }

            if (substr($src, 0, 1) !== '/') {
                $imgNode->setAttribute('src', $this->href($item->href . $src));
                continue;
            }
        }

        // retain the modified html
        $html = trim($doc->saveHtml($doc->documentElement));

        // strip the html and body tags added by DomDocument
        $html = substr(
            $html,
            strlen(''),
            -1 * strlen('')
        );

        // still may be whitespace all about
        return trim($html) . PHP_EOL;
    }

    protected function href(string $href) : string
    {
        return $href;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper\Body;

class Html extends Markup
{
    public function toHtml(string $body) : string
    {
        return $body;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper\Body;

use League\CommonMark\CommonMarkConverter;

class Markdown extends Markup
{
    protected $markdown;

    public function __construct()
    {
        $this->markdown = new CommonMarkConverter();
    }

    public function toHtml(string $body) : string
    {
        return $this->markdown->convertToHtml($body);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper\Body;

abstract class Markup
{
    abstract public function toHtml(string $body) : string;
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper\Body;

class Wordpress extends Markup
{
    public function toHtml(string $body) : string
    {
        return str_replace(
            ['â€“', 'â€œ', 'â€™', 'â€'],
            ['--',  '"',   "'",   '"'],
            $this->wp_autop($body)
        );
    }

    /**
     * Replaces double line-breaks with paragraph elements.
     * @package WordPress
     */
    public function wp_autop(string $pee, bool $br = true) : string
    {
        $pre_tags = array();

        if (trim($pee) === '') {
            return '';
        }

        // Just to make things a little easier, pad the end.
        $pee = $pee . "\n";

        /*
         * Pre tags shouldn't be touched by autop.
         * Replace pre tags with placeholders and bring them back after autop.
         */
        if (strpos($pee, '<pre') !== false) {
            $pee_parts = explode('
', $pee);
            $last_pee  = array_pop($pee_parts);
            $pee       = '';
            $i         = 0;

            foreach ($pee_parts as $pee_part) {
                $start = strpos($pee_part, '<pre');

                // Malformed html?
                if ($start === false) {
                    $pee .= $pee_part;
                    continue;
                }

                $name              = "

";
                $pre_tags[ $name ] = substr($pee_part, $start) . '
';

                $pee .= substr($pee_part, 0, $start) . $name;
                $i++;
            }

            $pee .= $last_pee;
        }
        // Change multiple
s into two line breaks, which will turn into paragraphs.
        $pee = preg_replace('|
\s*
|', "\n\n", $pee);

        $allblocks = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)';

        // Add a double line break above block-level opening tags.
        $pee = preg_replace('!(<' . $allblocks . '[\s/>])!', "\n\n$1", $pee);

        // Add a double line break below block-level closing tags.
        $pee = preg_replace('!(</' . $allblocks . '>)!', "$1\n\n", $pee);

        // Standardize newline characters to "\n".
        $pee = str_replace(array("\r\n", "\r"), "\n", $pee);

        // Find newlines in all elements and add placeholders.
        $pee = $this->wp_replace_in_html_tags($pee, array("\n" => '  '));

        // Collapse line breaks before and after <option> elements so they don't get autop'd.
        if (strpos($pee, '<option') !== false) {
            $pee = preg_replace('|\s*<option|', '<option', $pee);
            $pee = preg_replace('|</option>\s*|', '</option>', $pee);
        }

        /*
         * Collapse line breaks inside <object> elements, before <param> and <embed> elements
         * so they don't get autop'd.
         */
        if (strpos($pee, '</object>') !== false) {
            $pee = preg_replace('|(<object[^>]*>)\s*|', '$1', $pee);
            $pee = preg_replace('|\s*</object>|', '</object>', $pee);
            $pee = preg_replace('%\s*(</?(?:param|embed)[^>]*>)\s*%', '$1', $pee);
        }

        /*
         * Collapse line breaks inside <audio> and <video> elements,
         * before and after <source> and <track> elements.
         */
        if (strpos($pee, '<source') !== false || strpos($pee, '<track') !== false) {
            $pee = preg_replace('%([<\[](?:audio|video)[^>\]]*[>\]])\s*%', '$1', $pee);
            $pee = preg_replace('%\s*([<\[]/(?:audio|video)[>\]])%', '$1', $pee);
            $pee = preg_replace('%\s*(<(?:source|track)[^>]*>)\s*%', '$1', $pee);
        }

        // Collapse line breaks before and after
 elements.
        if (strpos($pee, ')|', '$1', $pee);
            $pee = preg_replace('|
\s*|', '
', $pee);
        }

        // Remove more than two contiguous line breaks.
        $pee = preg_replace("/\n\n+/", "\n\n", $pee);

        // Split up the contents into an array of strings, separated by double line breaks.
        $pees = preg_split('/\n\s*\n/', $pee, -1, PREG_SPLIT_NO_EMPTY);

        // Reset $pee prior to rebuilding.
        $pee = '';

        // Rebuild the content as a string, wrapping every bit with a
.
        foreach ($pees as $tinkle) {
            $pee .= '
' . trim($tinkle, "\n") . "
\n";
        }

        // Under certain strange conditions it could create a P of entirely whitespace.
        $pee = preg_replace('|
\s*
|', '', $pee);

        // Add a closing
 inside
, <address>, or <form> tag if missing.
        $pee = preg_replace('!
([^<]+)</(div|address|form)>!', '
$1
</$2>', $pee);

        // If an opening or closing block element tag is wrapped in a
, unwrap it.
        $pee = preg_replace('!
\s*(</?' . $allblocks . '[^>]*>)\s*
!', '$1', $pee);

        // In some cases
 may get wrapped in
, fix them.
        $pee = preg_replace('|
(|i', '

', $pee);
        $pee = str_replace('

', '

', $pee);

        // If an opening or closing block element tag is preceded by an opening
 tag, remove it.
        $pee = preg_replace('!
\s*(</?' . $allblocks . '[^>]*>)!', '$1', $pee);

        // If an opening or closing block element tag is followed by a closing
 tag, remove it.
        $pee = preg_replace('!(</?' . $allblocks . '[^>]*>)\s*
!', '$1', $pee);

        // Optionally insert line breaks.
        if ($br) {
            // Replace newlines that shouldn't be touched with a placeholder.
            $pee = preg_replace_callback('/<(script|style).*?<\/\\1>/s', [$this, 'wp_autop_newline_preservation_helper'], $pee);

            // Normalize

            $pee = str_replace(array('
', '
'), '
', $pee);

            // Replace any new line characters that aren't preceded by a
 with a
.
            $pee = preg_replace('|(?<!
)\s*\n|', "
\n", $pee);

            // Replace newline placeholders with newlines.
            $pee = str_replace('<WPPreserveNewline />', "\n", $pee);
        }

        // If a
 tag is after an opening or closing block tag, remove it.
        $pee = preg_replace('!(</?' . $allblocks . '[^>]*>)\s*
!', '$1', $pee);

        // If a
 tag is before a subset of opening or closing block tags, remove it.
        $pee = preg_replace('!
(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)!', '$1', $pee);
        $pee = preg_replace("|\n
$|", '
', $pee);

        // Replace placeholder
 tags with their original content.
        if (! empty($pre_tags)) {
            $pee = str_replace(array_keys($pre_tags), array_values($pre_tags), $pee);
        }

        // Restore newlines in all elements.
        if (false !== strpos($pee, '')) {
            $pee = str_replace(array('  ', ''), "\n", $pee);
        }

        return $pee;
    }

    /**
     * Separate HTML elements and comments from the text.
     * @package WordPress
     */
    public function wp_html_split(string $input) : array
    {
        return preg_split($this->wp_get_html_split_regex(), $input, -1, PREG_SPLIT_DELIM_CAPTURE);
    }

    /**
     * Retrieve the regular expression for an HTML element.
     * @package WordPress
     */
    public function wp_get_html_split_regex() : string
    {
        static $regex;

        if (! isset($regex)) {
            // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation
            $comments =
                '!'             // Start of comment, after the <.
                . '(?:'         // Unroll the loop: Consume everything until --> is found.
                .     '-(?!->)' // Dash not followed by end of comment.
                .     '[^\-]*+' // Consume non-dashes.
                . ')*+'         // Loop possessively.
                . '(?:-->)?';   // End of comment. If not found, match all input.

            $cdata =
                '!\[CDATA\['    // Start of comment, after the <.
                . '[^\]]*+'     // Consume non-].
                . '(?:'         // Unroll the loop: Consume everything until ]]> is found.
                .     '](?!]>)' // One ] not followed by end of comment.
                .     '[^\]]*+' // Consume non-].
                . ')*+'         // Loop possessively.
                . '(?:]]>)?';   // End of comment. If not found, match all input.

            $escaped =
                '(?='             // Is the element escaped?
                .    '!--'
                . '|'
                .    '!\[CDATA\['
                . ')'
                . '(?(?=!-)'      // If yes, which type?
                .     $comments
                . '|'
                .     $cdata
                . ')';

            $regex =
                '/('                // Capture the entire match.
                .     '<'           // Find start of element.
                .     '(?'          // Conditional expression follows.
                .         $escaped  // Find end of escaped element.
                .     '|'           // ... else ...
                .         '[^>]*>?' // Find end of normal element.
                .     ')'
                . ')/';
            // phpcs:enable
        }

        return $regex;
    }

    /**
     * Replace characters or phrases within HTML elements only.
     * @package WordPress
     */
    public function wp_replace_in_html_tags(string $haystack, array $replace_pairs) : string
    {
        // Find all elements.
        $textarr = $this->wp_html_split($haystack);
        $changed = false;

        // Optimize when searching for one item.
        if (1 === count($replace_pairs)) {
            // Extract $needle and $replace.
            foreach ($replace_pairs as $needle => $replace) {
            }

            // Loop through delimiters (elements) only.
            for ($i = 1, $c = count($textarr); $i < $c; $i += 2) {
                if (false !== strpos($textarr[ $i ], $needle)) {
                    $textarr[ $i ] = str_replace($needle, $replace, $textarr[ $i ]);
                    $changed       = true;
                }
            }
        } else {
            // Extract all $needles.
            $needles = array_keys($replace_pairs);

            // Loop through delimiters (elements) only.
            for ($i = 1, $c = count($textarr); $i < $c; $i += 2) {
                foreach ($needles as $needle) {
                    if (false !== strpos($textarr[ $i ], $needle)) {
                        $textarr[ $i ] = strtr($textarr[ $i ], $replace_pairs);
                        $changed       = true;
                        // After one strtr() break out of the foreach loop and look at next element.
                        break;
                    }
                }
            }
        }

        if ($changed) {
            $haystack = implode($textarr);
        }

        return $haystack;
    }

    /**
     * Newline preservation help function for wp_autop
     * @package WordPress
     */
    public function wp_autop_newline_preservation_helper(array $matches) : string
    {
        return str_replace("\n", '<WPPreserveNewline />', $matches[0]);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Argo\Domain\Content\Item;

class BodyLess extends Body
{
    protected function moreOrLess(Item $item, string $html) : string
    {
        $found = preg_match('/(.*)\n+\s*\<\!--\s*more\s*--\>\s*\n+(.*)/ms', $html, $matches);
        if ($found) {
            $html = $matches[1];
            $html .= "\n
href}#more\">Read more
\n";
        }

        return $html;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Argo\Domain\Content\Item;
use DomDocument;
use DomXpath;

class BodyPreview extends Body
{
    protected function moreOrLess(Item $item, string $html) : string
    {
        return $html;
    }

    protected function href(string $href) : string
    {
        return 'http://127.0.0.1:8081' . $href;
    }

    protected function retargetAnchor(Item $item, string $html) : string
    {
        $doc = new DomDocument();
        $doc->formatOutput = true;
        $doc->loadHtml(
            mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'),
            LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING
        );

        $xpath = new DomXpath($doc);
        $query = '//a';
        $aNodes = $xpath->query($query);

        foreach ($aNodes as $aNode) {
            $aNode->setAttribute('target', '_blank');
            $href = $aNode->getAttribute('href');

            if (strpos($href, '://') !== false) {
                continue;
            }

            if (substr($href, 0, 2) === './') {
                $aNode->setAttribute('href', $this->href($item->href . substr($href, 2)));
                continue;
            }

            if (substr($href, 0, 1) === '/') {
                $aNode->setAttribute('href', $this->href($href));
                continue;
            }

            if (substr($href, 0, 1) !== '/') {
                $aNode->setAttribute('href', $this->href($item->href . $href));
            }
        }

        // retain the modified html
        $html = trim($doc->saveHtml($doc->documentElement));

        // strip the html and body tags added by DomDocument
        $html = substr(
            $html,
            strlen(''),
            -1 * strlen('')
        );

        // still may be whitespace all about
        return trim($html) . PHP_EOL;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Argo\Domain\DateTime as DateTimeFormat;
use Aura\Html\Escaper;
use Aura\Html\Helper\AbstractHelper;

class DateTime extends AbstractHelper
{
    public function __construct(
        Escaper $escaper,
        DateTimeFormat $dateTime
    ) {
        parent::__construct($escaper);
        $this->dateTime = $dateTime;
    }

    public function __invoke() : self
    {
        return $this;
    }

    public function raw(string $time, string $format = 'c') : string
    {
        return $this->dateTime->local($time, $format);
    }

    public function html(string $time, string $format = 'c') : string
    {
        return $this->escaper->html($this->raw($time, $format));
    }

    public function attr(string $time, string $format = 'c') : string
    {
        return $this->escaper->attr($this->raw($time, $format));
    }

    public function tag(string $time, string $format = 'c', array $attr = []) : string
    {
        $attr['datetime'] = $this->raw($time, 'c');
        $attr = $this->escaper->attr($attr);
        return "<time $attr>" . $this->html($time, $format) . '</time>';
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use AutoRoute\Generator;

class Route
{
    protected $generator;

    public function __construct(Generator $generator)
    {
        $this->generator = $generator;
    }

    public function __invoke(string $class, ...$params) : string
    {
        return $this->generator->generate($class, ...$params);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View\Helper;

use Argo\View\Helper\Route;
use Aura\Html\Helper\Input;
use Aura\Html\Helper\Input\Generic;

class RouteSubmit
{
    protected $route;

    protected $input;

    public function __construct(
        Route $route,
        Input $input
    ) {
        $this->route = $route;
        $this->input = $input;
    }

    public function __invoke(string $value, string $class, ...$params) : Generic
    {
        $parts = explode('\\', $class);
        $last = end($parts);
        preg_match('/^(.+?)[A-Z]/', $last, $matches);
        $method = strtoupper($matches[1]);

        $route = ($this->route)($class, ...$params);
        return ($this->input)([
            'type' => 'button',
            'name' => strtolower(str_replace(' ', '_', $value)),
            'value' => $value,
            'attribs' => [
                'onclick' => "xhrSubmit('{$method}', this.form, '{$route}'); return false;"
            ]
        ]);
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View;

class View extends \Aura\View\View
{
    protected function penders(string $type) : string
    {
        $html = '';

        foreach ($this->penders[$type] ?? [] as $name) {
            $html .= $this->render("penders/{$type}/{$name}");
        }

        return $html;
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\View;

use Argo\Domain\Config\Config;
use Argo\Domain\Storage;
use Argo\Domain\DateTime as DateTimeFormat;
use Aura\Html\HelperLocatorFactory;
use Aura\View\TemplateRegistry;
use AutoRoute\Generator;
use Capsule\Di\Container;
use League\CommonMark\CommonMarkConverter;

class ViewFactory
{
    protected $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * @var array $dirs These need to be absolute, not relative, directories.
     */
    public function new(array $dirs) : View
    {
        $helpersFactory = new HelperLocatorFactory();

        $helpers = $helpersFactory->newInstance();

        $helpers->set('anchorLocal', function () use ($helpers) {
            return new Helper\AnchorLocal(
                $helpers->get('escape'),
                $helpers->get('anchor')
            );
        });

        $helpers->set('anchorOpenFolder', function () use ($helpers) {
            return new Helper\AnchorOpenFolder(
                $helpers->get('escape'),
                $helpers->get('anchor')
            );
        });

        $helpers->set('body', function () {
            return new Helper\Body(
                $this->container->get(Storage::CLASS)
            );
        });

        $helpers->set('bodyLess', function () {
            return new Helper\BodyLess(
                $this->container->get(Storage::CLASS)
            );
        });

        $helpers->set('bodyPreview', function () {
            return new Helper\BodyPreview(
                $this->container->get(Storage::CLASS)
            );
        });

        $helpers->set('dateTime', function () use ($helpers) {
            return new Helper\DateTime(
                $helpers->get('escape'),
                $this->container->get(DateTimeFormat::CLASS)
            );
        });

        $helpers->set('route', function () {
            return new Helper\Route(
                $this->container->get(Generator::CLASS)
            );
        });

        $helpers->set('routeSubmit', function () use ($helpers) {
            return new Helper\RouteSubmit(
                $helpers->get('route'),
                $helpers->get('input')
            );
        });

        return new View(
            new TemplateRegistry([], $dirs),
            new TemplateRegistry([], $dirs),
            $helpers
        );
    }
}


code:
<?php
declare(strict_types=1);

namespace Argo\App\Config;

use Argo\Domain\Json;

class FetchConfigTest extends \Argo\App\TestCase
{
    protected function setUp() : void
    {
        parent::setUp();
        $this->setUpArgo();
    }

    public function testNotFound()
    {
        $name = 'no-such-config';
        $payload = $this->invoke($name);
        $this->assertNotFound($payload);
    }

    public function testFound()
    {
        $name = 'general';
        $payload = $this->invoke($name);
        $this->assertFound($payload);
        $result = $payload->getResult();
        $this->assertSame('general', $result['name']);

        $expect = [
            'title' => 'Argo Blog Title',
            'tagline' => 'Argo Blog Tagline',
            'author' => 'boshag',
            'url' => 'http://example.com',
            'timezone' => 'UTC',
            'perPage' => 10,
            'theme' => 'bootstrap4',
        ];

        $this->assertJsonEquals($expect, $result['text']);
    }
}