1package sourcemap
2
3import (
4	"encoding/json"
5	"fmt"
6	"net/url"
7	"path"
8	"sort"
9)
10
11type sourceMap struct {
12	Version        int           `json:"version"`
13	File           string        `json:"file"`
14	SourceRoot     string        `json:"sourceRoot"`
15	Sources        []string      `json:"sources"`
16	SourcesContent []string      `json:"sourcesContent"`
17	Names          []json.Number `json:"names"`
18	Mappings       string        `json:"mappings"`
19
20	mappings []mapping
21}
22
23type v3 struct {
24	sourceMap
25	Sections []section `json:"sections"`
26}
27
28func (m *sourceMap) parse(sourcemapURL string) error {
29	if err := checkVersion(m.Version); err != nil {
30		return err
31	}
32
33	var sourceRootURL *url.URL
34	if m.SourceRoot != "" {
35		u, err := url.Parse(m.SourceRoot)
36		if err != nil {
37			return err
38		}
39		if u.IsAbs() {
40			sourceRootURL = u
41		}
42	} else if sourcemapURL != "" {
43		u, err := url.Parse(sourcemapURL)
44		if err != nil {
45			return err
46		}
47		if u.IsAbs() {
48			u.Path = path.Dir(u.Path)
49			sourceRootURL = u
50		}
51	}
52
53	for i, src := range m.Sources {
54		m.Sources[i] = m.absSource(sourceRootURL, src)
55	}
56
57	mappings, err := parseMappings(m.Mappings)
58	if err != nil {
59		return err
60	}
61
62	m.mappings = mappings
63	// Free memory.
64	m.Mappings = ""
65
66	return nil
67}
68
69func (m *sourceMap) absSource(root *url.URL, source string) string {
70	if path.IsAbs(source) {
71		return source
72	}
73
74	if u, err := url.Parse(source); err == nil && u.IsAbs() {
75		return source
76	}
77
78	if root != nil {
79		u := *root
80		u.Path = path.Join(u.Path, source)
81		return u.String()
82	}
83
84	if m.SourceRoot != "" {
85		return path.Join(m.SourceRoot, source)
86	}
87
88	return source
89}
90
91type section struct {
92	Offset struct {
93		Line   int `json:"line"`
94		Column int `json:"column"`
95	} `json:"offset"`
96	Map *sourceMap `json:"map"`
97}
98
99type Consumer struct {
100	sourcemapURL string
101	file         string
102	sections     []section
103}
104
105func Parse(sourcemapURL string, b []byte) (*Consumer, error) {
106	v3 := new(v3)
107	err := json.Unmarshal(b, v3)
108	if err != nil {
109		return nil, err
110	}
111
112	if err := checkVersion(v3.Version); err != nil {
113		return nil, err
114	}
115
116	if len(v3.Sections) == 0 {
117		v3.Sections = append(v3.Sections, section{
118			Map: &v3.sourceMap,
119		})
120	}
121
122	for _, s := range v3.Sections {
123		err := s.Map.parse(sourcemapURL)
124		if err != nil {
125			return nil, err
126		}
127	}
128
129	reverse(v3.Sections)
130	return &Consumer{
131		sourcemapURL: sourcemapURL,
132		file:         v3.File,
133		sections:     v3.Sections,
134	}, nil
135}
136
137func (c *Consumer) SourcemapURL() string {
138	return c.sourcemapURL
139}
140
141// File returns an optional name of the generated code
142// that this source map is associated with.
143func (c *Consumer) File() string {
144	return c.file
145}
146
147// Source returns the original source, name, line, and column information
148// for the generated source's line and column positions.
149func (c *Consumer) Source(
150	genLine, genColumn int,
151) (source, name string, line, column int, ok bool) {
152	for i := range c.sections {
153		s := &c.sections[i]
154		if s.Offset.Line < genLine ||
155			(s.Offset.Line+1 == genLine && s.Offset.Column <= genColumn) {
156			genLine -= s.Offset.Line
157			genColumn -= s.Offset.Column
158			return c.source(s.Map, genLine, genColumn)
159		}
160	}
161	return
162}
163
164func (c *Consumer) source(
165	m *sourceMap, genLine, genColumn int,
166) (source, name string, line, column int, ok bool) {
167	i := sort.Search(len(m.mappings), func(i int) bool {
168		m := &m.mappings[i]
169		if int(m.genLine) == genLine {
170			return int(m.genColumn) >= genColumn
171		}
172		return int(m.genLine) >= genLine
173	})
174
175	// Mapping not found.
176	if i == len(m.mappings) {
177		return
178	}
179
180	match := &m.mappings[i]
181
182	// Fuzzy match.
183	if int(match.genLine) > genLine || int(match.genColumn) > genColumn {
184		if i == 0 {
185			return
186		}
187		match = &m.mappings[i-1]
188	}
189
190	if match.sourcesInd >= 0 {
191		source = m.Sources[match.sourcesInd]
192	}
193	if match.namesInd >= 0 {
194		name = string(m.Names[match.namesInd])
195	}
196	line = int(match.sourceLine)
197	column = int(match.sourceColumn)
198	ok = true
199	return
200}
201
202// SourceContent returns the original source content for the source.
203func (c *Consumer) SourceContent(source string) string {
204	for i := range c.sections {
205		s := &c.sections[i]
206		for i, src := range s.Map.Sources {
207			if src == source {
208				if i < len(s.Map.SourcesContent) {
209					return s.Map.SourcesContent[i]
210				}
211				break
212			}
213		}
214	}
215	return ""
216}
217
218func checkVersion(version int) error {
219	if version == 3 || version == 0 {
220		return nil
221	}
222	return fmt.Errorf(
223		"sourcemap: got version=%d, but only 3rd version is supported",
224		version,
225	)
226}
227
228func reverse(ss []section) {
229	last := len(ss) - 1
230	for i := 0; i < len(ss)/2; i++ {
231		ss[i], ss[last-i] = ss[last-i], ss[i]
232	}
233}
234