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