├── tests ├── Unit │ ├── Ispell │ │ ├── fixtures │ │ │ ├── lib │ │ │ │ └── ispell │ │ │ │ │ ├── english.aff │ │ │ │ │ └── russian.aff │ │ │ ├── input.txt │ │ │ ├── bin │ │ │ │ └── ispell.sh │ │ │ └── check.txt │ │ └── IspellTest.php │ ├── Source │ │ ├── fixtures │ │ │ ├── test.txt │ │ │ └── test.xliff │ │ ├── Filter │ │ │ ├── StripAllFilterTest.php │ │ │ └── HtmlFilterTest.php │ │ ├── IconvSourceTest.php │ │ ├── StringSourceTest.php │ │ ├── XliffSourceTest.php │ │ ├── HtmlSourceTest.php │ │ └── FileSourceTest.php │ ├── Aspell │ │ ├── fixtures │ │ │ ├── input.txt │ │ │ ├── check_sv.txt │ │ │ ├── dicts.txt │ │ │ └── check.txt │ │ └── AspellTest.php │ ├── Hunspell │ │ ├── fixtures │ │ │ ├── input.txt │ │ │ ├── check.txt │ │ │ ├── bin │ │ │ │ └── hunspell.php │ │ │ └── dicts.txt │ │ └── HunspellTest.php │ ├── IssueTest.php │ ├── Helper │ │ └── LanguageMapperTest.php │ └── ExternalSpellerTest.php └── Functional │ ├── Aspell │ ├── fixtures │ │ └── custom.en.pws │ └── AspellTest.php │ └── AspellTestCase.php ├── src ├── Exception │ ├── SourceException.php │ ├── EnvironmentException.php │ ├── RuntimeException.php │ ├── PhpSpellerException.php │ └── ExternalProgramFailedException.php ├── Source │ ├── Filter │ │ ├── Filter.php │ │ ├── StripAllFilter.php │ │ └── HtmlFilter.php │ ├── EncodingAwareSource.php │ ├── MetaSource.php │ ├── StringSource.php │ ├── IconvSource.php │ ├── FileSource.php │ ├── HtmlSource.php │ └── XliffSource.php ├── Dictionary.php ├── Speller.php ├── Issue.php ├── Helper │ └── LanguageMapper.php ├── Aspell │ └── Aspell.php ├── Hunspell │ └── Hunspell.php ├── Ispell │ └── Ispell.php └── ExternalSpeller.php ├── .github ├── dependabot.yaml └── workflows │ └── build.yml ├── LICENSE ├── composer.json ├── examples └── spellcheck.php ├── CHANGELOG.md └── README.md /tests/Unit/Ispell/fixtures/lib/ispell/english.aff: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Unit/Ispell/fixtures/lib/ispell/russian.aff: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Unit/Source/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | Tiger, tiger, burning bright -------------------------------------------------------------------------------- /tests/Functional/Aspell/fixtures/custom.en.pws: -------------------------------------------------------------------------------- 1 | personal_ws-1.1 en 1 2 | Versicherungspolice -------------------------------------------------------------------------------- /tests/Unit/Aspell/fixtures/input.txt: -------------------------------------------------------------------------------- 1 | Tigr, tiger, burning страх 2 | In theforests of the night, 3 | What imortal hand or eey 4 | CCould frame thy fearful symmetry? -------------------------------------------------------------------------------- /tests/Unit/Ispell/fixtures/input.txt: -------------------------------------------------------------------------------- 1 | Tigr, tiger, burning страх 2 | In theforests of the night, 3 | What imortal hand or eey 4 | CCould frame thy fearful symmetry? -------------------------------------------------------------------------------- /tests/Unit/Hunspell/fixtures/input.txt: -------------------------------------------------------------------------------- 1 | Tigr, tiger, burning страх 2 | In theforests of the night, 3 | What imortal hand or eey 4 | CCould frame thy fearful symmetry? -------------------------------------------------------------------------------- /tests/Unit/Aspell/fixtures/check_sv.txt: -------------------------------------------------------------------------------- 1 | @(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1) 2 | & S:t 23 0: St, Set, Sot, Söt, Stl, Stå 3 | ? Petersburg 0 4: Peters 4 | * 5 | * 6 | * 7 | -------------------------------------------------------------------------------- /tests/Unit/Ispell/fixtures/bin/ispell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # ispell binary stub 4 | # 5 | 6 | folder=$(dirname $0) 7 | 8 | case "$*" in 9 | *'-a'*) 10 | cat "$folder/../check.txt" 11 | ;; 12 | esac 13 | exit 0 14 | -------------------------------------------------------------------------------- /tests/Unit/Aspell/fixtures/dicts.txt: -------------------------------------------------------------------------------- 1 | en 2 | en-variant_0 3 | en-variant_1 4 | en_GB 5 | en_GB-ise 6 | en_GB-ise-w_accents 7 | en_GB-ise-wo_accents 8 | en_GB-ize 9 | en_GB-ize-w_accents 10 | en_GB-ize-wo_accents 11 | ru 12 | ru-ye 13 | ru-yeyo 14 | ru-yo -------------------------------------------------------------------------------- /tests/Unit/Ispell/fixtures/check.txt: -------------------------------------------------------------------------------- 1 | @(#) International Ispell Version 3.4.00 8 Feb 2015 2 | & Tigr 2 0: Tier, Tiger 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | 11 | * 12 | & theforests 2 3: the forests, the-forests 13 | * 14 | * 15 | * 16 | 17 | * 18 | & imortal 2 5: immortal, mortal 19 | * 20 | * 21 | & eey 16 21: bey, dey, eeg, eel, eely, eery, Ely, eye, fey, gey, hey, key, ley, ney, rey, sey 22 | 23 | & CCould 1 0: Could 24 | * 25 | * 26 | * 27 | * 28 | -------------------------------------------------------------------------------- /src/Exception/SourceException.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Exception; 14 | 15 | /** 16 | * Fail to read from text source. 17 | * 18 | * @since 1.6 19 | */ 20 | class SourceException extends RuntimeException 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/EnvironmentException.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Exception; 14 | 15 | /** 16 | * Environment misconfiguration. 17 | * 18 | * @since 1.6 19 | */ 20 | class EnvironmentException extends RuntimeException 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Exception; 14 | 15 | /** 16 | * Runtime exception. 17 | * 18 | * @since 1.6 19 | */ 20 | class RuntimeException extends \RuntimeException implements PhpSpellerException 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/PhpSpellerException.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Exception; 14 | 15 | use Throwable; 16 | 17 | /** 18 | * Common interface for all Speller exceptions. 19 | * 20 | * @since 1.6 21 | */ 22 | interface PhpSpellerException extends Throwable 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Hunspell/fixtures/check.txt: -------------------------------------------------------------------------------- 1 | @(#) International Ispell Version 3.2.06 (but really Hunspell 1.3.2) 2 | & Tigr 9 0: Ti gr, Ti-gr, Tiger, Trig, Tier, Tigris, Grit, Tigress, Tagore 3 | * 4 | * 5 | # страх 21 6 | 7 | * 8 | & theforests 8 3: the forests, the-forests, reforests, deforests, reforest, therefore, disafforest, forestland 9 | * 10 | * 11 | * 12 | 13 | * 14 | & imortal 9 5: mortal, immortal, i mortal, immoral, important, immemorial, mortar, imitable, immutable 15 | * 16 | * 17 | & eey 10 21: ea, eye, eery, eel, hey, bey, fey, eek, key, Key 18 | 19 | & CCould 5 0: C Could, Could, Cold, Cuckold, Cloudy 20 | * 21 | * 22 | * 23 | * -------------------------------------------------------------------------------- /tests/Unit/Aspell/fixtures/check.txt: -------------------------------------------------------------------------------- 1 | @(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1) 2 | & Tigr 34 0: Ti gr, Ti-gr, Tiger, Tier 3 | * 4 | * 5 | 6 | * 7 | & theforests 10 3: the forests, the-forests, reforests, theorists, deforests, forests, theorist's, afforests, Forest's, forest's 8 | * 9 | * 10 | * 11 | 12 | * 13 | & imortal 6 5: immortal, mortal, immortally, immortals, immoral, immortal's 14 | * 15 | * 16 | & eey 13 21: eye, EEO, EEC, EEG, eek, eel, Key, bey, fey, hey, key, e'er, e'en 17 | 18 | & CCould 17 0: Could, Cold, Gould, Cloud, Coiled, Cloudy, Coaled, Cooled, Colt, Clod, Cult, Gold, Scold, Mould, Would, Clout, Clued 19 | * 20 | * 21 | * 22 | * 23 | -------------------------------------------------------------------------------- /tests/Unit/Hunspell/fixtures/bin/hunspell.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source\Filter; 14 | 15 | /** 16 | * Filter interface. 17 | * 18 | * Filters are used to filter out text, which does not require checking. 19 | * 20 | * @since 1.2 21 | */ 22 | interface Filter 23 | { 24 | /** 25 | * Filter string. 26 | * 27 | * @param string $string String to be filtered. 28 | * 29 | * @return string Filtered string. 30 | * 31 | * @since 1.2 32 | */ 33 | public function filter(string $string): string; 34 | } 35 | -------------------------------------------------------------------------------- /src/Dictionary.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller; 14 | 15 | /** 16 | * Representing any dictionary for a certain speller 17 | */ 18 | class Dictionary 19 | { 20 | /** @var string */ 21 | private $dictionaryPath; 22 | 23 | /** 24 | * @param string $path 25 | */ 26 | public function __construct(string $path) 27 | { 28 | $this->dictionaryPath = $path; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getPath(): string 35 | { 36 | return $this->dictionaryPath; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/Source/Filter/StripAllFilterTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source\Filter; 14 | 15 | use Mekras\Speller\Source\Filter\StripAllFilter; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Source\Filter\StripAllFilter 20 | * 21 | * @covers \Mekras\Speller\Source\Filter\StripAllFilter 22 | */ 23 | class StripAllFilterTest extends TestCase 24 | { 25 | /** 26 | * Test basic functional 27 | */ 28 | public function testBasics(): void 29 | { 30 | $filter = new StripAllFilter(); 31 | static::assertEquals(" \n\t ", $filter->filter("foo\n\tbar")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Source/Filter/StripAllFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source\Filter; 14 | 15 | /** 16 | * Filter which strips all input text 17 | * 18 | * All characters except new lines (\n), tabs (\t) and spaces will be replaces with spaces. 19 | * 20 | * @since 1.2 21 | */ 22 | class StripAllFilter implements Filter 23 | { 24 | /** 25 | * Filter string 26 | * 27 | * @param string $string string to be filtered 28 | * 29 | * @return string filtered string 30 | * 31 | * @since 1.2 32 | */ 33 | public function filter(string $string): string 34 | { 35 | return preg_replace('/\S/', ' ', $string); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Unit/IssueTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit; 14 | 15 | use Mekras\Speller\Issue; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Issue 20 | * 21 | * @covers \Mekras\Speller\Issue 22 | */ 23 | class IssueTest extends TestCase 24 | { 25 | /** 26 | * Test basic functions 27 | */ 28 | public function testBasics(): void 29 | { 30 | $issue = new Issue('foo'); 31 | static::assertEquals('foo', $issue->word); 32 | static::assertEquals(Issue::UNKNOWN_WORD, $issue->code); 33 | static::assertEquals([], $issue->suggestions); 34 | static::assertNull($issue->line); 35 | static::assertNull($issue->offset); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Source/EncodingAwareSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | use Mekras\Speller\Exception\SourceException; 16 | 17 | /** 18 | * Text source interface. 19 | * 20 | * @since 1.6 21 | * 22 | * @todo Remove in version 3.0. 23 | */ 24 | interface EncodingAwareSource 25 | { 26 | /** 27 | * Return source text encoding. 28 | * 29 | * @return string 30 | * 31 | * @since 1.6 32 | */ 33 | public function getEncoding(): string; 34 | 35 | /** 36 | * Return text as one string. 37 | * 38 | * @return string 39 | * 40 | * @throws SourceException Fail to read from text source. 41 | * @since 1.6 Throws {@see SourceException}. 42 | * @since 1.0 43 | */ 44 | public function getAsString(): string; 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/Source/IconvSourceTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source; 14 | 15 | use Mekras\Speller\Source\IconvSource; 16 | use Mekras\Speller\Source\StringSource; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | /** 20 | * Tests for Mekras\Speller\Source\IconvSource. 21 | * 22 | * @covers \Mekras\Speller\Source\IconvSource 23 | */ 24 | class IconvSourceTest extends TestCase 25 | { 26 | /** 27 | * Test basics. 28 | */ 29 | public function testBasics(): void 30 | { 31 | $source = new StringSource(iconv('utf-8', 'koi8-r', 'Привет'), 'koi8-r'); 32 | $converter = new IconvSource($source); 33 | static::assertEquals('UTF-8', $converter->getEncoding()); 34 | static::assertEquals('Привет', $converter->getAsString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Source/fixtures/test.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fooFoo 9 | 10 | 11 | bar 12 | 13 | Bar 14 | Bar 15 | {{ var }} 16 | 17 | 18 | 19 | 22 | 23 | 24 | baz 25 | 26 | Baz 29 | ]]> 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Functional/AspellTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Functional; 14 | 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * Check if aspell is installed otherwise skip tests 19 | * 20 | * @package Mekras\Speller\Tests\Functional 21 | * @author icanhazstring 22 | */ 23 | class AspellTestCase extends TestCase 24 | { 25 | public static function setUpBeforeClass(): void 26 | { 27 | $userBinary = '/usr/bin/aspell'; 28 | $libBinary = '/usr/lib/aspell'; 29 | $sharedBinary = '/usr/share/aspell'; 30 | 31 | if (!(file_exists($userBinary) || file_exists($libBinary) || file_exists($sharedBinary))) { 32 | self::markTestSkipped('skipping tests - aspell binary not installed'); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Михаил Красильников 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Exception/ExternalProgramFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Exception; 14 | 15 | /** 16 | * External program execution failed. 17 | * 18 | * @since 1.6 19 | */ 20 | class ExternalProgramFailedException extends RuntimeException 21 | { 22 | /** 23 | * Create new exception. 24 | * 25 | * @param string $command Failed command 26 | * @param string $message Error output. 27 | * @param int $code Exit code 28 | * @param \Exception|null $previous Previous exception if any. 29 | * 30 | * @since 1.6 31 | */ 32 | public function __construct($command, $message = '', $code = 0, \Exception $previous = null) 33 | { 34 | parent::__construct( 35 | sprintf('Failed to execute "%s": %s', $command, $message), 36 | $code, 37 | $previous 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Source/MetaSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | /** 16 | * Base class for meta sources. 17 | * 18 | * @since 1.6 19 | */ 20 | abstract class MetaSource implements EncodingAwareSource 21 | { 22 | /** 23 | * Wrapped source. 24 | * 25 | * @var EncodingAwareSource 26 | */ 27 | protected $source; 28 | 29 | /** 30 | * Create wrapper source. 31 | * 32 | * @param EncodingAwareSource $source Original source. 33 | * 34 | * @since 1.6 35 | */ 36 | public function __construct(EncodingAwareSource $source) 37 | { 38 | $this->source = $source; 39 | } 40 | 41 | /** 42 | * Return source text encoding. 43 | * 44 | * @return string 45 | * 46 | * @since 1.6 47 | */ 48 | public function getEncoding(): string 49 | { 50 | return $this->source->getEncoding(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Speller.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller; 14 | 15 | use Mekras\Speller\Source\EncodingAwareSource; 16 | 17 | /** 18 | * Speller interface. 19 | * 20 | * @since 1.0 21 | */ 22 | interface Speller 23 | { 24 | /** 25 | * Check text. 26 | * 27 | * Check given text and return an array of spelling issues. 28 | * 29 | * @param EncodingAwareSource $source Text source to check. 30 | * @param array $languages List of languages used in text (IETF language tag). 31 | * 32 | * @return Issue[] 33 | * 34 | * @link http://tools.ietf.org/html/bcp47 35 | * @since 1.0 36 | */ 37 | public function checkText(EncodingAwareSource $source, array $languages): array; 38 | 39 | /** 40 | * Return list of supported languages. 41 | * 42 | * @return string[] 43 | * 44 | * @since 1.0 45 | */ 46 | public function getSupportedLanguages(): array; 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mekras/php-speller", 3 | "description": "PHP spell check library", 4 | "type": "library", 5 | "keywords": [ 6 | "spelling", 7 | "aspell", 8 | "hunspell", 9 | "ispell" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Михаил Красильников", 15 | "email": "m.krasilnikov@yandex.ru" 16 | }, 17 | { 18 | "name": "Andreas Frömer", 19 | "email": "blubb0r05+github@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.4", 24 | "ext-dom": "*", 25 | "ext-iconv": "*", 26 | "ext-libxml": "*", 27 | "symfony/process": "^5.4.22 || ^6.0" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^9.6.10", 31 | "phpspec/prophecy-phpunit": "^2.0", 32 | "squizlabs/php_codesniffer": "^3.7.2" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Mekras\\Speller\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Mekras\\Speller\\Tests\\": "tests" 42 | } 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Source/StringSourceTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source; 14 | 15 | use Mekras\Speller\Source\StringSource; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Source\StringSource. 20 | * 21 | * @covers \Mekras\Speller\Source\StringSource 22 | */ 23 | class StringSourceTest extends TestCase 24 | { 25 | /** 26 | * Test basics. 27 | */ 28 | public function testBasics(): void 29 | { 30 | $source = new StringSource('foo bar'); 31 | static::assertEquals('foo bar', $source->getAsString()); 32 | static::assertEquals('UTF-8', $source->getEncoding()); 33 | } 34 | 35 | /** 36 | * Test encoding. 37 | */ 38 | public function testEncoding(): void 39 | { 40 | $source = new StringSource('foo'); 41 | static::assertEquals('UTF-8', $source->getEncoding()); 42 | 43 | $source = new StringSource('foo', 'koi8-r'); 44 | static::assertEquals('koi8-r', $source->getEncoding()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Source/XliffSourceTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source; 14 | 15 | use Mekras\Speller\Source\XliffSource; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Source\XliffSource. 20 | * 21 | * @covers \Mekras\Speller\Source\XliffSource 22 | */ 23 | class XliffSourceTest extends TestCase 24 | { 25 | /** 26 | * Test basics. 27 | */ 28 | public function testBasics(): void 29 | { 30 | $source = new XliffSource(__DIR__ . '/fixtures/test.xliff'); 31 | static::assertEquals('UTF-8', $source->getEncoding()); 32 | $source->addFilter('#\{\{[^}]+\}\}#ums'); 33 | $lines = explode("\n", $source->getAsString()); 34 | static::assertCount(36, $lines); 35 | static::assertEquals('Foo', substr($lines[7], 17, 3)); 36 | static::assertEquals('Bar', substr($lines[12], 20, 3)); 37 | static::assertEquals('Bar', substr($lines[13], 20, 3)); 38 | static::assertStringNotContainsString('var', $lines[14]); 39 | static::assertEquals('Baz', substr($lines[27], 42, 3)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Helper/LanguageMapperTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Helper; 14 | 15 | use Mekras\Speller\Helper\LanguageMapper; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Helper\LanguageMapper 20 | * 21 | * @covers \Mekras\Speller\Helper\LanguageMapper 22 | */ 23 | class LanguageMapperTest extends TestCase 24 | { 25 | /** 26 | * Test basic mapping 27 | */ 28 | public function testBasics(): void 29 | { 30 | $mapper = new LanguageMapper(); 31 | $result = $mapper->map( 32 | ['de', 'en', 'ru'], 33 | ['de_DE', 'en_GB.UTF-8', 'en_US', 'ru_RU', 'ru'] 34 | ); 35 | static::assertEquals(['de_DE', 'en_GB.UTF-8', 'ru'], $result); 36 | } 37 | 38 | /** 39 | * Test preferred mapping 40 | */ 41 | public function testPreferred(): void 42 | { 43 | $mapper = new LanguageMapper(); 44 | $mapper->setPreferredMappings(['en' => ['en_US', 'en_GB']]); 45 | $result = $mapper->map( 46 | ['de', 'en', 'ru'], 47 | ['de_DE', 'en_GB.UTF-8', 'en_US', 'ru_RU', 'ru'] 48 | ); 49 | static::assertEquals(['de_DE', 'en_US', 'ru'], $result); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Source/StringSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | /** 16 | * String as a text source. 17 | * 18 | * @since 1.6 Implements EncodingAwareSource. 19 | * @since 1.0 20 | */ 21 | class StringSource implements EncodingAwareSource 22 | { 23 | /** 24 | * Source text. 25 | * 26 | * @var string 27 | */ 28 | private $text; 29 | 30 | /** 31 | * Text encoding. 32 | * 33 | * @var string 34 | */ 35 | private $encoding; 36 | 37 | /** 38 | * Create text source from string. 39 | * 40 | * @param string $text Source text. 41 | * @param string $encoding Text encoding (default to "UTF-8"). 42 | * 43 | * @since 1.6 New argument — $encoding. 44 | * @since 1.0 45 | */ 46 | public function __construct(string $text, string $encoding = 'UTF-8') 47 | { 48 | $this->text = $text; 49 | $this->encoding = $encoding; 50 | } 51 | 52 | /** 53 | * Return text as one string 54 | * 55 | * @return string 56 | * 57 | * @since 1.0 58 | */ 59 | public function getAsString(): string 60 | { 61 | return $this->text; 62 | } 63 | 64 | /** 65 | * Return source text encoding. 66 | * 67 | * @return string 68 | * 69 | * @since 1.6 70 | */ 71 | public function getEncoding(): string 72 | { 73 | return $this->encoding; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Unit/Source/HtmlSourceTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source; 14 | 15 | use Mekras\Speller\Source\HtmlSource; 16 | use Mekras\Speller\Source\StringSource; 17 | use Mekras\Speller\Exception\SourceException; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * Tests for Mekras\Speller\Source\HtmlSource. 22 | * 23 | * @covers \Mekras\Speller\Source\HtmlSource 24 | */ 25 | class HtmlSourceTest extends TestCase 26 | { 27 | /** 28 | * Test basics. 29 | */ 30 | public function testBasics(): void 31 | { 32 | $source = new HtmlSource('Bar Baz'); 33 | static::assertEquals(' Foo Bar Baz', $source->getAsString()); 34 | } 35 | 36 | /** 37 | * Encoding should be detected from meta tags. 38 | */ 39 | public function testEncoding(): void 40 | { 41 | $source = new HtmlSource( 42 | '' 43 | ); 44 | static::assertEquals('koi8-r', $source->getEncoding()); 45 | } 46 | 47 | /** 48 | * HtmlSource should throw SourceException on invalid HTML. 49 | */ 50 | public function testInvalidHtml(): void 51 | { 52 | $this->expectException(SourceException::class); 53 | $this->expectExceptionMessage('Opening and ending tag mismatch: a and b at 1:11'); 54 | 55 | new HtmlSource(new StringSource('')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Functional/Aspell/AspellTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Functional\Aspell; 14 | 15 | use Mekras\Speller\Aspell\Aspell; 16 | use Mekras\Speller\Dictionary; 17 | use Mekras\Speller\Issue; 18 | use Mekras\Speller\Source\StringSource; 19 | use Mekras\Speller\Tests\Functional\AspellTestCase; 20 | 21 | /** 22 | * Functional test with aspell 23 | * 24 | * @package Mekras\Speller\Tests\Functional\Aspell 25 | * @author icanhazstring 26 | */ 27 | class AspellTest extends AspellTestCase 28 | { 29 | /** 30 | * Functional testing with aspell to check if personal dictionary is working 31 | */ 32 | public function testPersonalDictionary(): void 33 | { 34 | // Take german word for testing purpose so we won't get any suggestion 35 | $source = new StringSource('Versicherungspolica'); 36 | 37 | $aspell = new Aspell(); 38 | $issues = $aspell->checkText($source, ['en']); 39 | 40 | $this->assertEmpty($issues[0]->suggestions); 41 | $this->assertEquals(Issue::UNKNOWN_WORD, $issues[0]->code); 42 | 43 | $aspell->setPersonalDictionary(new Dictionary(__DIR__ . '/fixtures/custom.en.pws')); 44 | $issues = $aspell->checkText($source, ['en']); 45 | 46 | $this->assertCount(1, $issues); 47 | $this->assertCount(1, $issues[0]->suggestions); 48 | $this->assertEquals(Issue::UNKNOWN_WORD, $issues[0]->code); 49 | $this->assertEquals('Versicherungspolice', $issues[0]->suggestions[0]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Pipeline 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | coding-standard: 10 | name: "Coding Standard" 11 | runs-on: "${{ matrix.os }}" 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ "ubuntu-latest" ] 16 | php: [ "7.4" ] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@2.25.5 23 | with: 24 | php-version: ${{ matrix.php }} 25 | tools: composer:v2 26 | extensions: ds 27 | 28 | - name: Install dependencies 29 | run: composer install --no-progress --prefer-dist --optimize-autoloader 30 | 31 | - name: Check codestyle 32 | run: vendor/bin/phpcs --standard=PSR12 src/ tests/ 33 | 34 | unit-tests: 35 | name: "Unit Tests" 36 | runs-on: "${{ matrix.os }}" 37 | continue-on-error: "${{ matrix.experimental }}" 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | php: [ "7.4", "8.0", "8.1", "8.2" ] 42 | os: [ "ubuntu-latest" ] 43 | experimental: [ false ] 44 | 45 | steps: 46 | - name: Install packages 47 | run: sudo apt-get install -y aspell aspell-en 48 | 49 | - name: Checkout 50 | uses: actions/checkout@v3 51 | 52 | - name: Setup PHP 53 | uses: shivammathur/setup-php@2.25.5 54 | with: 55 | php-version: ${{ matrix.php }} 56 | tools: composer:v2 57 | extensions: ds 58 | 59 | - name: Install dependencies 60 | run: composer install --no-progress --prefer-dist --optimize-autoloader ${{ matrix.composer-options }} 61 | 62 | - name: Execute tests 63 | run: vendor/bin/phpunit --colors=always --coverage-text 64 | -------------------------------------------------------------------------------- /src/Issue.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller; 14 | 15 | /** 16 | * Spelling issue 17 | * 18 | * @since 1.0 19 | */ 20 | final class Issue 21 | { 22 | /** 23 | * Error code for: word not found in any dictionary 24 | * 25 | * @since 1.0 26 | */ 27 | public const UNKNOWN_WORD = 'Unknown word'; 28 | 29 | /** 30 | * Problem word 31 | * 32 | * @var string 33 | * 34 | * @since 1.0 35 | */ 36 | public $word; 37 | 38 | /** 39 | * Error code (see class constants) 40 | * 41 | * @var string 42 | * 43 | * @since 1.0 44 | */ 45 | public $code; 46 | 47 | /** 48 | * Suggested replacements 49 | * 50 | * @var string[] 51 | * 52 | * @since 1.0 53 | */ 54 | public $suggestions = []; 55 | 56 | /** 57 | * Text line containing problem word 58 | * 59 | * @var int|null line number or null if not known 60 | * @since 1.0 61 | */ 62 | public $line; 63 | 64 | /** 65 | * Problem word offset in the {@link $line} 66 | * 67 | * @var int|null offset in characters or null if not known 68 | * @since 1.0 69 | */ 70 | public $offset; 71 | 72 | /** 73 | * Create new issue 74 | * 75 | * @param string $word problem word 76 | * @param string $code error code (see class constants) 77 | * 78 | * @since 1.0 79 | */ 80 | public function __construct(string $word, string $code = self::UNKNOWN_WORD) 81 | { 82 | $this->word = $word; 83 | $this->code = $code; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/Hunspell/fixtures/dicts.txt: -------------------------------------------------------------------------------- 1 | ПУТЬ ПОИСКА: 2 | .::/usr/share/hunspell:/usr/share/myspell:/usr/share/myspell/dicts:/Library/Spelling:/home/mekras/.openoffice.org/3/user/wordbook:.openoffice.org2/user/wordbook:.openoffice.org2.0/user/wordbook:Library/Spelling:/opt/openoffice.org/basis3.0/share/dict/ooo:/usr/lib/openoffice.org/basis3.0/share/dict/ooo:/opt/openoffice.org2.4/share/dict/ooo:/usr/lib/openoffice.org2.4/share/dict/ooo:/opt/openoffice.org2.3/share/dict/ooo:/usr/lib/openoffice.org2.3/share/dict/ooo:/opt/openoffice.org2.2/share/dict/ooo:/usr/lib/openoffice.org2.2/share/dict/ooo:/opt/openoffice.org2.1/share/dict/ooo:/usr/lib/openoffice.org2.1/share/dict/ooo:/opt/openoffice.org2.0/share/dict/ooo:/usr/lib/openoffice.org2.0/share/dict/ooo 3 | ДОСТУПНЫЕ СЛОВАРИ (при использовании -d путь можно опустить): 4 | /usr/share/hunspell/en_GB 5 | /usr/share/hunspell/de_DE 6 | /usr/share/hunspell/de_BE 7 | /usr/share/hunspell/en_ZA 8 | /usr/share/hunspell/ru_RU 9 | /usr/share/hunspell/en_US 10 | /usr/share/hunspell/de_LU 11 | /usr/share/hunspell/en_AU 12 | /usr/share/myspell/dicts/en_GB 13 | /usr/share/myspell/dicts/hyph_en_GB 14 | /usr/share/myspell/dicts/hyph_en_CA 15 | /usr/share/myspell/dicts/hyph_es_ES 16 | /usr/share/myspell/dicts/hyph_ru_RU 17 | /usr/share/myspell/dicts/hyph_pt_PT 18 | /usr/share/myspell/dicts/hyph_en_US 19 | /usr/share/myspell/dicts/en_ZA 20 | /usr/share/myspell/dicts/hyph_da_DK 21 | /usr/share/myspell/dicts/hyph_nl_NL 22 | /usr/share/myspell/dicts/hyph_sk_SK 23 | /usr/share/myspell/dicts/hyph_cs_CZ 24 | /usr/share/myspell/dicts/hyph_el_GR 25 | /usr/share/myspell/dicts/hyph_is_IS 26 | /usr/share/myspell/dicts/en-GB 27 | /usr/share/myspell/dicts/hyph_id_ID 28 | /usr/share/myspell/dicts/hyph_pt_BR 29 | /usr/share/myspell/dicts/hyph_ga_IE 30 | /usr/share/myspell/dicts/hyph_uk_UA 31 | /usr/share/myspell/dicts/hyph_fi_FI 32 | ЗАГРУЖЕННЫЙ СЛОВАРЬ: 33 | /usr/share/hunspell/ru_RU.aff 34 | /usr/share/hunspell/ru_RU.dic 35 | -------------------------------------------------------------------------------- /src/Source/IconvSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | use Mekras\Speller\Exception\SourceException; 16 | 17 | /** 18 | * Convert text encoding using iconv. 19 | * 20 | * @since 1.6 21 | */ 22 | class IconvSource extends MetaSource 23 | { 24 | /** 25 | * Output encoding. 26 | * 27 | * @var string 28 | */ 29 | private $encoding; 30 | 31 | /** 32 | * Create new converter. 33 | * 34 | * @param EncodingAwareSource $source Original source. 35 | * @param string $encoding Output encoding (default to "UTF-8"). 36 | * 37 | * @since 1.6 38 | */ 39 | public function __construct(EncodingAwareSource $source, string $encoding = 'UTF-8') 40 | { 41 | parent::__construct($source); 42 | $this->encoding = $encoding; 43 | } 44 | 45 | /** 46 | * Return text in the specified encoding. 47 | * 48 | * @return string 49 | * 50 | * @throws SourceException 51 | * @since 1.6 52 | */ 53 | public function getAsString(): string 54 | { 55 | $text = iconv( 56 | $this->source->getEncoding(), 57 | $this->getEncoding(), 58 | $this->source->getAsString() 59 | ); 60 | if (false === $text) { 61 | throw new SourceException('iconv failed to convert source text'); 62 | } 63 | 64 | return $text; 65 | } 66 | 67 | /** 68 | * Return output text encoding. 69 | * 70 | * @return string 71 | * 72 | * @since 1.6 73 | */ 74 | public function getEncoding(): string 75 | { 76 | return $this->encoding; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/Source/FileSourceTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source; 14 | 15 | use Mekras\Speller\Source\FileSource; 16 | use Mekras\Speller\Exception\SourceException; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | /** 20 | * Tests for Mekras\Speller\Source\FileSource. 21 | * 22 | * @covers \Mekras\Speller\Source\FileSource 23 | */ 24 | class FileSourceTest extends TestCase 25 | { 26 | /** 27 | * Test basics. 28 | */ 29 | public function testBasics(): void 30 | { 31 | $filename = __DIR__ . '/fixtures/test.txt'; 32 | $source = new FileSource($filename); 33 | static::assertEquals('Tiger, tiger, burning bright', $source->getAsString()); 34 | static::assertEquals($filename, $source->getFilename()); 35 | } 36 | 37 | /** 38 | * Test encoding. 39 | */ 40 | public function testEncoding(): void 41 | { 42 | $source = new FileSource(__DIR__ . '/fixtures/test.txt'); 43 | static::assertEquals('UTF-8', $source->getEncoding()); 44 | 45 | $source = new FileSource(__DIR__ . '/fixtures/test.txt', 'koi8-r'); 46 | static::assertEquals('koi8-r', $source->getEncoding()); 47 | } 48 | 49 | /** 50 | * getAsString should throw SourceException if file not exists. 51 | */ 52 | public function testFileNotExists(): void 53 | { 54 | $noExistedFilePath = 'non-existent.file'; 55 | 56 | $source = new FileSource($noExistedFilePath); 57 | 58 | $this->expectException(SourceException::class); 59 | $this->expectExceptionMessage(sprintf('File "%s" not exists', $noExistedFilePath)); 60 | 61 | $source->getAsString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/Ispell/IspellTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Ispell; 14 | 15 | use Mekras\Speller\Ispell\Ispell; 16 | use Mekras\Speller\Source\StringSource; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | /** 20 | * Tests for Mekras\Speller\Ispell\Ispell. 21 | * 22 | * @covers \Mekras\Speller\ExternalSpeller 23 | * @covers \Mekras\Speller\Ispell\Ispell 24 | */ 25 | class IspellTest extends TestCase 26 | { 27 | /** 28 | * Test retrieving list of supported languages. 29 | */ 30 | public function testGetSupportedLanguages(): void 31 | { 32 | $ispell = new Ispell(__DIR__ . '/fixtures/bin/ispell.sh'); 33 | static::assertEquals(['english', 'russian'], $ispell->getSupportedLanguages()); 34 | } 35 | 36 | /** 37 | * Test spell checking. 38 | * 39 | * See fixtures/input.txt for the source text. 40 | */ 41 | public function testCheckText(): void 42 | { 43 | $ispell = new Ispell(__DIR__ . '/fixtures/bin/ispell.sh'); 44 | $source = new StringSource(''); 45 | $issues = $ispell->checkText($source, ['en']); 46 | static::assertCount(5, $issues); 47 | static::assertEquals('Tigr', $issues[0]->word); 48 | static::assertEquals(1, $issues[0]->line); 49 | static::assertEquals(0, $issues[0]->offset); 50 | static::assertEquals(['Tier', 'Tiger'], $issues[0]->suggestions); 51 | 52 | static::assertEquals('theforests', $issues[1]->word); 53 | static::assertEquals(2, $issues[1]->line); 54 | static::assertEquals(3, $issues[1]->offset); 55 | static::assertCount(2, $issues[1]->suggestions); 56 | 57 | static::assertEquals('CCould', $issues[4]->word); 58 | static::assertEquals(4, $issues[4]->line); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Source/FileSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | use Mekras\Speller\Exception\SourceException; 16 | 17 | /** 18 | * File as text source. 19 | * 20 | * @since 1.6 Implements EncodingAwareSource. 21 | * @since 1.2 22 | */ 23 | class FileSource implements EncodingAwareSource 24 | { 25 | /** 26 | * File name. 27 | * 28 | * @var string 29 | * @since 1.2 30 | */ 31 | protected $filename; 32 | 33 | /** 34 | * Text encoding. 35 | * 36 | * @var string 37 | */ 38 | private $encoding; 39 | 40 | /** 41 | * Create new source. 42 | * 43 | * @param string $filename 44 | * @param string $encoding File encoding (default to "UTF-8"). 45 | * 46 | * @since 1.6 New argument — $encoding. 47 | * @since 1.2 48 | */ 49 | public function __construct(string $filename, string $encoding = 'UTF-8') 50 | { 51 | $this->filename = $filename; 52 | $this->encoding = $encoding; 53 | } 54 | 55 | /** 56 | * Return text as one string 57 | * 58 | * @return string 59 | * 60 | * @throws SourceException Fail to read from text source. 61 | * @since 1.6 Throws {@see SourceException}. 62 | * @since 1.2 63 | */ 64 | public function getAsString(): string 65 | { 66 | if ('php://stdin' !== $this->filename) { 67 | if (!file_exists($this->filename)) { 68 | throw new SourceException(sprintf('File "%s" not exists', $this->filename)); 69 | } 70 | 71 | if (!is_readable($this->filename)) { 72 | throw new SourceException(sprintf('File "%s" is not readable', $this->filename)); 73 | } 74 | } 75 | 76 | return file_get_contents($this->filename); 77 | } 78 | 79 | /** 80 | * Return source text encoding. 81 | * 82 | * @return string 83 | * 84 | * @since 1.6 85 | */ 86 | public function getEncoding(): string 87 | { 88 | return $this->encoding; 89 | } 90 | 91 | /** 92 | * Return file name with text to check. 93 | * 94 | * This can be used by backends with file checking support. 95 | * 96 | * @return string 97 | * 98 | * @since 1.2 99 | */ 100 | public function getFilename(): string 101 | { 102 | return $this->filename; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Unit/Hunspell/HunspellTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Hunspell; 14 | 15 | use Mekras\Speller\Hunspell\Hunspell; 16 | use Mekras\Speller\Source\StringSource; 17 | use PHPUnit\Framework\TestCase; 18 | use ReflectionMethod; 19 | 20 | /** 21 | * Tests for Mekras\Speller\Hunspell\Hunspell 22 | * 23 | * @covers \Mekras\Speller\ExternalSpeller 24 | * @covers \Mekras\Speller\Hunspell\Hunspell 25 | */ 26 | class HunspellTest extends TestCase 27 | { 28 | /** 29 | * Test hunspell argument escaping 30 | * @throws \ReflectionException 31 | */ 32 | public function testArgumentEscaping(): void 33 | { 34 | $hunspell = new Hunspell(); 35 | $method = new ReflectionMethod(get_class($hunspell), 'composeCommand'); 36 | $method->setAccessible(true); 37 | static::assertEquals(['hunspell', '-d', 'foo,bar'], $method->invoke($hunspell, ['-d', 'foo,bar'])); 38 | } 39 | 40 | /** 41 | * Test retrieving list of supported languages 42 | */ 43 | public function testGetSupportedLanguages(): void 44 | { 45 | $hunspell = new Hunspell($this->getBinary()); 46 | static::assertEquals( 47 | ['de_BE', 'de_DE', 'de_LU', 'en-GB', 'en_AU', 'en_GB', 'en_US', 'en_ZA', 'ru_RU'], 48 | $hunspell->getSupportedLanguages() 49 | ); 50 | } 51 | 52 | /** 53 | * Test spell checking 54 | * 55 | * See fixtures/input.txt for the source text. 56 | */ 57 | public function testCheckText(): void 58 | { 59 | $hunspell = new Hunspell($this->getBinary()); 60 | $source = new StringSource(''); 61 | $issues = $hunspell->checkText($source, ['en']); 62 | static::assertCount(6, $issues); 63 | static::assertEquals('Tigr', $issues[0]->word); 64 | static::assertEquals(1, $issues[0]->line); 65 | static::assertEquals(0, $issues[0]->offset); 66 | static::assertEquals( 67 | ['Ti gr', 'Ti-gr', 'Tiger', 'Trig', 'Tier', 'Tigris', 'Grit', 'Tigress', 'Tagore'], 68 | $issues[0]->suggestions 69 | ); 70 | 71 | static::assertEquals('страх', $issues[1]->word); 72 | static::assertEquals(1, $issues[1]->line); 73 | static::assertEquals(21, $issues[1]->offset); 74 | static::assertCount(0, $issues[1]->suggestions); 75 | 76 | static::assertEquals('CCould', $issues[5]->word); 77 | static::assertEquals(4, $issues[5]->line); 78 | } 79 | 80 | /** 81 | * Return hunspell binary stub. 82 | * 83 | * @return string 84 | */ 85 | private function getBinary(): string 86 | { 87 | return __DIR__ . '/fixtures/bin/hunspell.php'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Helper/LanguageMapper.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Helper; 14 | 15 | /** 16 | * Map given list of language tags to supported ones 17 | * 18 | * For example some speller supports de_DE, en_US and ru_RU languages. But your application uses 19 | * short versions of language tags: de, en, ru. LanguageMapper maps your list of language tags 20 | * to supported by speller. 21 | * 22 | * @since 1.0 23 | */ 24 | class LanguageMapper 25 | { 26 | /** 27 | * Preferred mappings 28 | * 29 | * @var array[] 30 | */ 31 | private $preferred = []; 32 | 33 | /** 34 | * Map given list of language tags to supported ones 35 | * 36 | * @param string[] $requested list of requested languages 37 | * @param string[] $supported list of supported languages 38 | * 39 | * @return string[] 40 | * 41 | * @link http://tools.ietf.org/html/bcp47 42 | * @since 1.0 43 | */ 44 | public function map(array $requested, array $supported): array 45 | { 46 | $index = []; 47 | foreach ($supported as $tag) { 48 | $key = strtolower(preg_replace('/_-\./', '', $tag)); 49 | $index[$key] = $tag; 50 | } 51 | 52 | $result = []; 53 | foreach ($requested as $source) { 54 | if (array_key_exists($source, $this->preferred)) { 55 | $preferred = $this->preferred[$source]; 56 | foreach ($preferred as $tag) { 57 | if (in_array($tag, $supported, true)) { 58 | $result[] = $tag; 59 | continue 2; 60 | } 61 | } 62 | } 63 | 64 | if (in_array($source, $supported, true)) { 65 | $result[] = $source; 66 | continue; 67 | } 68 | 69 | $tag = strtolower(preg_replace('/_-\./', '', $source)); 70 | foreach ($index as $key => $target) { 71 | if (strpos($key, $tag) === 0) { 72 | $result[] = $target; 73 | break; 74 | } 75 | } 76 | } 77 | 78 | return $result; 79 | } 80 | 81 | /** 82 | * Set preferred mappings 83 | * 84 | * Examples: 85 | * 86 | * ``` 87 | * $mapper->setPreferredMappings(['en' => ['en_US', 'en_GB']]); 88 | * ``` 89 | * 90 | * @param array $mappings 91 | * 92 | * @since 1.1 93 | */ 94 | public function setPreferredMappings(array $mappings): void 95 | { 96 | foreach ($mappings as $language => $map) { 97 | $this->preferred[$language] = (array) $map; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/spellcheck.php: -------------------------------------------------------------------------------- 1 | Backend to use: aspell, hunspell (default) or ispell. 24 | -E Dictionary internal encoding. 25 | -e Source text encoding (default UTF-8). 26 | -f Treat source as: html, xliff. 27 | -i Read text from a given file (default is STDIN). 28 | -l Comma separated list of source text languages (default is system locale). 29 | EOT; 30 | } 31 | 32 | $options = getopt('b:E:e:f:i:l:'); 33 | 34 | /* Choose backend. */ 35 | if (array_key_exists('b', $options)) { 36 | switch ($options['b']) { 37 | case 'aspell': 38 | $speller = new Aspell(); 39 | break; 40 | case 'ispell': 41 | $speller = new Ispell(); 42 | break; 43 | case 'hunspell': 44 | $speller = new Hunspell(); 45 | break; 46 | default: 47 | fprintf(STDERR, "Invalid backend: %s\n", $options['b']); 48 | exit(-1); 49 | } 50 | } else { 51 | $speller = new Hunspell(); 52 | } 53 | 54 | /* Source text encoding */ 55 | $encoding = 'UTF-8'; 56 | if (array_key_exists('e', $options)) { 57 | $encoding = $options['e']; 58 | } 59 | 60 | /* Text source. */ 61 | $filename = 'php://stdin'; 62 | if (array_key_exists('i', $options)) { 63 | $filename = $options['i']; 64 | } 65 | $source = new FileSource($filename, $encoding); 66 | 67 | /* Text source format. */ 68 | if (array_key_exists('f', $options)) { 69 | switch ($options['f']) { 70 | case 'html': 71 | $source = new HtmlSource($source); 72 | break; 73 | case 'xliff': 74 | $source = new XliffSource($source); 75 | break; 76 | default: 77 | fprintf(STDERR, "Invalid format: %s\n", $options['f']); 78 | exit(-1); 79 | } 80 | } 81 | 82 | /* Source language. */ 83 | $languages = []; 84 | if (array_key_exists('l', $options)) { 85 | $languages = explode(',', $options['l']); 86 | } 87 | 88 | /* Dictionary encoding */ 89 | if (array_key_exists('E', $options)) { 90 | $source = new IconvSource($source, $options['E']); 91 | } 92 | 93 | try { 94 | $issues = $speller->checkText($source, $languages); 95 | } catch (PhpSpellerException $e) { 96 | fprintf(STDERR, $e->getMessage() . PHP_EOL); 97 | exit(-1); 98 | } 99 | foreach ($issues as $issue) { 100 | print_r($issue); 101 | } 102 | -------------------------------------------------------------------------------- /src/Source/HtmlSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | use Mekras\Speller\Exception\SourceException; 16 | use Mekras\Speller\Source\Filter\HtmlFilter; 17 | 18 | /** 19 | * HTML document as a text source. 20 | * 21 | * @since 1.6 derived from MetaSource. 22 | * @since 1.5 23 | */ 24 | class HtmlSource extends MetaSource 25 | { 26 | /** 27 | * Source text. 28 | * 29 | * @var string 30 | */ 31 | private $text; 32 | 33 | /** 34 | * Text encoding. 35 | * 36 | * @var string 37 | */ 38 | private $encoding; 39 | 40 | /** 41 | * Create text source from HTML. 42 | * 43 | * @param EncodingAwareSource|string $source 44 | * 45 | * @throws SourceException 46 | * 47 | * @since 1.6 Accepts EncodingAwareSource 48 | * @since 1.5 49 | * 50 | * @todo deprecate string $source in version 2.0 51 | */ 52 | public function __construct($source) 53 | { 54 | if (!$source instanceof EncodingAwareSource) { 55 | $source = new StringSource($source); 56 | } 57 | parent::__construct($source); 58 | $html = $this->source->getAsString(); 59 | $document = $this->createDomDocument($html); 60 | $this->encoding = $document->encoding; 61 | $filter = new HtmlFilter(); 62 | $this->text = $filter->filter($html); 63 | } 64 | 65 | /** 66 | * Return text as one string 67 | * 68 | * @return string 69 | * 70 | * @since 1.0 71 | */ 72 | public function getAsString(): string 73 | { 74 | return $this->text; 75 | } 76 | 77 | /** 78 | * Return source text encoding. 79 | * 80 | * @return string 81 | * 82 | * @since 1.6 83 | */ 84 | public function getEncoding(): string 85 | { 86 | return $this->encoding; 87 | } 88 | 89 | /** 90 | * Create DOMDocument from HTML string. 91 | * 92 | * @param string $html 93 | * 94 | * @return \DOMDocument 95 | * 96 | * @throws SourceException On invalid HTML. 97 | * 98 | * @since 1.7 99 | */ 100 | protected function createDomDocument($html): \DOMDocument 101 | { 102 | $document = new \DOMDocument('1.0'); 103 | $previousValue = libxml_use_internal_errors(true); 104 | libxml_clear_errors(); 105 | $document->loadHTML($html); 106 | /** @var \LibXMLError[] $errors */ 107 | $errors = libxml_get_errors(); 108 | libxml_clear_errors(); 109 | libxml_use_internal_errors($previousValue); 110 | 111 | foreach ($errors as $error) { 112 | if (LIBXML_ERR_ERROR === $error->level || LIBXML_ERR_FATAL === $error->level) { 113 | throw new SourceException( 114 | sprintf('%s at %d:%d', trim($error->message), $error->line, $error->column), 115 | $error->code 116 | ); 117 | } 118 | } 119 | 120 | return $document; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Unit/Source/Filter/HtmlFilterTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Tests\Unit\Source\Filter; 14 | 15 | use Mekras\Speller\Source\Filter\HtmlFilter; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Tests for Mekras\Speller\Source\Filter\HtmlFilter. 20 | * 21 | * @covers \Mekras\Speller\Source\Filter\HtmlFilter 22 | */ 23 | class HtmlFilterTest extends TestCase 24 | { 25 | /** 26 | * Test basics. 27 | */ 28 | public function testBasics(): void 29 | { 30 | $filter = new HtmlFilter(); 31 | $html = "
foo® \nbaz"; 32 | $text = " foo \n bar \nbaz "; 33 | static::assertEquals($text, $filter->filter($html)); 34 | } 35 | 36 | /** 37 | * Only for "keywords" and "description" meta tags "content" attr should be treated as string. 38 | */ 39 | public function testMetaContent(): void 40 | { 41 | $filter = new HtmlFilter(); 42 | $html = 43 | '' . "\n" . 44 | '' . "\n" . 45 | '' . "\n" . 46 | ''; 47 | $text = 48 | " \n" . 49 | " Foo \n" . 50 | " \n" . 51 | ' Bar '; 52 | static::assertEquals($text, $filter->filter($html)); 53 | } 54 | 55 | /** 56 | * "; 62 | $text = " Foo \n \n "; 63 | static::assertEquals($text, $filter->filter($html)); 64 | } 65 | 66 | public function testMalformedAttribute(): void 67 | { 68 | $filter = new HtmlFilter(); 69 | $html = '

test

'; 70 | $text = ' test '; 71 | static::assertEquals($text, $filter->filter($html)); 72 | 73 | $html = '

test

'; 74 | $text = ' test '; 75 | static::assertEquals($text, $filter->filter($html)); 76 | 77 | $html = '

test

'; 78 | $text = ' test '; 79 | static::assertEquals($text, $filter->filter($html)); 80 | 81 | $html = '

test

'; 86 | $text = ' test '; 87 | static::assertEquals($text, $filter->filter($html)); 88 | 89 | $html = '

bar

"; 98 | $text = "foo/ bar "; 99 | static::assertEquals($text, $filter->filter($html)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Source/XliffSource.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Source; 14 | 15 | use Mekras\Speller\Source\Filter\Filter; 16 | use Mekras\Speller\Source\Filter\HtmlFilter; 17 | use Mekras\Speller\Source\Filter\StripAllFilter; 18 | 19 | /** 20 | * XLIFF translations as text source. 21 | * 22 | * @since 1.6 derived from MetaSource. 23 | * @since 1.2 24 | * 25 | * @link http://docs.oasis-open.org/xliff/xliff-core/v2.0/xliff-core-v2.0.html 26 | */ 27 | class XliffSource extends MetaSource 28 | { 29 | /** 30 | * Text filters 31 | * 32 | * @var Filter[]|null[] 33 | */ 34 | private $filters = [ 35 | '##ums' => null, // Comments 36 | '#<[^>]+>#ums' => null // Top level tags 37 | ]; 38 | 39 | /** 40 | * Create text source from XLIFF document. 41 | * 42 | * @param EncodingAwareSource|string $source Source of filename 43 | * 44 | * @throws \Mekras\Speller\Exception\SourceException 45 | * 46 | * @since 1.6 Accepts EncodingAwareSource 47 | * 48 | * @todo deprecate string $source in version 2.0 49 | */ 50 | public function __construct($source) 51 | { 52 | if (!$source instanceof EncodingAwareSource) { 53 | $source = new FileSource($source); 54 | } 55 | parent::__construct($source); 56 | } 57 | 58 | /** 59 | * Add custom pattern to be filtered. 60 | * 61 | * Matched text will be filtered with a given filter or with 62 | * {@link Mekras\Speller\Source\Filter\StripAllFilter} if $filter is null. 63 | * 64 | * @param string $pattern PCRE pattern. It is recommended to use "ums" PCRE modifiers. 65 | * @param Filter $filter Filter to be applied. 66 | * 67 | * @since 1.2 68 | */ 69 | public function addFilter(string $pattern, Filter $filter = null): void 70 | { 71 | $this->filters[$pattern] = $filter; 72 | } 73 | 74 | /** 75 | * Return text as one string 76 | * 77 | * @return string 78 | * 79 | * @throws \Mekras\Speller\Exception\SourceException 80 | * @since 1.2 81 | */ 82 | public function getAsString(): string 83 | { 84 | $text = $this->source->getAsString(); 85 | 86 | $stripAll = new StripAllFilter(); 87 | $htmlFilter = new HtmlFilter(); 88 | 89 | /* Removing CDATA tags */ 90 | $text = preg_replace_callback( 91 | '##ums', 92 | function ($match) use ($htmlFilter) { 93 | $string = $htmlFilter->filter($match[1]); 94 | 95 | // ]*?)?>)([^<>]*)()#um', 104 | function ($match) use ($stripAll) { 105 | if (strtolower($match[2]) === 'target') { 106 | $replace = $stripAll->filter($match[1]) . $match[4] 107 | . $stripAll->filter($match[5]); 108 | } else { 109 | $replace = $stripAll->filter($match[0]); 110 | } 111 | 112 | return $replace; 113 | }, 114 | $text 115 | ); 116 | 117 | /* Other replacements */ 118 | foreach ($this->filters as $pattern => $filter) { 119 | if (null === $filter) { 120 | $filter = $stripAll; 121 | } 122 | $text = preg_replace_callback( 123 | $pattern, 124 | function ($match) use ($filter) { 125 | return $filter->filter($match[0]); 126 | }, 127 | $text 128 | ); 129 | } 130 | 131 | return $text; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Aspell/Aspell.php: -------------------------------------------------------------------------------- 1 | 7 | * @author Михаил Красильников 8 | * @license http://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Mekras\Speller\Aspell; 14 | 15 | use Mekras\Speller\Dictionary; 16 | use Mekras\Speller\Exception\ExternalProgramFailedException; 17 | use Mekras\Speller\Ispell\Ispell; 18 | use Mekras\Speller\Source\EncodingAwareSource; 19 | use Symfony\Component\Process\Exception\InvalidArgumentException; 20 | use Symfony\Component\Process\Exception\LogicException; 21 | use Symfony\Component\Process\Exception\RuntimeException; 22 | 23 | /** 24 | * Aspell adapter. 25 | * 26 | * @since 1.6 27 | */ 28 | class Aspell extends Ispell 29 | { 30 | /** 31 | * Cache for list of supported languages 32 | * 33 | * @var string[]|null 34 | */ 35 | private $supportedLanguages; 36 | 37 | /** 38 | * @var Dictionary 39 | */ 40 | private $personalDictionary; 41 | 42 | /** 43 | * Create new aspell adapter. 44 | * 45 | * @param string $binaryPath Path to aspell binary (default "aspell"). 46 | * 47 | * @since 1.6 48 | */ 49 | public function __construct(string $binaryPath = 'aspell') 50 | { 51 | parent::__construct($binaryPath); 52 | } 53 | 54 | /** 55 | * Return list of supported languages. 56 | * 57 | * @return string[] 58 | * 59 | * @throws ExternalProgramFailedException 60 | * @throws InvalidArgumentException 61 | * @throws LogicException 62 | * @throws RuntimeException 63 | * @since 1.6 64 | */ 65 | public function getSupportedLanguages(): array 66 | { 67 | if (null === $this->supportedLanguages) { 68 | $process = $this->createProcess(['dump', 'dicts']); 69 | $process->run(); 70 | if (!$process->isSuccessful()) { 71 | throw new ExternalProgramFailedException( 72 | $process->getCommandLine(), 73 | $process->getErrorOutput(), 74 | $process->getExitCode() 75 | ); 76 | } 77 | $this->resetProcess(); 78 | 79 | $languages = []; 80 | 81 | $output = explode(PHP_EOL, $process->getOutput()); 82 | foreach ($output as $line) { 83 | $name = trim($line); 84 | if (strpos($name, '-variant') !== false) { 85 | // Skip variants 86 | continue; 87 | } 88 | $languages[$name] = true; 89 | } 90 | 91 | $languages = array_keys($languages); 92 | sort($languages); 93 | $this->supportedLanguages = $languages; 94 | } 95 | 96 | return $this->supportedLanguages; 97 | } 98 | 99 | /** 100 | * @param Dictionary $dictionary 101 | */ 102 | public function setPersonalDictionary(Dictionary $dictionary): void 103 | { 104 | $this->personalDictionary = $dictionary; 105 | $this->resetProcess(); 106 | } 107 | 108 | /** 109 | * Create arguments for external speller. 110 | * 111 | * @param EncodingAwareSource $source Text source to check. 112 | * @param array $languages List of languages used in text (IETF language tag). 113 | * 114 | * @return string[] 115 | * 116 | * @since 1.6 117 | */ 118 | protected function createArguments(EncodingAwareSource $source, array $languages): array 119 | { 120 | $args = [ 121 | // Input encoding 122 | '--encoding=' . ($source instanceof EncodingAwareSource ? $source->getEncoding( 123 | ) : 'UTF-8'), 124 | '-a' // ispell compatible output 125 | ]; 126 | 127 | if (count($languages) > 0) { 128 | $args[] = '--lang=' . $languages[0]; 129 | } 130 | 131 | if ($this->personalDictionary !== null) { 132 | $args[] = '--personal=' . $this->personalDictionary->getPath(); 133 | } 134 | 135 | return $args; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | 6 | ## 2.3.0 - 2023-04-14 7 | ### Added 8 | - Added build updates for php 8.2 9 | ### Removed 10 | - Dropped support for php 7.3 11 | - Dropped support for symfony < 5.4 12 | ### Fixed 13 | - Added `resetProcess` to `Aspell::getSupportedLanguages` to avoid cached process 14 | - Added `resetProcess` to `ExternalSpeller::checkText` to avoid cached process 15 | 16 | ## 2.2.0 - 2022-03-31 17 | ### Removed 18 | - Dropped support for php 7.1 and 7.2 19 | 20 | ## 2.1.4 - 2021-03-15 21 | ### Changed 22 | - Add support for PHP 8 23 | ### Removed 24 | - Removed `getMessage` from `PhpSpellerException` as it already declared from `Throwable` 25 | 26 | ## 2.1.3 - 2020-09-23 27 | ### Fixed 28 | - Fix incorrect escaping of hunspell command (#29) 29 | 30 | ## 2.1.2 - 2020-08-20 31 | ### Fixed 32 | - HtmlFilter raises Error with malformed HTML tags (#27) 33 | 34 | ## 2.1.1 - 2020-06-18 35 | ### Fixed 36 | - Fixed an issue where `:` (colon) made the suggestions break for swedish as its a valid text sign (#25) 37 | 38 | ## 2.1 - 2020-05-06 39 | ### Fixed 40 | - Fixed issue with removed and deprecated methods for `symfony/process:^4.0` 41 | 42 | ### Changed 43 | - Add requirement for `php:^7.2` 44 | - Changed minimum requirement for `symfony/process` to `^4.4` 45 | 46 | ## 2.0.4 - 2020-04-30 47 | ### Added 48 | - Added support for `symfony/process:^5.0` to work with laravel 7 (thanks [@](https://github.com/ibarrajo) [#21](https://github.com/mekras/php-speller/pull/21)) 49 | 50 | ## 2.0.3 - 2020-04-28 51 | ### Fixed 52 | - Resolve an issue with malformed html input (thanks to [@rouliane](https://github.com/rouliane) [#20](https://github.com/mekras/php-speller/pull/20)) 53 | 54 | ## 2.0.2 - 2019-01-25 55 | ### Fixed 56 | - backward support for `php:^7.1 and `symfony/process: ^3.4` 57 | 58 | ## 2.0.1 - 2019-01-18 59 | ### Fixed 60 | - Fixed support for hunspell paths on windows OS 61 | 62 | ## 2.0 - 2019-01-17 63 | ### Added 64 | - Add custom dictionary support for aspell 65 | 66 | ### Changed 67 | - Raise PHP requirement to 7.2 68 | 69 | ### Removed 70 | - Dropped `@deprecated` interfaces 71 | 72 | ## 1.7.2 - 2017-04-30 73 | ### Fixed 74 | - HtmlFilter: `