Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Translation\Command; Chris@0: Chris@0: use Symfony\Component\Console\Command\Command; Chris@0: use Symfony\Component\Console\Exception\RuntimeException; Chris@0: use Symfony\Component\Console\Input\InputInterface; Chris@0: use Symfony\Component\Console\Input\InputOption; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@0: use Symfony\Component\Console\Style\SymfonyStyle; Chris@0: Chris@0: /** Chris@0: * Validates XLIFF files syntax and outputs encountered errors. Chris@0: * Chris@0: * @author Grégoire Pineau Chris@0: * @author Robin Chalas Chris@0: * @author Javier Eguiluz Chris@0: */ Chris@0: class XliffLintCommand extends Command Chris@0: { Chris@0: protected static $defaultName = 'lint:xliff'; Chris@0: Chris@0: private $format; Chris@0: private $displayCorrectFiles; Chris@0: private $directoryIteratorProvider; Chris@0: private $isReadableProvider; Chris@0: Chris@0: public function __construct($name = null, $directoryIteratorProvider = null, $isReadableProvider = null) Chris@0: { Chris@0: parent::__construct($name); Chris@0: Chris@0: $this->directoryIteratorProvider = $directoryIteratorProvider; Chris@0: $this->isReadableProvider = $isReadableProvider; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function configure() Chris@0: { Chris@0: $this Chris@0: ->setDescription('Lints a XLIFF file and outputs encountered errors') Chris@0: ->addArgument('filename', null, 'A file or a directory or STDIN') Chris@0: ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') Chris@0: ->setHelp(<<%command.name% command lints a XLIFF file and outputs to STDOUT Chris@0: the first encountered syntax error. Chris@0: Chris@0: You can validates XLIFF contents passed from STDIN: Chris@0: Chris@0: cat filename | php %command.full_name% Chris@0: Chris@0: You can also validate the syntax of a file: Chris@0: Chris@0: php %command.full_name% filename Chris@0: Chris@0: Or of a whole directory: Chris@0: Chris@0: php %command.full_name% dirname Chris@0: php %command.full_name% dirname --format=json Chris@0: Chris@0: EOF Chris@0: ) Chris@0: ; Chris@0: } Chris@0: Chris@0: protected function execute(InputInterface $input, OutputInterface $output) Chris@0: { Chris@0: $io = new SymfonyStyle($input, $output); Chris@0: $filename = $input->getArgument('filename'); Chris@0: $this->format = $input->getOption('format'); Chris@0: $this->displayCorrectFiles = $output->isVerbose(); Chris@0: Chris@0: if (!$filename) { Chris@0: if (!$stdin = $this->getStdin()) { Chris@0: throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); Chris@0: } Chris@0: Chris@4: return $this->display($io, [$this->validate($stdin)]); Chris@0: } Chris@0: Chris@0: if (!$this->isReadable($filename)) { Chris@0: throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); Chris@0: } Chris@0: Chris@4: $filesInfo = []; Chris@0: foreach ($this->getFiles($filename) as $file) { Chris@0: $filesInfo[] = $this->validate(file_get_contents($file), $file); Chris@0: } Chris@0: Chris@0: return $this->display($io, $filesInfo); Chris@0: } Chris@0: Chris@0: private function validate($content, $file = null) Chris@0: { Chris@0: // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input Chris@0: if ('' === trim($content)) { Chris@4: return ['file' => $file, 'valid' => true]; Chris@0: } Chris@0: Chris@0: libxml_use_internal_errors(true); Chris@0: Chris@0: $document = new \DOMDocument(); Chris@0: $document->loadXML($content); Chris@0: if ($document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd')) { Chris@4: return ['file' => $file, 'valid' => true]; Chris@0: } Chris@0: Chris@0: $errorMessages = array_map(function ($error) { Chris@4: return [ Chris@0: 'line' => $error->line, Chris@0: 'column' => $error->column, Chris@0: 'message' => trim($error->message), Chris@4: ]; Chris@0: }, libxml_get_errors()); Chris@0: Chris@0: libxml_clear_errors(); Chris@0: libxml_use_internal_errors(false); Chris@0: Chris@4: return ['file' => $file, 'valid' => false, 'messages' => $errorMessages]; Chris@0: } Chris@0: Chris@0: private function display(SymfonyStyle $io, array $files) Chris@0: { Chris@0: switch ($this->format) { Chris@0: case 'txt': Chris@0: return $this->displayTxt($io, $files); Chris@0: case 'json': Chris@0: return $this->displayJson($io, $files); Chris@0: default: Chris@0: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); Chris@0: } Chris@0: } Chris@0: Chris@0: private function displayTxt(SymfonyStyle $io, array $filesInfo) Chris@0: { Chris@4: $countFiles = \count($filesInfo); Chris@0: $erroredFiles = 0; Chris@0: Chris@0: foreach ($filesInfo as $info) { Chris@0: if ($info['valid'] && $this->displayCorrectFiles) { Chris@0: $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); Chris@0: } elseif (!$info['valid']) { Chris@0: ++$erroredFiles; Chris@0: $io->text(' ERROR '.($info['file'] ? sprintf(' in %s', $info['file']) : '')); Chris@0: $io->listing(array_map(function ($error) { Chris@0: // general document errors have a '-1' line number Chris@0: return -1 === $error['line'] ? $error['message'] : sprintf('Line %d, Column %d: %s', $error['line'], $error['column'], $error['message']); Chris@0: }, $info['messages'])); Chris@0: } Chris@0: } Chris@0: Chris@0: if (0 === $erroredFiles) { Chris@0: $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); Chris@0: } else { Chris@0: $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); Chris@0: } Chris@0: Chris@0: return min($erroredFiles, 1); Chris@0: } Chris@0: Chris@0: private function displayJson(SymfonyStyle $io, array $filesInfo) Chris@0: { Chris@0: $errors = 0; Chris@0: Chris@0: array_walk($filesInfo, function (&$v) use (&$errors) { Chris@0: $v['file'] = (string) $v['file']; Chris@0: if (!$v['valid']) { Chris@0: ++$errors; Chris@0: } Chris@0: }); Chris@0: Chris@0: $io->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); Chris@0: Chris@0: return min($errors, 1); Chris@0: } Chris@0: Chris@0: private function getFiles($fileOrDirectory) Chris@0: { Chris@0: if (is_file($fileOrDirectory)) { Chris@0: yield new \SplFileInfo($fileOrDirectory); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { Chris@4: if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: yield $file; Chris@0: } Chris@0: } Chris@0: Chris@0: private function getStdin() Chris@0: { Chris@0: if (0 !== ftell(STDIN)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $inputs = ''; Chris@0: while (!feof(STDIN)) { Chris@0: $inputs .= fread(STDIN, 1024); Chris@0: } Chris@0: Chris@0: return $inputs; Chris@0: } Chris@0: Chris@0: private function getDirectoryIterator($directory) Chris@0: { Chris@0: $default = function ($directory) { Chris@0: return new \RecursiveIteratorIterator( Chris@0: new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), Chris@0: \RecursiveIteratorIterator::LEAVES_ONLY Chris@0: ); Chris@0: }; Chris@0: Chris@0: if (null !== $this->directoryIteratorProvider) { Chris@4: return \call_user_func($this->directoryIteratorProvider, $directory, $default); Chris@0: } Chris@0: Chris@0: return $default($directory); Chris@0: } Chris@0: Chris@0: private function isReadable($fileOrDirectory) Chris@0: { Chris@0: $default = function ($fileOrDirectory) { Chris@0: return is_readable($fileOrDirectory); Chris@0: }; Chris@0: Chris@0: if (null !== $this->isReadableProvider) { Chris@4: return \call_user_func($this->isReadableProvider, $fileOrDirectory, $default); Chris@0: } Chris@0: Chris@0: return $default($fileOrDirectory); Chris@0: } Chris@0: }