diff --git a/Makefile b/Makefile index 5782039b43..9c7dd8975f 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ clean: clear-cache rm -rf build/artifacts/* clear-cache: - php build/aws-clear-cache.php + php build/WorkflowCommandRunner.php clear-cache test: AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar AWS_SESSION_TOKEN= \ @@ -33,10 +33,10 @@ test: test-phar: package [ -f build/artifacts/behat.phar ] || (cd build/artifacts && \ - wget https://github.com/Behat/Behat/releases/download/v3.0.15/behat.phar) + wget https://github.com/Behat/Behat/releases/download/v3.13.0/behat.phar) [ -f build/artifacts/phpunit.phar ] || (cd build/artifacts && \ wget https://phar.phpunit.de/phpunit.phar) - php -dopcache.enable_cli=1 build/phar-test-runner.php --format=progress + php -dopcache.enable_cli=1 build/WorkflowCommandRunner.php phar-test-runner --format=progress coverage: @AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar AWS_CSM_ENABLED=false \ @@ -78,7 +78,7 @@ smoke-noassumerole: # Packages the phar and zip package: - php build/packager.php $(SERVICE) + php build/WorkflowCommandRunner.php package$(if $(SERVICE), --service=$(SERVICE),) api-get-phpdocumentor: mkdir -p build/artifacts @@ -89,19 +89,19 @@ api: api-get-phpdocumentor [ -d build/artifacts/staging ] || make package # Delete a previously built API build to avoid the prompt. rm -rf build/artifacts/docs - php build/remove-method-annotations.php + php build/WorkflowCommandRunner.php remove-method-annotations php build/artifacts/phpDocumentor.phar run --config build/docs/phpdoc.dist.xml - php build/normalize-docs-files.php + php build/WorkflowCommandRunner.php normalize-docs-files make api-models make redirect-map api-models: # Build custom docs - php build/docs.php $(if $(ISSUE_LOGGING_ENABLED),--issue-logging-enabled,) + php build/WorkflowCommandRunner.php docs $(if $(ISSUE_LOGGING_ENABLED),--issue-logging-enabled,) redirect-map: # Build redirect map - php build/build-redirect-map.php + php build/WorkflowCommandRunner.php build-redirect-map api-show: open build/artifacts/docs/index.html @@ -110,23 +110,23 @@ api-package: zip -r build/artifacts/aws-docs-api.zip build/artifacts/docs/build api-manifest: - php build/build-manifest.php + php build/WorkflowCommandRunner.php build-manifest make clear-cache # Compiles JSON data files and prints the names of PHP data files created or # updated. compile-json: - php -dopcache.enable_cli=1 build/compile-json.php + php -dopcache.enable_cli=1 build/WorkflowCommandRunner.php compile-json git diff --name-only | grep ^src/data/.*\.json\.php$ || true annotate-clients: clean - php build/annotate-clients.php --all + php build/WorkflowCommandRunner.php annotate-clients --all annotate-client-locator: clean - php build/annotate-client-locator.php + php build/WorkflowCommandRunner.php annotate-client-locator build-manifest: - php build/build-manifest.php >/dev/null + php build/WorkflowCommandRunner.php build-manifest >/dev/null build: | build-manifest compile-json annotate-clients annotate-client-locator @@ -159,7 +159,7 @@ tag: check-tag release: check-tag package git push origin master git push origin $(TAG) - php build/gh-release.php $(TAG) + php build/WorkflowCommandRunner.php gh-release --tag=$(TAG) # Tags the repo and publishes a release. full_release: tag release diff --git a/build/Command/AbstractCommand.php b/build/Command/AbstractCommand.php new file mode 100644 index 0000000000..2c6bae518f --- /dev/null +++ b/build/Command/AbstractCommand.php @@ -0,0 +1,80 @@ +projectRoot = $projectRoot ?? dirname(__DIR__, 2); + } + + public function execute(array $args): int + { + if (in_array('--help', $args, true) || in_array('-h', $args, true)) { + $name = $this->getName(); + $this->output($name); + $this->output(str_repeat('-', strlen($name))); + $this->output($this->getDescription()); + $this->output(''); + $this->output('Usage:'); + $this->output(' ' . $this->getUsage()); + return 0; + } + + if (in_array('--verbose', $args, true) || in_array('-v', $args, true)) { + $this->verbose = true; + } + + return $this->doExecute($args); + } + + abstract protected function doExecute(array $args): int; + + protected function output(string $msg): void + { + fwrite(STDOUT, $msg . "\n"); + } + + protected function error(string $msg): void + { + fwrite(STDERR, "[ERROR] $msg\n"); + } + + protected function verbose(string $msg): void + { + if ($this->verbose) { + fwrite(STDOUT, $msg . "\n"); + } + } + + protected function parseOptions(array $args): array + { + $options = []; + foreach ($args as $arg) { + if (str_starts_with($arg, '--')) { + $arg = substr($arg, 2); + if (str_contains($arg, '=')) { + [$key, $value] = explode('=', $arg, 2); + $options[$key] = $value; + } else { + $options[$arg] = true; + } + } + } + return $options; + } + + protected function getProjectRoot(): string + { + return $this->projectRoot; + } + + protected static function getBuildDir(): string + { + return dirname(__DIR__); + } +} diff --git a/build/Command/AnnotateClientLocatorCommand.php b/build/Command/AnnotateClientLocatorCommand.php new file mode 100644 index 0000000000..7eb676d7cb --- /dev/null +++ b/build/Command/AnnotateClientLocatorCommand.php @@ -0,0 +1,54 @@ +update(); + + return 0; + } +} diff --git a/build/Command/AnnotateClientsCommand.php b/build/Command/AnnotateClientsCommand.php new file mode 100644 index 0000000000..ab2f6260bc --- /dev/null +++ b/build/Command/AnnotateClientsCommand.php @@ -0,0 +1,83 @@ +] [--tag=]'; + } + + protected function doExecute(array $args): int + { + $options = $this->parseOptions($args) + ['class' => [], 'tag' => []]; + + // make sure all options are arrays + array_walk($options, function (&$value) { + if (!is_array($value)) { + $value = [$value]; + } + }); + + if (isset($options['all'])) { + $options['class'] = \Aws\flatmap(\Aws\manifest(), function (array $manifest) { + return $this->getClientClasses($manifest['namespace']); + }); + } + + foreach ($options['tag'] as $tag) { + if ('latest' === $tag) { + $tag = trim(`git tag | tail -n 1`); + } + + exec("git diff-index --name-only --cached $tag", $files); + $alteredApiFiles = array_filter($files, function ($file) { + return preg_match('/api-2.json$/', $file); + }); + + $clientsWithChangedApis = \Aws\flatmap($alteredApiFiles, function ($file) { + $file = str_replace('src/data/', '', $file); + $endpoint = substr($file, 0, strpos($file, '/')); + return $this->getClientClasses(\Aws\manifest($endpoint)['namespace']); + }); + $options['class'] = \Aws\flatmap( + [$options['class'], $clientsWithChangedApis], + function ($class) { return $class; } + ); + } + + foreach ($options['class'] as $classToUpdate) { + $annotator = new \ClientAnnotator($classToUpdate); + + if (!$annotator->updateApiMethodAnnotations()) { + trigger_error( + "Unable to update annotations on $classToUpdate", + E_USER_WARNING + ); + } + } + + return 0; + } + + private function getClientClasses(string $namespace): array + { + $clients = ["Aws\\{$namespace}\\{$namespace}Client"]; + if (class_exists("Aws\\{$namespace}\\{$namespace}MultiRegionClient")) { + $clients[] = "Aws\\{$namespace}\\{$namespace}MultiRegionClient"; + } + + return $clients; + } +} diff --git a/build/Command/BuildChangelogCommand.php b/build/Command/BuildChangelogCommand.php new file mode 100644 index 0000000000..5d74d08b6f --- /dev/null +++ b/build/Command/BuildChangelogCommand.php @@ -0,0 +1,40 @@ +verbose; + $params['base_dir'] = $this->getProjectRoot() . '/'; + + $changelogBuilder = new ChangelogBuilder($params); + + $changelogBuilder->buildChangelog(); + + // Omit fixEndpointFile() call - method doesn't exist on ChangelogBuilder + + $changelogBuilder->cleanNextReleaseFolder(); + + return 0; + } +} diff --git a/build/Command/BuildManifestCommand.php b/build/Command/BuildManifestCommand.php new file mode 100644 index 0000000000..4097f472f8 --- /dev/null +++ b/build/Command/BuildManifestCommand.php @@ -0,0 +1,135 @@ + [ + 'latest' => [ + '2015-06-01', + ], + ], + 'cloudfront' => [ + 'latest' => [ + '2016-01-13', + '2015-09-17', + ], + '2015-07-27' => [ + '2015-04-17', + '2014-11-06' + ], + ], + 'ec2' => [ + 'latest' => [ + '2015-04-15', + ], + ], + 'events' => [ + 'latest' => [ + '2014-02-03', + ], + ], + 'inspector' => [ + 'latest' => [ + '2015-08-18', + ] + ], + ]; + + public function getName(): string + { + return 'build-manifest'; + } + + public function getDescription(): string + { + return 'Rebuilds src/data/manifest.json.'; + } + + public function getUsage(): string + { + return 'php build/WorkflowCommandRunner.php build-manifest'; + } + + protected function doExecute(array $args): int + { + $projectRoot = $this->getProjectRoot(); + + // Create a list of possible namespaces + $possibleNamespaces = []; + $skip = ['.', '..', 'Api', 'data', 'Multipart', 'Signature']; + foreach (scandir($projectRoot . '/src') as $dir) { + if (!in_array($dir, $skip) && is_dir($projectRoot . '/src/' . $dir)) { + $possibleNamespaces[strtolower($dir)] = $dir; + } + } + + $manifest = []; + foreach (glob($projectRoot . '/src/data/**/**/api-2.json') as $file) { + $model = json_decode(file_get_contents($file), true); + preg_match('@src/data/([^/]+)/[0-9]{4}-[0-9]{2}-[0-9]{2}/api-2.json$@', $file, $matches); + $identifier = $matches[1]; + $metadata = $model['metadata'] + ['compatibleApiVersions' => []]; + if (empty($manifest[$identifier])) { + // Calculate a namespace for the service. + $ns = isset($metadata['serviceAbbreviation']) + ? $metadata['serviceAbbreviation'] + : $metadata['serviceFullName']; + $ns = str_replace(['Amazon', 'AWS', 'Beta', '(', ')', ' ', '/', '-'], '', $ns); + + // check if it's a grandfathered namespace + $grandfatheredServices = json_decode( + file_get_contents($projectRoot . '/src/data/grandfathered-services.json'), + true + )['grandfathered-services']; + if (!in_array($ns, $grandfatheredServices)) { + $ns = str_replace(' ', '', ucwords($metadata['serviceId'])); + } + + if (!isset($possibleNamespaces[strtolower($ns)])) { + throw new \Exception('NS not found: ' . $ns); + } + + $ns = $possibleNamespaces[strtolower($ns)]; + + $manifest[$identifier] = [ + 'namespace' => $ns, + 'versions' => [], + ]; + } + + $manifest[$identifier]['versions'][$metadata['apiVersion']] + = $metadata['apiVersion']; + + $serviceIdentifier = isset($metadata['serviceId']) + ? str_replace(' ', '_', strtolower($metadata['serviceId'])) + : ''; + + if (!empty($serviceIdentifier)) { + $manifest[$identifier]['serviceIdentifier'] = $serviceIdentifier; + } + } + + foreach ($manifest as $identifier => &$metadata) { + $metadata['versions']['latest'] = max(array_keys($metadata['versions'])); + foreach ($metadata['versions'] as $name => $version) { + if (isset(self::COMPATIBLE_API_VERSIONS[$identifier][$name])) { + foreach (self::COMPATIBLE_API_VERSIONS[$identifier][$name] as $compatVersion) { + $metadata['versions'][$compatVersion] + = $metadata['versions'][$name]; + } + } + } + + krsort($metadata['versions']); + } + + $data = json_encode($manifest, JSON_PRETTY_PRINT); + $file = $projectRoot . '/src/data/manifest.json'; + file_put_contents($file, "$data\n"); + + $this->output("Wrote the following data to {$file}:\n>>>>>\n{$data}<<<<<"); + return 0; + } +} diff --git a/build/Command/BuildRedirectMapCommand.php b/build/Command/BuildRedirectMapCommand.php new file mode 100644 index 0000000000..3320f57659 --- /dev/null +++ b/build/Command/BuildRedirectMapCommand.php @@ -0,0 +1,35 @@ +build(); + + $this->output('Redirect map built.'); + return 0; + } +} diff --git a/build/Command/BuildServiceCommand.php b/build/Command/BuildServiceCommand.php new file mode 100644 index 0000000000..2e415d1fd8 --- /dev/null +++ b/build/Command/BuildServiceCommand.php @@ -0,0 +1,56 @@ + --model= [--clientPath=] [--exceptionPath=]'; + } + + protected function doExecute(array $args): int + { + $options = $this->parseOptions($args); + + if (empty($options['namespace']) || empty($options['model'])) { + $this->error( + "You must specify a namespace (--namespace=) and path to an api model " + . "(--model=) to build a service" + ); + return 1; + } + + $options['model'] = \Aws\load_compiled_json($options['model']); + $options['namespace'] = ucfirst($options['namespace']); + + $projectRoot = $this->getProjectRoot(); + $options += [ + 'clientPath' => $projectRoot + . "/src/{$options['namespace']}/{$options['namespace']}Client.php", + 'exceptionPath' => $projectRoot + . "/src/{$options['namespace']}/Exception/{$options['namespace']}Exception.php", + ]; + + (new \ServiceBuilder( + $options['namespace'], + $options['model'], + $options['clientPath'], + $options['exceptionPath'] + )) + ->buildClient() + ->buildException(); + + return 0; + } +} diff --git a/build/Command/ClearCacheCommand.php b/build/Command/ClearCacheCommand.php new file mode 100644 index 0000000000..03cdf6e186 --- /dev/null +++ b/build/Command/ClearCacheCommand.php @@ -0,0 +1,28 @@ +output('JMESPath cache cleared.'); + return 0; + } +} diff --git a/build/Command/CommandInterface.php b/build/Command/CommandInterface.php new file mode 100644 index 0000000000..5b6900ebf3 --- /dev/null +++ b/build/Command/CommandInterface.php @@ -0,0 +1,14 @@ +getProjectRoot() . '/src/data'); + $dataFilesIterator = \Aws\recursive_dir_iterator($dataDir); + $count = 0; + + foreach (new \RegexIterator($dataFilesIterator, '/\.json$/') as $dataFile) { + (new \JsonCompiler($dataFile)) + ->compile("$dataFile.php"); + $count++; + } + + $this->output("Compiled $count JSON files."); + return 0; + } +} diff --git a/build/Command/DocsCommand.php b/build/Command/DocsCommand.php new file mode 100644 index 0000000000..2e0d467867 --- /dev/null +++ b/build/Command/DocsCommand.php @@ -0,0 +1,79 @@ +getProjectRoot(); + + // Setup directories. + $xmlObj = simplexml_load_file($buildDir . '/docs/phpdoc.dist.xml'); + $xmlJson = json_encode($xmlObj); + $config = json_decode($xmlJson, true); + $outputDir = $buildDir . '/artifacts/docs'; + $apiProvider = \Aws\Api\ApiProvider::defaultProvider(); + + // Extract the built homepage into a template file. + $xml = new \DOMDocument(); + @$xml->loadHTMLFile($buildDir . '/artifacts/docs/index.html'); + $ele = $xml->getElementById('content'); + $ele->nodeValue = '{{ contents }}'; + $template = $xml->saveHTML(); + + $sourceDirs = array_map(function ($dirRelativeToProjectRoot) use ($projectRoot) { + return $projectRoot . '/' . $dirRelativeToProjectRoot; + }, is_array($config['version']['api']['source']['path']) + ? $config['version']['api']['source']['path'] + : [$config['version']['api']['source']['path']] + ); + $sourceFiles = []; + foreach ($sourceDirs as $dir) { + $sourceFiles = array_merge( + $sourceFiles, + array_filter( + array_map('realpath', iterator_to_array(\Aws\recursive_dir_iterator($dir))), + function ($path) { + return preg_match('/(?parseOptions($args); + $issueLoggingEnabled = isset($options['issue-logging-enabled']); + + // Generate API docs + $builder = new DocsBuilder( + $apiProvider, + $outputDir, + $template, + 'http://docs.aws.amazon.com/aws-sdk-php/v3/api/', + [], + $sourceFiles, + $issueLoggingEnabled + ); + $builder->build(); + + return 0; + } +} diff --git a/build/Command/GhReleaseCommand.php b/build/Command/GhReleaseCommand.php new file mode 100644 index 0000000000..92b2b21078 --- /dev/null +++ b/build/Command/GhReleaseCommand.php @@ -0,0 +1,180 @@ + php build/WorkflowCommandRunner.php gh-release --tag='; + } + + protected function doExecute(array $args): int + { + $options = $this->parseOptions($args); + + $token = getenv('OAUTH_TOKEN'); + if (!$token) { + $this->error('An OAUTH_TOKEN environment variable is required'); + return 1; + } + + if (empty($options['tag'])) { + $this->error('A --tag option is required. Usage: gh-release --tag=X.Y.Z'); + return 1; + } + + $tag = $options['tag']; + $owner = 'aws'; + $repo = 'aws-sdk-php'; + + // Grab and validate the tag annotation + chdir($this->getProjectRoot()); + $message = `chag contents -t "$tag"`; + if (!$message) { + $this->error('Chag could not find or parse the tag'); + return 1; + } + + // Add retry middleware + $stack = \GuzzleHttp\HandlerStack::create(); + $stack->push(\GuzzleHttp\Middleware::retry( + function ($retries, $request, $response) { + $statusCode = $response->getStatusCode(); + if ($retries < self::MAX_ATTEMPTS && !in_array($statusCode, [200, 201, 202])) { + $this->output("Attempt failed with status code {$statusCode}: " + . $response->getBody()); + return true; + } + return false; + }, + function ($retries) { + return 1000 * (1 + $retries); + } + )); + + // Create a GitHub client. + $client = new \GuzzleHttp\Client([ + 'base_uri' => 'https://api.github.com/', + 'headers' => ['Authorization' => "token $token"], + 'handler' => $stack, + ]); + + // Create a Github client with no retry middleware + $uploadClient = new \GuzzleHttp\Client([ + 'base_uri' => 'https://api.github.com/', + 'headers' => ['Authorization' => "token $token"] + ]); + + // Publish the release + $response = $client->post("repos/{$owner}/{$repo}/releases", [ + 'json' => [ + 'tag_name' => $tag, + 'name' => "Version {$tag}", + 'body' => $message, + ] + ]); + $releaseBody = json_decode($response->getBody(), true); + + // Grab the location of the new release + $url = $response->getHeaderLine('Location'); + $this->output("Release successfully published to: $url"); + + // Uploads go to uploads.github.com + $uploadUrl = new Uri($url); + $uploadUrl = $uploadUrl->withHost('uploads.github.com'); + + // Upload aws.zip + $zipAttempts = $this->retryUpload($client, $uploadClient, $owner, $repo, $releaseBody, $uploadUrl, 'aws.zip'); + if ($zipAttempts === false) { + $this->output("aws.zip upload failed after " . self::MAX_ATTEMPTS . " attempts."); + } else { + $this->output("aws.zip upload succeeded after {$zipAttempts} attempt(s)."); + } + + // Upload aws.phar + $pharAttempts = $this->retryUpload($client, $uploadClient, $owner, $repo, $releaseBody, $uploadUrl, 'aws.phar'); + if ($pharAttempts === false) { + $this->output("aws.phar upload failed after " . self::MAX_ATTEMPTS . " attempts."); + } else { + $this->output("aws.phar upload succeeded after {$pharAttempts} attempt(s)."); + } + + return 0; + } + + /** + * Attempts an artifact upload and retries up to MAX_ATTEMPTS times + * + * @return bool|int + */ + private function retryUpload($client, $uploadClient, $owner, $repo, $releaseBody, $uploadUrl, $filename) + { + $isSuccessful = false; + $attempts = 0; + $filetype = substr($filename, strpos($filename, '.') + 1); + $buildDir = self::getBuildDir(); + + while (!$isSuccessful && $attempts < self::MAX_ATTEMPTS) { + try { + $attempts++; + $response = $uploadClient->post("{$uploadUrl}/assets?name={$filename}", [ + 'headers' => ['Content-Type' => "application/{$filetype}"], + 'body' => Psr7\Utils::tryFopen($buildDir . "/artifacts/{$filename}", 'r') + ]); + $this->output("{$filename} uploaded to: " . json_decode($response->getBody(), true)['browser_download_url']); + $isSuccessful = true; + } catch (\GuzzleHttp\Exception\ServerException $e) { + $this->output("{$filename} failed to upload:"); + $this->error($e->getMessage()); + + // Fetch and inspect assets for failed downloads + $response = $client->get("/repos/{$owner}/{$repo}/releases/{$releaseBody['id']}/assets", []); + $assets = json_decode($response->getBody(), true); + + foreach ($assets as $asset) { + if ($asset['state'] !== 'uploaded') { + try { + $response = $uploadClient->delete("/repos/{$owner}/{$repo}/releases/assets/{$asset['id']}", []); + + if ($response->getStatusCode() == 204) { + $this->output("Failed upload of {$asset['name']} at {$asset['browser_download_url']} has successfully been deleted."); + } else { + $this->output("Failed upload of {$asset['name']} at {$asset['browser_download_url']} was unable to be deleted."); + } + } catch (\GuzzleHttp\Exception\ClientException $e) { + $response = $e->getResponse(); + if ($response->getStatusCode() == 204) { + $this->output("Failed upload of {$asset['name']} at {$asset['browser_download_url']} has successfully been deleted."); + } else { + $this->output("Failed upload of {$asset['name']} at {$asset['browser_download_url']} was unable to be deleted."); + $this->error($e->getMessage()); + } + } + } + } + } + } + + if ($isSuccessful) { + return $attempts; + } + + return false; + } +} diff --git a/build/Command/ListCommand.php b/build/Command/ListCommand.php new file mode 100644 index 0000000000..e27fd9d617 --- /dev/null +++ b/build/Command/ListCommand.php @@ -0,0 +1,53 @@ +commandMap = $commandMap; + } + + public function getName(): string + { + return 'list'; + } + + public function getDescription(): string + { + return 'Lists all available commands with descriptions.'; + } + + public function getUsage(): string + { + return 'php build/WorkflowCommandRunner.php list'; + } + + protected function doExecute(array $args): int + { + $this->output('Available commands:'); + $this->output(''); + + $maxLen = 0; + $commands = []; + foreach ($this->commandMap as $name => $class) { + if ($name === 'list') { + $commands[$name] = $this->getDescription(); + } else { + $command = new $class(); + $commands[$name] = $command->getDescription(); + } + $maxLen = max($maxLen, strlen($name)); + } + + foreach ($commands as $name => $description) { + $this->output(sprintf(" %-{$maxLen}s %s", $name, $description)); + } + + return 0; + } +} diff --git a/build/Command/NormalizeDocsFilesCommand.php b/build/Command/NormalizeDocsFilesCommand.php new file mode 100644 index 0000000000..31eaae2270 --- /dev/null +++ b/build/Command/NormalizeDocsFilesCommand.php @@ -0,0 +1,272 @@ +normalizeAndMoveFiles($namespacesDirectory, "namespace-"); + $this->normalizeAndMoveFiles($classesDirectory, "class-"); + $this->normalizeAndMoveFiles($packagesDirectory, "package-"); + + // Update hrefs in HTML files + $this->updateHtmlHrefs($parentDirectory); + + // Updates search index urls with generated api pages + $this->updateSearchIndex($parentDirectory . '/js/searchIndex.js'); + + // Add the SNS validator notice + $this->insertSnsValidatorNotice($parentDirectory); + + // Move static assets + $this->copyDirectory($buildDir . '/docs/static', $parentDirectory . '/static'); + $this->copyDirectory($buildDir . '/docs/js', $parentDirectory . '/js'); + $this->copyDirectory($buildDir . '/docs/css', $parentDirectory . '/css'); + + // Remove unnecessary files/directories + $removableDirs = ['classes', 'files', 'graphs', 'indices', 'namespaces', 'packages', 'reports']; + + foreach ($removableDirs as $dir) { + $this->deleteDirectory("{$parentDirectory}/{$dir}"); + } + + $this->output("All tasks completed."); + return 0; + } + + private function normalizeAndMoveFiles(string $directory, string $prefix): void + { + $files = glob($directory . '/*'); + foreach ($files as $file) { + if (is_file($file)) { + $basename = ucfirst(basename($file)); + $modifiedBasename = str_replace("-", ".", $basename); + $newFilename = $prefix . $modifiedBasename; + $newPath = dirname($directory) . '/' . $newFilename; + rename($file, $newPath); + $this->output("Moved and renamed $file to $newPath"); + } + } + } + + private function updateHtmlHrefs(string $directory): void + { + $htmlFiles = glob($directory . '/*.html'); + foreach ($htmlFiles as $file) { + $doc = new \DOMDocument(); + @$doc->loadHTMLFile($file); + + // Remove tags + while (($baseTags = $doc->getElementsByTagName('base')) && $baseTags->length) { + $baseTag = $baseTags->item(0); + $baseTag->parentNode->removeChild($baseTag); + } + + $links = $doc->getElementsByTagName('a'); + + foreach ($links as $link) { + if ($link->hasAttribute('href')) { + $href = $link->getAttribute('href'); + $href = preg_replace_callback( + '/(namespaces|classes|packages)\/([a-zA-Z])/', + function ($matches) { + if ($matches[1] === 'classes') { + $prefix = 'class'; + } else { + $prefix = substr($matches[1], 0, -1); + } + + return $prefix . '-' . strtoupper($matches[2]); + }, + $href + ); + $href = preg_replace_callback( + '/(namespace-|class-|package-)([\w-]+)\.html/', + function ($matches) { + $suffix = str_replace("-", ".", $matches[2]); + return $matches[1] . $suffix . '.html'; + }, + $href + ); + $link->setAttribute('href', $href); + } + } + + // Remove