Remove legacy files
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
/node_modules
|
node_modules
|
||||||
/var
|
dist
|
||||||
/vendor
|
.astro
|
||||||
|
.git
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ root = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.{html,twig,yml,yaml,xml}]
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[package.json]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[Makefile]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
max_line_length = 80
|
max_line_length = 80
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,10 +7,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
.astro/
|
||||||
|
|
||||||
# Legacy (удалить после завершения миграции)
|
|
||||||
output_*
|
|
||||||
var/
|
|
||||||
vendor/
|
|
||||||
.php_cs.cache
|
|
||||||
.php-cs-fixer.cache
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/* vim: set ft=php: */
|
|
||||||
|
|
||||||
$finder = PhpCsFixer\Finder::create()
|
|
||||||
->in(__DIR__.'/app')
|
|
||||||
->in(__DIR__.'/bundle')
|
|
||||||
;
|
|
||||||
|
|
||||||
return (new PhpCsFixer\Config())
|
|
||||||
->setFinder($finder)
|
|
||||||
->setRules([
|
|
||||||
'@Symfony' => true,
|
|
||||||
'@PHP73Migration' => true,
|
|
||||||
'array_syntax' => ['syntax' => 'short'],
|
|
||||||
'no_useless_return' => true,
|
|
||||||
'ordered_imports' => true,
|
|
||||||
'phpdoc_order' => true,
|
|
||||||
'semicolon_after_instruction' => false,
|
|
||||||
'yoda_style' => false,
|
|
||||||
])
|
|
||||||
;
|
|
||||||
13
README.md
13
README.md
@@ -1,10 +1,17 @@
|
|||||||
Source for [vakhrushev.me](https://vakhrushev.me).
|
Source for [vakhrushev.me](https://vakhrushev.me).
|
||||||
|
|
||||||
Build:
|
Built with [Astro](https://astro.build/), Tailwind CSS, Vue 3.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
task build-docker
|
task build-docker
|
||||||
task build
|
task install
|
||||||
|
|
||||||
Deploy:
|
## Development
|
||||||
|
|
||||||
|
task dev
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
task build-prod
|
||||||
task deploy
|
task deploy
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Homepage\HtmlPrettierBundle\HtmlPrettierBundle;
|
|
||||||
use Homepage\SiteMapBundle\SiteMapBundle;
|
|
||||||
use Homepage\TwigExtensionBundle\TwigExtensionBundle;
|
|
||||||
use Sculpin\Bundle\SculpinBundle\HttpKernel\AbstractKernel;
|
|
||||||
|
|
||||||
class SculpinKernel extends AbstractKernel
|
|
||||||
{
|
|
||||||
protected function getAdditionalSculpinBundles(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
TwigExtensionBundle::class,
|
|
||||||
SiteMapBundle::class,
|
|
||||||
HtmlPrettierBundle::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
sculpin:
|
|
||||||
ignore:
|
|
||||||
- _assets/
|
|
||||||
|
|
||||||
sculpin_content_types:
|
|
||||||
posts:
|
|
||||||
enabled: false
|
|
||||||
articles:
|
|
||||||
type: path
|
|
||||||
path: _articles
|
|
||||||
singular_name: article
|
|
||||||
layout: article
|
|
||||||
permalink: articles/:year-:month-:day-:basename/
|
|
||||||
publish_drafts: false
|
|
||||||
enabled: true
|
|
||||||
taxonomies:
|
|
||||||
- tags
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: Антон Вахрушев
|
|
||||||
author: Антон Вахрушев
|
|
||||||
email: anton@vakhrushev.me
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\HtmlPrettierBundle\DependencyInjection;
|
|
||||||
|
|
||||||
use Symfony\Component\Config\FileLocator;
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
||||||
use Symfony\Component\DependencyInjection\Loader;
|
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
|
||||||
|
|
||||||
class HtmlPrettierExtension extends Extension
|
|
||||||
{
|
|
||||||
public function load(array $configs, ContainerBuilder $container)
|
|
||||||
{
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
|
||||||
$loader->load('services.yml');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\HtmlPrettierBundle;
|
|
||||||
|
|
||||||
use Sculpin\Core\Event\SourceSetEvent;
|
|
||||||
use Sculpin\Core\Sculpin;
|
|
||||||
use Sculpin\Core\Source\SourceInterface;
|
|
||||||
use Sculpin\Core\Source\SourceSet;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
|
|
||||||
class HtmlPrettier implements EventSubscriberInterface
|
|
||||||
{
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Sculpin::EVENT_AFTER_FORMAT => ['formatHtml', -100],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function formatHtml(SourceSetEvent $event): void
|
|
||||||
{
|
|
||||||
$config = [
|
|
||||||
'output-html' => true,
|
|
||||||
'drop-empty-elements' => false,
|
|
||||||
'indent' => true,
|
|
||||||
'wrap' => 120,
|
|
||||||
];
|
|
||||||
|
|
||||||
$sources = $this->filterSource($event->sourceSet());
|
|
||||||
|
|
||||||
/** @var SourceInterface $source */
|
|
||||||
foreach ($sources as $source) {
|
|
||||||
$html = $source->formattedContent();
|
|
||||||
$tidy = new \tidy();
|
|
||||||
$tidy->parseString($html, $config, 'utf8');
|
|
||||||
$tidy->cleanRepair();
|
|
||||||
$source->setFormattedContent((string) $tidy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function filterSource(SourceSet $sourceSet): \Generator
|
|
||||||
{
|
|
||||||
/** @var SourceInterface $source */
|
|
||||||
foreach ($sourceSet->allSources() as $source) {
|
|
||||||
$filename = $source->filename();
|
|
||||||
|
|
||||||
$isSuitable = $this->endsWith($filename, '.md')
|
|
||||||
|| $this->endsWith($filename, '.html.twig')
|
|
||||||
;
|
|
||||||
|
|
||||||
if ($isSuitable) {
|
|
||||||
yield $source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function endsWith($haystack, $needle): bool
|
|
||||||
{
|
|
||||||
$length = \strlen($needle);
|
|
||||||
|
|
||||||
return $length === 0 || (substr($haystack, -$length) === $needle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\HtmlPrettierBundle;
|
|
||||||
|
|
||||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
|
||||||
|
|
||||||
class HtmlPrettierBundle extends Bundle
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
services:
|
|
||||||
homepage.html_prettier:
|
|
||||||
class: \Homepage\HtmlPrettierBundle\HtmlPrettier
|
|
||||||
tags:
|
|
||||||
- { name: kernel.event_subscriber }
|
|
||||||
public: yes
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\SiteMapBundle\DependencyInjection;
|
|
||||||
|
|
||||||
use Symfony\Component\Config\FileLocator;
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
||||||
use Symfony\Component\DependencyInjection\Loader;
|
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
|
||||||
|
|
||||||
class SiteMapExtension extends Extension
|
|
||||||
{
|
|
||||||
public function load(array $configs, ContainerBuilder $container)
|
|
||||||
{
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
|
||||||
$loader->load('services.yml');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
services:
|
|
||||||
homepage.site_map_generator:
|
|
||||||
class: \Homepage\SiteMapBundle\SiteMapGenerator
|
|
||||||
tags:
|
|
||||||
- { name: kernel.event_subscriber }
|
|
||||||
- { name: sculpin.data_provider, alias: sitemap }
|
|
||||||
public: yes
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\SiteMapBundle;
|
|
||||||
|
|
||||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
|
||||||
|
|
||||||
class SiteMapBundle extends Bundle
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\SiteMapBundle;
|
|
||||||
|
|
||||||
use Sculpin\Core\DataProvider\DataProviderInterface;
|
|
||||||
use Sculpin\Core\Event\SourceSetEvent;
|
|
||||||
use Sculpin\Core\Sculpin;
|
|
||||||
use Sculpin\Core\Source\SourceInterface;
|
|
||||||
use Sculpin\Core\Source\SourceSet;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
|
|
||||||
class SiteMapGenerator implements DataProviderInterface, EventSubscriberInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var array|null
|
|
||||||
*/
|
|
||||||
private $siteMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var SourceSet
|
|
||||||
*/
|
|
||||||
private $sources;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Sculpin::EVENT_BEFORE_RUN => 'saveSourceSet',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide data.
|
|
||||||
*/
|
|
||||||
public function provideData(): array
|
|
||||||
{
|
|
||||||
$this->buildSiteMap();
|
|
||||||
|
|
||||||
return $this->siteMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Before run.
|
|
||||||
*
|
|
||||||
* @param SourceSetEvent $sourceSetEvent Source Set Event
|
|
||||||
*/
|
|
||||||
public function saveSourceSet(SourceSetEvent $sourceSetEvent)
|
|
||||||
{
|
|
||||||
$this->sources = $sourceSetEvent->sourceSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function buildSiteMap(): array
|
|
||||||
{
|
|
||||||
if ($this->siteMap !== null) {
|
|
||||||
return $this->siteMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->siteMap = $this->createSiteMap();
|
|
||||||
|
|
||||||
return $this->siteMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createSiteMap(): array
|
|
||||||
{
|
|
||||||
$siteMap = [];
|
|
||||||
|
|
||||||
/** @var \Sculpin\Core\Source\FileSource $source */
|
|
||||||
foreach ($this->sources->allSources() as $source) {
|
|
||||||
$url = $this->createSiteUrlFromSource($source);
|
|
||||||
if (!$url) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$siteMap[$url['loc']] = $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $siteMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createSiteUrlFromSource(SourceInterface $source): array
|
|
||||||
{
|
|
||||||
$data = $source->data()->export();
|
|
||||||
|
|
||||||
if (empty($data) || $source->useFileReference()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('draft', $data) && $data['draft']) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$siteMapData = $data['sitemap'] ?? [];
|
|
||||||
|
|
||||||
if (array_key_exists('_exclude', $siteMapData)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$loc = $data['canonical'] ?? $data['url'];
|
|
||||||
|
|
||||||
if (is_callable([$source, 'file'])) {
|
|
||||||
$lastmod = date(DATE_W3C, $source->file()->getMTime());
|
|
||||||
} else {
|
|
||||||
$lastmod = date(DATE_W3C);
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = [
|
|
||||||
'loc' => $loc,
|
|
||||||
'lastmod' => $lastmod,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($data['sitemap'])) {
|
|
||||||
$url = array_merge($url, $data['sitemap']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\TwigExtensionBundle\DependencyInjection;
|
|
||||||
|
|
||||||
use Symfony\Component\Config\FileLocator;
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
||||||
use Symfony\Component\DependencyInjection\Loader;
|
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
|
||||||
|
|
||||||
class TwigExtensionExtension extends Extension
|
|
||||||
{
|
|
||||||
public function load(array $configs, ContainerBuilder $container)
|
|
||||||
{
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
|
||||||
$loader->load('services.yml');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
services:
|
|
||||||
homepage.twig_extension:
|
|
||||||
class: \Homepage\TwigExtensionBundle\TwigExtension
|
|
||||||
arguments:
|
|
||||||
- '%sculpin.output_dir%'
|
|
||||||
public: yes
|
|
||||||
tags:
|
|
||||||
- { name: twig.extension }
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\TwigExtensionBundle;
|
|
||||||
|
|
||||||
use Twig\Extension\AbstractExtension;
|
|
||||||
use Twig\TwigFunction;
|
|
||||||
|
|
||||||
class TwigExtension extends AbstractExtension
|
|
||||||
{
|
|
||||||
private $outputDir;
|
|
||||||
|
|
||||||
public function __construct(string $outputDir)
|
|
||||||
{
|
|
||||||
$this->outputDir = $outputDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFunctions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
new TwigFunction('hashed_asset', [$this, 'createHashedFileLink']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createHashedFileLink(string $path): string
|
|
||||||
{
|
|
||||||
$fullPath = $this->join($this->outputDir, $path);
|
|
||||||
$realPath = realpath($fullPath);
|
|
||||||
|
|
||||||
if (!file_exists($realPath)) {
|
|
||||||
return sprintf('%s?v=%s', $path, time());
|
|
||||||
}
|
|
||||||
|
|
||||||
$hash = md5_file($realPath);
|
|
||||||
|
|
||||||
return sprintf('%s?v=%s', $path, $hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function join(string $base, string $path): string
|
|
||||||
{
|
|
||||||
return $path ? rtrim($base, '/').'/'.ltrim($path, '/') : $base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Homepage\TwigExtensionBundle;
|
|
||||||
|
|
||||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
|
||||||
|
|
||||||
class TwigExtensionBundle extends Bundle
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "anwinged/homepage",
|
|
||||||
"description": "Anton Vakhrushev homepage",
|
|
||||||
"type": "project",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Anton Vakhrushev",
|
|
||||||
"email": "anwinged@ya.ru"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"require": {
|
|
||||||
"php": "^8.0",
|
|
||||||
"ext-tidy": "*"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"sculpin/sculpin": "^3.0"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Homepage\\": "bundle/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"allow-plugins": {
|
|
||||||
"sculpin/sculpin-theme-composer-plugin": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3522
composer.lock
generated
3522
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
|||||||
FROM php:8.1-cli
|
|
||||||
|
|
||||||
ENV COMPOSER_FUND=0
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
gnupg \
|
|
||||||
gzip \
|
|
||||||
libtidy-dev \
|
|
||||||
rsync \
|
|
||||||
zip \
|
|
||||||
;
|
|
||||||
|
|
||||||
RUN docker-php-ext-install tidy \
|
|
||||||
&& docker-php-ext-enable tidy
|
|
||||||
|
|
||||||
# Composer and required tools
|
|
||||||
RUN curl -sLO https://getcomposer.org/download/2.8.4/composer.phar \
|
|
||||||
&& mv composer.phar /usr/local/bin/composer \
|
|
||||||
&& chmod +x /usr/local/bin/composer
|
|
||||||
|
|
||||||
|
|
||||||
# PHP-CS-Fixer
|
|
||||||
RUN curl -sLO https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v3.65.0/php-cs-fixer.phar \
|
|
||||||
&& mv php-cs-fixer.phar /usr/local/bin/php-cs-fixer \
|
|
||||||
&& chmod +x /usr/local/bin/php-cs-fixer
|
|
||||||
|
|
||||||
WORKDIR /srv/app
|
|
||||||
1845
package-lock.json
generated
1845
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
layout: internal
|
|
||||||
title: Страница не найдена
|
|
||||||
description: Станица не найдена
|
|
||||||
styles:
|
|
||||||
- /static/index.css
|
|
||||||
---
|
|
||||||
|
|
||||||
# 404
|
|
||||||
|
|
||||||
Ой, страница не найдена.
|
|
||||||
|
|
||||||
Давайте посмотрим, что интересное есть на [главной](/).
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
---
|
|
||||||
title: Гадалка Шеннона
|
|
||||||
description: Демо-версия электронной гадалки Шеннона
|
|
||||||
keywords: [гадалка, угадыватель, шеннон, чет-нечет]
|
|
||||||
styles:
|
|
||||||
- /static/predictor.css
|
|
||||||
scripts:
|
|
||||||
- /static/predictor.js
|
|
||||||
---
|
|
||||||
|
|
||||||
В студенческое время я наткнулся на интересную статью об [игре "Чет-нечет"][game]
|
|
||||||
на домашней страничке пользователя [ltwood][ltwood].
|
|
||||||
|
|
||||||
Правила очень простые. Игрок загадывает один вариант из двух: "чет" или "нечет",
|
|
||||||
а оппонент пытается угадать выбор игрока. Если угадать не удалось, то очко получает
|
|
||||||
загадавший, а если угадать получилось - то угадывающий. Кто первым наберет 20 очков,
|
|
||||||
тот и молодец!
|
|
||||||
|
|
||||||
Кажется, что в этой игре все случайно. Случайно загадывается число, потом случайно
|
|
||||||
второй игрок пытается угадать что же было загадано. Я очень сильно удивился, когда
|
|
||||||
попробовал поиграть в эту игру с программой и за десять попыток так ни разу и не выиграл.
|
|
||||||
|
|
||||||
Парадокс в том, что мы _думаем_ что загадываем числа случайно. На самом деле все не так,
|
|
||||||
и последовательность загаданных чисел не случайна.
|
|
||||||
|
|
||||||
Исходного кода оригинальной гадалки в открытом доступе нет, есть только [описание алгоритма][algo],
|
|
||||||
по которому я сделал свою реализацию на TypeScrypt.
|
|
||||||
|
|
||||||
## Демоверсия
|
|
||||||
|
|
||||||
Попробуйте набрать 50 очков и выиграть. Чтобы выбирать вариант с клавиатуры,
|
|
||||||
кликните внутри серой рамки, а потом пользуйтесь клавишами "1" - нечет или "2" - чет.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div id="app"></div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Как Это работает
|
|
||||||
|
|
||||||
Математически алгоритм на [странице][algo] сайта ltwood.
|
|
||||||
Я рассмотрю простой пример, чтобы показать принцип.
|
|
||||||
|
|
||||||
В основе алгоритма находится популяция "демонов" - автоматов, которые на основании ходов
|
|
||||||
игрока и предсказанных значениях выдают новое предсказание. Демонами управляет
|
|
||||||
супервайзер. Задача супервайзера в том, чтобы опросить всех демонов, выбрать ответ
|
|
||||||
от одного их них, а после получения ответа игрока пометить тех, кто выдал правильный ответ.
|
|
||||||
|
|
||||||
Алгоритм состоит из двух шагов:
|
|
||||||
|
|
||||||
- предсказать следующих ход игрока;
|
|
||||||
- учесть реальный ход игрока, добавив веса тому демону, который предугадал ход.
|
|
||||||
|
|
||||||
Рассмотрим работу на примере одного демона.
|
|
||||||
|
|
||||||
Пусть у нас есть демон, который смотрит на последний хода игрока
|
|
||||||
и на свое последнее предсказание.
|
|
||||||
|
|
||||||
Строим два вектора:
|
|
||||||
|
|
||||||
- `[<1 ход демона>, <1 ход игрока>, 0]`
|
|
||||||
- `[<1 ход демона>, <1 ход игрока>, 1]`
|
|
||||||
|
|
||||||
В самом начале, когда у демона нет никакой информации о ходах игрока, эти векторы
|
|
||||||
будут выглядеть как `[0]` и `[1]`. Но с накоплением данных, они всегда будут каждый
|
|
||||||
по 5 элементов.
|
|
||||||
|
|
||||||
После чего смотрим, который из таких наборов в прошлом приносил победу чаще,
|
|
||||||
и соответственно выбираем или вариант с 0, или с 1.
|
|
||||||
|
|
||||||
После получения действительного хода игрока, мы увеличиваем вес того набора,
|
|
||||||
который оказался верным. И далее снова предсказываем ход.
|
|
||||||
|
|
||||||
Теперь с числами.
|
|
||||||
|
|
||||||
#### Ход 1
|
|
||||||
|
|
||||||
У демона нет информации, наборы `[0]` и `[1]` равнозначны, выбираем `[0]`,
|
|
||||||
а значит предсказываем ход игрока 0.
|
|
||||||
|
|
||||||
Игрок загадывал 1. Обновляем веса:
|
|
||||||
|
|
||||||
```
|
|
||||||
[0] = 0
|
|
||||||
[1] = 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Ход 2
|
|
||||||
|
|
||||||
Строим векторы на основе последних ходов:
|
|
||||||
|
|
||||||
```
|
|
||||||
0: [0, 1, 0]
|
|
||||||
1: [0, 1, 1]
|
|
||||||
```
|
|
||||||
|
|
||||||
Для этих векторов тоже еще нет весов, так что снова выбираем первый, предсказываем 0.
|
|
||||||
|
|
||||||
Игрок снова выбрал 1. Обновляем веса (помним, что еще были прошлые вектора из одного элемента):
|
|
||||||
|
|
||||||
```
|
|
||||||
[0] = 0
|
|
||||||
[1] = 1
|
|
||||||
[0, 1, 0] = 0
|
|
||||||
[0, 1, 1] = 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Ход 3
|
|
||||||
|
|
||||||
Картина такая же, как на втором ходу, но отличие в том, что у нас есть веса с прошлого хода:
|
|
||||||
|
|
||||||
```
|
|
||||||
0: [0, 1, 0] - 0
|
|
||||||
1: [0, 1, 1] - 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Выбираем вариант 1, игрок снова выбирает 1. Предсказание удалось!
|
|
||||||
|
|
||||||
## Расширение алгоритма
|
|
||||||
|
|
||||||
Это был самый элементарный вариант. Понятно, что на таком далеко не уедешь,
|
|
||||||
и никого не обыграешь. Чтобы хорошо предугадывать ходы игроков, используется
|
|
||||||
несколько демонов с разной величиной просматриваемой истории. Следит за ними
|
|
||||||
"супервайзер", который ведет для каждого демона рейтинг. На основе этого рейтинга
|
|
||||||
выбираются ответы тех демонов, которые были наиболее успешны в своих предсказаниях.
|
|
||||||
|
|
||||||
## Ссылки
|
|
||||||
|
|
||||||
- [Код гадалки][repo]
|
|
||||||
- [Описание алгоритма][algo]
|
|
||||||
- [Описание игры у ltwood][game]
|
|
||||||
|
|
||||||
[ltwood]: https://sites.google.com/site/ltwood/
|
|
||||||
[game]: https://sites.google.com/site/ltwood/projects/heshby
|
|
||||||
[algo]: https://sites.google.com/site/ltwood/projects/heshby/algorithm
|
|
||||||
[repo]: https://github.com/anwinged/predictor
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
title: Сериализация в PHP
|
|
||||||
description: Проблема долговременного хранения сериализованных данных
|
|
||||||
keywords: [php, serialization, сериализация, пхп]
|
|
||||||
---
|
|
||||||
|
|
||||||
В PHP есть две функции для сериализации и десериализации данных: `serialize()` и
|
|
||||||
`unserialize()`. Функции встроены в язык, не требуют дополнительных модулей.
|
|
||||||
|
|
||||||
В один момент кто-то решает использовать их для долговременного хранения
|
|
||||||
объектов. В базе данных, на диске, еще где-то.
|
|
||||||
|
|
||||||
```
|
|
||||||
namespace Test\Serialize;
|
|
||||||
|
|
||||||
class A {}
|
|
||||||
|
|
||||||
$a = new A();
|
|
||||||
serialize($a);
|
|
||||||
```
|
|
||||||
|
|
||||||
И тут начинаются проблемы.
|
|
||||||
|
|
||||||
Дело в том, что при сериалзации объектов классов кроме самих данных объекта
|
|
||||||
сохраняется еще и информация о классе. Его имя, пространство имен.
|
|
||||||
|
|
||||||
Результатом сериализации в примере выше будет:
|
|
||||||
|
|
||||||
```
|
|
||||||
O:16:"Test\Serialize\A":0:{}O:16:"Test\Serialize\A":0:{}
|
|
||||||
```
|
|
||||||
|
|
||||||
Если теперь произвести рефакторинг, переместить класс, изменить пространство
|
|
||||||
имен, то десериализация уже не сработает. И будет больно.
|
|
||||||
|
|
||||||
Не делайте так.
|
|
||||||
|
|
||||||
Контролируйте процесс сериализации. Например, используйте JSON и специальные
|
|
||||||
функции для превращения объекта в массив и обратно.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
title: Сторителлинг
|
|
||||||
description: Конспект видеолекций Алексея Каптерева о сторителлинге и презентации
|
|
||||||
keywords: [story, storytelling, lectures, история, сторителлинг, каптерев]
|
|
||||||
---
|
|
||||||
|
|
||||||
Это конспект трех лекций Алексея Каптерева, где он рассказывает о презентациях.
|
|
||||||
Как придумать историю, как подготовить слайды и что рассказывать.
|
|
||||||
|
|
||||||
- [https://youtu.be/PaoKhNLyxdk](https://youtu.be/PaoKhNLyxdk)
|
|
||||||
- [https://youtu.be/18M9ZJRU2wI](https://youtu.be/18M9ZJRU2wI)
|
|
||||||
- [https://youtu.be/KyyAgiw8B6I](https://youtu.be/KyyAgiw8B6I)
|
|
||||||
|
|
||||||
## История
|
|
||||||
|
|
||||||
Самый главный вопрос: "Чего я хочу?", а второй - "Что им нужно?" Далее нужно
|
|
||||||
найти пересечение ответов на эти два вопроса. "Что я хочу, чтобы у вас
|
|
||||||
получилось?"
|
|
||||||
|
|
||||||
Эмоции = Мотивация
|
|
||||||
|
|
||||||
Я хочу, чтобы они
|
|
||||||
|
|
||||||
- поняли, что...
|
|
||||||
- изменили мнение о...
|
|
||||||
- сделали...
|
|
||||||
|
|
||||||
Кто? Кому? Что? Зачем? Как?
|
|
||||||
|
|
||||||
История = интересная тема + близость темы + сценарий
|
|
||||||
|
|
||||||
История = факты + смысл + эмоции
|
|
||||||
|
|
||||||
Основные составляющие истории:
|
|
||||||
|
|
||||||
- **Герой** Кто хочет? Я сам, клиент, клиент клиента.
|
|
||||||
- **Цель** Что хочет?
|
|
||||||
- **Проблема / слабость** Что мешает, какая проблема? Почему он не может без
|
|
||||||
этого жить?
|
|
||||||
- **Злодей** Кто мешает?
|
|
||||||
- **Решение** Пути обхода? В чем инсайт?
|
|
||||||
- **Цена** Плата за решение?
|
|
||||||
- **Мораль** В чем призыв к действию? Если вы сделаете Х, то будет Y, иначе Z.
|
|
||||||
|
|
||||||
Факт -> Проблема -> Решение -> Но не все так просто -> Теперь уж точно
|
|
||||||
|
|
||||||
План, зум (уточнение) частей плана - деревья представления.
|
|
||||||
|
|
||||||
Концовка - самое тяжелое, начните с концовки.
|
|
||||||
|
|
||||||
Говорите то, что вас зажигает.
|
|
||||||
|
|
||||||
Чтобы заинтересовать человека в письменном виде, нужно сделать письмо
|
|
||||||
максимально похожим на устную речь. С героем, целью и так далее.
|
|
||||||
|
|
||||||
## Презентации
|
|
||||||
|
|
||||||
Таблицы для анализа, презентации для результата.
|
|
||||||
|
|
||||||
1. Монотонность - враг истории.
|
|
||||||
2. Дело не в "умении выступать" (владейте сутью, слова найдутся).
|
|
||||||
3. История - это не сложно. Экспозиция, проблематизация, решение, выводы.
|
|
||||||
4. Не начинайте с PowerPoint.
|
|
||||||
5. Вы не можете сказать всего (Скажите 5% от того, что вы знаете). Одна хорошая
|
|
||||||
мысль и _очень_ много дисциплины.
|
|
||||||
6. Лучше один раз увидеть (чем 100 раз услышать и 10 раз потрогать).
|
|
||||||
7. Слайды - они как дети. Уродливые, зато свои. Не увлекаться форматированием.
|
|
||||||
Чтобы выделить главное, нужно ответить на вопрос "Какова цель этого слайда?"
|
|
||||||
8. Дизайн - это вычитание. Пропорции, цвета, типографика.
|
|
||||||
9. Контакт глаз. При выступлении смотрите людям в глаза. Это значит, что вы не
|
|
||||||
врете.
|
|
||||||
10. Правда делает нас свободными. Верьте в то, что говорите. Аудитория хочет
|
|
||||||
правды, настоящности. Правда, сострадание, самоирония.
|
|
||||||
|
|
||||||
## Слайды
|
|
||||||
|
|
||||||
Цель слайда: напомнить, впечатлить, объяснить, доказать. Определить цель, делать
|
|
||||||
простым, смотреть на слайд со стороны аудитории.
|
|
||||||
|
|
||||||
- Фотографии. Размер (большой), смысл, честность, логика. Не использовать
|
|
||||||
фотографии для схем.
|
|
||||||
- Схемы. Простота, пошаговость, направление. Пошаговое разжевывание схем.
|
|
||||||
Схемы "как это устроено" и "как это работает". Схема-исория лучше
|
|
||||||
схемы-модели.
|
|
||||||
- Статистика. Результат (а не анализ), ничего лишнего, честность.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
title: Яндекс.Диск для хостинга картинок
|
|
||||||
keywords: [яндекс.диск, хостинг картинок, yandex disk, image hosting, hosting]
|
|
||||||
---
|
|
||||||
|
|
||||||
У [Яндекс.Диска][ya-disk] есть замечательная функция. Он может создавать превью
|
|
||||||
загруженных фотографий. Эта функциональность не афишируется, но описана
|
|
||||||
в [документации][ya-api-preview].
|
|
||||||
|
|
||||||
У меня есть фотография на Диске `/img/kemsky.jpg`. Чтобы получить ее превью,
|
|
||||||
нужно выполнить запрос:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /img/kemsky.jpg?preview&size=XS
|
|
||||||
User-Agent: my_application/0.0.1
|
|
||||||
Host: webdav.yandex.ru
|
|
||||||
Authorization: OAuth 0c4182a7c2cf4521964a72ff57a34a07
|
|
||||||
```
|
|
||||||
|
|
||||||
Но есть проблема. Для запросов нужен токен. Без токена не получится использовать
|
|
||||||
это API для публичного хостинга.
|
|
||||||
|
|
||||||
Решение - сервер [Caddy][caddy] в качестве прокси. Caddy очень
|
|
||||||
удобно использовать в качестве фронтенда для внутренних сервисов.
|
|
||||||
Он просто настраивается, а самое главное - поддерживает автоматический
|
|
||||||
выпуск и обновление SSL-сертификатов буквально одной строчкой конфига.
|
|
||||||
Скроем токен в конфигурации сервера, и будем передавать его при обращении
|
|
||||||
к Яндекс.Диску:
|
|
||||||
|
|
||||||
```
|
|
||||||
preview.vakhrushev.me {
|
|
||||||
proxy /img https://webdav.yandex.ru {
|
|
||||||
transparent
|
|
||||||
header_upstream User-Agent "yandex-disk-previewer/1.0"
|
|
||||||
header_upstream Authorization "OAuth 0c4182a7c2cf4521964a72ff57a34a07"
|
|
||||||
}
|
|
||||||
|
|
||||||
tls anwinged@ya.ru
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Директива `proxy /img` будет направлять все запросы с `preview.vakhrushev.me/img`
|
|
||||||
на `https://webdav.yandex.ru/img`. Таким образом во внешний
|
|
||||||
мир будет смотреть только директория `img`, а остальные останутся скрытыми.
|
|
||||||
|
|
||||||
Кроме OAuth авторизации можно использовать Basic, передавая логин и
|
|
||||||
[пароль приложения][app-password]. Мне этот способ удобнее,
|
|
||||||
чтобы не заморачиваться с OAuth. Логин и пароль я храню
|
|
||||||
зашифрованными с помощью [Ansible Vault][vault].
|
|
||||||
И строчка с заголовком тогда будет выглядеть так:
|
|
||||||
|
|
||||||
```
|
|
||||||
header_upstream Authorization "Basic {{ '{{' }} (yandex_disk.login ~ ':' ~ yandex_disk.password) | b64encode {{ '}}' }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
А так будет выглядеть ссылка на картинку:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://preview.vakhrushev.me/img/kemsky.jpg?preview&size=XXL
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[ya-disk]: https://disk.yandex.ru
|
|
||||||
[ya-api]: https://yandex.ru/dev/disk/doc/dg/concepts/quickstart-docpage/
|
|
||||||
[ya-api-preview]: https://yandex.ru/dev/disk/doc/dg/reference/preview-docpage/
|
|
||||||
[caddy]: https://caddyserver.com/
|
|
||||||
[app-password]: https://yandex.ru/support/passport/authorization/app-passwords.html
|
|
||||||
[vault]: https://docs.ansible.com/ansible/latest/user_guide/vault.html
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
title: Как проектировать хайлоад, видео
|
|
||||||
keywords: [highload, высоконагруженные системы, онтико, олег бунин]
|
|
||||||
---
|
|
||||||
|
|
||||||
Нашел три замечательных видеоролика о высоконагруженных системах.
|
|
||||||
Архитектура, подходы, планирование, проблемы.
|
|
||||||
|
|
||||||
## Часть 1
|
|
||||||
|
|
||||||
<div class="youtube-embed-container">
|
|
||||||
<iframe src="https://www.youtube.com/embed/KmIE5K6adus" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Часть 2
|
|
||||||
|
|
||||||
<div class="youtube-embed-container">
|
|
||||||
<iframe src="https://www.youtube.com/embed/sCm4qUw28y4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Часть 3
|
|
||||||
|
|
||||||
<div class="youtube-embed-container">
|
|
||||||
<iframe src="https://www.youtube.com/embed/MG8-HmgOXlk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
||||||
</div>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
title: Интересные блоги о программировании
|
|
||||||
description: Сборник интересных блогов и статей о программирования для себя
|
|
||||||
keywords: [блоги, программирование, сборник, чистый код, ооп, haskell]
|
|
||||||
---
|
|
||||||
|
|
||||||
## Дизайн приложений
|
|
||||||
|
|
||||||
- Сайт Мартина Фаулера - [martinfowler.com](https://martinfowler.com/).
|
|
||||||
Архитектура, рефакторинг, чистый код.
|
|
||||||
- Блог Роберта Мартина, или дядюшки Боба, о чистом коде - [The Clean Code Blog](https://blog.cleancoder.com/).
|
|
||||||
|
|
||||||
## Функциональное программирование
|
|
||||||
|
|
||||||
- Хорошие статьи о функциональном программировании с примерами на F# - [F# for fun and profit](https://fsharpforfunandprofit.com/).
|
|
||||||
- Блог [Alexis King](https://lexi-lambda.github.io/) о Haskell и Racket.
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
---
|
|
||||||
title: О полях-дискриминаторах
|
|
||||||
description: Заметка о том, как записывать конфигурацию для сложных объектов
|
|
||||||
keywords: [чистый код, дискриминатор, php, конфигурация]
|
|
||||||
---
|
|
||||||
|
|
||||||
Поля-дискриминаторы - это удобный прием для обработки нескольких типов
|
|
||||||
данных со схожей структурой.
|
|
||||||
|
|
||||||
Лучше начать с примера.
|
|
||||||
|
|
||||||
Допустим, у нас есть объект-фильтр для целых чисел. Можно применить фильтр
|
|
||||||
к последовательности чисел и получить новую последовательность.
|
|
||||||
Его параметры выглядят следующим образом:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"from": 0,
|
|
||||||
"to": 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Отлично.
|
|
||||||
Фильтр пропустит только числа от 0 до 10.
|
|
||||||
По такой конфигурации без проблем можно создать объект.
|
|
||||||
|
|
||||||
Теперь добавим второй фильтр - он будет отсекать нечетные числа.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"odd": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Создаем фильтр на основе структуры
|
|
||||||
|
|
||||||
Теперь есть два фильтра, и нужно понимать какой их них создать.
|
|
||||||
Простое и наивное решение - смотреть на структуру полей.
|
|
||||||
Если есть поле `odd`, то фильтр нечетных чисел, иначе - фильтр по диапазону.
|
|
||||||
|
|
||||||
```php
|
|
||||||
if (isset($config['odd'])) {
|
|
||||||
// фильтр нечетных чисел
|
|
||||||
} else {
|
|
||||||
// фильтр диапазона
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
У этого решения масса недостатков.
|
|
||||||
Например, если поле `odd` понадобится двум фильтрам сразу, то условие усложнится.
|
|
||||||
Или появятся поля с разными названиями, но одинаковым смыслом.
|
|
||||||
|
|
||||||
## Добавляем поле-дискриминатор
|
|
||||||
|
|
||||||
Решение проблемы в добавление специального поля, в котором содержится имя фильтра.
|
|
||||||
Назовем это поле `type`. Тогда конфигурации фильтров будут выглядеть следующим образом:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "range",
|
|
||||||
"from": 0,
|
|
||||||
"to": 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "odd_control",
|
|
||||||
"odd": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Поле `type` - это и есть поле-дискриминатор.
|
|
||||||
По нему можно точно определить какой фильтр перед нами.
|
|
||||||
|
|
||||||
```php
|
|
||||||
switch ($config['type']) {
|
|
||||||
case 'range':
|
|
||||||
// фильтр диапазона
|
|
||||||
break;
|
|
||||||
case 'odd_control':
|
|
||||||
// фильтр нечетных чисел
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new \LogicException(sprintf(
|
|
||||||
'Unknown filter type "%s"', $config['type']));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Выделить зависимые поля
|
|
||||||
|
|
||||||
Решение еще можно улучшить.
|
|
||||||
Сейчас на одном уровне в структуре конфига есть и обязательные поля, и необязательные поля.
|
|
||||||
Мы можем перенести все необязательные поля на дополнительный уровень,
|
|
||||||
например, в поле `params`.
|
|
||||||
Эти поля необязательны для всей конфигурации, но обязательны для конкретного фильтра.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "range",
|
|
||||||
"params": {
|
|
||||||
"from": 0,
|
|
||||||
"to": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Что дает такой маневр?
|
|
||||||
На верхнем уровне конфигурации у нас будет постоянная структура.
|
|
||||||
Поле `params` можно до определенного времени рассматривать как черный ящик,
|
|
||||||
просто зная, что это некий словарь параметров.
|
|
||||||
Когда будет понятно какой фильтр создавать, тогда этот набор параметров
|
|
||||||
послужит базой для создания объекта-фильтра.
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
- Если структура конфигурации предполагает создание нескольких разных объектов,
|
|
||||||
то лучше использовать специальное поле-дискриминатор для точного указания
|
|
||||||
типа создаваемого объекта.
|
|
||||||
- Все поля, которые зависят от типа, лучше перенести на дополнительный уровень,
|
|
||||||
тем самым сохранив структуру верхнего уровня постоянной (на верхнем уровне
|
|
||||||
будет всегда один и тот же набор полей).
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
---
|
|
||||||
title: Организация доступа к nullable полям класса
|
|
||||||
description: Заметка о том, лучше организовать доступ к полям класса, которые могут содержать значение null
|
|
||||||
keywords: [чистый код, php, null, поля класса]
|
|
||||||
---
|
|
||||||
|
|
||||||
Нередкая ситуация, когда в классе есть поле, которое может содержать `null`.
|
|
||||||
|
|
||||||
```php
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
private Email $email;
|
|
||||||
private ?string $name;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Пользователь может указать имя, а может и не указывать,
|
|
||||||
ограничившись только почтовым адресом.
|
|
||||||
|
|
||||||
А далее мы пишем код, которые работает с полем имени.
|
|
||||||
|
|
||||||
```php
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
private ?string $name;
|
|
||||||
|
|
||||||
public function hasName(): bool
|
|
||||||
{
|
|
||||||
return $this->name !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): ?string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(?string $name): void
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
И использование этого кода:
|
|
||||||
|
|
||||||
```php
|
|
||||||
/** @var User $user */
|
|
||||||
|
|
||||||
if ($user->hasName()) {
|
|
||||||
do_something_with_name($user->getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
function do_something_with_name(string $name) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Выглядит хорошо.
|
|
||||||
Сначала убедились, что имя установлено, а потом использовали его.
|
|
||||||
|
|
||||||
Но статический анализатор нам обязательно припомнит, что мы пытаемся передать
|
|
||||||
в функцию `do_something_with_name` значение типа `string|null`, хотя функция
|
|
||||||
ожидает значение типа `string`.
|
|
||||||
И получается дурацкая ситуация, что формально мы должны дописать
|
|
||||||
еще одну проверку.
|
|
||||||
|
|
||||||
```php
|
|
||||||
/** @var User $user */
|
|
||||||
|
|
||||||
if ($user->hasName()) {
|
|
||||||
$name = $user->getName();
|
|
||||||
if ($name !== null) {
|
|
||||||
do_something_with_name($name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function do_something_with_name(string $name) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Статический анализатор наш друг, он помогает находить ошибки и несоответствия
|
|
||||||
в коде.
|
|
||||||
И здесь он нашел такое формальное несоответствие типов.
|
|
||||||
|
|
||||||
Статический анализатор прав, а мы, как проектировщики интерфейса, не правы.
|
|
||||||
На самом деле мы смешали два подхода, когда описывали методы в нашем классе:
|
|
||||||
|
|
||||||
1. Получить и проверить
|
|
||||||
2. Проверить и получить
|
|
||||||
|
|
||||||
## Получить и проверить
|
|
||||||
|
|
||||||
И сразу начнем с примера использования.
|
|
||||||
|
|
||||||
```php
|
|
||||||
$name = $user->getName();
|
|
||||||
if ($name !== null) {
|
|
||||||
do_something_with_name($name);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Сначала мы получаем значение поля, а потом проверяем, соответствует ли это
|
|
||||||
значение нашим требованиям. Класс при этом будет построен вот так:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
private ?string $name;
|
|
||||||
|
|
||||||
public function getName(): ?string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(?string $name): void
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Заметьте, здесь уже нет метода `hasName()`, потому что этот метод перестал быть
|
|
||||||
нужным. Его роль исполняет метод `getName()`.
|
|
||||||
|
|
||||||
## Проверить и получить
|
|
||||||
|
|
||||||
Второй подход: сначала проверяем значение, а потом работаем с ним:
|
|
||||||
|
|
||||||
```php
|
|
||||||
if ($user->hasName()) {
|
|
||||||
do_something_with_name($user->getName());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Структура класса:
|
|
||||||
|
|
||||||
```php
|
|
||||||
class User
|
|
||||||
{
|
|
||||||
private ?string $name;
|
|
||||||
|
|
||||||
public function hasName(): bool
|
|
||||||
{
|
|
||||||
return $this->name !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
if ($this->name === null) {
|
|
||||||
throw new \LogicException('Name is not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(?string $name): void
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Смотрите отличия.
|
|
||||||
Метод `hasName()` остается.
|
|
||||||
А вот метод `getName()` теперь возвращает значение типа `string`.
|
|
||||||
Он выбросит исключение, если мы попытаемся получить значение,
|
|
||||||
которое не установлено.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
Теперь встает вопрос, когда и какой подход следует использовать.
|
|
||||||
|
|
||||||
- Если ситуация, когда поле не установлено, скорее исключительная, нежели
|
|
||||||
обычная, то можно использовать второй подход, а проверку опустить.
|
|
||||||
Исключение в методе `getName()` позволит обнаружить странное поведение.
|
|
||||||
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
|
|
||||||
будет удобнее, все равно нужно делать проверку.
|
|
||||||
|
|
||||||
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
|
|
||||||
случае, и не использовать их одновременно.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
@import 'vars';
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
font-size: $base-font-size;
|
|
||||||
font-family: $base-font-family;
|
|
||||||
color: $base-font-color;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
font-size: $base-font-size - 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not(:visited) {
|
|
||||||
color: $link-color;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover,
|
|
||||||
a:focus,
|
|
||||||
a:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hr-line {
|
|
||||||
height: 0;
|
|
||||||
display: block;
|
|
||||||
border-bottom: 1px solid $rule-color;
|
|
||||||
margin: 2em 0;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
|
|
||||||
$button-border-radius: 0.5em;
|
|
||||||
$button-background-color: desaturate(darken($link-color, 5%), 20%);
|
|
||||||
|
|
||||||
%button {
|
|
||||||
display: inline-block;
|
|
||||||
color: #fff;
|
|
||||||
background-color: $button-background-color;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
border: none;
|
|
||||||
font-size: 100%;
|
|
||||||
font-family: inherit;
|
|
||||||
border-radius: $button-border-radius;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: darken($button-background-color, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
|
|
||||||
.social {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 2.4em 0;
|
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0.25em 0.5em;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link:hover {
|
|
||||||
color: $link-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
@import '../base_style';
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: $preferred-width;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
width: auto;
|
|
||||||
margin: {
|
|
||||||
left: $mobile-margin;
|
|
||||||
right: $mobile-margin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@import 'social';
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-top: 0.8em;
|
|
||||||
font-size: 2.4em;
|
|
||||||
margin-bottom: 0.6em;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
margin-top: $mobile-margin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.year-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 1.2em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
margin-top: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
font-family: $base-monospace-font-family;
|
|
||||||
font-size: 90%;
|
|
||||||
padding: 1em;
|
|
||||||
background-color: #f6f8fa; // from github
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: $base-monospace-font-family;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
color: $rule-color;
|
|
||||||
border-top: 1px solid $rule-color;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: none;
|
|
||||||
margin-block-start: 1em;
|
|
||||||
margin-block-end: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.6em;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
|
|
||||||
.navigation {
|
|
||||||
display: block;
|
|
||||||
border-bottom: 1px solid $rule-color;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
&__actions {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__action {
|
|
||||||
display: inline-block;
|
|
||||||
margin: {
|
|
||||||
top: 1em;
|
|
||||||
bottom: 1em;
|
|
||||||
right: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@import '../vars';
|
|
||||||
@import '../base_style';
|
|
||||||
@import 'navigation';
|
|
||||||
|
|
||||||
.page {
|
|
||||||
width: $preferred-width;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
width: auto;
|
|
||||||
margin-left: $mobile-margin;
|
|
||||||
margin-right: $mobile-margin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin-bottom: 3em;
|
|
||||||
|
|
||||||
@import 'content-tags';
|
|
||||||
@import 'youtube-embed';
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.youtube-embed-container {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 56.25%;
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
iframe,
|
|
||||||
object,
|
|
||||||
embed {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app" tabindex="0" v-on:keyup="press">
|
|
||||||
<div v-if="isHumanWin">
|
|
||||||
<p>Победа! Было очень сложно, но вы справились, поздравляю!</p>
|
|
||||||
<button class="restart-button" v-on:click.prevent="restart">
|
|
||||||
Заново
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="isRobotWin">
|
|
||||||
<p>
|
|
||||||
Упс, железяка победила. Оказывается, предсказать выбор человека
|
|
||||||
не так уж и сложно, да?
|
|
||||||
</p>
|
|
||||||
<button class="restart-button" v-on:click.prevent="restart">
|
|
||||||
Заново
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<p class="score">{{ predictor.score }}</p>
|
|
||||||
<p class="step">Ход {{ step }}</p>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="pass-button __left"
|
|
||||||
value="0"
|
|
||||||
v-on:click.prevent="click(0)"
|
|
||||||
>
|
|
||||||
Нечет
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="pass-button __right"
|
|
||||||
value="1"
|
|
||||||
v-on:click.prevent="click(1)"
|
|
||||||
>
|
|
||||||
Чет
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Predictor from '@anwinged/predictor';
|
|
||||||
|
|
||||||
const MAX_SCORE = 50;
|
|
||||||
|
|
||||||
function make_predictor() {
|
|
||||||
return new Predictor({
|
|
||||||
base: 2,
|
|
||||||
daemons: [
|
|
||||||
{ human: 3, robot: 3 },
|
|
||||||
{ human: 4, robot: 4 },
|
|
||||||
{ human: 5, robot: 5 },
|
|
||||||
{ human: 6, robot: 6 },
|
|
||||||
{ human: 8, robot: 8 },
|
|
||||||
{ human: 12, robot: 12 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
predictor: make_predictor(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isHumanWin() {
|
|
||||||
return this.predictor.score >= MAX_SCORE;
|
|
||||||
},
|
|
||||||
isRobotWin() {
|
|
||||||
return this.predictor.score <= -MAX_SCORE;
|
|
||||||
},
|
|
||||||
step() {
|
|
||||||
return this.predictor.stepCount() + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
click(v) {
|
|
||||||
const value = v ? 1 : 0;
|
|
||||||
this.pass(value);
|
|
||||||
},
|
|
||||||
press(evt) {
|
|
||||||
const value = evt.key === '1' ? 0 : 1;
|
|
||||||
this.pass(value);
|
|
||||||
},
|
|
||||||
pass(value) {
|
|
||||||
const oldScore = this.predictor.score;
|
|
||||||
if (Math.abs(oldScore) >= MAX_SCORE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* const prediction = */ this.predictor.pass(+value);
|
|
||||||
},
|
|
||||||
restart() {
|
|
||||||
this.predictor = make_predictor();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '../vars';
|
|
||||||
@import '../components/button';
|
|
||||||
|
|
||||||
.app {
|
|
||||||
display: block;
|
|
||||||
margin: 2em auto;
|
|
||||||
padding: 2em;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
@media (max-width: $first-media-step) {
|
|
||||||
padding: {
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $rule-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.score {
|
|
||||||
font-size: 400%;
|
|
||||||
margin-top: 0.2em;
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restart-button {
|
|
||||||
@extend %button;
|
|
||||||
padding: {
|
|
||||||
left: 1.4em;
|
|
||||||
right: 1.4em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pass-button {
|
|
||||||
@extend %button;
|
|
||||||
flex-grow: 0;
|
|
||||||
min-width: 7em;
|
|
||||||
margin: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pass-button.__left {
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pass-button.__right {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import Demo from './demo.vue';
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
render: h => h(Demo),
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
$base-font-family: 'PT Serif', serif;
|
|
||||||
$base-monospace-font-family: 'Source Code Pro', monospace;
|
|
||||||
|
|
||||||
// Базовый размер шрифта
|
|
||||||
$base-font-size: 20px;
|
|
||||||
$base-font-color: #24292e; // from github
|
|
||||||
|
|
||||||
// Цвет ссылок
|
|
||||||
$link-color: #0366d6; // from github
|
|
||||||
|
|
||||||
// Ширина страницы
|
|
||||||
$preferred-width: 740px;
|
|
||||||
|
|
||||||
$first-media-step: $preferred-width - 1px;
|
|
||||||
|
|
||||||
$mobile-margin: 0.8em;
|
|
||||||
|
|
||||||
$rule-color: #e6e6e6;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{% for year in range(2099, 2010, -1) %}
|
|
||||||
|
|
||||||
{% set items = [] %}
|
|
||||||
|
|
||||||
{% for article in data.articles %}
|
|
||||||
{% if article.date|date('Y') == year %}
|
|
||||||
{% set items = items|merge([article]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if items %}
|
|
||||||
<h2 class="year-title">{{ year }}</h2>
|
|
||||||
{% for article in items %}
|
|
||||||
<h3 class="article-title">
|
|
||||||
<a href="{{ article.url }}">{{ article.title }}</a>
|
|
||||||
</h3>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!-- Yandex.Metrika counter -->
|
|
||||||
<script type="text/javascript" >
|
|
||||||
(function (d, w, c) {
|
|
||||||
(w[c] = w[c] || []).push(function() {
|
|
||||||
try {
|
|
||||||
w.yaCounter41913764 = new Ya.Metrika({
|
|
||||||
id:41913764,
|
|
||||||
clickmap:true,
|
|
||||||
trackLinks:true,
|
|
||||||
accurateTrackBounce:true,
|
|
||||||
trackHash:true
|
|
||||||
});
|
|
||||||
} catch(e) { }
|
|
||||||
});
|
|
||||||
|
|
||||||
var n = d.getElementsByTagName("script")[0],
|
|
||||||
s = d.createElement("script"),
|
|
||||||
f = function () { n.parentNode.insertBefore(s, n); };
|
|
||||||
s.type = "text/javascript";
|
|
||||||
s.async = true;
|
|
||||||
s.src = "https://cdn.jsdelivr.net/npm/yandex-metrica-watch/watch.js";
|
|
||||||
|
|
||||||
if (w.opera == "[object Opera]") {
|
|
||||||
d.addEventListener("DOMContentLoaded", f, false);
|
|
||||||
} else { f(); }
|
|
||||||
})(document, window, "yandex_metrika_callbacks");
|
|
||||||
</script>
|
|
||||||
<noscript><div><img src="https://mc.yandex.ru/watch/41913764" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
|
||||||
<!-- /Yandex.Metrika counter -->
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/brands.css" integrity="sha384-n9+6/aSqa9lBidZMRCQHTHKJscPq6NW4pCQBiMmHdUCvPN8ZOg2zJJTkC7WIezWv" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{# Charset #}
|
|
||||||
<meta charset="utf-8">
|
|
||||||
|
|
||||||
{# Title #}
|
|
||||||
{% if page.title is defined %}
|
|
||||||
<title>{{ page.title }} - {{ site.title }}</title>
|
|
||||||
{% else %}
|
|
||||||
<title>{{ site.title }}</title>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Description #}
|
|
||||||
{% if page.description is defined %}
|
|
||||||
<meta name="description" content="{{ page.description }}">
|
|
||||||
{% elseif page.title is defined %}
|
|
||||||
<meta name="description" content="{{ page.title }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if page.keywords is defined and page.keywords %}
|
|
||||||
{% if page.keywords is iterable %}
|
|
||||||
<meta name="keywords" content="{{ page.keywords|join(',') }}">
|
|
||||||
{% else %}
|
|
||||||
<meta name="keywords" content="{{ page.keywords }}">
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Other meta fields #}
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="yandex-verification" content="eb6443fccb57d7d2" />
|
|
||||||
|
|
||||||
{# Social #}
|
|
||||||
<meta property="og:site_name" content="Anton Vakhrushev">
|
|
||||||
<meta property="og:title" content="{{ page.og.title ?? page.title ?? site.title }}">
|
|
||||||
<meta property="og:description" content="{{ page.og.description ?? page.description ?? site.description ?? '' }}">
|
|
||||||
<meta property="og:url" content="{{ page.og.url ?? (site.url | trim('/', side='right')) ~ (page.url != '/.' ? page.url : '') }}">
|
|
||||||
<meta property="og:locale" content="ru_RU">
|
|
||||||
|
|
||||||
{# Links and styles #}
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=PT+Serif:400,700&subset=cyrillic" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400&display=swap&subset=cyrillic" rel="stylesheet">
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<nav class="navigation">
|
|
||||||
<ul class="navigation__actions">
|
|
||||||
<li class="navigation__action">
|
|
||||||
<a class="navigation__link" href="/">Главная</a>
|
|
||||||
</li>
|
|
||||||
<li class="navigation__action">
|
|
||||||
<a class="navigation__link" href="/articles/">Заметки</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{% extends 'internal.html.twig' %}
|
|
||||||
|
|
||||||
{% block content_wrapper %}
|
|
||||||
|
|
||||||
<h1>{{ page.title }}</h1>
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
|
|
||||||
<span class="hr-line"></span>
|
|
||||||
|
|
||||||
<p>{{ page.date|date('d.m.Y') }}, <a href="mailto:anton@vakhrushev.me">anton@vakhrushev.me</a></p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
{% include 'head.twig' %}
|
|
||||||
{% block css %}{% endblock %}
|
|
||||||
{% for s in page.styles | default([]) %}
|
|
||||||
<link rel="stylesheet" href="{{ hashed_asset(s) }}">
|
|
||||||
{% endfor %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div><!-- Этот div сдается для вашей рекламы --></div>
|
|
||||||
|
|
||||||
{# block for main page content #}
|
|
||||||
{% block page_content %}{% endblock page_content %}
|
|
||||||
|
|
||||||
{# Analytics counters #}
|
|
||||||
{% include 'counters.twig' %}
|
|
||||||
|
|
||||||
{# Scripts #}
|
|
||||||
{% block js %}{% endblock %}
|
|
||||||
{% for s in page.scripts | default([]) %}
|
|
||||||
<script async src="{{ hashed_asset(s) }}"></script>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# Extra fonts #}
|
|
||||||
{% include 'font-awesome.twig' %}
|
|
||||||
|
|
||||||
<!-- Deployed wih Circle CI -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{% extends 'base.html.twig' %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ parent() }}
|
|
||||||
<link rel="stylesheet" href="{{ hashed_asset('/static/layout_internal.css') }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script async src="{{ hashed_asset('/static/layout_internal.js') }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block page_content %}
|
|
||||||
<div class="page">
|
|
||||||
{% include 'navigation.twig' %}
|
|
||||||
{% block main %}
|
|
||||||
<main class="content">
|
|
||||||
{% block content_wrapper %}
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
layout: internal
|
|
||||||
title: Заметки
|
|
||||||
description: Заметки
|
|
||||||
use: [articles]
|
|
||||||
---
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% include 'article_list.twig' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
use: ["articles"]
|
|
||||||
permalink: none
|
|
||||||
sitemap:
|
|
||||||
_exclude: yes
|
|
||||||
---
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
||||||
<title><![CDATA[{{ site.title }}]]></title>
|
|
||||||
<link href="{{ site.url }}/atom.xml" rel="self"/>
|
|
||||||
<link href="{{ site.url }}/"/>
|
|
||||||
<updated>{{ site.calculated_date | date('c') }}</updated>
|
|
||||||
<id>{{ site.url }}/</id>
|
|
||||||
{% if site.author or site.email %}
|
|
||||||
<author>
|
|
||||||
{% if site.author %}<name><![CDATA[{{ site.author }}]]></name>{% endif %}
|
|
||||||
{% if site.email %}<email><![CDATA[{{ site.email }}]]></email>{% endif %}
|
|
||||||
</author>
|
|
||||||
{% endif %}
|
|
||||||
<generator uri="http://sculpin.io/">Sculpin</generator>
|
|
||||||
{% for article in data.articles|slice(0, 10) %}
|
|
||||||
<entry>
|
|
||||||
<title type="html"><![CDATA[{{ article.title }}]]></title>
|
|
||||||
<link href="{{ site.url }}{{ article.url }}"/>
|
|
||||||
<updated>{{ article.date|date('c') }}</updated>
|
|
||||||
<id>{{ site.url }}{{ article.url }}</id>
|
|
||||||
<content type="html"><![CDATA[{{ article.blocks.content|raw }}]]></content>
|
|
||||||
</entry>
|
|
||||||
{% endfor %}
|
|
||||||
</feed>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
layout: base
|
|
||||||
description: Личный сайт Антона Вахрушева
|
|
||||||
use: [articles]
|
|
||||||
---
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ parent() }}
|
|
||||||
<link rel="stylesheet" href="{{ hashed_asset('/static/index.css') }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script async src="{{ hashed_asset('/static/index.js') }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block page_content %}
|
|
||||||
|
|
||||||
<main class="content">
|
|
||||||
|
|
||||||
<h1 class="name">Антон Вахрушев</h1>
|
|
||||||
|
|
||||||
<p class="info">
|
|
||||||
Я веб-программист.
|
|
||||||
Работаю в <a href="https://playkot.com/" target="_blank">Playkot</a>.
|
|
||||||
Пишу на PHP.
|
|
||||||
Разбираюсь в back-end, экспериментирую с front-end, интересуюсь функциональным программированием.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<span class="hr-line"></span>
|
|
||||||
|
|
||||||
{% include 'article_list.twig' %}
|
|
||||||
|
|
||||||
<span class="hr-line"></span>
|
|
||||||
|
|
||||||
<ul class="social">
|
|
||||||
<li class="social__item">
|
|
||||||
<a class="social__link" href="mailto:anton@vakhrushev.me" target="_blank" title="Написать на почту">
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="social__item">
|
|
||||||
<a class="social__link" href="https://git.vakhrushev.me" target="_blank" title="Код на Гитхабе">
|
|
||||||
<i class="fab fa-git-square"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="social__item">
|
|
||||||
<a class="social__link" href="https://www.notion.so/6cbeaaa60dd146ba8d1d001a322e0139" target="_blank" title="Вишлист">
|
|
||||||
<i class="fas fa-socks"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: none
|
|
||||||
sitemap:
|
|
||||||
_exclude: yes
|
|
||||||
---
|
|
||||||
User-agent: *
|
|
||||||
Sitemap: {{ site.url }}/sitemap.xml
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
permalink: none
|
|
||||||
use:
|
|
||||||
- sitemap
|
|
||||||
sitemap:
|
|
||||||
_exclude: yes
|
|
||||||
---
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
{% for url in data.sitemap %}
|
|
||||||
<url>
|
|
||||||
|
|
||||||
{# Last slash for pretty url #}
|
|
||||||
<loc>{{ site.url }}{{ url.loc != '/.' ? (url.loc|trim('/', side='right') ~ '/') : '' }}</loc>
|
|
||||||
|
|
||||||
<lastmod>{{ url.lastmod }}</lastmod>
|
|
||||||
|
|
||||||
{% if url.changefreq %}
|
|
||||||
<changefreq>{{ url.changefreq }}</changefreq>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if url.priority %}
|
|
||||||
<priority>{{ url.priority }}</priority>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</url>
|
|
||||||
{% endfor %}
|
|
||||||
</urlset>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const autoprefixer = require('autoprefixer');
|
|
||||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
|
|
||||||
function collect_entries() {
|
|
||||||
const assets = glob.sync('./source/_assets/**/{index.js,style.[s]css}');
|
|
||||||
const entries = {};
|
|
||||||
assets.forEach(f => {
|
|
||||||
const parts = f.split('/');
|
|
||||||
const name = parts[parts.length - 2];
|
|
||||||
entries[name] = f;
|
|
||||||
});
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (env = {}) => {
|
|
||||||
const is_prod = !!env.production;
|
|
||||||
const dist_dir = is_prod ? './output_prod' : './output_dev';
|
|
||||||
|
|
||||||
const STYLE_LOADER = { loader: 'style-loader' };
|
|
||||||
|
|
||||||
const CSS_LOADER = {
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
importLoaders: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const POSTCSS_LOADER = {
|
|
||||||
loader: 'postcss-loader',
|
|
||||||
options: {
|
|
||||||
plugins: [autoprefixer()],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCSS_LOADER = { loader: 'sass-loader' };
|
|
||||||
|
|
||||||
const MINI_CSS_LOADER = MiniCssExtractPlugin.loader;
|
|
||||||
|
|
||||||
const BABEL_LOADER = {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env'],
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-transform-runtime',
|
|
||||||
'@babel/plugin-proposal-class-properties',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const VUE_LOADER = {
|
|
||||||
loader: 'vue-loader',
|
|
||||||
options: {
|
|
||||||
loaders: {
|
|
||||||
css: [
|
|
||||||
MINI_CSS_LOADER, //'vue-style-loader',
|
|
||||||
CSS_LOADER,
|
|
||||||
POSTCSS_LOADER,
|
|
||||||
],
|
|
||||||
scss: [
|
|
||||||
MINI_CSS_LOADER, //'vue-style-loader',
|
|
||||||
CSS_LOADER,
|
|
||||||
POSTCSS_LOADER,
|
|
||||||
SCSS_LOADER,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode: is_prod ? 'production' : 'development',
|
|
||||||
entry: collect_entries(),
|
|
||||||
output: {
|
|
||||||
filename: '[name].js',
|
|
||||||
path: path.resolve(__dirname, `${dist_dir}/static`),
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [BABEL_LOADER],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [MINI_CSS_LOADER, CSS_LOADER, POSTCSS_LOADER],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
use: [
|
|
||||||
MINI_CSS_LOADER,
|
|
||||||
CSS_LOADER,
|
|
||||||
POSTCSS_LOADER,
|
|
||||||
SCSS_LOADER,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.vue$/,
|
|
||||||
use: [VUE_LOADER],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new VueLoaderPlugin(),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
// Options similar to the same options in webpackOptions.output
|
|
||||||
// both options are optional
|
|
||||||
filename: '[name].css',
|
|
||||||
// chunkFilename: "[id].css"
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user