- Work done to clarify a bit every section, and a hugh code refractoring also was made to the code base.

This commit is contained in:
Dave Mc Nicoll 2024-11-11 19:54:22 +00:00
parent a98e46c343
commit 6b655bdc76
15 changed files with 260 additions and 145 deletions

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -15,93 +15,42 @@ class EntityDescriptor
{ {
use DescriptorTrait; use DescriptorTrait;
public string $entityTitle = <<<HTML
<div style="margin-left:15px">
<h4 class='entity-name'>%s</h4>
<div class='description'>%s</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>%s</ol>
<hr style="margin-top:15px;">
<h4 class='request-name'>%s</h4>
<h5>Requêtes</h5>
<div class='description' style="margin-bottom:10px">%s</div>
<ol class='requests'>%s</ol>
</div>
HTML;
public string $entityField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Champ SQL</u> : %s</div>
<div style="padding:5px"><u>Attribut</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> %s</div>
<div style="padding:5px"><u>Taille</u> : %s</div>
<div style="padding:5px"><u>Lecture seule</u> : %s</div>
</div>
</li>
HTML;
public string $searchRequestField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Paramètre de requête (GET)</u> : %s</div>
<div style="padding:5px"><u>Attribut</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> : %s</div>
<div style="padding:5px"><u>Valeur par défault</u> : %s</div>
</div>
</li>
HTML;
public function __construct() {} public function __construct() {}
/** public function getEntities(...$entities) : array
* @return Returns HTML describing entity fields
*/
public function describe(...$entities) : string
{ {
$html = ""; $list = [];
foreach($entities as $entity) { foreach($entities as $entity) {
$propertyHtml = $searchRequestHtml = ""; $fields = [];
$entityName = is_object($entity) ? $entity::class : $entity; $entityName = is_object($entity) ? $entity::class : $entity;
$reflector = new ObjectReflection($entityName); $reflector = new ObjectReflection($entityName);
$table = $reflector->reflectClass()->getAttribute(Table::class); $table = $reflector->reflectClass()->getAttribute(Table::class);
foreach($reflector->reflectProperties() as $property) { foreach ($reflector->reflectProperties() as $property) {
$entityField = $property->getAttribute(EntityField::class); $entityField = $property->getAttribute(EntityField::class);
$field = $property->getAttribute(Field::class); $field = $property->getAttribute(Field::class);
if ($field) { if ($field) {
$types = $property->getTypes(); $types = $property->getTypes();
$propertyHtml .= sprintf(
$this->entityField, $fields[] = [
$property->name, 'name' => $property->name,
$entityField->object->description ?? "", 'description' => $entityField->object->description ?? "",
$field->object->name ?: $property->name, 'fieldName' => $field->object->name ?: $property->name,
$field->tag, 'tag' => $field->tag,
$field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)), 'type' => $field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)),
$this->yesOrNo($property->allowsNull()), 'allowNulls' => $property->allowsNull(),
$this->length($field->object->length ?? null), 'length' => $field->object->length ?? null,
$this->yesOrNo($field->object->readonly), 'readonly' => $field->object->readonly,
); ];
} }
} }
$searchFields = [];
$searchRequestReflector = new ObjectReflection($entityName::searchRequest()::class); $searchRequestReflector = new ObjectReflection($entityName::searchRequest()::class);
$searchRequestParameter = $searchRequestReflector->reflectClass()->getAttribute(SearchRequestParameter::class); $searchRequestParameter = $searchRequestReflector->reflectClass()->getAttribute(SearchRequestParameter::class);
foreach($searchRequestReflector->reflectProperties() as $property) { foreach($searchRequestReflector->reflectProperties() as $property) {
@ -110,23 +59,27 @@ class EntityDescriptor
if ($field) { if ($field) {
$types = $property->getTypes(); $types = $property->getTypes();
$searchRequestHtml .= sprintf( $searchFields[] = [
$this->searchRequestField, 'name' => $property->name,
$property->name, 'description' => $field->object->description,
$field->object->description, 'parameter' => $field->object->parameter ? implode(', ', $field->object->getParameters()) : $property->name,
$field->object->parameter ? implode(', ', $field->object->getParameters()) : $property->name, 'tag' => $field->tag,
$field->tag, 'type' => implode(' | ', array_map(fn($e) => $e->type, $types)),
implode(' | ', array_map(fn($e) => $e->type, $types)), 'allowNulls' => $property->allowsNull(),
$this->yesOrNo($property->allowsNull()), 'default' => $this->displayValue($property->value ?? "")
$this->displayValue($property->value ?? "<i>aucune</i>") ];
);
} }
} }
$html .= sprintf($this->entityTitle, $reflector->reflectClass()->getClassName(), $table->object->description ?? null, $propertyHtml, $reflector->reflectClass()->getClassName() . "::searchRequest()", $searchRequestParameter->object->description ?? "", $searchRequestHtml); $list[$entity] = [
'className' => $reflector->reflectClass()->getClassName(),
'description' => $table->object->description ?? null,
'fields' => $fields,
'searchRequestDescription' => $searchRequestParameter->object->description ?? "",
'searchRequestFields' => $searchFields,
];
} }
return $html; return $list;
} }
} }

View File

@ -9,46 +9,15 @@ class FormDescriptor
{ {
use DescriptorTrait; use DescriptorTrait;
public string $formTitle = <<<HTML public function getForms(...$forms) : array
<div style="margin-left:15px">
<h4 style="background:#e0ecf7" class='form-name'>%s</h4>
<div class='description'>%s</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>%s</ol>
</div>
HTML;
public string $formField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Variable POST / champ JSON</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> : %s</div>
<div style="padding:5px"><u>Pattern regex</u> : %s</div>
<div style="padding:5px"><u>Taille min.</u> : %s</div>
<div style="padding:5px"><u>Taille max.</u> : %s</div>
<div style="padding:5px"><u>Exemple</u> : %s</div>
</div>
</li>
HTML;
/**
* @return Returns HTML describing given controller's routes
*/
public function describe(...$forms) : string
{ {
$html = ""; $list = [];
foreach($forms as $form) { foreach($forms as $form) {
$fields = [];
$propertyHtml = ""; $propertyHtml = "";
$entityName = is_object($form) ? $form::class : $form; $formName = is_object($form) ? $form::class : $form;
$reflector = new ObjectReflection($entityName); $reflector = new ObjectReflection($formName);
$context = $reflector->reflectClass()->getAttribute(ContextField::class); $context = $reflector->reflectClass()->getAttribute(ContextField::class);
@ -57,24 +26,26 @@ class FormDescriptor
if ($field) { if ($field) {
$types = $property->getTypes(); $types = $property->getTypes();
$propertyHtml .= sprintf( $fields[] = [
$this->formField, 'name' => $property->name,
$property->name, 'description' => $field->object->description,
$field->object->description, 'type' => $field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)),
$property->name, 'allowNulls' => $property->allowsNull(),
$field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)), 'regexPattern' =>$field->object->regexFormat ?? "aucun",
$this->yesOrNo($property->allowsNull()), 'minLength' =>$field->object->minLength ?? "aucune",
$field->object->regexFormat ?? "aucun", 'maxLength' =>$field->object->maxLength ?? "aucune",
$field->object->minLength ?? "aucune", 'example' =>$field->object->example,
$field->object->maxLength ?? "aucune", ];
$field->object->example
);
} }
} }
$html .= sprintf($this->formTitle, $reflector->reflectClass()->getClassName(), $context->object->description ?? null, $propertyHtml, ); $list[$formName] = [
'className' => $reflector->reflectClass()->getClassName(),
'description' => $context->object->description ?? null,
'fields' => $fields,
];
} }
return $html; return $list;
} }
} }

View File

@ -8,6 +8,8 @@ use Psr\Http\Message\ResponseInterface;
trait LeanApiTrait trait LeanApiTrait
{ {
use DescriptorTrait;
#[Ignore] #[Ignore]
public function renderMarkdown(string $filepath, array $entities = [], array $forms = [], int $code = 200, array $headers = []) : ResponseInterface public function renderMarkdown(string $filepath, array $entities = [], array $forms = [], int $code = 200, array $headers = []) : ResponseInterface
{ {
@ -19,21 +21,23 @@ trait LeanApiTrait
if (str_contains($markdown, '{route:descriptor}')) if (str_contains($markdown, '{route:descriptor}'))
{ {
$describe = (new RouteDescriptor($this, $this->picea->compiler->getExtensionFromToken('url')))->describe(); $markdown = str_replace('{route:descriptor}', $this->renderRawView('lean-api/route_descriptor', [
$markdown = str_replace('{route:descriptor}', $describe, $markdown); 'routes' => (new RouteDescriptor($this, $this->picea->compiler->getExtensionFromToken('url')))->getRoutes()
]), $markdown);
} }
if (str_contains($markdown, '{entity:descriptor}')) if (str_contains($markdown, '{entity:descriptor}'))
{ {
$describe = (new EntityDescriptor())->describe(... $entities); $markdown = str_replace('{entity:descriptor}', $this->renderRawView('lean-api/entity_descriptor', [
$markdown = str_replace('{entity:descriptor}', $describe, $markdown); 'entities' => (new EntityDescriptor())->getEntities(... $entities)
]), $markdown);
} }
if (str_contains($markdown, '{form:descriptor}')) if (str_contains($markdown, '{form:descriptor}'))
{ {
$describe = (new FormDescriptor())->describe(... $forms); $markdown = str_replace('{form:descriptor}', $this->renderRawView('lean-api/form_descriptor', [
$markdown = str_replace('{form:descriptor}', $describe, $markdown); 'forms' => (new FormDescriptor())->getForms(... $forms)
]), $markdown);
} }
return $this->renderView("lean/layout/docs", get_defined_vars()); return $this->renderView("lean/layout/docs", get_defined_vars());

View File

@ -22,12 +22,9 @@ HTML;
protected UrlExtension $urlExtension, protected UrlExtension $urlExtension,
) {} ) {}
/** public function getRoutes() : array
* @return Returns HTML describing given controller's routes
*/
public function describe() : string
{ {
$html = ""; $routes = [];
$reflector = new ObjectReflection($this->controller::class); $reflector = new ObjectReflection($this->controller::class);
@ -42,11 +39,18 @@ HTML;
$cleaned = $this->cleanRouteFromRegex($base.$path); $cleaned = $this->cleanRouteFromRegex($base.$path);
$url = $this->urlExtension->buildUrl($cleaned); $url = $this->urlExtension->buildUrl($cleaned);
$html .= sprintf($this->routeLine, $url, $base.$path, $cleaned, $route->description, implode(', ', (array)$route->method), $route->name ); $routes[] = [
'name' => $route->name,
'route' => $url,
'path' => $base.$path,
'cleaned' => $cleaned,
'description'=> $route->description,
'methods' =>implode(', ', (array)$route->method),
];
} }
} }
return sprintf('<ul>%s</ul>', $html); return $routes;
} }
protected function cleanRouteFromRegex(string $route) : string protected function cleanRouteFromRegex(string $route) : string

View File

@ -0,0 +1,76 @@
{% function yesOrNo(bool $toggle) : void %}
<span style="color: {{ $toggle ? 'green' : '#ac1b1b' }}">{{ $toggle ? 'oui' : 'non' }}</span>
{% endfunction %}
{% function length(int|null $length): void %}
<span style="color:{{ $length ? 'black' : 'gray' }}">{{ $length ?? "non-défini" }}</span>
{% endfunction %}
<div class="entities">
{% foreach $entities as $name => $entity %}
<div class="entity-wrapper" style="padding-left: 15px;border-left: 3px solid #eaa1af;">
<h4 class='entity-name'>{{ $entity['className'] }}</h4>
<div class='description'>{{ $entity['description'] }}</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>
{% foreach $entity['fields'] as $field %}
<li class="odd-even">
<div class="title">
<div><strong style="font-family:monospace">${{ $field['name'] }}</strong> <span style='margin-left:15px'>{{ $field['description'] }}</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Champ SQL</u> : {{ $field['fieldName'] }}</div>
<div style="padding:5px"><u>Attribut</u> : {{ $field['tag'] }}</div>
<div style="padding:5px"><u>Type(s)</u> : {{ $field['type'] }}</div>
<div style="padding:5px"><u>Nullable</u> {{ yesOrNo($field['allowNulls']) }}</div>
<div style="padding:5px"><u>Taille</u> : {{ length($field['length']) }}</div>
<div style="padding:5px"><u>Lecture seule</u> : {{ yesOrNo($field['readonly']) }}</div>
</div>
</li>
{% endforeach %}
</ol>
</div>
<div class="search-request-wrapper">
<h4 class='search-request-name'>{{ $entity['className'] }}::searchRequest()</h4>
<h5>Requêtes</h5>
<div class='description' style="margin-bottom:10px">{{ $entity['searchRequestDescription'] }}</div>
<ol class='requests'>
{% foreach $entity['searchRequestFields'] as $field %}
<li class="odd-even">
<div class="title">
<div><strong style="font-family:monospace">{{ $field['name'] }}</strong> <span style='margin-left:15px'>{{ $field['description'] }}</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div><u>Paramètre de requête (GET)</u> : {{ $field['parameter'] }}</div>
<div><u>Attribut</u> : {{ $field['tag'] }}</div>
<div><u>Type(s)</u> : {{ $field['type'] }}</div>
<div><u>Nullable</u> : {{ yesOrNo($field['allowNulls']) }}</div>
{% if $field['default'] !== "" %}
<div><u>Valeur par défault</u> : {{ $this->displayValue($field['default']) }}</div>
{% endif %}
</div>
</li>
{% endforeach %}
</ol>
</div>
{% endforeach %}
</div>
<style>
.entity-name {background:#eaa1af}
.entity-wrapper ol {background: #f0ddcd;}
.entity-wrapper li {border-color: #ae8585;}
.search-request-name {background: #b6e6ae}
.search-request-wrapper {padding-left: 15px;border-left: 3px solid #72886a;}
.search-request-wrapper ol {background: #d8f0cd;}
.search-request-wrapper li {border-color: #72886a;}
</style>

View File

@ -0,0 +1,38 @@
{% function yesOrNo(bool $toggle) : void %}
<span style="color: {{ $toggle ? 'green' : '#ac1b1b' }}">{{ $toggle ? 'oui' : 'non' }}</span>
{% endfunction %}
<div class="forms">
{% foreach $forms as $form %}
<div class="single-form" style="padding-left:15px;border-left: 3px solid #9ce6bc;">
<h4 style="background:#9ce6bc" class='form-name'>{{ $form['className'] }}</h4>
<div class='description'>{{ $form['description'] }}</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>
{% foreach $form['fields'] as $field %}
<li class="odd-even">
<div class="title">
<div>
<strong style="font-family:monospace">{{ $field['name'] }}</strong>
<span style='margin-left:15px'>{{ $field['description'] }}</span>
</div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div><u>Variable POST / champ JSON</u> : {{ $field['name'] }}</div>
<div><u>Type(s)</u> : {{ $field['type'] }}</div>
<div><u>Nullable</u> : {{ yesOrNo($field['allowNulls']) }}</div>
<div><u>Pattern regex</u> : {{ $field['regexPattern'] }}</div>
<div><u>Taille min.</u> : {{ $field['minLength'] }}</div>
<div><u>Taille max.</u> : {{ $field['maxLength'] }}</div>
<div><u>Exemple</u> : {{ $field['example'] }}</div>
</div>
</li>
{% endforeach %}
</ol>
</div>
{% endforeach %}
</div>

View File

@ -0,0 +1,9 @@
<ul>
{% foreach $routes as $route %}
<li>
<span><a href="{{ $route['route'] }}" title="{{ $route['path'] }}" style='font-family:monospace;font-size:.85em'>{{ $route['cleaned'] }}</a> - {{ $route['description'] }}</span>
<span style='color:#ac1b1b'>{{ $route['methods'] }}</span>
<small style="color:#374300">{{ $route['name'] }}</small>
</li>
{% endforeach %}
</ul>

View File

@ -0,0 +1,60 @@
{% 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" %}{% lang 'api.application_name' %} - {{ title() }}{% endsection %}</title>
<link rel="apple-touch-icon" sizes="180x180" href="{% asset 'static/lean-api/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="96x96" href="{% asset 'static/lean-api/favicon/favicon-96x96.png' %}">
<link rel="manifest" href="{% asset 'static/lean-api/favicon/site.webmanifest' %}">
<link rel="mask-icon" href="{% asset 'static/lean-api/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}
h3 {display: flex;align-items: center;height: 60px;padding: 0 15px 0 15px;font-variant: small-caps;}
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 {background:#f4f4f4; list-style: none; padding-left:20px}
ul ul {margin: 0;border: 0;padding: 5px 30px;}
ul li + li {margin-top: 12px;}
ol .title { display: flex;justify-content: space-between;background: #ffffffb2;padding: 9px 5px;border: 1px solid #fff;}
.field-desc > div {padding:5px;}
.forms ol {background: #ccf2dd;}
.forms li {border-color: #85ae97;}
</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>