diff --git a/Classes/Controller/ReadabilityController.php b/Classes/Controller/ReadabilityController.php new file mode 100644 index 0000000..702dcfb --- /dev/null +++ b/Classes/Controller/ReadabilityController.php @@ -0,0 +1,25 @@ +getParsedBody(); + $readabilityCalculator = $this->factory->fromLanguage($data['language']); + $readabilityResult = $readabilityCalculator->calculateReadability(strip_tags($data['text'] ?? '')); + return new JsonResponse($readabilityResult->jsonSerialize()); + } +} diff --git a/Classes/Readability/Calculator/AbstractReadabilityCalculator.php b/Classes/Readability/Calculator/AbstractReadabilityCalculator.php new file mode 100644 index 0000000..7860566 --- /dev/null +++ b/Classes/Readability/Calculator/AbstractReadabilityCalculator.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/Classes/Readability/Calculator/FleschKincaidEnglish.php b/Classes/Readability/Calculator/FleschKincaidEnglish.php new file mode 100644 index 0000000..cf60d24 --- /dev/null +++ b/Classes/Readability/Calculator/FleschKincaidEnglish.php @@ -0,0 +1,68 @@ +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; + } +} diff --git a/Classes/Readability/Calculator/FleschKincaidGerman.php b/Classes/Readability/Calculator/FleschKincaidGerman.php new file mode 100644 index 0000000..71ce417 --- /dev/null +++ b/Classes/Readability/Calculator/FleschKincaidGerman.php @@ -0,0 +1,65 @@ +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); + } +} diff --git a/Classes/Readability/ReadabilityCalculatorFactory.php b/Classes/Readability/ReadabilityCalculatorFactory.php new file mode 100644 index 0000000..534b416 --- /dev/null +++ b/Classes/Readability/ReadabilityCalculatorFactory.php @@ -0,0 +1,17 @@ +registry->findByLanguage($language); + } +} diff --git a/Classes/Readability/ReadabilityCalculatorInterface.php b/Classes/Readability/ReadabilityCalculatorInterface.php new file mode 100644 index 0000000..e7e7b90 --- /dev/null +++ b/Classes/Readability/ReadabilityCalculatorInterface.php @@ -0,0 +1,13 @@ + + */ + 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 + ); + } +} diff --git a/Classes/Readability/ReadabilityCalculatorRegistryInterface.php b/Classes/Readability/ReadabilityCalculatorRegistryInterface.php new file mode 100644 index 0000000..99a69b2 --- /dev/null +++ b/Classes/Readability/ReadabilityCalculatorRegistryInterface.php @@ -0,0 +1,10 @@ +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, + ]; + } +} diff --git a/Classes/Service/ReadingEaseService.php b/Classes/Service/ReadingEaseService.php new file mode 100644 index 0000000..9f73496 --- /dev/null +++ b/Classes/Service/ReadingEaseService.php @@ -0,0 +1,10 @@ + [ @@ -18,4 +19,9 @@ 'target' => CkEditorController::class . '::getEditMaskAction', 'methods' => ['GET'], ], + 'deeplwrite_readability' => [ + 'path' => '/deepl-write/readability/calculate', + 'target' => ReadabilityController::class . '::calculate', + 'methods' => ['post'], + ], ]; diff --git a/Configuration/Services.php b/Configuration/Services.php new file mode 100644 index 0000000..556c2c3 --- /dev/null +++ b/Configuration/Services.php @@ -0,0 +1,13 @@ +registerForAutoconfiguration(ReadabilityCalculatorInterface::class)->addTag('deepl.readability'); +}; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index be5b1f0..04625c5 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -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 @@ -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 diff --git a/README.md b/README.md index 6020203..3dd5b25 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,28 @@ configured for both extension in their respective extension configuration. > api key is required for this extension, which can also be used for the > `deepltranslate-core` or using there a free key. +### RTE Styling implementation + +A styling implementation is added to improve your writing directly inside your +RTE. This needs some adjustments to your RTE configuration: + +```yaml +editor: + config: + importModules: + # your already existing modules + - { module: '@web-vision/deepl-write/deeplwrite-plugin.js', exports: [ 'Deeplwrite' ] } + toolbar: + items: + # Your existing configuration + - '|' + - deeplwrite +``` + +This adds a button to the RTE controls, which allows you to use the overlay for +the writing style. The button is shown disabled, if your API key is not allowed +using the DeepL Write API or no API key is set. + ## Sponsors We appreciate very much the sponsorships of the developments and features in diff --git a/Resources/Private/Backend/Templates/CkEditor/Edit.html b/Resources/Private/Backend/Templates/CkEditor/Edit.html index 367031f..af86494 100644 --- a/Resources/Private/Backend/Templates/CkEditor/Edit.html +++ b/Resources/Private/Backend/Templates/CkEditor/Edit.html @@ -4,6 +4,67 @@ xmlns:deeplWrite="http://typo3.org/ns/WebVision/DeeplWrite/ViewHelpers" data-namespace-typo3-fluid="true" > +

@@ -88,12 +149,24 @@

- - + +
+ + 65% +
+
- - + +
+ + 0% +
+
diff --git a/Resources/Private/Language/de.locallang_cke.xlf b/Resources/Private/Language/de.locallang_cke.xlf index 6d72862..8f86920 100644 --- a/Resources/Private/Language/de.locallang_cke.xlf +++ b/Resources/Private/Language/de.locallang_cke.xlf @@ -47,6 +47,14 @@ Apply changes Änderungen übernehmen + + Original | Reading Index (Flesh-Kincaid) + Original | Lesbarkeitsindex (Flesh-Kincaid) + + + Optimized | Reading Index (Flesh-Kincaid) + Optimiert | Lesbarkeitsindex (Flesh-Kincaid) + Default Standard diff --git a/Resources/Private/Language/locallang_cke.xlf b/Resources/Private/Language/locallang_cke.xlf index 138e166..817b070 100644 --- a/Resources/Private/Language/locallang_cke.xlf +++ b/Resources/Private/Language/locallang_cke.xlf @@ -37,6 +37,12 @@ Apply changes + + Original | Reading Index (Flesh-Kincaid) + + + Optimized | Reading Index (Flesh-Kincaid) + Default diff --git a/Resources/Public/JavaScript/Ckeditor/deeplwrite-plugin.js b/Resources/Public/JavaScript/Ckeditor/deeplwrite-plugin.js index 18bc99b..c3ea1a5 100644 --- a/Resources/Public/JavaScript/Ckeditor/deeplwrite-plugin.js +++ b/Resources/Public/JavaScript/Ckeditor/deeplwrite-plugin.js @@ -24,9 +24,14 @@ export class Deeplwrite extends Plugin { .get() .then(async function (response) { const deeplConfiguration = await response.resolve(); + console.log(deeplConfiguration); const content = document.createElement('div'); content.innerHTML = deeplConfiguration; - content.querySelector('#original').value = editor.getData(); + const originalContent = editor.getData(); + const originalReadability = content.querySelector('#original-readability'); + Deeplwrite.calculateReadability(originalContent, editor.locale.contentLanguage, originalReadability); + + content.querySelector('#original').value = originalContent; const optimizeModal = Modal.advanced({ content: content, size: Modal.sizes.large, @@ -59,6 +64,8 @@ export class Deeplwrite extends Plugin { .then(async function (response){ const value = await response.resolve(); content.querySelector('#optimized').value = value.result; + const optimizedReadability = content.querySelector('#optimized-readability'); + Deeplwrite.calculateReadability(value.result, editor.locale.contentLanguage, optimizedReadability); }) } }, @@ -93,4 +100,27 @@ export class Deeplwrite extends Plugin { return button; }); } + + /** + * @param {string} text + * @param {string} language + * @param {Element} element + */ + static calculateReadability(text, language, element) { + + new AjaxRequest(TYPO3.settings.ajaxUrls.deeplwrite_readability) + .post({ + text: text, + language: language + }) + .then(async (response) => { + const readability = await response.resolve(); + const value = Math.max(0, Math.min(100, Number(readability.score) || 0)).toFixed(2); + element.style.setProperty('--value', value); + element.setAttribute('aria-valuenow', String(value)); + const label = element.querySelector('.label'); + if (label) label.textContent = `${value}%`; + return await response.resolve(); + }) + } } diff --git a/composer.json b/composer.json index 4528307..88e2ccc 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "require": { "php": "^8.1 || ^8.2 || ^8.3 || ^8.4", "ext-dom": "*", + "org_heigl/hyphenator": "^3.1", "typo3/cms-backend": "^12.4 || ^13.4", "typo3/cms-core": "^12.4 || ^13.4", "web-vision/deepl-base": "^1.0.1",