This commit is contained in:
Dave M. 2024-09-25 14:38:02 -04:00
commit ffe66e522c
19 changed files with 344 additions and 88 deletions

View File

@ -87,6 +87,7 @@
"meta/definitions/email.php",
"meta/definitions/event.php",
"meta/definitions/http.php",
"meta/definitions/orm.php",
"meta/definitions/language.php",
"meta/definitions/routes.php",
"meta/definitions/software.php",

View File

@ -35,7 +35,7 @@ return [
public function execute(Routing $routing, Route $attribute) : void
{
if (null !== ($name = $attribute->name ?? null)) {
$this->extension->registerRoute($name, $attribute->getRoute(), $attribute->class, $attribute->classMethod, $attribute->methods ?? (array)$attribute->method);
$this->extension->registerRoute($name, $attribute->getRoute(), $attribute->class, $attribute->classMethod, (array) ( $attribute->method ?? $attribute->methods ));
}
}
},

View File

@ -1,29 +1,37 @@
<?php
use function DI\autowire, DI\create, DI\get;
use Tell\I18n;
use Psr\Container\ContainerInterface;
use Notes\Tell\LanguageHandler;
return [
Tell\I18n::class => create( Tell\I18n::class ) ->constructor(
get(Tell\Reader\JsonReader::class),
get(Tell\Reader\PhpReader::class),
get('tell.fallback') /* getenv("DEBUG") ? create(Tell\PrintMissingKey::class) : get('tell.fallback') */
),
use function DI\autowire, DI\create, DI\get, DI\add;
'tell.fallback' => function($c) {
$i18n = new Tell\I18n( $c->get(Tell\Reader\JsonReader::class), $c->get(Tell\Reader\PhpReader::class), new Tell\PrintMissingKey() );
return [
I18n::class => function(ContainerInterface $container) {
$i18n = new I18n($container->get(Tell\Reader\JsonReader::class), $container->get(Tell\Reader\PhpReader::class), $container->get('tell.fallback'));
$i18n->locale(getenv('DEFAULT_LOCAL'));
$i18n->initialize(! getenv("DEBUG"));
return $i18n;
},
'tell.fallback' => function(ContainerInterface $container) {
$i18n = new Tell\I18n( $container->get(Tell\Reader\JsonReader::class), $container->get(Tell\Reader\PhpReader::class), new Tell\PrintMissingKey() );
$i18n->locale(getenv('DEFAULT_LOCAL_FALLBACK') ?: "en_US");
$i18n->initialize(true);
return $i18n;
},
Tell\Reader\PhpReader::class => function($c) {
return new Tell\Reader\PhpReader($c->get(Lean\Lean::class)->getI18n('php'), false);
Tell\Reader\PhpReader::class => function(ContainerInterface $container) {
return new Tell\Reader\PhpReader($container->get(Lean\Lean::class)->getI18n('php'), false);
},
Tell\Reader\JsonReader::class => function($c) {
return new Tell\Reader\JsonReader($c->get(Lean\Lean::class)->getI18n('json'), true, \JSON_PRETTY_PRINT);
Tell\Reader\JsonReader::class => function(ContainerInterface $container) {
return new Tell\Reader\JsonReader($container->get(Lean\Lean::class)->getI18n('json'), true, \JSON_PRETTY_PRINT);
},
'lean.autoload' => add([
I18n::class,
]),
];

18
meta/definitions/orm.php Normal file
View File

@ -0,0 +1,18 @@
<?php
use Ulmus\Container\AdapterProxy;
use function DI\autowire, DI\create, DI\get, DI\add;
return [
'lean.autoload' => add([
AdapterProxy::class,
]),
AdapterProxy::class => function (ContainerInterface $c) {
return new AdapterProxy(
$c->get('lean:adapter.sqlite'),
$c->get(ConnectionAdapter::class),
);
},
];

View File

@ -54,7 +54,7 @@ return [
# PostRequestAuthenticationMiddleware::class,
],
'routes.list' => function($c) {
Lean\Routing\RouteDefinitionInterface::class => function($c) {
return function (ContainerInterface $container) {
$router = $container->get(Router::class);

View File

@ -1,6 +1,6 @@
<?php
use function DI\autowire, DI\create, DI\get;
use Tell\I18n;
use TheBugs\JavascriptMiddleware;
@ -10,12 +10,14 @@ use Storage\Cookie,
Storage\Session,
Storage\SessionMiddleware;
use function DI\autowire, DI\create, DI\get, DI\add;
$dir = dirname(__DIR__, 2);
return [
'lean.default' => [
'picea' => [
'context' => "Lean\\View",
'context' => \Lean\View::class,
'view' => [
[
@ -74,6 +76,10 @@ return [
],
],
'lean.autoload' => add([
Lean::class,
]),
Lean::class => autowire(Lean::class),
JavascriptMiddleware::class => create(JavascriptMiddleware::class),

View File

@ -1,6 +1,7 @@
<?php
use Ulmus\ConnectionAdapter;
use Ulmus\{ ConnectionAdapter, Container\AdapterProxy };
use Lean\Lean;
return [
'lean:adapter.sqlite' => function($c) {

8
skeleton/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea
.env
composer.lock
/private/
/public/static/
/public/.htaccess
/vendor/
/var/

152
skeleton/composer.json.dist Normal file
View File

@ -0,0 +1,152 @@
{
"name": "my_namespace/my_project",
"description": "",
"type": "app",
"license": "MIT",
"authors": [
],
"minimum-stability": "dev",
"require": {
"php": "^8.2",
"ext-apcu": "*",
"ext-json": "*",
"league/route": "^5.0.0-dev",
"laminas/laminas-diactoros": "2.24.x-dev",
"laminas/laminas-httphandlerrunner": "2.5.x-dev",
"vlucas/phpdotenv": "^3.4@dev",
"middlewares/whoops": "dev-master",
"ralouphie/getallheaders": "dev-master",
"mcnd/dump": "dev-master",
"mcnd/storage": "dev-master",
"mcnd/event": "dev-master",
"mcnd/ulmus": "dev-master",
"mcnd/ulmus-api": "dev-master",
"mcnd/ulmus-user": "dev-master",
"mcnd/picea": "dev-master",
"mcnd/picea-ui": "dev-master",
"mcnd/picea-asset": "dev-master",
"mcnd/cronard": "dev-master",
"mcnd/lean-console": "dev-master",
"mcnd/kash": "dev-master",
"mcnd/taxus": "dev-master",
"mcnd/notes": "dev-master",
"mcnd/thebugs": "dev-master",
"mcnd/negundo-client": "dev-master"
},
"repositories": [
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/cronard.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/dump.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/storage.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/lean.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/lean-console.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/ulmus.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/ulmus-api.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/ulmus-user.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/ulmus-api-gitea.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/picea.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/storage.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/picea-ui.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/picea-asset.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/notes.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/the-bugs.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/tell.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/taxus.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/dump.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/kash.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/event.git"
},
{
"type": "vcs",
"url": "https://git.mcnd.ca/mcndave/negundo-client.git"
}
],
"autoload": {
"psr-4": {
"MyProject\\Namespace": "src/"
}
},
"scripts": {
"post-install-cmd": [
"Lean\\Composer::postInstall"
],
"post-update-cmd": [
"Lean\\Composer::postUpdate"
]
},
"extra" : {
"lean" : {
"autoload": {
"definitions" : [
"meta/definitions/definitions.php",
"meta/definitions/storage.php",
"meta/definitions/security.php",
"meta/definitions/auth.php",
"meta/definitions/env.php"
],
"config" : [
"meta/config.php"
]
}
}
}
}

View File

@ -0,0 +1,3 @@
<?php
return require("env/" . ( getenv('APP_ENV') ?? 'prod' ) . ".php");

View File

@ -4,7 +4,7 @@ use Picea\Picea;
use Negundo\Client\{ NegundoMiddleware, SoftwareConfig };
use Laminas\Diactoros\Response\HtmlResponse;
use Lean\Factory\HttpFactory;
use Psr\Http\Server\MiddlewareInterface,
Psr\Http\Message\ServerRequestInterface,
@ -35,7 +35,7 @@ return [
return function(\Throwable $exception) use ($picea) {
error_log($exception->getMessage());
return new HtmlResponse($picea->renderHtml('lean/error/500', [
return HttpFactory::createHtmlResponse($picea->renderHtml('lean/error/500', [
'title' => "Une erreur s'est produite lors de l'exécution du script.",
'subtitle' => "Êtes-vous connecté avec le bon compte ?",
'message' => $exception->getMessage(),

View File

@ -6,14 +6,28 @@ use Taxus\{ Privilege, Taxus, PermissionGrantInterface, DefaultPermissionGrant }
use Psr\Http\Message\ServerRequestInterface;
use function DI\autowire, DI\create, DI\get;
use function DI\{ create, get, add };
return [
Taxus::class => function ($c) {
return ( new Taxus( $c->get(PermissionGrantInterface::class) ) )->add(
$c->get(Lean\Lean::class)->getTaxusPrivileges()
);
$taxus = new Taxus( ... $c->get('taxus.gates') );
$list = [];
foreach($c->get(Lean\Lean::class)->getTaxusPrivileges() as $key => $privilege) {
foreach($privilege as $name => $description) {
$list[] = [ new Privilege($name, $description), $key ];
}
}
$taxus->add(... $list);
return $taxus;
},
'taxus.gates' => add([
get(PermissionGrantInterface::class),
]),
PermissionGrantInterface::class => create(%NAMESPACE%\PrivilegeGrantAccess::class)->constructor(get(ServerRequestInterface::class), get(Session::class)),
];

View File

@ -1,12 +1,22 @@
<IfModule mod_rewrite.c>
RewriteEngine On
# RewriteBase /
# Force HTTPS (disabled by default)
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Reverse Proxy ; Force HTTPS (disabled by default)
# RewriteCond %{HTTP:X-Forwarded-SSL} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slashes from request URL
#RewriteCond %{REQUEST_FILENAME} !-d
#RewriteCond %{REQUEST_URI} (.+)/$
#RewriteRule ^ %1 [R=301,L]
# Redirect everything into app
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
RewriteRule ^([^?]*)$ %1 [NC,L,QSA]
</IfModule>

View File

@ -1,8 +1,8 @@
<?php
try {
require_once("../src/Kernel.php");
require_once(dirname(__DIR__)."/src/Kernel.php");
}
catch(\Throwable $t) {
echo $t->getMessage();
echo sprintf("%s in <strong>%s</strong> <pre>%s</pre>", $t->getMessage(), $t->getFile() . ":" . $t->getLine(), $t->getTraceAsString());
}

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Remove trailing slash" stopProcessing="true">
<match url="(.*)/$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Redirect" redirectType="Permanent" url="{R:1}" />
</rule>
<rule name="Framework Routing" stopProcessing="true">
<match url="." ignoreCase="false" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
</rules>
</rewrite>
<staticContent>
<remove fileExtension=".js" />
<mimeMap fileExtension=".js" mimeType="application/x-javascript; charset=UTF-8" />
<remove fileExtension=".css" />
<mimeMap fileExtension=".css" mimeType="text/css; charset=UTF-8" />
<remove fileExtension=".woff" />
<remove fileExtension=".eot" />
<remove fileExtension=".ttf" />
<remove fileExtension=".svg" />
<mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
<mimeMap fileExtension=".ttf" mimeType="application/font-sfnt" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
</staticContent>
<handlers>
<remove name="PHP7.3" />
<add name="PHP7.3" path="*.php" verb="*" modules="FastCgiModule" scriptProcessor="C:\php\7.4.0\php-cgi.exe" resourceType="File" requireAccess="Script" />
</handlers>
<httpErrors errorMode="Detailed" />
</system.webServer>
</configuration>

View File

@ -3,11 +3,13 @@
namespace Lean;
use Lean\Factory\HttpFactory;
use Notes\Attribute\Ignore;
use Notes\Route\Attribute\Object\Route;
use Notes\Security\Attribute\Security;
use Picea, Picea\Ui\Method\FormContext;
use TheBugs\Email\MailerInterface;
use Storage\Session;
use League\CommonMark\CommonMarkConverter;
use Psr\Http\Message\{ ServerRequestInterface, ResponseInterface };
@ -26,6 +28,7 @@ trait ControllerTrait {
public array $contextList = [];
#[Ignore]
public function exportJson(ServerRequestInterface $request, string $entityClass, bool $includeRelations = true, ? callable $callback = null) : ResponseInterface
{
foreach($entityClass::repository()->filterServerRequest( $entityClass::searchRequest()->fromRequest($request->withQueryParams($request->getQueryParams() + ['limit' => PHP_INT_MAX,])) )->loadAll() as $entity) {
@ -35,6 +38,7 @@ trait ControllerTrait {
return $this->renderJson( array_filter($data ?? [], function($row) { return $row !== null; }));
}
#[Ignore]
public function renderRawView(string $view, ?array $variables = null) : string
{
if ( null === $content = $this->picea->renderHtml($view, $variables ?? [], $this) ) {
@ -44,6 +48,7 @@ trait ControllerTrait {
return $content;
}
#[Ignore]
public function renderView(string $view, ?array $variables = null) : ResponseInterface
{
return static::renderHtml(
@ -51,6 +56,7 @@ trait ControllerTrait {
);
}
#[Ignore]
public function renderError(string $errorCode, ?array $variables = null) : ResponseInterface
{
return static::renderHtml(
@ -58,66 +64,78 @@ trait ControllerTrait {
);
}
#[Ignore]
public function renderDocumentation(string $filename) : ResponseInterface
{
return $this->renderMarkdown( file_get_contents(getenv("PROJECT_PATH") . "/meta/doc/$filename.md") );
}
#[Ignore]
protected function redirect(string $url, int $code = 302, array $headers = []) {
return HttpFactory::createRedirectResponse($url, $code, $headers);
}
#[Ignore]
public static function renderNothing(int $code = 204, array $headers = []) : ResponseInterface
{
return HttpFactory::createEmptyResponse($code, $headers);
}
#[Ignore]
public static function renderText(string $text, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createTextResponse($text, $code, $headers);
}
#[Ignore]
public static function renderHtml(string $html, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createHtmlResponse($html, $code, $headers);
}
#[Ignore]
public static function renderJson(mixed $data, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createJsonResponse($data, $code, $headers);
}
#[Ignore]
public function renderPdf($rawdata, int $status = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createPdfResponse($rawdata, $status, $headers);
}
#[Ignore]
public static function renderDownloadable(string $data, string $filename, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createDownloadableResponse($data, $filename, $code, $headers);
}
#[Ignore]
public static function renderImage(string $data, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createImageResponse($data, $code, $headers);
}
#[Ignore]
public static function renderAsset(string $path, int $code = 200, array $headers = []) : ResponseInterface
{
return HttpFactory::createFileDownloadResponse($path, $code, $headers);
}
#[Ignore]
public function renderMarkdown(string $filepath, int $code = 200, array $headers = []) : ResponseInterface
{
if ( ! class_exists(CommonMarkConverter::class)) {
throw new \BadFunctionCallException("League\CommonMark seems to be missing, please install dependency before trying to render Markdown content");
}
$markdown = ( new CommonMarkConverter() )->convertToHtml(file_get_contents($filepath));
$markdown = ( new CommonMarkConverter() )->convert(file_get_contents($filepath));
return $this->renderView("docs", get_defined_vars());
return $this->renderView("lean/layout/docs", get_defined_vars());
}
#[Ignore]
public function renderCLI(ServerRequestInterface $request, mixed $data) : ResponseInterface
{
if ($data instanceof \JsonSerializable ) {
@ -134,6 +152,7 @@ trait ControllerTrait {
);
}
#[Ignore]
public function fromResponse(ResponseInterface $response)
{
if ( $response->getStatusCode() === 200 ) {
@ -145,6 +164,7 @@ trait ControllerTrait {
return null;
}
#[Ignore]
public function asset(string $url, array $parameters = []) : string
{
if (! $this->picea ) {
@ -154,6 +174,7 @@ trait ControllerTrait {
return $this->picea->compiler->getExtensionFromToken('asset')->buildAssetUrl($url, $parameters);
}
#[Ignore]
public function url(string $url, array $parameters = []) : string
{
if (! $this->picea ) {
@ -163,6 +184,7 @@ trait ControllerTrait {
return $this->picea->compiler->getExtensionFromToken('url')->buildUrl($url, $parameters);
}
#[Ignore]
public function route(string $name, array $parameters = []) : string
{
if (! $this->picea ) {
@ -172,11 +194,13 @@ trait ControllerTrait {
return $this->picea->compiler->getExtensionFromToken('route')->buildRouteUrl($name, $parameters);
}
#[Ignore]
public function json($data, int $flags = 0) : string
{
return htmlentities(json_encode($data, $flags), ENT_QUOTES, 'UTF-8');
}
#[Ignore]
public function pushContext(FormContext $context) : FormContext
{
$this->contextList[$context->formName ?? uniqid("context_")] = $context;
@ -184,11 +208,13 @@ trait ControllerTrait {
return $context;
}
#[Ignore]
public function context(? string $name = null) : ? FormContext
{
return $name ? $this->contextList[$name] : array_values($this->contextList)[0] ?? null;
}
#[Ignore]
public function isRoute(mixed $name, ServerRequestInterface $request) : bool
{
foreach((array) $name as $item) {

View File

@ -35,8 +35,6 @@ class Kernel {
public string $projectPath;
public string $routeDefinitionList = 'routes.list';
public function __construct(string $projectPath)
{
$this->projectPath = $projectPath;
@ -138,15 +136,14 @@ class Kernel {
protected function serviceContainer() : self
{
$this->container->has(AdapterProxy::class) and $this->container->get(AdapterProxy::class);
$this->container->has('ulmus.caching') and ( Ulmus::$cache = $this->container->get('ulmus.caching') );
$this->container->has(Lean::class) and $this->container->get(Lean::class);
if ($this->container->has(I18n::class)) {
$i18n = $this->container->get(I18n::class);
$i18n->locale($this->locale);
$i18n->initialize(!getenv("DEBUG"));
if ($this->container->has('lean.autoload')) {
foreach($this->container->get('lean.autoload') as $class) {
$this->container->get($class);
}
}
# Must be removed from KERNEL !
$this->container->has('ulmus.caching') and ( Ulmus::$cache = $this->container->get('ulmus.caching') );
return $this;
}
@ -155,8 +152,7 @@ class Kernel {
{
ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);
// Router
$routeFactory = $this->container->get($this->routeDefinitionList);
$routeFactory = $this->container->get(Routing\RouteDefinitionInterface::class);
$router = $routeFactory($this->container);

View File

@ -0,0 +1,5 @@
<?php
namespace Lean\Routing;
interface RouteDefinitionInterface {}

View File

@ -0,0 +1,53 @@
{% title "Documentation" %}
<!DOCTYPE html>
<html lang="fr">
{% section "head" %}
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{% section "head.title" %}{{ title() }}{% endsection %}</title>
<link rel="apple-touch-icon" sizes="180x180" href="{% asset 'static/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% asset 'static/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% asset 'static/favicon/favicon-16x16.png' %}">
<link rel="manifest" href="{% asset 'static/favicon/site.webmanifest' %}">
<link rel="mask-icon" href="{% asset 'static/favicon/safari-pinned-tab.svg' %}" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2d89ef">
<meta name="theme-color" content="#ffffff">
<style>
a{color:#e44a00}h3{background:#e1e1e1;padding:6px 12px}ul{background:#e9e9fd;padding-top:20px;padding-bottom:20px;border:1px solid #e3e3ec}ul li + li{margin-top:8px}li > em{font-size:0.75rem;color:gray}body{font-family:Helvetica, 'Helvetica Neuve', Arial, Tahoma, sans-serif;font-size:17px;color:#333}h1,h2,h3,h4,h5,h6{color:#222;margin:0 0 20px}dl,ol,p,pre,table,ul{margin:0 0 20px}h1,h2,h3{line-height:1.1}h1{font-size:20px;text-align:right;color:#387eea;font-weight:bold}h2{color:#393939}h3,h4,h5,h6{color:#494949}h3{display:flex}h3 > code{margin-right:5px;color:#b52dac}h3 > strong{margin-left:auto}a{color:#39c;font-weight:400;text-decoration:none}a small{font-size:11px;color:#777;margin-top:-0.6em;display:block}.wrapper{width:860px;margin:0 auto}blockquote{border-left:1px solid #e5e5e5;margin:0;padding:0 0 0 20px;font-style:italic}code,pre{font-size:12px}pre{padding:8px 15px;background:#f8f8f8;border-radius:5px;border:1px solid #e5e5e5;overflow-x:auto}table{width:100%;border-collapse:collapse}td,th{text-align:left;padding:5px 10px;border-bottom:1px solid #e5e5e5}dt{color:#444;font-weight:700}th{color:#444}img{max-width:100%}header{width:270px;float:left;position:fixed}header ul{list-style:none;height:40px;padding:0;background:#eee;background:-moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd));background:-webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);background:-o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);background:-ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%);background:linear-gradient(top, #f8f8f8 0%,#dddddd 100%);border-radius:5px;border:1px solid #d2d2d2;box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;width:270px}header li{width:89px;float:left;border-right:1px solid #d2d2d2;height:40px}header ul a{line-height:1;font-size:11px;color:#999;display:block;text-align:center;padding-top:6px;height:40px}strong{color:#222;font-weight:700}header ul li + li{width:88px;border-left:1px solid #fff}header ul li + li + li{border-right:none;width:89px}header ul a strong{font-size:14px;display:block;color:#222}section{width:500px;float:right;padding-bottom:50px}small{font-size:11px}hr{border:0;background:#e5e5e5;height:1px;margin:0 0 20px}footer{width:270px;float:left;position:fixed;bottom:50px}@media print, screen and (max-width: 960px){div.wrapper{width:auto;margin:0}footer,header,section{float:none;position:static;width:auto}header{padding-right:320px}section{border:1px solid #e5e5e5;border-width:1px 0;padding:20px 0;margin:0 0 20px}header a small{display:inline}header ul{position:absolute;right:50px;top:52px}}@media print, screen and (max-width: 720px){body{word-wrap:break-word}header{padding:0}header p.view,header ul{position:static}code,pre{word-wrap:normal}}@media print, screen and (max-width: 480px){body{padding:15px}header ul{display:none}}@media print{body{padding:0.4in;font-size:12pt;color:#444}}#wrapper{margin-left:auto;margin-right:auto;background-color:white}.ca-menu{list-style:none;padding:0;margin:20px auto}#navi{padding-top:15px;padding-right:15px;float:right;width:420px}#title{padding-left:15px;width:460px;float:left}div.clear{clear:both}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.2em}h5{font-size:1em;font-weight:bold}h6{font-size:1em;font-weight:bold}h1,h2,h3,h4,h5,h6{font-weight:normal;line-height:2.5rem;margin:1rem 0}.post p{max-width:580px}ol.list,ul.list{padding-left:3.333em;max-width:580px}.post h2{border-bottom:1px solid #EDEDED}h1:nth-child(1),h2:nth-child(1),h3:nth-child(1),h4:nth-child(1),h5:nth-child(1),h6:nth-child(1){margin-top:0}body{padding:1em}#wrapper{padding:1em}@media (min-width: 43.75em){body{padding:2em}#wrapper{padding:2em}}@media (min-width: 62em){body{padding:3em}#wrapper{max-width:740px;padding:3em}}
ol{background: #eff4f2;padding-top:20px;padding-bottom:20px;border:1px solid #e3e3ec}
h4{background:#e0f7ed;padding:6px 12px; font-weight: bold!important;font-size:100%;margin-top:0}
h5{text-decoration: underline}
li.odd-even{border-top:1px solid #ccc;margin:10px 0;padding:15px 15px 10px 5px}
li.odd-even:first-child{border:0}
input, button {padding:5px; font-size:1em;margin-top:10px}
ul ul {margin: 0;border: 0;padding: 5px 30px;}
</style>
</head>
{% endsection %}
{% section "body" %}
<body>
{% section "body.header" %}{% endsection %}
{% section "header" %}{% endsection %}
{% section "message" %}
{% view "lean/widget/message" %}
{% endsection %}
<main role="main" class="content">
{% section "main" %}
<div>{{= $markdown }}</div>
{% endsection %}
</main>
<footer>{% section "footer" %}{% endsection %}</footer>
{% section "body.footer" %}{% endsection %}
</body>
{% endsection %}
</html>