Passed
Pull Request — master (#43)
by
unknown
07:34
created

Ispell::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpSpellcheck\Spellchecker;
6
7
use PhpSpellcheck\Exception\ProcessHasErrorOutputException;
8
use PhpSpellcheck\Misspelling;
9
use PhpSpellcheck\Utils\CommandLine;
10
use PhpSpellcheck\Utils\IspellOutputParser;
11
use PhpSpellcheck\Utils\ProcessRunner;
12
use Symfony\Component\Process\Process;
13
use Webmozart\Assert\Assert;
14
15
class Ispell implements SpellcheckerInterface
16
{
17
    /**
18
     * @var string[]|null
19
     */
20
    private $supportedLanguages;
21
22
    /**
23
     * @var CommandLine
24
     */
25
    private $ispellCommandLine;
26
27
    /**
28
     * @var CommandLine|null
29
     */
30
    private $shellEntryPoint;
31
32 5
    public function __construct(CommandLine $ispellCommandLine, ?CommandLine $shellEntryPoint = null)
33
    {
34 5
        $this->ispellCommandLine = $ispellCommandLine;
35 5
        $this->shellEntryPoint = $shellEntryPoint;
36 5
    }
37
38
    /**
39
     * {@inheritdoc}
40
     */
41 3
    public function check(string $text, array $languages = [], array $context = []): iterable
42
    {
43 3
        Assert::greaterThan($languages, 1, 'Ispell spellchecker doesn\'t support multiple languages check');
44
45 3
        $cmd = $this->ispellCommandLine->addArg('-a');
46
47 3
        if (!empty($languages)) {
48 2
            $cmd = $cmd->addArgs(['-d', implode(',', $languages)]);
49
        }
50
51 3
        $process = new Process($cmd->getArgs());
52
53
        // Add prefix characters putting Ispell's type of spellcheckers in terse-mode,
54
        // ignoring correct words and thus speeding execution
55 3
        $process->setInput('!' . PHP_EOL . self::preprocessInputForPipeMode($text) . PHP_EOL . '%');
56
57 3
        $output = ProcessRunner::run($process)->getOutput();
58
59 3
        if ($process->getErrorOutput() !== '') {
60 1
            throw new ProcessHasErrorOutputException($process->getErrorOutput(), $text, $process->getCommandLine());
61
        }
62
63 2
        $misspellings = IspellOutputParser::parseMisspellings($output, $context);
64
65
        return self::postprocessMisspellings($misspellings);
66
    }
67
68
    public function getCommandLine(): CommandLine
69
    {
70
        return $this->ispellCommandLine;
71
    }
72
73
    /**
74 2
     * {@inheritdoc}
75
     */
76
    public function getSupportedLanguages(): iterable
77
    {
78 2
        if ($this->supportedLanguages === null) {
79 2
            $shellEntryPoint = $this->shellEntryPoint ?? new CommandLine([]);
80 2
            $whichCommand = clone $shellEntryPoint;
81 2
            $process = new Process(
82
                $whichCommand
83 2
                    ->addArg('which')
84 2
                    ->addArg('ispell')
85 2
                    ->getArgs()
86
            );
87 2
            $process->mustRun();
88 2
            $binaryPath = trim($process->getOutput());
89
90 2
            $lsCommand = clone $shellEntryPoint;
91 2
            $process = new Process(
92
                $lsCommand
93 2
                    ->addArg('ls')
94 2
                    ->addArg(\dirname($binaryPath, 2) . '/lib/ispell')
95 2
                    ->getArgs()
96
            );
97 2
            $process->mustRun();
98
99 2
            $listOfFiles = trim($process->getOutput());
100
101 2
            $this->supportedLanguages = [];
102 2
            foreach (explode(PHP_EOL, $listOfFiles) as $file) {
103 2
                if (strpos($file, '.aff', -4) === false) {
104 1
                    continue;
105
                }
106
107 2
                yield \Safe\substr($file, 0, -4);
108
            }
109
        }
110 2
111
        return $this->supportedLanguages;
112
    }
113
114
    public static function create(?string $ispellCommandLineAsString): self
115 2
    {
116
        return new self(new CommandLine($ispellCommandLineAsString ?? 'ispell'));
117
    }
118 1
119
    /**
120 1
     * Preprocess the source text so that aspell/ispell pipe mode instruction are ignored.
121
     *
122
     * in pipe mode some special characters at the beginning of the line are instructions for aspell/ispell
123
     * see http://aspell.net/man-html/Through-A-Pipe.html#Through-A-Pipe
124
     * users put these chars innocently at the beginning of lines
125
     * we must tell the spellchecker to not interpret them using the ^ symbol
126
     *
127
     * @param string $text the text to preprocess
128
     *
129
     * @return string the result of the preprocessing
130
     */
131
    public static function preprocessInputForPipeMode(string $text): string
132
    {
133
        $lines = explode("\n", $text);
134
135
        $prefixedLines = array_map(
136
            function ($line) {
137
                return \strlen($line) === 0 ? "$line" : "^$line";
138
            },
139
            $lines
140
        );
141
142
        $preprocessedText = implode(
143
            "\n",
144
            $prefixedLines
145
        );
146
147
        return $preprocessedText;
148
    }
149
150
    /**
151
     * Adapts misspellings to compensate the additional caret added in self::preprocessInputForPipeMode.
152
     *
153
     * @param iterable<Misspelling> $misspellings
154
     *
155
     * @return iterable<Misspelling> copies of input misspells with corrected offset
156
     */
157
    public static function postprocessMisspellings(iterable $misspellings): iterable
158
    {
159
        foreach ($misspellings as $m) {
160
            $offset = $m->getOffset();
161
            if ($offset !== null) {
162
                $offset--;
163
            }
164
            yield new Misspelling(
165
                $m->getWord(),
166
                $offset,
167
                $m->getLineNumber(),
168
                $m->getSuggestions(),
169
                $m->getContext()
170
            );
171
        }
172
    }
173
}
174