Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.52% covered (danger)
18.52%
10 / 54
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MappingsGenerator
18.52% covered (danger)
18.52%
10 / 54
33.33% covered (danger)
33.33%
2 / 6
89.90
0.00% covered (danger)
0.00%
0 / 1
 nextSourceFile
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 consumeSource
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 outputSpace
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 outputToken
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 appendNumber
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Copyright 2022 Wikimedia Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 * @file
18 * @license Apache-2.0
19 * @license MIT
20 * @license GPL-2.0-or-later
21 * @license LGPL-2.1-or-later
22 */
23
24namespace Wikimedia\Minify;
25
26/**
27 * Utility class to generate the "mappings" string of a source map.
28 *
29 * @internal
30 */
31class MappingsGenerator {
32    /** @var string */
33    private $source = '';
34
35    /** @var int The current source file offset in bytes */
36    private $curSourceOffset = 0;
37
38    /** @var int The current source file index */
39    private $curSourceFile = -1;
40    /** @var int The current source file line number */
41    private $curSourceLine = 0;
42    /** @var int The current source file column in UTF-16 code units */
43    private $curSourceColumn = 0;
44    /** @var int The current output file line number */
45    private $curOutLine = 0;
46    /** @var int The current output file column in UTF-16 code units */
47    private $curOutColumn = 0;
48
49    /** @var int The base of the delta encoding for source file index */
50    private $prevSourceFile = 0;
51    /** @var int The base of the delta encoding for source file line number */
52    private $prevSourceLine = 0;
53    /** @var int The base of the delta encoding for source file line column */
54    private $prevSourceColumn = 0;
55    /** @var int The base of the delta encoding for output file line number */
56    private $prevOutLine = 0;
57    /** @var int The base of the delta encoding for output file column */
58    private $prevOutColumn = 0;
59
60    /** @var bool Whether to omit a leading separator when generating a segment */
61    private $isFirstSegment = true;
62
63    /** @var string The accumulated mapping string */
64    private $mappings = '';
65
66    /** @var string The base-64 encoding table */
67    private const BASE64_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
68
69    /**
70     * Advance to the next source file.
71     *
72     * @param string $source The contents of the new file
73     */
74    public function nextSourceFile( $source ) {
75        $this->source = $source;
76        $this->curSourceFile++;
77        $this->curSourceOffset = 0;
78        $this->curSourceLine = 0;
79        $this->curSourceColumn = 0;
80    }
81
82    /**
83     * Advance the source position by the specified number of bytes.
84     *
85     * @param int $length
86     */
87    public function consumeSource( $length ) {
88        $newOffset = $this->curSourceOffset + $length;
89        $lineCount = substr_count( $this->source, "\n", $this->curSourceOffset, $length );
90        if ( $lineCount ) {
91            $lineStartPos =
92                strrpos(
93                    substr( $this->source, $this->curSourceOffset, $length ),
94                    "\n"
95                ) + $this->curSourceOffset + 1;
96            $this->curSourceLine += $lineCount;
97            $this->curSourceColumn = Utils::getJsLength(
98                substr( $this->source, $lineStartPos, $newOffset - $lineStartPos ) );
99        } else {
100            $this->curSourceColumn += Utils::getJsLength(
101                substr( $this->source, $this->curSourceOffset, $length ) );
102        }
103        $this->curSourceOffset = $newOffset;
104    }
105
106    /**
107     * Notify the source map generator of the generated text output, which
108     * should not generate a mapping segment.
109     *
110     * @param string $out
111     */
112    public function outputSpace( $out ) {
113        $lineCount = substr_count( $out, "\n" );
114        if ( $lineCount ) {
115            $lineStartPos = strrpos( $out, "\n" ) + 1;
116            $this->curOutLine += $lineCount;
117            $this->curOutColumn = Utils::getJsLength(
118                substr( $out, $lineStartPos, strlen( $out ) - $lineStartPos ) );
119        } else {
120            $this->curOutColumn += Utils::getJsLength( $out );
121        }
122    }
123
124    /**
125     * Notify the source map generator of the generated text output, which
126     * should generate a mapping segment. Append the mapping segment to the
127     * internal buffer.
128     *
129     * @param string $out
130     */
131    public function outputToken( $out ) {
132        $outLineDelta = $this->curOutLine - $this->prevOutLine;
133        if ( $outLineDelta > 0 ) {
134            $this->mappings .= str_repeat( ';', $outLineDelta );
135            $this->prevOutColumn = 0;
136            $this->isFirstSegment = false;
137        } elseif ( $this->isFirstSegment ) {
138            $this->isFirstSegment = false;
139        } else {
140            $this->mappings .= ',';
141        }
142
143        $this->appendNumber( $this->curOutColumn - $this->prevOutColumn );
144        $this->appendNumber( $this->curSourceFile - $this->prevSourceFile );
145        $this->appendNumber( $this->curSourceLine - $this->prevSourceLine );
146        $this->appendNumber( $this->curSourceColumn - $this->prevSourceColumn );
147
148        $this->prevSourceFile = $this->curSourceFile;
149        $this->prevOutLine = $this->curOutLine;
150        $this->prevOutColumn = $this->curOutColumn;
151        $this->prevSourceLine = $this->curSourceLine;
152        $this->prevSourceColumn = $this->curSourceColumn;
153
154        $this->outputSpace( $out );
155    }
156
157    /**
158     * Append a VLQ encoded number to the buffer.
159     *
160     * @param int $n
161     */
162    private function appendNumber( $n ) {
163        $encoded = '';
164
165        // The sign bit goes in the LSB for some reason
166        $vlq = $n < 0 ? ( -$n << 1 ) | 1 : $n << 1;
167
168        do {
169            $digit = $vlq & 0x1f;
170            $vlq >>= 5;
171            if ( $vlq > 0 ) {
172                $digit |= 0x20;
173            }
174            $encoded .= self::BASE64_TABLE[$digit];
175        } while ( $vlq > 0 );
176
177        $this->mappings .= $encoded;
178    }
179
180    /**
181     * Get the generated mappings string.
182     *
183     * @return string
184     */
185    public function getMap() {
186        return $this->mappings;
187    }
188}