diff --git a/.travis.yml b/.travis.yml index 0d95f3a..952f91d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: php php: - - 5.4 - - 5.5 - - 5.6 + - '7.0' + - '7.1' script: - - bin/behat + - vendor/bin/phpunit before_script: - composer self-update - composer install diff --git a/composer.json b/composer.json index 22da7f6..100bf84 100644 --- a/composer.json +++ b/composer.json @@ -10,18 +10,15 @@ "ext-curl": "*" }, "require-dev": { - "behat/behat": "2.4.*@stable", - "phpunit/phpunit": "4.6.*" + "phpunit/phpunit": "~6.3" }, "bin": [ "doc-parser" ], - "config": { - "bin-dir": "bin/" - }, "autoload": { "psr-4": { - "DocParser\\": ["src/"] + "DocParser\\": ["src/"], + "DocParser\\Tests\\": ["tests/"] } }, "autoload-dev": { diff --git a/doc-parser.php b/doc-parser.php index c41055c..a67685d 100644 --- a/doc-parser.php +++ b/doc-parser.php @@ -29,8 +29,10 @@ // Setup cli commands $runCmd = new \DocParser\Command\RunCommand(); +$singleCmd = new \DocParser\Command\SingleCommand(); $application = new \Symfony\Component\Console\Application(); $application->add($runCmd); +$application->add($singleCmd); $application->setDefaultCommand($runCmd->getName()); $application->run(); \ No newline at end of file diff --git a/features/availability.feature b/features/availability.feature deleted file mode 100644 index 29c7438..0000000 --- a/features/availability.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: availability - In order to choose which languages for PHP manual - As a parser script - I want to download php.net/download-docs.php and list available languages - - Scenario: Run the CLI tool parser.php which lets you choose the languages you want - When I want a list all available languages - Then I should get these and more: - | lang | title | - | en | English | - | es | Spanish | - | de | German | \ No newline at end of file diff --git a/features/bootstrap/AvailabilityContext.php b/features/bootstrap/AvailabilityContext.php deleted file mode 100644 index b815b13..0000000 --- a/features/bootstrap/AvailabilityContext.php +++ /dev/null @@ -1,42 +0,0 @@ -languages = $avail->listPackages(); - } - - /** - * @Then /^I should get these and more:$/ - */ - public function iShouldGetTheseAndMore(TableNode $table) - { - foreach ($table->getHash() as $row) { - assertArrayHasKey($row['lang'], $this->languages, 'Language ' . $row['lang'] . ' must be among available languages.'); - assertEquals($row['title'], $this->languages[$row['lang']]); - } - foreach ($this->languages as $code => $title) { - assertNotEmpty($title); - } - assertGreaterThan(10, count($this->languages), 'There are missing languages for sure!'); - } - -} diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php deleted file mode 100644 index 60a07ad..0000000 --- a/features/bootstrap/FeatureContext.php +++ /dev/null @@ -1,21 +0,0 @@ -useContext('availability_subcontext_alias', new AvailabilityContext()); - $this->useContext('package_subcontext_alias', new PackageContext()); - $this->useContext('parser_subcontext_alias', new ParserContext()); - $this->useContext('parser_result_subcontext_alias', new ParserResultContext()); - $this->useContext('utils_subcontext_alias', new UtilsContext()); - } - -} \ No newline at end of file diff --git a/features/bootstrap/PackageContext.php b/features/bootstrap/PackageContext.php deleted file mode 100644 index ce3d554..0000000 --- a/features/bootstrap/PackageContext.php +++ /dev/null @@ -1,70 +0,0 @@ -package = new Package($lang, $mirror); - } - - /** - * @Then /^The package can be downloaded to system\'s tmp dir$/ - */ - public function thePackageCanBeDownloadedToSystemSTmpDir() - { - $this->dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-doc-parser-test'; - @mkdir($this->dir); - - $file = $this->dir . DIRECTORY_SEPARATOR . $this->package->getOrigFilename(); - - $this->package->download($file); - } - - /** - * @Given /^Unpack files to the same directory:$/ - */ - public function unpackFilesToTheSameDirectory(PyStringNode $string) - { - $expectedFiles = $string->getLines(); - $this->pagesDir = $this->package->unpack($expectedFiles); - assertFileExists($this->pagesDir); - - chdir($this->pagesDir); - $foundFiles = array_flip(glob('*.html')); - assertCount(count($expectedFiles), $foundFiles); - - foreach ($expectedFiles as $file) { - assertArrayHasKey($file, $foundFiles); - } - } - - /** - * @Given /^Cleanup files when it\'s all done$/ - */ - public function cleanupFilesWhenItSAllDone() - { - $this->package->cleanup(); - assertFileNotExists($this->pagesDir, 'Directory "' . $this->pagesDir . '" wasn\'t removed.'); - } - -} diff --git a/features/bootstrap/ParserContext.php b/features/bootstrap/ParserContext.php deleted file mode 100644 index 3a3e47f..0000000 --- a/features/bootstrap/ParserContext.php +++ /dev/null @@ -1,106 +0,0 @@ -parser = new Parser(); -// var_dump($this->testFilesDir);exit; - } - - /** - * @Given /^these test files:$/ - */ - public function theseTestFiles(TableNode $table) - { - $this->testFiles = $table->getHash(); - } - - /** - * @Then /^match them with files from "([^"]*)"$/ - */ - public function matchThemWithFilesFrom($dir) - { - $this->testFilesDir = realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..') . DIRECTORY_SEPARATOR . $dir; - - foreach ($this->testFiles as $row) { - $expected = json_decode(file_get_contents($this->testFilesDir . DIRECTORY_SEPARATOR . $row['expected-json-result']), true); - - $result = $this->parser->processFile($this->testFilesDir . DIRECTORY_SEPARATOR . $row['source-filename'], Parser::INCLUDE_EXAMPLES); - $funcName = $result->getFuncNames()[0]; - - $actual = array_merge($result->getResult($funcName), [ - 'examples' => $result->getExamples($funcName) - ]); - -// echo json_encode($actual, JSON_PRETTY_PRINT); - - assertEquals($expected, $actual); - } - } - - /** - * @Then /^download manual from "([^"]*)" package from "([^"]*)"$/ - */ - public function downloadManualFromPackageFrom($lang, $mirror) - { - $this->package = new Package($lang, $mirror); - - $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-doc-parser-test'; - @mkdir($dir); - $file = $dir . DIRECTORY_SEPARATOR . $this->package->getOrigFilename(); - - $this->package->download($file); - $testUnpackDir = $this->package->unpack(array_map(function($item) { return $item['source-filename']; }, $this->testFiles)); - - $this->unpackedFilesDir = $this->package->getUnpackedDir(); - - assertEquals($testUnpackDir, $this->unpackedFilesDir); - assertCount(count($this->testFiles), glob($this->unpackedFilesDir . DIRECTORY_SEPARATOR . '*.html')); - } - - /** - * @Given /^test downloaded files against them as well$/ - */ - public function testDownloadedFilesAgainstThemAsWell() - { -// $files = glob($this->downloadedFilesDir . DIRECTORY_SEPARATOR . '*.html'); - - foreach ($this->testFiles as $row) { - $file = $row['source-filename']; - $expectedFileSize = filesize($this->unpackedFilesDir . DIRECTORY_SEPARATOR . $file); - $testFileSize = filesize($this->testFilesDir . DIRECTORY_SEPARATOR . $file); - - if ($expectedFileSize != $testFileSize) { - echo "File sizes doesn't match for \"${file}\"! Documentation might not match the tested sample.\n"; - } - -// $actual = $this->parser->processFile($this->testFilesDir . DIRECTORY_SEPARATOR . $file); - } - - $this->package->cleanup(); - } -} \ No newline at end of file diff --git a/features/bootstrap/ParserResultContext.php b/features/bootstrap/ParserResultContext.php deleted file mode 100644 index 1b584a4..0000000 --- a/features/bootstrap/ParserResultContext.php +++ /dev/null @@ -1,84 +0,0 @@ -getHash() as $row) { - $result = new ParserResult(); - $file = $row['test-file']; - $result->setResult($file, $row['result']); - if ($row['examples']) { - foreach (explode(', ', $row['examples']) as $example) { - $result->addExample($file, $example); - } - } - -// $result->addWarning($file, $row['warning'] ? explode(', ', $row['warning']) : []); - if ($row['warning']) { - foreach (explode(', ', $row['warning']) as $warning) { - $result->addWarning($file, $warning); - } - } - - if ($row['skip']) { - $result->addSkipped($file); - } - - if (isset($this->results[$file])) { - $this->results[$file]->mergeWithResult($result); - } else { - $this->results[$file] = $result; - } - } - } - - /** - * @Then /^after merging them I\'m expecting one result with:$/ - */ - public function afterMergingThemIMExpectingOneResultWith(TableNode $table) - { - $result = new ParserResult(); - foreach ($this->results as $r) { - $result->mergeWithResult($r); - } - - $warningsCount = $examplesCount = 0; - foreach ($table->getHash() as $row) { - $file = $row['test-file']; - - $warningsCount += count(explode(', ', $row['warning'])); - $examplesCount += count(explode(', ', $row['examples'])); - - assertEquals(explode(', ', $row['warning']), $result->getWarnings($file)); - assertEquals(explode(', ', $row['examples']), $result->getExamples($file)); - assertEquals($row['result'], $result->getResult($file)); - if ($row['skip']) { - assertTrue($result->isSkipped($file)); - } else { - assertFalse($result->isSkipped($file)); - } - } - - assertEquals($warningsCount, $result->countAllWarnings()); - assertEquals($examplesCount, $result->countAllExamples()); - } - -} \ No newline at end of file diff --git a/features/bootstrap/UtilsContext.php b/features/bootstrap/UtilsContext.php deleted file mode 100644 index 0aa18f7..0000000 --- a/features/bootstrap/UtilsContext.php +++ /dev/null @@ -1,42 +0,0 @@ -testArrays = $table->getHash(); - } - - /** - * @Then /^I need to convert it recursively to a low level more efficient SplFixedArray$/ - */ - public function iNeedToConvertItRecursivelyToALowLevelMoreEfficientSplfixedarray() - { - /** - * Probably not very helpful - */ - foreach ($this->testArrays as $json) { - $array = json_decode($json['test-array-as-json'], true); -// $fixed = Utils::toFixedArray($array); -// var_dump(memory_get_peak_usage(true) / 1000000); - } - - } - -} \ No newline at end of file diff --git a/features/package.feature b/features/package.feature deleted file mode 100644 index 4ce8fa9..0000000 --- a/features/package.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: package - In order to parse the documentation - As a parser script - I need to download and unpack the manual to a temporary directory - - Scenario: - When I need "fr" manual from "cz1.php.net" - Then The package can be downloaded to system's tmp dir - And Unpack files to the same directory: - """ - class.pdo.html - class.splheap.html - datetime.diff.html - function.cos.html - function.is-string.html - function.print-r.html - function.strcmp.html - """ - Then Cleanup files when it's all done \ No newline at end of file diff --git a/features/parser-result.feature b/features/parser-result.feature deleted file mode 100644 index c1a585f..0000000 --- a/features/parser-result.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: ParserResult - In order to work with parser - As a developer - I need to merge multiple parser results - - Scenario: - When I have multiple parser results like: - | test-file | warning | skip | result | examples | - | file1 | msg1 | | res1 | ex1, ex2 | - | file2 | msg2, msg3 | 1 | res2 | ex4 | - | file1 | | | res1 | ex3 | - | file2 | msg4 | | res4 | | - Then after merging them I'm expecting one result with: - | test-file | warning | skip | result | examples | - | file1 | msg1 | | res1 | ex1, ex2, ex3 | - | file2 | msg2, msg3, msg4 | 1 | res4 | ex4 | \ No newline at end of file diff --git a/features/parser.feature b/features/parser.feature deleted file mode 100644 index 9f797bb..0000000 --- a/features/parser.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: Parser - In order to get a manual page in JSON format - As a developer - I need to run series of XPath queries that process the HTML page - - Scenario: - Given these test files: - | source-filename | expected-json-result | - | datetime.setdate.html | datetime.setdate.json | - | eventhttp.setcallback.html | eventhttp.setcallback.json | - | function.array-diff.html | function.array-diff.json | - | function.chown.html | function.chown.json | - | function.json-encode.html | function.json-encode.json | - | function.str-replace.html | function.str-replace.json | - | function.strrpos.html | function.strrpos.json | - | reflectionclass.getname.html | reflectionclass.getname.json | - | splfileobject.fgetcsv.html | splfileobject.fgetcsv.json | - Then match them with files from "test-manual-files" - Then download manual from "en" package from "cz1.php.net" - And test downloaded files against them as well diff --git a/features/utils.feature b/features/utils.feature deleted file mode 100644 index a82bec2..0000000 --- a/features/utils.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Utils - In order to do some common things when parsing manual pages - As a developer - I need to have a set of static methods that can be used independently - - Scenario: - When I have a very large multi-dimensional array: - | test-array-as-json | - | [ "a", "b", ["c", ["d"]], "e"] | - | { "k1": "a", "k2": { "k3": [ 1, 2, 3 ] }, "k4": [ 4, 5 ] } | - Then I need to convert it recursively to a low level more efficient SplFixedArray \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5912aeb --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/src/Availability.php b/src/Availability.php index 2eecdd5..e4dd3dd 100644 --- a/src/Availability.php +++ b/src/Availability.php @@ -16,14 +16,7 @@ class Availability { * @throws \Exception */ public function listPackages() { - $curl = curl_init(self::DOWNLOADS_URL); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($curl); - - $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - if (200 != $statusCode) { - throw new \Exception("Invalid status lang ${statusCode} for page " . self::DOWNLOADS_URL); - } + $response = $this->download(); $dom = new \DOMDocument(); @$dom->loadHTML($response); @@ -62,4 +55,17 @@ public function listPackages() { return $langs; } + private function download() { + $curl = curl_init(self::DOWNLOADS_URL); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($curl); + + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + if (200 != $statusCode) { + throw new \Exception("Invalid status lang ${statusCode} for page " . self::DOWNLOADS_URL); + } + + return $response; + } + } \ No newline at end of file diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 679c8de..6babb3c 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -16,7 +16,22 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; -class RunCommand extends Command { +class RunCommand extends Command +{ + /** + * Don't parser examples at all + */ + const SKIP_EXAMPLES = 0; + + /** + * Parse examples and include them in function definitions + */ + const INCLUDE_EXAMPLES = 1; + + /** + * Parse examples but keep them in separate arrays + */ + const EXPORT_EXAMPLES = 2; /** * @var Availability @@ -44,7 +59,8 @@ class RunCommand extends Command { private $output; - protected function configure() { + protected function configure() + { $this ->setName('parser:run') ->setDescription('Run interactive parser console') @@ -53,11 +69,12 @@ protected function configure() { ->addOption('examples', 'e', InputOption::VALUE_REQUIRED, 's = skip, i = include, e = export') ->addOption('out-dir', 'o', InputOption::VALUE_REQUIRED, 'Output directory for parsed JSON manual', './output') ->addOption('tmp-dir', 't', InputOption::VALUE_REQUIRED, 'Directory for temporary files', sys_get_temp_dir()) - ->addOption('pretty', null, InputOption::VALUE_NONE, 'Pretty print JSON output') + ->addOption('pretty', 'p', InputOption::VALUE_NONE, 'Pretty print JSON output') ; } - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output) + { $this->avail = new Availability(); $this->output = $output; $this->input = $input; @@ -103,21 +120,20 @@ protected function execute(InputInterface $input, OutputInterface $output) { $this->downloadPackage($package); $this->unpackPackage($package); -// $results = $this->process($unpackedDir, $includeExamples); $this->parse($package, $outDir, $includeExamples); $package->cleanup(); } } - private function chooseLanguages() { + private function chooseLanguages() + { $default = 'en'; $choices = array_merge(['all' => 'All'], $this->allLanguages); $msg = "Choose languages you want to download.\nUse comma to separate multiple languages (default: ${default}):"; $question = new ChoiceQuestion($msg, $choices, $default); $question->setAutocompleterValues(null); -// $languages = $this->languages; $question->setValidator(function($selected) use ($choices) { $selected = array_map(function($val) { return trim($val); @@ -133,13 +149,15 @@ private function chooseLanguages() { return $this->helper->ask($this->input, $this->output, $question); } - private function chooseMirror() { + private function chooseMirror() + { $default = 'php.net'; $question = new Question("Choose mirror site (default: ${default}): ", $default); return $this->helper->ask($this->input, $this->output, $question); } - private function chooseIncludeExamples() { + private function chooseIncludeExamples() + { $default = 'i'; $choices = [ 'i' => 'Include examples', @@ -155,17 +173,8 @@ private function chooseIncludeExamples() { return $this->helper->ask($this->input, $this->output, $question); } - private function stringExamplesParamsToConst($include) { - if ($include == 'i') { - return Parser::INCLUDE_EXAMPLES; - } elseif ($include == 'e') { - return Parser::EXPORT_EXAMPLES; - } else { - return Parser::SKIP_EXAMPLES; - } - } - - private function downloadPackage(Package $package) { + private function downloadPackage(Package $package) + { $url = $package->getUrl(); preg_match('/\/(php_manual.*)\//U', $url, $matches); @@ -190,14 +199,16 @@ private function downloadPackage(Package $package) { } } - public function unpackPackage(Package $package) { + public function unpackPackage(Package $package) + { $this->output->writeln('Unpacking ' . $package->getOrigFilename() . ' ...'); $manualDir = $package->unpack(); $this->output->writeln('Unpacked to ' . $manualDir . "\n"); return $manualDir; } - private function parse(Package $package, $outDir, $includeExamples) { + private function parse(Package $package, $outDir, $includeExamples = false) + { $parser = new Parser(); $bar = $this->createProgressBar(count($parser->getFilesToProcess($package->getUnpackedDir()))); @@ -219,11 +230,11 @@ private function parse(Package $package, $outDir, $includeExamples) { $examplesRes = $results->getExamples($funcName); if ($examplesRes && is_array($arrayRes)) { - if ($includeExamples == Parser::INCLUDE_EXAMPLES) { + if ($includeExamples == RunCommand::INCLUDE_EXAMPLES) { $arrayRes = array_merge($arrayRes, [ 'examples' => $examplesRes ]); - } elseif ($includeExamples == Parser::EXPORT_EXAMPLES) { + } elseif ($includeExamples == RunCommand::EXPORT_EXAMPLES) { $examples[$funcName] = $examplesRes; } } @@ -240,7 +251,7 @@ private function parse(Package $package, $outDir, $includeExamples) { $this->saveFunctionsList($basePath, $functions); - if ($includeExamples == Parser::EXPORT_EXAMPLES) { + if ($includeExamples == RunCommand::EXPORT_EXAMPLES) { $this->saveExamples($basePath, $examples); $this->output->writeln("Total examples: " . $results->countAllExamples() . ""); } @@ -258,10 +269,10 @@ private function parse(Package $package, $outDir, $includeExamples) { $this->output->writeln("Skipped ${filename}"); } } -// $this->output->writeln("Total: ${count} examples"); } - private function saveOutput($basePath, $functions) { + private function saveOutput($basePath, $functions) + { $json = json_encode($functions, $this->getJsonEncoderFlags()); $filePath = $basePath . '.json'; file_put_contents($filePath, $json); @@ -270,7 +281,8 @@ private function saveOutput($basePath, $functions) { $this->printJsonError(); } - private function saveExamples($basePath, $examples) { + private function saveExamples($basePath, $examples) + { $json = json_encode($examples, $this->getJsonEncoderFlags()); $filePath = $basePath . '.examples.json'; file_put_contents($filePath, $json); @@ -279,7 +291,8 @@ private function saveExamples($basePath, $examples) { $this->printJsonError(); } - private function saveFunctionsList($basePath, $functions) { + private function saveFunctionsList($basePath, $functions) + { $normalized = array_map(function($name) { return strtolower($name); }, array_keys($functions)); @@ -291,17 +304,20 @@ private function saveFunctionsList($basePath, $functions) { $this->printJsonError(); } - private function getTmpDir() { + private function getTmpDir() + { return ($this->input->getOption('tmp-dir') ?: sys_get_temp_dir()); } - private function printJsonError() { + private function printJsonError() + { if (json_last_error()) { $this->output->writeln('' . json_last_error_msg() . ''); } } - private function checkMemoryLimit() { + private function checkMemoryLimit() + { $memLimit = ini_get('memory_limit'); $minimumRequired = 128; $bytes = Utils::convertSize(ini_get('memory_limit')); @@ -311,7 +327,8 @@ private function checkMemoryLimit() { } } - private function createProgressBar($max = 100) { + private function createProgressBar($max = 100) + { $bar = new ProgressBar($this->output, $max); $bar->setBarCharacter('#'); $bar->setProgressCharacter('#'); @@ -319,8 +336,18 @@ private function createProgressBar($max = 100) { return $bar; } - private function getJsonEncoderFlags() { + private function getJsonEncoderFlags() + { return ($this->input->getOption('pretty') ? JSON_PRETTY_PRINT : 0) | JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE; } + private function stringExamplesParamsToConst($include) { + if ($include == 'i') { + return self::INCLUDE_EXAMPLES; + } elseif ($include == 'e') { + return self::EXPORT_EXAMPLES; + } else { + return self::SKIP_EXAMPLES; + } + } } \ No newline at end of file diff --git a/src/Command/SingleCommand.php b/src/Command/SingleCommand.php new file mode 100644 index 0000000..d628aa5 --- /dev/null +++ b/src/Command/SingleCommand.php @@ -0,0 +1,68 @@ +setName('parser:single') + ->setDescription('Parse a single HTML file') + ->addArgument('file', InputArgument::REQUIRED, 'Path to the file you want to process') + ->addOption('examples', 'e', InputOption::VALUE_NONE, 'Include examples') + ->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Write the output to a file') + ->addOption('pretty', 'p', InputOption::VALUE_NONE, 'Pretty print JSON output') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $file = $input->getArgument('file'); + $parseExamples = boolval($input->getOption('examples')); + + $outputFile = $input->getOption('output'); + $pretty = boolval($input->getOption('pretty')); + + if (!file_exists($file) || !is_readable($file)) { + $output->writeln("File \"${file}\" doesn't exist or is not readable."); + return 1; + } + + $parser = new Parser(); + $parserResult = $parser->processFile($file, $parseExamples); + + $funcName = $parserResult->getFuncNames()[0]; + $result = $parserResult->getResult($funcName); + + if ($parseExamples) { + $result['examples'] = $parserResult->getExamples($funcName); + } + + $jsonData = json_encode($result, $pretty ? JSON_PRETTY_PRINT : 0); + + if ($outputFile) { + file_put_contents($outputFile, $jsonData); + } else { + $output->write($jsonData); + } + } + +} diff --git a/src/Package.php b/src/Package.php index e874eeb..279ec35 100644 --- a/src/Package.php +++ b/src/Package.php @@ -15,20 +15,26 @@ class Package { const ARCHIVE_INNER_DIR = 'php-chunked-xhtml'; - public function __construct($lang, $mirror) { + public function __construct($lang, $mirror) + { $this->lang = $lang; $this->mirror = $mirror; } - public function download($filePath, $progressCallback = null) { - $this->filePath = $filePath; + public function download($filePath = null, $progressCallback = null) + { + if (!$filePath) { + $tmpDir = $this->getTmpDir(); + $filePath = $tmpDir . DIRECTORY_SEPARATOR . $this->getOrigFilename(); + @mkdir($tmpDir); + } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->getUrl()); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - $f = fopen($this->filePath, 'w+'); + $f = fopen($filePath, 'w+'); curl_setopt($ch, CURLOPT_FILE, $f); if ($progressCallback) { curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, $progressCallback); @@ -39,13 +45,17 @@ public function download($filePath, $progressCallback = null) { fclose($f); - $this->isPackageFileValid(); + $this->isPackageFileValid($filePath); + $this->filePath = $filePath; $this->cleanupFiles[] = $this->filePath; + + return $filePath; } - public function unpack($files = []) { - $this->isPackageFileValid(); + public function unpack($files = []) + { + $this->isPackageFileValid($this->filePath); // decompress from gz $tarFile = str_replace('.tar.gz', '.tar', $this->filePath); @@ -65,44 +75,57 @@ public function unpack($files = []) { return $this->unpackedDir; } - public function cleanup() { + public function cleanup() + { $fs = new Filesystem(); try { $fs->remove($this->cleanupFiles); - } catch (IOExceptionInterface $e) { - new IOException("Unable to remove files/directories: " . implode(', ', $this->cleanupFiles)); + } catch (IOException $e) { + throw new IOException("Unable to remove files/directories: " . implode(', ', $this->cleanupFiles)); } } - private function isPackageFileValid() { - if (!file_exists($this->filePath)) { - throw new \Exception('File "' . $this->filePath . '" doesn\'t exist. Did you call Package::download() before Package::unpack()?'); + private function isPackageFileValid($filePath) + { + if (!file_exists($filePath)) { + throw new \Exception('File "' . $filePath . '" doesn\'t exist. Did you call Package::download() before Package::unpack()?'); } - if (0 == filesize($this->filePath)) { - throw new \Exception('File "' . $this->filePath . '" has 0 length. You might have a wrong mirror site or language file. ' . + if (0 == filesize($filePath)) { + throw new \Exception('File "' . $filePath . '" has 0 length. You might have a wrong mirror site or language file. ' . 'Try opening this this URL in a browser: ' . $this->getUrl()); } } - public function getUrl() { + private function getTmpDir() + { + $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-doc-parser-test'; + return $dir; + } + + public function getUrl() + { return 'http://' . $this->mirror . '/get/' . $this->getOrigFilename() . '/from/this/mirror'; } - public function getOrigFilename() { + public function getOrigFilename() + { return 'php_manual_' . $this->lang . '.tar.gz'; } - public function getLang() { + public function getLang() + { return $this->lang; } - public function getMirror() { + public function getMirror() + { return $this->mirror; } - public function getUnpackedDir() { + public function getUnpackedDir() + { return $this->unpackedDir; } } \ No newline at end of file diff --git a/src/Parser.php b/src/Parser.php index 9a97fae..0d79b06 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -2,41 +2,21 @@ namespace DocParser; -use Symfony\Component\Filesystem\Filesystem; -use DocParser\Utils; - class Parser { - - /** - * Don't parser examples at all - */ - const SKIP_EXAMPLES = 0; - - /** - * Parse examples and include them in function definitions - */ - const INCLUDE_EXAMPLES = 1; - - /** - * Parse examples but keep them in separate arrays - */ - const EXPORT_EXAMPLES = 2; - - /** * Process all files in the directory with selected by getFilesToProcess() method. * - * @param $dir Directory to search for files - * @param int $parseExamples Whether or not include also examples + * @param string $dir Directory to search for files + * @param boolean $parseExamples Whether or not include also examples * @param callable $progressCallback Optional callback used to monitor progress * @return ParserResult Parse result including all warnings and skipped files */ - public function processDir($dir, $parseExamples = 0, \Closure $progressCallback = null) { + public function processDir($dir, $parseExamples = true, callable $progressCallback = null) { $results = new ParserResult(); $files = $this->getFilesToProcess($dir); $processed = 0; - // Parse each file. + // Parse each file foreach ($files as $file) { $progressCallback(basename($file), count($files), $processed); $this->processFile($file, $parseExamples, $results); @@ -46,7 +26,7 @@ public function processDir($dir, $parseExamples = 0, \Closure $progressCallback } /** - * @param $dir Directory where we want to search files + * @param string $dir Directory where we want to search files * @return array */ public function getFilesToProcess($dir) { @@ -56,15 +36,20 @@ public function getFilesToProcess($dir) { /** * Process single file. * - * @param $file Source file - * @param $parseExamples Whether or not include also examples + * @param string $file Source file + * @param boolean $parseExamples Whether or not include also examples + * @throws \Exception * @return ParserResult Parse result including all warnings and skipped files */ - public function processFile($file, $parseExamples, $result = null) { + public function processFile($file, $parseExamples = true, $result = null) { if (!$result) { $result = new ParserResult(); } + if (!file_exists($file)) { + throw new \Exception("File \"${file}\" doesn't exist."); + } + $dom = new \DOMDocument(); @$dom->loadHTML(file_get_contents($file)); // Most important object used to traverse HTML DOM structure. @@ -72,7 +57,7 @@ public function processFile($file, $parseExamples, $result = null) { $function = []; - // Parse function name. + // Parse function name $h1 = $xpath->query('//h1[@class="refname"]'); // Check if it managed to find function name. @@ -81,7 +66,7 @@ public function processFile($file, $parseExamples, $result = null) { return $result; } - // Function short description. + // Function short description $description = $xpath->query('//span[@class="dc-title"]'); if ($description->length > 0) { // some functions don't have any description $function['desc'] = trim(Utils::simplifyString($description->item(0)->textContent), '.') . '.'; @@ -96,7 +81,7 @@ public function processFile($file, $parseExamples, $result = null) { $function['long_desc'] = trim($function['long_desc'], '.') . '.'; } - // PHP version since this function is available. + // PHP version since this function is available $version = $xpath->query('//p[@class="verinfo"]'); if ($version->length > 0) { // check from which PHP version is it available $function['ver'] = trim($version->item(0)->textContent, '()'); @@ -134,7 +119,7 @@ public function processFile($file, $parseExamples, $result = null) { $function['params'] = []; $funcDescription = $xpath->query('//div[@class="refsect1 description"]/div[@class="methodsynopsis dc-description"]'); foreach ($funcDescription as $index => $description) { - // Function name for this parameter list. + // Function name for this parameter list $altName = str_replace('->', '::', $xpath->query('./span[@class="methodname"]', $description)->item(0)->textContent); $parsedParams = [ 'list' => [], @@ -147,7 +132,7 @@ public function processFile($file, $parseExamples, $result = null) { $parsedParams['ret_type'] = $span->item(0)->textContent; } - // Parameter containers. + // Parameter containers $params = $xpath->query('span[@class="methodparam"]', $description); // skip empty parameter list (function declaration that doesn't take any parameter) @@ -162,20 +147,19 @@ public function processFile($file, $parseExamples, $result = null) { $paramDescriptions = null; $varName = $paramNodes->item(1)->textContent; - foreach ($allParameters as $index => $paramDesc) { + foreach ($allParameters as $j => $paramDesc) { if (ltrim($varName, '$&') == $paramDesc->textContent) { - $paramDescriptions = $xpath->query('//div[contains(@class,"parameters")]/dl/dd[' . ($index + 1) . ']'); + $paramDescriptions = $xpath->query('//div[contains(@class,"parameters")]/dl/dd[' . ($j + 1) . ']'); break; } } - // Single parameter. + // Single parameter $param = array( 'type' => $paramNodes->item(0) ? $paramNodes->item(0)->textContent : 'unknown', // type 'var' => $varName, // variable name 'beh' => $params->length - $optional > $i ? 'required' : 'optional', // required/optional - // parameter description - 'desc' => $paramDescriptions ? Utils::extractFormattedText($xpath->query($descPattern, $paramDescriptions->item(0)), $xpath) : null, + 'desc' => $paramDescriptions ? Utils::extractFormattedText($xpath->query($descPattern, $paramDescriptions->item(0)), $xpath) : null, // parameter description ); // Default value for this parameter if ($paramNodes->length >= 3) { @@ -193,17 +177,22 @@ public function processFile($file, $parseExamples, $result = null) { return $result; } - $funcName = strtolower($function['params'][0]['name']); + // Find all possible names for this function + $names = []; foreach ($function['params'] as $index => $param) { - // Use all alternative names just as a reference to the first (primary) name. - $name = strtolower($function['params'][$index]['name']); + $names[] = strtolower($function['params'][$index]['name']); + } + $funcName = $names[0]; + + // Use all alternative names just as a reference to the first (primary) name + foreach (array_unique($names) as $index => $name) { $result->setResult($name, $index == 0 ? $function : $funcName); } - // Parse all examples in this file. - if ($parseExamples != self::SKIP_EXAMPLES) { - // Find all source code containers. + // Parse all examples in this file + if ($parseExamples) { + // Find all source code containers $exampleDiv = $xpath->query('//div[@class="example" or @class="informalexample"]'); for ($i=0; $i < $exampleDiv->length; $i++) { $output = null; @@ -214,11 +203,11 @@ public function processFile($file, $parseExamples, $result = null) { $output = $xpath->query('.//div[@class="cdata"]', $exampleDiv->item($i))->item(0)->textContent; } - // Example title, strip beginning and ending php tags. + // Example title, strip beginning and ending php tags $ps = $xpath->query('p', $exampleDiv->item($i)); if ($ps->length > 0) { $title = $ps->item(0)->textContent; - // Remove some unnecessary stuff. + // Remove some unnecessary stuff $title = trim(preg_replace('/^(Example #\d+|Beispiel #\d+|Exemplo #\d+|Exemple #\d+|Przykład #\d+)/', '', $title)); $title = trim(preg_replace('/\s+/', ' ', $title)); } else { @@ -231,7 +220,7 @@ public function processFile($file, $parseExamples, $result = null) { 'output' => trim($output) ?: null, ]; - // Skip examples with malformed UTF-8 characters. + // Skip examples with malformed UTF-8 characters // @todo: check where's the problem json_encode($example, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); if (json_last_error()) { @@ -245,5 +234,4 @@ public function processFile($file, $parseExamples, $result = null) { return $result; } - } \ No newline at end of file diff --git a/src/Utils.php b/src/Utils.php index a1cd469..6766a91 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -4,34 +4,15 @@ class Utils { - public static function simplifyString($str) { + public static function simplifyString($str) + { $str = preg_replace(array("/\n+/", "/ {2,}/"), ' ', $str); $str = trim($str); return $str; } -// public static function is_class($name) { -// if (strpos($name, '::') !== false || strpos($name, '->') !== false) { -// return true; -// } else { -// return false; -// } -// } - -// public static function rewrite_names($name) { -// global $rewritename; -// $lname = strtolower($name); -// foreach ($rewritename as $pattern => $newName) { -// if (substr($lname, 0, strlen($pattern)) == $pattern) { -// $name = substr($newName, 0, strlen($pattern)) . substr($name, strlen($pattern)); -// } -// } -// return $name; -// } - - - - public static function clearSourceCode($source) { + public static function clearSourceCode($source) + { $source = trim(html_entity_decode(strip_tags(str_replace(array('
', '
', '
'), "\n", $source)))); if (substr($source, 0, 5) == 'length == 0) { @@ -94,7 +76,8 @@ public static function extractFormattedText($elms, $xpath = null) { return self::simplifyString(implode('\n\n', $textParts)); } - static public function convertSize($size) { + static public function convertSize($size) + { // http://stackoverflow.com/questions/11807115/php-convert-kb-mb-gb-tb-etc-to-bytes $number = substr($size, 0, -1); switch(strtoupper(substr($size,-1))){ @@ -108,15 +91,4 @@ static public function convertSize($size) { return $size; } } - -// static public function toFixedArray($array) { -// $fixed = \SplFixedArray::fromArray($array, true); -// -// foreach ($fixed as $key => $value) { -// if (is_array($value)) { -// $fixed[$key] = self::toFixedArray($value); -// } -// } -// return $fixed; -// } } diff --git a/tests/AvailabilityTest.php b/tests/AvailabilityTest.php new file mode 100644 index 0000000..0c67485 --- /dev/null +++ b/tests/AvailabilityTest.php @@ -0,0 +1,23 @@ + 'English', + 'es' => 'Spanish', + 'de' => 'German', + ]; + + $avail = new Availability(); + $languages = $avail->listPackages(); + + $this->assertArraySubset($expected, $languages); + } +} \ No newline at end of file diff --git a/tests/PackageTest.php b/tests/PackageTest.php new file mode 100644 index 0000000..d820c59 --- /dev/null +++ b/tests/PackageTest.php @@ -0,0 +1,46 @@ +assertEquals('php_manual_en.tar.gz', $package->getOrigFilename()); + } + + public function testGetUrl() + { + $package = new Package('en', 'cz1.php.net'); + $this->assertEquals('http://cz1.php.net/get/php_manual_en.tar.gz/from/this/mirror', $package->getUrl()); + } + + public function testUnpack() + { + $expectedFiles = [ + 'class.pdo.html', + 'class.splheap.html', + 'datetime.diff.html', + 'function.cos.html', + 'function.is-string.html', + 'function.print-r.html', + 'function.strcmp.html', + ]; + + $package = new Package('en', 'cz1.php.net'); + $tmpFile = $package->download(); + $dir = $package->unpack($expectedFiles); + + $unpackedFiles = scandir($dir); + foreach ($expectedFiles as $file) { + $this->assertContains($file, $unpackedFiles); + } + + $package->cleanup(); + $this->assertFileNotExists($tmpFile); + } +} \ No newline at end of file diff --git a/tests/ParserResultTest.php b/tests/ParserResultTest.php new file mode 100644 index 0000000..3362cd3 --- /dev/null +++ b/tests/ParserResultTest.php @@ -0,0 +1,83 @@ +setResult($file, $row[3]); + if ($row[4]) { + foreach (explode(',', $row[4]) as $example) { + $result->addExample($file, trim($example)); + } + } + + if ($row[1]) { + foreach (explode(',', $row[1]) as $warning) { + $result->addWarning($file, trim($warning)); + } + } + + if ($row[2]) { + $result->addSkipped($file); + } + + if (isset($allResults[$file])) { + $allResults[$file]->mergeWithResult($result); + } else { + $allResults[$file] = $result; + } + } + + // Check results + $mergedResult = new ParserResult(); + foreach ($allResults as $r) { + $mergedResult->mergeWithResult($r); + } + + $warningsCount = $examplesCount = 0; + foreach ($expected as $row) { + $file = $row[0]; + + $warningsCount += count(explode(',', $row[1])); + $examplesCount += count(explode(',', $row[4])); + + $this->assertEquals(array_map(function($s) { return trim ($s); }, explode(',', $row[1])), $mergedResult->getWarnings($file)); + $this->assertEquals(array_map(function($s) { return trim ($s); }, explode(',', $row[4])), $mergedResult->getExamples($file)); + $this->assertEquals($row[3], $mergedResult->getResult($file)); + if ($row[2]) { + assertTrue($mergedResult->isSkipped($file)); + } else { + assertFalse($mergedResult->isSkipped($file)); + } + } + + $this->assertEquals($warningsCount, $mergedResult->countAllWarnings()); + $this->assertEquals($examplesCount, $mergedResult->countAllExamples()); + } + +} \ No newline at end of file diff --git a/tests/ParserTest.php b/tests/ParserTest.php new file mode 100644 index 0000000..c1f9121 --- /dev/null +++ b/tests/ParserTest.php @@ -0,0 +1,42 @@ + 'datetime.setdate.json', + 'eventhttp.setcallback.html' => 'eventhttp.setcallback.json', + 'function.array-diff.html' => 'function.array-diff.json', + 'function.chown.html' => 'function.chown.json', + 'function.json-encode.html' => 'function.json-encode.json', + 'function.str-replace.html' => 'function.str-replace.json', + 'function.strrpos.html' => 'function.strrpos.json', + 'function.strtr.html' => 'function.strtr.json', + 'reflectionclass.getname.html' => 'reflectionclass.getname.json', + 'splfileobject.fgetcsv.html' => 'splfileobject.fgetcsv.json', + ]; + + $parser = new Parser(); + + foreach ($expectedFiles as $file => $jsonFile) { + $parserResult = $parser->processFile(self::TEST_FILES_DIR . DIRECTORY_SEPARATOR . $file); + $expected = json_decode(file_get_contents(self::TEST_FILES_DIR . DIRECTORY_SEPARATOR . $jsonFile), true); + + $funcName = $parserResult->getFuncNames()[0]; + $actual = $parserResult->getResult($funcName); + $actual['examples'] = $parserResult->getExamples($funcName); + + $this->assertEquals($expected, $actual); + } + } + +} diff --git a/features/test-manual-files/datetime.setdate.html b/tests/test-manual-files/datetime.setdate.html similarity index 100% rename from features/test-manual-files/datetime.setdate.html rename to tests/test-manual-files/datetime.setdate.html diff --git a/features/test-manual-files/datetime.setdate.json b/tests/test-manual-files/datetime.setdate.json similarity index 100% rename from features/test-manual-files/datetime.setdate.json rename to tests/test-manual-files/datetime.setdate.json diff --git a/features/test-manual-files/eventhttp.setcallback.html b/tests/test-manual-files/eventhttp.setcallback.html similarity index 100% rename from features/test-manual-files/eventhttp.setcallback.html rename to tests/test-manual-files/eventhttp.setcallback.html diff --git a/features/test-manual-files/eventhttp.setcallback.json b/tests/test-manual-files/eventhttp.setcallback.json similarity index 100% rename from features/test-manual-files/eventhttp.setcallback.json rename to tests/test-manual-files/eventhttp.setcallback.json diff --git a/features/test-manual-files/function.array-diff.html b/tests/test-manual-files/function.array-diff.html similarity index 100% rename from features/test-manual-files/function.array-diff.html rename to tests/test-manual-files/function.array-diff.html diff --git a/features/test-manual-files/function.array-diff.json b/tests/test-manual-files/function.array-diff.json similarity index 100% rename from features/test-manual-files/function.array-diff.json rename to tests/test-manual-files/function.array-diff.json diff --git a/features/test-manual-files/function.chown.html b/tests/test-manual-files/function.chown.html similarity index 100% rename from features/test-manual-files/function.chown.html rename to tests/test-manual-files/function.chown.html diff --git a/features/test-manual-files/function.chown.json b/tests/test-manual-files/function.chown.json similarity index 100% rename from features/test-manual-files/function.chown.json rename to tests/test-manual-files/function.chown.json diff --git a/features/test-manual-files/function.json-encode.html b/tests/test-manual-files/function.json-encode.html similarity index 100% rename from features/test-manual-files/function.json-encode.html rename to tests/test-manual-files/function.json-encode.html diff --git a/features/test-manual-files/function.json-encode.json b/tests/test-manual-files/function.json-encode.json similarity index 100% rename from features/test-manual-files/function.json-encode.json rename to tests/test-manual-files/function.json-encode.json diff --git a/features/test-manual-files/function.str-replace.html b/tests/test-manual-files/function.str-replace.html similarity index 100% rename from features/test-manual-files/function.str-replace.html rename to tests/test-manual-files/function.str-replace.html diff --git a/features/test-manual-files/function.str-replace.json b/tests/test-manual-files/function.str-replace.json similarity index 100% rename from features/test-manual-files/function.str-replace.json rename to tests/test-manual-files/function.str-replace.json diff --git a/features/test-manual-files/function.strrpos.html b/tests/test-manual-files/function.strrpos.html similarity index 100% rename from features/test-manual-files/function.strrpos.html rename to tests/test-manual-files/function.strrpos.html diff --git a/features/test-manual-files/function.strrpos.json b/tests/test-manual-files/function.strrpos.json similarity index 100% rename from features/test-manual-files/function.strrpos.json rename to tests/test-manual-files/function.strrpos.json diff --git a/tests/test-manual-files/function.strtr.html b/tests/test-manual-files/function.strtr.html new file mode 100644 index 0000000..0b1cb82 --- /dev/null +++ b/tests/test-manual-files/function.strtr.html @@ -0,0 +1,220 @@ + + + + + Translate characters or replace substrings + + +
+ + +
String Functions
+
PHP Manual
+

+
+

strtr

+

(PHP 4, PHP 5, PHP 7)

strtrTranslate characters or replace substrings

+ +
+ +
+

Description

+
+ string strtr + ( string $str + , string $from + , string $to + )
+ +
+ string strtr + ( string $str + , array $replace_pairs + )
+ +

+ If given three arguments, this function returns a copy of + str where all occurrences of each (single-byte) + character in from have been translated to the + corresponding character in to, i.e., every + occurrence of $from[$n] has been replaced with + $to[$n], where $n is a valid + offset in both arguments. +

+

+ If from and to have + different lengths, the extra characters in the longer of the two + are ignored. The length of str will be the same as + the return value's. +

+

+ If given two arguments, the second should be an array in the + form array('from' => 'to', ...). The return value is + a string where all the occurrences of the array keys have been + replaced by the corresponding values. The longest keys will be tried first. + Once a substring has been replaced, its new value will not be searched + again. +

+

+ In this case, the keys and the values may have any length, provided that + there is no empty key; additionally, the length of the return value may + differ from that of str. + However, this function will be the most efficient when all the keys have the + same size. +

+
+ + +
+

Parameters

+

+

+ + +
+str
+ +
+ +

+ The string being translated. +

+
+ + + +
+from
+ +
+ +

+ The string being translated to to. +

+
+ + + +
+to
+ +
+ +

+ The string replacing from. +

+
+ + + +
+replace_pairs
+ +
+ +

+ The replace_pairs parameter may be used instead of + to and from, in which case it's an + array in the form array('from' => 'to', ...). +

+
+ + +
+ +

+
+ + +
+

Return Values

+

+ Returns the translated string. +

+

+ If replace_pairs contains a key which + is an empty string (""), + FALSE will be returned. If the str is not a scalar + then it is not typecasted into a string, instead a warning is raised and + NULL is returned. +

+
+ + +
+

Examples

+

+

+

Example #1 strtr() example

+
+
+<?php
//In this form, strtr() does byte-by-byte translation
//Therefore, we are assuming a single-byte encoding here:
$addr strtr($addr"äåö""aao");
?> +
+
+
+ +
+

+

+ The next example shows the behavior of strtr() when + called with only two arguments. Note the preference of the replacements + ("h" is not picked because there are longer matches) + and how replaced text was not searched again. +

+
+

Example #2 strtr() example with two arguments

+
+
+<?php
$trans 
= array("h" => "-""hello" => "hi""hi" => "hello");
echo 
strtr("hi all, I said hello"$trans);
?> +
+
+
+ +

The above example will output:

+
+
+hello all, I said hi
+
+
+
+

+ The two modes of behavior are substantially different. With three arguments, + strtr() will replace bytes; with two, it may replace + longer substrings. +

+
+

Example #3 strtr() behavior comparison

+
+
+<?php
echo strtr("baab""ab""01"),"\n";

$trans = array("ab" => "01");
echo 
strtr("baab"$trans);
?> +
+
+
+ +

The above example will output:

+
+
+1001
+ba01
+
+
+
+
+ + +
+

See Also

+

+

+

+
+ +

+ + +
String Functions
+
PHP Manual
+
diff --git a/tests/test-manual-files/function.strtr.json b/tests/test-manual-files/function.strtr.json new file mode 100644 index 0000000..3cb32ff --- /dev/null +++ b/tests/test-manual-files/function.strtr.json @@ -0,0 +1,72 @@ +{ + "desc": "Translate characters or replace substrings.", + "long_desc": "If given three arguments, this function returns a copy of `str` where all occurrences of each (single-byte) character in `from` have been translated to the corresponding character in `to`, i.e., every occurrence of `$from[$n]` has been replaced with `$to[$n]`, where `$n` is a valid offset in both arguments.\\n\\nIf `from` and `to` have different lengths, the extra characters in the longer of the two are ignored. The length of `str` will be the same as the return value's.\\n\\nIf given two arguments, the second should be an array in the form `array('from' => 'to', ...)`. The return value is a string where all the occurrences of the array keys have been replaced by the corresponding values. The longest keys will be tried first. Once a substring has been replaced, its new value will not be searched again.\\n\\nIn this case, the keys and the values may have any length, provided that there is no empty key; additionally, the length of the return value may differ from that of `str`. However, this function will be the most efficient when all the keys have the same size.", + "ver": "PHP 4, PHP 5, PHP 7", + "ret_desc": "Returns the translated string.", + "seealso": [ + "str_replace", + "preg_replace" + ], + "filename": "function.strtr", + "params": [ + { + "list": [ + { + "type": "string", + "var": "$str", + "beh": "required", + "desc": "The string being translated." + }, + { + "type": "string", + "var": "$from", + "beh": "required", + "desc": "The string being translated to `to`." + }, + { + "type": "string", + "var": "$to", + "beh": "required", + "desc": "The string replacing `from`." + } + ], + "name": "strtr", + "ret_type": "string" + }, + { + "list": [ + { + "type": "string", + "var": "$str", + "beh": "required", + "desc": "The string being translated." + }, + { + "type": "array", + "var": "$replace_pairs", + "beh": "required", + "desc": "The `replace_pairs` parameter may be used instead of `to` and `from`, in which case it's an array in the form `array('from' => 'to', ...)`." + } + ], + "name": "strtr", + "ret_type": "string" + } + ], + "examples": [ + { + "title": "strtr() example", + "source": "\/\/In this form, strtr() does byte-by-byte translation\n\/\/Therefore, we are assuming a single-byte encoding here:\n$addr = strtr($addr, \"\u00e4\u00e5\u00f6\", \"aao\");", + "output": null + }, + { + "title": "strtr() example with two arguments", + "source": "$trans = array(\"h\" => \"-\", \"hello\" => \"hi\", \"hi\" => \"hello\");\necho strtr(\"hi all, I said hello\", $trans);", + "output": "hello all, I said hi" + }, + { + "title": "strtr() behavior comparison", + "source": "echo strtr(\"baab\", \"ab\", \"01\"),\"\\n\";\n\n$trans = array(\"ab\" => \"01\");\necho strtr(\"baab\", $trans);", + "output": "1001\nba01" + } + ] +} \ No newline at end of file diff --git a/features/test-manual-files/reflectionclass.getname.html b/tests/test-manual-files/reflectionclass.getname.html similarity index 100% rename from features/test-manual-files/reflectionclass.getname.html rename to tests/test-manual-files/reflectionclass.getname.html diff --git a/features/test-manual-files/reflectionclass.getname.json b/tests/test-manual-files/reflectionclass.getname.json similarity index 100% rename from features/test-manual-files/reflectionclass.getname.json rename to tests/test-manual-files/reflectionclass.getname.json diff --git a/features/test-manual-files/splfileobject.fgetcsv.html b/tests/test-manual-files/splfileobject.fgetcsv.html similarity index 100% rename from features/test-manual-files/splfileobject.fgetcsv.html rename to tests/test-manual-files/splfileobject.fgetcsv.html diff --git a/features/test-manual-files/splfileobject.fgetcsv.json b/tests/test-manual-files/splfileobject.fgetcsv.json similarity index 100% rename from features/test-manual-files/splfileobject.fgetcsv.json rename to tests/test-manual-files/splfileobject.fgetcsv.json