Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.56% covered (success)
97.56%
40 / 41
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangelogParser
97.56% covered (success)
97.56%
40 / 41
50.00% covered (danger)
50.00%
1 / 2
11
0.00% covered (danger)
0.00%
0 / 1
 parse
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
6
 extractEntries
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Changelog\Parser;
21
22use FastForward\DevTools\Changelog\Document\ChangelogDocument;
23use FastForward\DevTools\Changelog\Document\ChangelogRelease;
24use FastForward\DevTools\Changelog\Entry\ChangelogEntryType;
25use function Safe\preg_match;
26use function Safe\preg_match_all;
27use function Safe\preg_split;
28use function array_values;
29use function preg_quote;
30use function trim;
31
32/**
33 * Parses the subset of Keep a Changelog structure managed by dev-tools.
34 */
35 class ChangelogParser implements ChangelogParserInterface
36{
37    /**
38     * Parses markdown content into a changelog document.
39     *
40     * @param string $contents
41     */
42    public function parse(string $contents): ChangelogDocument
43    {
44        if ('' === trim($contents)) {
45            return ChangelogDocument::create();
46        }
47
48        preg_match_all(
49            '/^## \[(?<version>[^\]]+)\](?: - (?<date>\d{4}-\d{2}-\d{2}))?$/m',
50            $contents,
51            $matches,
52            \PREG_OFFSET_CAPTURE,
53        );
54
55        if ([] === $matches[0]) {
56            return ChangelogDocument::create();
57        }
58
59        $releases = [];
60        $sectionCount = \count($matches[0]);
61
62        for ($index = 0; $index < $sectionCount; ++$index) {
63            $heading = $matches[0][$index][0];
64            $offset = $matches[0][$index][1];
65            $bodyStart = $offset + \strlen((string) $heading);
66            $bodyEnd = $matches[0][$index + 1][1] ?? \strlen($contents);
67            $body = trim(substr($contents, $bodyStart, $bodyEnd - $bodyStart));
68
69            $entries = [];
70
71            foreach (ChangelogEntryType::ordered() as $type) {
72                $entries[$type->value] = $this->extractEntries($body, $type);
73            }
74
75            $releases[] = new ChangelogRelease(
76                $matches['version'][$index][0],
77                '' === ($matches['date'][$index][0] ?? '') ? null : $matches['date'][$index][0],
78                $entries,
79            );
80        }
81
82        return new ChangelogDocument($releases);
83    }
84
85    /**
86     * Extracts bullet entries for one changelog category.
87     *
88     * @param string $body
89     * @param ChangelogEntryType $type
90     *
91     * @return list<string>
92     */
93    private function extractEntries(string $body, ChangelogEntryType $type): array
94    {
95        $pattern = \sprintf('/^### %s\s*(?:\R(?<body>.*?))?(?=^### |\z)/ms', preg_quote($type->value, '/'));
96
97        if (1 !== preg_match($pattern, $body, $matches)) {
98            return [];
99        }
100
101        $lines = preg_split('/\R/', trim($matches['body'] ?? ''));
102        $entries = [];
103
104        foreach ($lines as $line) {
105            $line = trim((string) $line);
106
107            if (! str_starts_with($line, '- ')) {
108                continue;
109            }
110
111            $entry = trim(substr($line, 2));
112
113            if ('' === $entry) {
114                continue;
115            }
116
117            $entries[] = $entry;
118        }
119
120        return array_values(array_unique($entries));
121    }
122}