1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the league/commonmark package.
7 *
8 * (c) Colin O'Dell <colinodell@gmail.com>
9 *
10 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11 *  - (c) John MacFarlane
12 *
13 * For the full copyright and license information, please view the LICENSE
14 * file that was distributed with this source code.
15 */
16
17namespace League\CommonMark\Util;
18
19/**
20 * @psalm-immutable
21 */
22final class Html5EntityDecoder
23{
24    /**
25     * @psalm-pure
26     */
27    public static function decode(string $entity): string
28    {
29        if (\substr($entity, -1) !== ';') {
30            return $entity;
31        }
32
33        if (\substr($entity, 0, 2) === '&#') {
34            if (\strtolower(\substr($entity, 2, 1)) === 'x') {
35                return self::fromHex(\substr($entity, 3, -1));
36            }
37
38            return self::fromDecimal(\substr($entity, 2, -1));
39        }
40
41        return \html_entity_decode($entity, \ENT_QUOTES | \ENT_HTML5, 'UTF-8');
42    }
43
44    /**
45     * @param mixed $number
46     *
47     * @psalm-pure
48     */
49    private static function fromDecimal($number): string
50    {
51        // Only convert code points within planes 0-2, excluding NULL
52        // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found
53        if (empty($number) || $number > 0x2FFFF) {
54            return self::fromHex('fffd');
55        }
56
57        $entity = '&#' . $number . ';';
58
59        $converted = \mb_decode_numericentity($entity, [0x0, 0x2FFFF, 0, 0xFFFF], 'UTF-8');
60
61        if ($converted === $entity) {
62            return self::fromHex('fffd');
63        }
64
65        return $converted;
66    }
67
68    /**
69     * @psalm-pure
70     */
71    private static function fromHex(string $hexChars): string
72    {
73        return self::fromDecimal(\hexdec($hexChars));
74    }
75}
76