Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Classes/Controller/ReadabilityController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\JsonResponse;
use WebVision\DeeplWrite\Readability\ReadabilityCalculatorFactory;

final class ReadabilityController
{
public function __construct(private readonly ReadabilityCalculatorFactory $factory)
{
}

public function calculate(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getParsedBody();
$readabilityCalculator = $this->factory->fromLanguage($data['language']);
$readabilityResult = $readabilityCalculator->calculateReadability(strip_tags($data['text'] ?? ''));
return new JsonResponse($readabilityResult->jsonSerialize());
}
}
48 changes: 48 additions & 0 deletions Classes/Readability/Calculator/AbstractReadabilityCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability\Calculator;

use Org\Heigl\Hyphenator\Hyphenator;
use WebVision\DeeplWrite\Readability\ReadabilityCalculatorInterface;

abstract class AbstractReadabilityCalculator implements ReadabilityCalculatorInterface
{
protected const LANGUAGE = 'not-supported';
protected const SENTENCE_SPLIT = '/([!\.\?] )/';
protected const HYPHENATED_SPLIT = '/([(\s)+!\.\?|])/';

final protected function countSentences(string $text): int
{
$sentences = preg_split(self::SENTENCE_SPLIT, $text);
if ($sentences === false) {
return 0;
}
return count($sentences);
}

protected function countWords(string $text): int
{
return str_word_count($text);
}

final protected function countSyllables(string $text): int
{
$hyphenator = new Hyphenator();
$hyphenator->getOptions()->setHyphen('|');
$result = $hyphenator->hyphenate($text);
$splitted = preg_split(self::HYPHENATED_SPLIT, $result);
return count($splitted);
}

protected function countCharacters(string $text): int
{
return mb_strlen($text);
}

public function getLanguage(): string
{
return static::LANGUAGE;
}
}
68 changes: 68 additions & 0 deletions Classes/Readability/Calculator/FleschKincaidEnglish.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability\Calculator;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use WebVision\DeeplWrite\Readability\Result\ReadabilityResult;

/**
* This class is an implementation generating the Flesch Reading Ease score for German.
* It calculates as follows:
*
* FRE = 206.835 - (1.015 * Average sentence Length (ASL)) - (84.6 * Average word length (AWL))
*
* ASL = (number of words) / (number of sentences)
* ASW = (number of syllables) / (number of words)
*
* The corresponding score is between 0 and 100, where
* * 0 means really difficult to read
* * 100 means really easy to read
*
* For a better overview of the different scoring levels,
* @see https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests#Flesch_reading_ease
*/
#[AsTaggedItem('deepl.readability')]
final class FleschKincaidEnglish extends AbstractReadabilityCalculator
{
protected const LANGUAGE = 'en-us';
public function calculateReadability(string $text): ReadabilityResult
{
$sentences = $this->countSentences($text);
$words = $this->countWords($text);
$syllables = $this->countSyllables($text);
$characters = $this->countCharacters($text);
return new ReadabilityResult(
$text,
$sentences,
$words,
$syllables,
$characters,
$this->calculateScore($words, $sentences, $syllables)
);
}

private function calculateScore(
int $words,
int $sentences,
int $syllables
): float {
if ($sentences <= 0) {
$sentences = 1;
}
if ($words <= 0) {
throw new \InvalidArgumentException(
'The number of words can not be negative or zero!',
1757680362
);
}

// Too easy sentences and short texts COULD result in calculating a value above 100. In this case
// set the result to 100, as this is the maximum.
// This is a known issue in this formula, but can be ignored for a quick overview, as
// 100 means very easy to read.
$fleschKincaid = 206.835 - 1.015 * ($words/$sentences) - (84.6 * $syllables/$words);
return ($fleschKincaid <= 100.0) ? $fleschKincaid : 100.0;
}
}
65 changes: 65 additions & 0 deletions Classes/Readability/Calculator/FleschKincaidGerman.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability\Calculator;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use WebVision\DeeplWrite\Readability\Result\ReadabilityResult;

/**
* This class is an implementation generating the Flesch Reading Ease score for German.
* It calculates as follows:
*
* FRE = 180 - (Average sentence Length (ASL)) - (58.5 * Average word length (AWL))
*
* ASL = (number of words) / (number of sentences)
* ASW = (number of syllables) / (number of words)
*
* The corresponding score is between 0 and 100, where
* * 0 means really difficult to read
* * 100 means really easy to read
*
* For a better overview of the different scoring levels,
* @see https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests#Flesch_reading_ease
*
* For German calculation,
* @see https://de.wikipedia.org/wiki/Lesbarkeitsindex#F%C3%BCr_Deutsch
*/
#[AsTaggedItem('deepl.readability')]
final class FleschKincaidGerman extends AbstractReadabilityCalculator
{
protected const LANGUAGE = 'de';
public function calculateReadability(string $text): ReadabilityResult
{
$sentences = $this->countSentences($text);
$words = $this->countWords($text);
$syllables = $this->countSyllables($text);
$characters = $this->countCharacters($text);
return new ReadabilityResult(
$text,
$sentences,
$words,
$syllables,
$characters,
$this->calculateScore($words, $sentences, $syllables)
);
}

private function calculateScore(
int $words,
int $sentences,
int $syllables
): float {
if ($sentences <= 0) {
$sentences = 1;
}
if ($words <= 0) {
throw new \InvalidArgumentException(
'The number of words can not be negative or zero!',
1757679534
);
}
return 180 - ($words/$sentences) - (58.5 * $syllables/$words);
}
}
17 changes: 17 additions & 0 deletions Classes/Readability/ReadabilityCalculatorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability;

final class ReadabilityCalculatorFactory
{
public function __construct(private readonly ReadabilityCalculatorRegistryInterface $registry)
{
}

public function fromLanguage(string $language): ReadabilityCalculatorInterface
{
return $this->registry->findByLanguage($language);
}
}
13 changes: 13 additions & 0 deletions Classes/Readability/ReadabilityCalculatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability;

use WebVision\DeeplWrite\Readability\Result\ReadabilityResult;

interface ReadabilityCalculatorInterface
{
public function getLanguage(): string;
public function calculateReadability(string $text): ReadabilityResult;
}
32 changes: 32 additions & 0 deletions Classes/Readability/ReadabilityCalculatorRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability;

final class ReadabilityCalculatorRegistry implements ReadabilityCalculatorRegistryInterface
{
/**
* @var array<ReadabilityCalculatorInterface>
*/
private array $services;
public function __construct(iterable $calculators)
{
foreach ($calculators as $calculator) {
$this->services[] = $calculator;
}
}

public function findByLanguage(string $language): ReadabilityCalculatorInterface
{
foreach ($this->services as $service) {
if ($service->getLanguage() === $language) {
return $service;
}
}
throw new \InvalidArgumentException(
sprintf('No service found for langauge "%s"', $language),
1757686580
);
}
}
10 changes: 10 additions & 0 deletions Classes/Readability/ReadabilityCalculatorRegistryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability;

interface ReadabilityCalculatorRegistryInterface
{
public function findByLanguage(string $language): ReadabilityCalculatorInterface;
}
46 changes: 46 additions & 0 deletions Classes/Readability/Result/ReadabilityResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Readability\Result;

/**
* Represents the result of a readability analysis performed on a given text.
* It provides metrics such as sentence, word, syllable, and character counts,
* as well as a calculated readability score and averages per sentence or word.
*/
final class ReadabilityResult implements \JsonSerializable
{
public function __construct(
public readonly string $text,
public readonly int $sentences,
public readonly int $words,
public readonly int $syllables,
public readonly int $characters,
public readonly float $score
) {
}

public function getAverageWordsPerSentence(): float
{
return round($this->words/$this->sentences, 2);
}

public function getAverageSyllablesPerWord(): float
{
return round($this->syllables/$this->words, 2);
}

public function jsonSerialize(): array
{
return [
'sentences' => $this->sentences,
'words' => $this->words,
'syllables' => $this->syllables,
'characters' => $this->characters,
'avgSyllables' => $this->getAverageSyllablesPerWord(),
'avgWords' => $this->getAverageWordsPerSentence(),
'score' => $this->score,
];
}
}
10 changes: 10 additions & 0 deletions Classes/Service/ReadingEaseService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite\Service;

final class ReadingEaseService
{

}
6 changes: 6 additions & 0 deletions Configuration/Backend/AjaxRoutes.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use WebVision\DeeplWrite\Controller\CkEditorController;
use WebVision\DeeplWrite\Controller\ReadabilityController;

return [
'deeplwrite_ckeditor_configuration' => [
Expand All @@ -18,4 +19,9 @@
'target' => CkEditorController::class . '::getEditMaskAction',
'methods' => ['GET'],
],
'deeplwrite_readability' => [
'path' => '/deepl-write/readability/calculate',
'target' => ReadabilityController::class . '::calculate',
'methods' => ['post'],
],
];
13 changes: 13 additions & 0 deletions Configuration/Services.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace WebVision\DeeplWrite;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use WebVision\DeeplWrite\Readability\ReadabilityCalculatorInterface;

return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder): void {
$containerBuilder->registerForAutoconfiguration(ReadabilityCalculatorInterface::class)->addTag('deepl.readability');
};
13 changes: 13 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:

WebVision\DeeplWrite\Controller\CkEditorController:
public: true
WebVision\DeeplWrite\Controller\ReadabilityController:
public: true

WebVision\DeeplWrite\Configuration\ConfigurationInterface:
class: WebVision\DeeplWrite\Configuration\Configuration
Expand All @@ -36,3 +38,14 @@ services:
identifier: 'deepl-write/translation-dropdown'
event: WebVision\Deepl\Base\Event\ViewHelpers\ModifyInjectVariablesViewHelperEvent
after: 'deepl-base/default-translation, deepltranslate-core/translation-dropdown'

WebVision\DeeplWrite\Readability\Calculator\FleschKincaidEnglish:
tags:
- name: deepl.readability
WebVision\DeeplWrite\Readability\Calculator\FleschKincaidGerman:
tags:
- name: deepl.readability

WebVision\DeeplWrite\Readability\ReadabilityCalculatorRegistry:
arguments:
- !tagged_iterator deepl.readability
Loading