summaryrefslogtreecommitdiff
path: root/odoo/tools/sourcemap_generator.py
blob: 1d82582157cbcff4149c39fc425c7576c5e3c25e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from functools import lru_cache
import json


class SourceMapGenerator:
    """
    The SourceMapGenerator creates the sourcemap maps the asset bundle to the js/css files.

    What is a sourcemap ? (https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map)
    In brief: a source map is what makes possible to debug your processed/compiled/minified code as if you were
    debugging the original, non-altered source code. It is a file that provides a mapping original <=> processed for
    the browser to read.

    This implementation of the SourceMapGenerator is a translation and adaptation of this implementation
    in js https://github.com/mozilla/source-map. For performance purposes, we have removed all unnecessary
    functions/steps for our use case. This simpler version does a line by line mapping, with the ability to
    add offsets at the start and end of a file. (when we have to add comments on top a transpiled file by example).
    """
    def __init__(self, source_root=None):
        self._file = None
        self._source_root = source_root
        self._sources = {}
        self._mappings = []
        self._sources_contents = {}
        self._version = 3
        self._cache = {}

    def _serialize_mappings(self):
        """
        A source map mapping is encoded with the base 64 VLQ format.
        This function encodes the readable source to the format.

        :return the encoded content
        """
        previous_generated_line = 1
        previous_original_line = 0
        previous_source = 0
        encoded_column = base64vlq_encode(0)
        result = ""
        for mapping in self._mappings:
            if mapping["generatedLine"] != previous_generated_line:
                while mapping["generatedLine"] > previous_generated_line:
                    result += ";"
                    previous_generated_line += 1

            if mapping["source"] is not None:
                sourceIdx = self._sources[mapping["source"]]
                source = sourceIdx - previous_source
                previous_source = sourceIdx

                # lines are stored 0-based in SourceMap spec version 3
                line = mapping["originalLine"] - 1 - previous_original_line
                previous_original_line = mapping["originalLine"] - 1

            if (source, line) not in self._cache:
                self._cache[(source, line)] = "".join([
                    encoded_column,
                    base64vlq_encode(source),
                    base64vlq_encode(line),
                    encoded_column,
                ])

            result += self._cache[source, line]
        return result

    def to_json(self):
        """
        Generates the json sourcemap.
        It is the main function that assembles all the pieces.

        :return {str} valid sourcemap in json format
        """
        mapping = {
            "version": self._version,
            "sources": list(self._sources.keys()),
            "mappings": self._serialize_mappings(),
            "sourcesContent": [self._sources_contents[source] for source in self._sources]
        }
        if self._file:
            mapping["file"] = self._file

        if self._source_root:
            mapping["sourceRoot"] = self._source_root

        return mapping

    def get_content(self):
        """Generates the content of the sourcemap.

        :return the content of the sourcemap as a string encoded in UTF-8.
        """
        # Store with XSSI-prevention prefix
        return b")]}'\n" + json.dumps(self.to_json()).encode('utf8')

    def add_source(self, source_name, source_content, last_index, start_offset=0):
        """Adds a new source file in the sourcemap. All the lines of the source file will be mapped line by line
        to the generated file from the (last_index + start_offset). All lines between
        last_index and (last_index + start_offset) will
        be mapped to line 1 of the source file.

        Example:
            ls 1 = Line 1 from new source file
            lg 1 = Line 1 from genereted file
            ls 1 <=> lg 1 Line 1 from new source file is map to  Line 1 from genereted file
            nb_ls = number of lines in the new source file

            Step 1:
            ls 1 <=> lg last_index + 1

            Step 2:
            ls 1 <=> lg last_index + start_offset + 1
            ls 2 <=> lg last_index + start_offset + 2
            ...
            ls nb_ls <=> lg last_index + start_offset + nb_ls


        :param source_name: name of the source to add
        :param source_content: content of the source to add
        :param last_index: Line where we start to map the new source
        :param start_offset: Number of lines to pass in the generated file before starting mapping line by line
        """
        source_line_count = len(source_content.split("\n"))

        self._sources.setdefault(source_name, len(self._sources))

        self._sources_contents[source_name] = source_content
        if start_offset > 0:
            # adds a mapping between the first line of the source
            # and the first line of the corresponding code in the generated file.
            self._mappings.append({
                "generatedLine": last_index + 1,
                "originalLine": 1,
                "source": source_name,
            })
        for i in range(1, source_line_count + 1):
            self._mappings.append({
                "generatedLine": last_index + i + start_offset,
                "originalLine": i,
                "source": source_name,
            })


B64CHARS = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
SHIFTSIZE, FLAG, MASK = 5, 1 << 5, (1 << 5) - 1


@lru_cache(maxsize=64)
def base64vlq_encode(*values):
    """
    Encode Base64 VLQ encoded sequences
    https://gist.github.com/mjpieters/86b0d152bb51d5f5979346d11005588b
    Base64 VLQ is used in source maps.
    VLQ values consist of 6 bits (matching the 64 characters of the Base64
    alphabet), with the most significant bit a *continuation* flag. If the
    flag is set, then the next character in the input is part of the same
    integer value. Multiple VLQ character sequences so form an unbounded
    integer value, in little-endian order.
    The *first* VLQ value consists of a continuation flag, 4 bits for the
    value, and the last bit the *sign* of the integer:
    +-----+-----+-----+-----+-----+-----+
    |  c  |  b3 |  b2 |  b1 |  b0 |  s  |
    +-----+-----+-----+-----+-----+-----+
    while subsequent VLQ characters contain 5 bits of value:
    +-----+-----+-----+-----+-----+-----+
    |  c  |  b4 |  b3 |  b2 |  b1 |  b0 |
    +-----+-----+-----+-----+-----+-----+
    For source maps, Base64 VLQ sequences can contain 1, 4 or 5 elements.
    """
    results = []
    add = results.append
    for v in values:
        # add sign bit
        v = (abs(v) << 1) | int(v < 0)
        while True:
            toencode, v = v & MASK, v >> SHIFTSIZE
            add(toencode | (v and FLAG))
            if not v:
                break
    return bytes(map(B64CHARS.__getitem__, results)).decode()