Skip to content

Commit

Permalink
Making webfont CSS generator service...
Browse files Browse the repository at this point in the history
  • Loading branch information
Finesse committed Jul 30, 2017
1 parent c7fe0b0 commit 4ce93e2
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/logs/*
!/logs/README.md
/config/settings-local.php
/public/fonts/*
!/public/fonts/README.md
7 changes: 7 additions & 0 deletions config/dependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
return $logger;
};

// webfonts css code generator
$container['webfontCSSGenerator'] = function (ContainerInterface $c) {
$fonts = $c->get('settings')['fonts'];
$request = $c->get('request');
return new \Src\Services\WebfontCSSGenerator($fonts, $request->getUri()->getBasePath());
};
3 changes: 2 additions & 1 deletion config/settings-local.php.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ return [
'displayErrorDetails' => true,
'logger' => [
'level' => \Monolog\Logger::DEBUG
]
],
'fonts' => [],
];
3 changes: 3 additions & 0 deletions config/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
'path' => __DIR__ . '/../logs/app.log',
'level' => \Monolog\Logger::DEBUG,
],

// List of webfonts
'fonts' => [],
], require __DIR__ . '/settings-local.php');
1 change: 1 addition & 0 deletions public/fonts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Put font files to this directory.
88 changes: 81 additions & 7 deletions src/Controllers/CSSGeneratorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace Src\Controllers;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Src\Services\WebfontCSSGenerator;

/**
* Class CSSGeneratorController
Expand Down Expand Up @@ -32,17 +33,90 @@ public function __construct(ContainerInterface $container)
/**
* Runs the controller action.
*
* @param RequestInterface $request
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function __invoke(RequestInterface $request, ResponseInterface $response): ResponseInterface
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$response = $response->withHeader('Content-Type', 'text/css; charset=UTF-8');
// Getting and checking the request data
$requestParams = $request->getQueryParams();
if (!isset($requestParams['family'])) {
return $this->createErrorResponse('The `family` query parameter is not set');
}
try {
$requestedFonts = $this->parseFamilyParameter($requestParams['family']);
} catch (\InvalidArgumentException $error) {
return $this->createErrorResponse($error->getMessage());
}

$body = $response->getBody();
$body->write('body {color: #555;}');
// Generating the CSS code
/** @var WebfontCSSGenerator $webfontCSSGenerator */
$webfontCSSGenerator = $this->container->get('webfontCSSGenerator');
try {
$cssCode = $webfontCSSGenerator->makeCSS($requestedFonts);
} catch (\InvalidArgumentException $error) {
return $this->createErrorResponse($error->getMessage());
}

return $response;
// Sending the response
return $response
->withHeader('Content-Type', 'text/css; charset=UTF-8')
->write($cssCode);
}

/**
* Converts family parameter value to families data array.
*
* @param string $value Parameter value. Example: `Open Sans:400,700|Roboto:100,100i,400,400i`.
* @return array[] Value data. Example:
* <pre>
* [
* 'Open Sans' => ['400', '700'],
* 'Roboto' => ['100', '100i', '400', '400i']
* ]
* </pre>
* @throws \InvalidArgumentException If the parameter value is formatted badly. The message may be sent back to the
* client.
*/
protected function parseFamilyParameter(string $value): array
{
$result = [];
$families = explode('|', $value);

foreach ($families as $family) {
$familyDetails = explode(':', $family, 2);

$name = $familyDetails[0];
if ($name === '') {
throw new \InvalidArgumentException('An empty font family name is set');
}

$stylesValue = $familyDetails[1] ?? null;
if ($stylesValue === null || $stylesValue === '') {
$stylesValue = '400';
}

$styles = explode(',', $stylesValue);
$result[$name] = $styles;
}

return $result;
}

/**
* Creates a response with the client side error message.
*
* @param string $message Error message for the client
* @param int $status HTTP status code
* @return ResponseInterface
*
* @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes HTTP status codes
*/
protected function createErrorResponse(string $message, int $status = 422): ResponseInterface
{
return $this->container->get('response')
->withStatus($status)
->write($message);
}
}
33 changes: 33 additions & 0 deletions src/Helpers/CSSHelpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Src\Helpers;

/**
* Class CSSHelpers
*
* Helps functions for working with CSS code.
*
* @author Finesse
* @package Src\Helpers
*/
class CSSHelpers
{
/**
* Format string for putting to CSS code as a string value. Add required quotation marks at the begin and at the end.
*
* Examples of usage:
* - making values for `before` and `after` properties: `"before: ".CSSHelpers::formatString($text)`;
* - formatting background URLs: `"background-image: url(".CSSHelpers::formatString($url).")"`;
*
* @param string $text
* @return string
*/
public static function formatString(string $text): string
{
return "'".str_replace(
[ "'", "\\", "\n"],
["\\'", "\\\\", "\\\n"],
$text
)."'";
}
}
171 changes: 171 additions & 0 deletions src/Services/WebfontCSSGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

namespace Src\Services;

use Src\Helpers\CSSHelpers;

/**
* Class WebfontCSSGenerator
*
* Generates CSS code for embedding webfonts.
*
* @author Finesse
* @package Src\Services
*/
class WebfontCSSGenerator
{
/**
* The name of the fonts directory in the public directory. May contain slashes for subdirectories.
*/
const FONTS_DIRECTORY = 'fonts';

/**
* @var array[] Information about available fonts from settings
*/
protected $fonts;

/**
* @var string The URL of the root fonts directory. Doesn't end with slash.
*/
protected $fontsDirectoryURL;

/**
* @param array[] $availableFonts Information about available fonts from settings
* @param string $rootURL The site root URL. With or without a domain and a protocol.
*/
public function __construct(array $availableFonts = [], string $rootURL = '')
{
$this->fonts = $availableFonts;
$this->fontsDirectoryURL = rtrim($rootURL, '/').'/'.static::FONTS_DIRECTORY;
}

/**
* Makes CSS code for the given families.
*
* @param string[][] $requestedFamilies The list of families. The indexes are families names, the values are lists
* of family styles. The styles must have format `[0-9]+i?`. Example:
* <pre>
* [
* 'Open Sans' => ['400', '700'],
* 'Roboto' => ['100', '100i', '400', '400i']
* ]
* </pre>
* @return string
* @throws \InvalidArgumentException
*/
public function makeCSS(array $requestedFamilies): string
{
$cssCode = '';

foreach ($requestedFamilies as $fontName => $styles) {
$cssCode .= $this->makeFontFamilyCSS($fontName, $styles);
}

return $cssCode;
}

/**
* Makes CSS code for the given font family.
*
* @param string $name Family name
* @param string[] $styles Font styles. The styles must have format `[0-9]+i?`.
* @return string
* @throws \InvalidArgumentException
*/
protected function makeFontFamilyCSS(string $name, array $styles = ['400']): string
{
$cssCode = '';
$readyStyles = [];

foreach ($styles as $style) {
if (isset($readyStyles[$style])) {
continue;
}

$styleCssCode = $this->makeFontStyleCSS($name, $style);
if ($styleCssCode !== '') {
$cssCode .= $styleCssCode."\n";
}

$readyStyles[$style] = true;
}

return $cssCode;
}

/**
* Makes CSS code for the given font style.
*
* @param string $familyName Font family name
* @param string $styleName Font style. The styles must have format `[0-9]+i?`.
* @return string
* @throws \InvalidArgumentException
*/
protected function makeFontStyleCSS(string $familyName, string $styleName): string
{
// Check the $styleName argument
if (!preg_match('/^([0-9]+)(i?)$/', $styleName, $matches)) {
throw new \InvalidArgumentException("The font style name `$styleName` can't be recognized");
}
$weight = (int)$matches[1];
$isItalic = !empty($matches[2]);

// Does the style exist in the configuration?
if (!isset($this->fonts[$familyName]['styles'][$styleName])) {
return '';
}
$familyInfo = $this->fonts[$familyName];
$styleInfo = $familyInfo['styles'][$styleName];

// Does the style has any font files?
$files = $this->getFontFilesURLs($familyName, $styleName);
if (empty($files)) {
return '';
}

// Building CSS code
$sources = [];
if (!($styleInfo['forbidLocal'] ?? $familyInfo['forbidLocal'] ?? false)) {
$sources[] = "local(".CSSHelpers::formatString($familyName).")";
}
if (isset($files['eot'])) {
$sources[] = "url(".CSSHelpers::formatString($files['eot'])."?#iefix) format('embedded-opentype')";
}
if (isset($files['woff2'])) {
$sources[] = "url(".CSSHelpers::formatString($files['woff2']).") format('woff2')";
}
if (isset($files['woff'])) {
$sources[] = "url(".CSSHelpers::formatString($files['woff']).") format('woff')";
}
if (isset($files['ttf'])) {
$sources[] = "url(".CSSHelpers::formatString($files['ttf']).") format('truetype')";
}
if (isset($files['svg'])) {
$sources[] = "url(".CSSHelpers::formatString($files['svg'])."#webfontregular) format('svg')";
}

return "@font-face {\n"
. "\tfont-family: ".CSSHelpers::formatString($familyName).";\n"
. "\tfont-weight: $weight;\n"
. "\tfont-style: ".($isItalic ? 'italic' : 'normal').";\n"
. (isset($files['eot']) ? "\tsrc: url(".CSSHelpers::formatString($files['oet']).");\n" : '')
. "\tsrc: ".implode(', ', $sources).";\n"
. "}";
}

protected function getFontFilesURLs(string $fontName, string $style)
{
// todo: Implement
return ['woff2' => $this->fontsDirectoryURL.'/test.woff2'];

if (!isset($this->fonts[$fontName]['styles'][$style])) {
return [];
}

$path = [$this->fontsDirectoryURL];

if (isset($this->fonts[$fontName]['directory'])) {
$pathComponents[] = $this->fonts[$fontName]['directory'];
}
}
}
13 changes: 12 additions & 1 deletion tests/Functional/CssGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ class CssGeneratorTest extends BaseTestCase
*/
public function testStatusAndContentType()
{
$response = $this->runApp('GET', '/css');
$response = $this->runApp('GET', '/css?family=Open+Sans:400,700');

$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue((bool)preg_match('~^text/css(;|$)~', $response->getHeaderLine('Content-Type')));
}

/**
* Tests that the route returns an error status with bad requests
*/
public function testBadRequests()
{
$this->assertEquals(422, $this->runApp('GET', '/css?family=')->getStatusCode());
$this->assertEquals(422, $this->runApp('GET', '/css?family=|Open+Sans:400,700')->getStatusCode());
$this->assertEquals(422, $this->runApp('GET', '/css?family=:400,700')->getStatusCode());
$this->assertEquals(422, $this->runApp('GET', '/css?family=Open+Sans:foo')->getStatusCode());
}
}

0 comments on commit 4ce93e2

Please sign in to comment.