diff --git a/.gitignore b/.gitignore index 5ae8e33..d2d08b4 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,8 @@ _backup* /*.phar /composer.lock +/make/*.md + /examples/config.json !/src/bin /tests/assets/config.save.json diff --git a/composer.json b/composer.json index 87773a5..ee478ac 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,9 @@ "webklex/php-imap": "^4|^5" }, "require-dev": { + "nette/php-generator": "*", "pdepend/pdepend": "^2", + "phpdocumentor/reflection-docblock": "^5.3", "phploc/phploc": "^7", "phpmd/phpmd": "^2", "phpstan/phpstan": "^1.8", diff --git a/make/genmethoddocs.php b/make/genmethoddocs.php new file mode 100644 index 0000000..26336c3 --- /dev/null +++ b/make/genmethoddocs.php @@ -0,0 +1,658 @@ +indentation = ' '; + } +} + +class ExtractClass +{ + /** + * The class to analyze + * + * @var string + */ + protected $className = ""; + + /** + * Constructor + * + * @param string $className + */ + public function __construct(string $className) + { + $this->className = $className; + } + + /** + * Returns the current classnane + * + * @return string + * */ + public function getClassName(): string + { + return $this->className; + } + + /** + * Returns the base name of the current classname + * + * @return string + */ + public function getClassBasename(): string + { + $classParts = explode('\\', $this->className); + return end($classParts); + } + + /** + * Magic method __toString, String converstion + * + * @return string + * @throws InvalidArgumentException + * @throws PcreException + * @throws LogicException + */ + public function __toString() + { + return $this->getJson(); + } + + /** + * Returns the result as array + * + * @return array + * @throws InvalidArgumentException + * @throws PcreException + * @throws LogicException + */ + public function getArray(): array + { + $reflection = new ReflectionClass($this->className); + $classDocComment = $reflection->getDocComment(); + $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); // Only public methods + $docBlockFactory = DocBlockFactory::createInstance(); + $result = []; + + if ($classDocComment !== false) { + $classDocBlock = $docBlockFactory->create($classDocComment); + $deprecatedTag = $classDocBlock->getTagsByName('deprecated'); + $result['class'] = [ + 'summary' => $classDocBlock->getSummary() ?: '', + 'description' => (string)$classDocBlock->getDescription() ?: '', + 'deprecated' => !empty($deprecatedTag) ? (string)$deprecatedTag[0] : '' + ]; + } else { + $result['class'] = [ + 'summary' => '', + 'description' => '', + 'deprecated' => '' + ]; + } + + foreach ($methods as $method) { + $docComment = $method->getDocComment(); + $parameters = []; + $returnDetails = [ + 'type' => 'void', + 'description' => '' + ]; + $methodDetails = [ + 'summary' => '', + 'description' => '', + 'static' => false, + 'abstract' => false, + 'final' => false, + 'hasadditional' => false, + 'deprecated' => '', + ]; + + if ($docComment !== false) { + $docBlock = $docBlockFactory->create($docComment); + + // Extract summary and description + $methodDetails['summary'] = $docBlock->getSummary() ?: 'No summary available.'; + $methodDetails['description'] = (string)$docBlock->getDescription() ?: ''; + $methodDetails['static'] = $method->isStatic(); + $methodDetails['abstract'] = $method->isAbstract(); + $methodDetails['final'] = $method->isFinal(); + $methodDetails['hasadditional'] = $method->isStatic() || $method->isAbstract() || $method->isFinal(); + $deprecatedTag = $docBlock->getTagsByName('deprecated'); + if (!empty($deprecatedTag)) { + $methodDetails['deprecated'] = (string)$deprecatedTag[0]; + } + + // Parse @param tags + $paramDescriptions = []; + foreach ($docBlock->getTagsByName('param') as $tag) { + if ($tag instanceof Param) { + $paramDescriptions[$tag->getVariableName()] = [ + 'type' => (string) $tag->getType(), + 'description' => (string) $tag->getDescription() + ]; + } + } + + // Parse @return tag + $returnTag = $docBlock->getTagsByName('return'); + if (!empty($returnTag) && $returnTag[0] instanceof Return_) { + $returnDetails['type'] = (string) $returnTag[0]->getType(); + $returnDetails['description'] = (string) $returnTag[0]->getDescription(); + } + } + + // Get method parameters and match them with DocBlock descriptions + foreach ($method->getParameters() as $parameter) { + $parameterName = $parameter->getName(); + $parameterType = $parameter->getType(); + + $parameters[] = [ + 'name' => $parameterName, + 'type' => $parameterType ? $parameterType->getName() : 'mixed', + 'isNullable' => $parameterType && $parameterType->allowsNull(), + 'defaultValueavailable' => $parameter->isOptional() ? ($parameter->isDefaultValueAvailable() ? true : false) : false, + 'defaultValue' => $parameter->isOptional() ? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null) : null, + 'description' => $paramDescriptions[$parameterName]['description'] ?? '' + ]; + } + + $result['methods'][$method->getName()] = [ + 'methodDetails' => $methodDetails, + 'parameters' => $parameters, + 'return' => $returnDetails + ]; + } + + return $result; + } + + /** + * Returns the result as JSON string + * + * @return string + * @throws InvalidArgumentException + * @throws PcreException + * @throws LogicException + */ + public function getJson(): string + { + return json_encode($this->getArray(), JSON_PRETTY_PRINT); + } + + /** + * Save Json to file + * + * @param string $filename + * @return void + * @throws InvalidArgumentException + */ + public function saveJson(string $filename): void + { + file_put_contents($filename, $this->getJson()); + } +} + +class MarkDownGenerator +{ + /** + * Extractor + * + * @var ExtractClass + */ + protected $extractor = null; + + /** + * The lines for the MD + * + * @var string[] + */ + protected $lines = []; + + /** + * Constructor + * + * @param ExtractClass $extractor + */ + public function __construct(ExtractClass $extractor) + { + $this->extractor = $extractor; + } + + /** + * Generate markdown + * + * @return MarkDownGenerator + */ + public function generateMarkdown(): MarkDownGenerator + { + $metaData = $this->extractor->getArray(); + + $this->addLineH2("Summary"); + + $phpPrinter = new CustomPhpPrinter(); + $phpClass = new ClassType($this->extractor->getClassBasename()); + + if (!empty($metaData['class']['summary'])) { + $this->addLine($metaData['class']['summary'] ?? "")->addEmptyLine(); + } + + if (!empty($metaData['class']['description'])) { + $this->addLine($metaData['class']['description'] ?? "")->addEmptyLine(); + } + + if (!empty($metaData['class']['deprecated'])) { + $this->addLine("> [!CAUTION]"); + $this->addLine("> Deprecated %s", $metaData['class']['deprecated']); + $this->addEmptyLine(); + } + + $this->addExample(dirname(__FILE__) . sprintf('/md/%s.md', $this->extractor->getClassBasename()), true); + + if (!empty($metaData['methods'])) { + $this->addLineH2("Methods"); + } + + foreach ($metaData['methods'] as $methodName => $methodData) { + $this->addLineH3($methodName, $methodData["methodDetails"]["hasadditional"] === false); + + if ($methodData["methodDetails"]["static"] === true) { + $this->addToLastLine('``[static]``', " "); + } + + if ($methodData["methodDetails"]["abstract"] === true) { + $this->addToLastLine('``[abstract]``', " "); + } + + if ($methodData["methodDetails"]["final"] === true) { + $this->addToLastLine('``[final]``', " "); + } + + if ($methodData["methodDetails"]["hasadditional"] === true) { + $this->addEmptyLine(); + } + + if (!empty($methodData["methodDetails"]["deprecated"])) { + $this->addLine("> [!CAUTION]"); + $this->addLine("> Deprecated %s", $methodData["methodDetails"]["deprecated"]); + $this->addEmptyLine(); + } + + $this->addLineH4("Summary"); + + if (!empty($methodData["methodDetails"]["summary"])) { + $this->addLineItalic($methodData["methodDetails"]["summary"])->addEmptyLine(); + } + + if (!empty($methodData["methodDetails"]["description"])) { + $this->addLineItalic($methodData["methodDetails"]["description"])->addEmptyLine(); + } + + $this->addLineH4("Signature"); + + echo $methodName . PHP_EOL; + + $phpMethod = $phpClass->addMethod($methodName); + $phpMethod->setPublic(); + $phpMethod->setStatic($methodData["methodDetails"]["static"] === true); + $phpMethod->setAbstract($methodData["methodDetails"]["abstract"] === true); + $phpMethod->setFinal($methodData["methodDetails"]["final"] === true); + if ( + $methodData["return"]["type"] == 'string[]' || + $methodData["return"]["type"] == '\ZugferdMailAccount[]' || + $methodData["return"]["type"] == '\ZugferdMailHandlerAbstract[]' || + $methodData["return"]["type"] == 'callable[]' + ) { + $phpMethod->setReturnType('array'); + } else { + $phpMethod->setReturnType($methodData["return"]["type"]); + } + $phpMethod->setBody(null); + + foreach ($methodData["parameters"] as $parameter) { + $phpParameter = $phpMethod + ->addParameter($parameter["name"]) + ->setType($parameter["type"]) + ->setNullable($parameter["isNullable"]); + + if ($parameter['defaultValueavailable'] === true) { + $phpParameter->setDefaultValue($parameter["defaultValue"]); + } + } + + $this->addLineRaw("```php"); + $this->addLineRaw($phpPrinter->printMethod($phpMethod)); + $this->addLineRaw("```"); + + if (!empty($methodData["parameters"])) { + $this->addLineH4("Parameters"); + $this->addLine("| Name | Type | Allows Null | Description"); + $this->addLine("| :------ | :------ | :-----: | :------"); + + foreach ($methodData["parameters"] as $parameter) { + $this->addLine( + "| %s | %s | %s | %s", + $parameter["name"], + $parameter["type"], + $this->boolToMarkDown($parameter["isNullable"] ? "Yes" : "No"), + $parameter["description"] ?? "", + ); + } + + $this->addEmptyLine(); + } else { + $this->addEmptyLine(); + } + + if (!empty($methodData["return"]["type"])) { + $this->addLineH4("Returns"); + $this->addLineRaw(sprintf("Returns a value of type __%s__", $methodData["return"]["type"])); + $this->addEmptyLine(); + } + + $this->addExample(dirname(__FILE__) . sprintf('/md/%s_%s.md', $this->extractor->getClassBasename(), $methodName)); + } + + return $this; + } + + /** + * Save MD to file + * + * @param string $filename + * @return MarkDownGenerator + */ + public function saveToFile(string $filename): MarkDownGenerator + { + file_put_contents($filename, implode(PHP_EOL, $this->lines)); + + return $this; + } + + /** + * Add a line to internal container + * + * @param string $string + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addLine(string $string, ...$args): MarkDownGenerator + { + if (StringUtils::stringIsNullOrEmpty($string)) { + return $this; + } + + $this->lines[] = $this->sanatizeString(sprintf($string, ...$args)); + + return $this; + } + + /** + * Add a line to internal container + * + * @param string $string + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addLineRaw(string $string, ...$args): MarkDownGenerator + { + if (StringUtils::stringIsNullOrEmpty($string)) { + return $this; + } + + $this->lines[] = sprintf($string, ...$args); + + return $this; + } + + /** + * Add a line to internal container + * + * @param string $string + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addLineRawAllowEmpty(string $string, ...$args): MarkDownGenerator + { + $this->lines[] = sprintf($string, ...$args); + + return $this; + } + + /** + * Add an empty line to internal container + * + * @return MarkDownGenerator + */ + private function addEmptyLine(): MarkDownGenerator + { + $this->lines[] = ""; + + return $this; + } + + /** + * Add an H1-Line to internal container + * + * @param string $string + * @param boolean $newLine + * @return MarkDownGenerator + */ + private function addLineH1(string $string, bool $newLine = true): MarkDownGenerator + { + $this->addLine("# %s", $string); + + if ($newLine) { + $this->addEmptyLine(); + } + + return $this; + } + + /** + * Add an H2-Line to internal container + * + * @param string $string + * @param boolean $newLine + * @return MarkDownGenerator + */ + private function addLineH2(string $string, bool $newLine = true): MarkDownGenerator + { + $this->addLine("## %s", $string); + + if ($newLine) { + $this->addEmptyLine(); + } + + return $this; + } + + /** + * Add an H3-Line to internal container + * + * @param string $string + * @param boolean $newLine + * @return MarkDownGenerator + */ + private function addLineH3(string $string, bool $newLine = true): MarkDownGenerator + { + $this->addLine("### %s", $string); + + if ($newLine) { + $this->addEmptyLine(); + } + + return $this; + } + + /** + * Add an H4-Line to internal container + * + * @param string $string + * @param boolean $newLine + * @return MarkDownGenerator + */ + private function addLineH4(string $string, bool $newLine = true): MarkDownGenerator + { + $this->addLine("#### %s", $string); + + if ($newLine) { + $this->addEmptyLine(); + } + + return $this; + } + + /** + * Add a string to the latest line which was added + * + * @param string $string + * @param string $delimiter + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addToLastLine(string $string, string $delimiter = "", ...$args): MarkDownGenerator + { + if (empty($this->lines)) { + return $this->addLine($string, ...$args); + } + + $lastIndex = count($this->lines) - 1; + $this->lines[$lastIndex] = $this->lines[$lastIndex] . $delimiter . sprintf($string, ...$args); + + return $this; + } + + /** + * Add line as italic formatted + * + * @param string $string + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addLineItalic(string $string, ...$args): MarkDownGenerator + { + return $this->addLine(sprintf("_%s_", $string), ...$args); + } + + /** + * Add line as bold formatted + * + * @param string $string + * @param mixed ...$args + * @return MarkDownGenerator + */ + private function addLineBold(string $string, ...$args): MarkDownGenerator + { + return $this->addLine(sprintf("__%s__", $string), ...$args); + } + + /** + * Import an example from a markdown file + * + * @param string $exampleFilename + * @param bool $isClass + * @return MarkDownGenerator + */ + private function addExample(string $exampleFilename, bool $isClass = false): MarkDownGenerator + { + if (!file_exists($exampleFilename)) { + return $this; + } + + $exampleFileContent = file_get_contents($exampleFilename); + + if ($exampleFileContent === false) { + return $this; + } + + if ($isClass === true) { + $this->addLineH2("Example"); + } else { + $this->addLineH4("Example"); + } + + $exampleFileContent = str_replace(array("\r\n", "\r", "\n"), "\n", $exampleFileContent); + + foreach (explode("\n", $exampleFileContent) as $exampleFileContentLine) { + $this->lines[] = $exampleFileContentLine; + } + + $this->addEmptyLine(); + + return $this; + } + + /** + * Sanatize a string + * + * @param string $string + * @return string + */ + private function sanatizeString(string $string): string + { + $string = str_replace("\n", "
", $string); + $string = str_replace("__BT-, From __", "", $string); + $string = str_replace("__BT-, From", "__BT-??, From", $string); + $string = trim($string); + + return $string; + } + + /** + * Convert yes/no to markdown markup + * + * @param string $boolText + * @return string + */ + private function boolToMarkDown(string $boolText): string + { + return strcasecmp($boolText, "no") === 0 ? ":x:" : ":heavy_check_mark:"; + } +} + +class BatchMarkDownGenerator +{ + /** + * Start a batch documentation creation + * + * @param array $classes + * @return void + * @throws InvalidArgumentException + * @throws PcreException + * @throws LogicException + */ + public static function generate(array $classes) + { + foreach ($classes as $className => $toFilename) { + $extractor = new ExtractClass($className); + $generator = new MarkDownGenerator($extractor); + $generator->generateMarkdown(); + $generator->saveToFile($toFilename); + } + } +} + +BatchMarkDownGenerator::generate([ + ZugferdMailReader::class => dirname(__FILE__) . '/Class-ZugferdMailReader.md', + ZugferdMailConfig::class => dirname(__FILE__) . '/Class-ZugferdMailConfig.md', + ZugferdMailAccount::class => dirname(__FILE__) . '/Class-ZugferdMailAccount.md', +]);