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;
25
26use function Safe\preg_match;
27use function Safe\preg_match_all;
28use function Safe\preg_split;
29use function array_values;
30use function preg_quote;
31use function trim;
32
33/**
34 * Parses the subset of Keep a Changelog structure managed by dev-tools.
35 */
36 class ChangelogParser implements ChangelogParserInterface
37{
38    /**
39     * Parses markdown content into a changelog document.
40     *
41     * @param string $contents
42     */
43    public function parse(string $contents): ChangelogDocument
44    {
45        if ('' === trim($contents)) {
46            return ChangelogDocument::create();
47        }
48
49        preg_match_all(
50            '/^## \[(?<version>[^\]]+)\](?: - (?<date>\d{4}-\d{2}-\d{2}))?$/m',
51            $contents,
52            $matches,
53            \PREG_OFFSET_CAPTURE,
54        );
55
56        if ([] === $matches[0]) {
57            return ChangelogDocument::create();
58        }
59
60        $releases = [];
61        $sectionCount = \count($matches[0]);
62
63        for ($index = 0; $index < $sectionCount; ++$index) {
64            $heading = $matches[0][$index][0];
65            $offset = $matches[0][$index][1];
66            $bodyStart = $offset + \strlen((string) $heading);
67            $bodyEnd = $matches[0][$index + 1][1] ?? \strlen($contents);
68            $body = trim(substr($contents, $bodyStart, $bodyEnd - $bodyStart));
69
70            $entries = [];
71
72            foreach (ChangelogEntryType::ordered() as $type) {
73                $entries[$type->value] = $this->extractEntries($body, $type);
74            }
75
76            $releases[] = new ChangelogRelease(
77                $matches['version'][$index][0],
78                '' === ($matches['date'][$index][0] ?? '') ? null : $matches['date'][$index][0],
79                $entries,
80            );
81        }
82
83        return new ChangelogDocument($releases);
84    }
85
86    /**
87     * Extracts bullet entries for one changelog category.
88     *
89     * @param string $body
90     * @param ChangelogEntryType $type
91     *
92     * @return list<string>
93     */
94    private function extractEntries(string $body, ChangelogEntryType $type): array
95    {
96        $pattern = \sprintf('/^### %s\s*(?:\R(?<body>.*?))?(?=^### |\z)/ms', preg_quote($type->value, '/'));
97
98        if (1 !== preg_match($pattern, $body, $matches)) {
99            return [];
100        }
101
102        $lines = preg_split('/\R/', trim($matches['body'] ?? ''));
103        $entries = [];
104
105        foreach ($lines as $line) {
106            $line = trim((string) $line);
107
108            if (! str_starts_with($line, '- ')) {
109                continue;
110            }
111
112            $entry = trim(substr($line, 2));
113
114            if ('' === $entry) {
115                continue;
116            }
117
118            $entries[] = $entry;
119        }
120
121        return array_values(array_unique($entries));
122    }
123}