1// Package internal contains common API functions and structures shared between lexer packages.
2package internal
3
4import (
5	"path/filepath"
6	"sort"
7	"strings"
8
9	"github.com/alecthomas/chroma"
10)
11
12var (
13	ignoredSuffixes = [...]string{
14		// Editor backups
15		"~", ".bak", ".old", ".orig",
16		// Debian and derivatives apt/dpkg/ucf backups
17		".dpkg-dist", ".dpkg-old", ".ucf-dist", ".ucf-new", ".ucf-old",
18		// Red Hat and derivatives rpm backups
19		".rpmnew", ".rpmorig", ".rpmsave",
20		// Build system input/template files
21		".in",
22	}
23)
24
25// Registry of Lexers.
26var Registry = struct {
27	Lexers  chroma.Lexers
28	byName  map[string]chroma.Lexer
29	byAlias map[string]chroma.Lexer
30}{
31	byName:  map[string]chroma.Lexer{},
32	byAlias: map[string]chroma.Lexer{},
33}
34
35// Names of all lexers, optionally including aliases.
36func Names(withAliases bool) []string {
37	out := []string{}
38	for _, lexer := range Registry.Lexers {
39		config := lexer.Config()
40		out = append(out, config.Name)
41		if withAliases {
42			out = append(out, config.Aliases...)
43		}
44	}
45	sort.Strings(out)
46	return out
47}
48
49// Get a Lexer by name, alias or file extension.
50func Get(name string) chroma.Lexer {
51	if lexer := Registry.byName[name]; lexer != nil {
52		return lexer
53	}
54	if lexer := Registry.byAlias[name]; lexer != nil {
55		return lexer
56	}
57	if lexer := Registry.byName[strings.ToLower(name)]; lexer != nil {
58		return lexer
59	}
60	if lexer := Registry.byAlias[strings.ToLower(name)]; lexer != nil {
61		return lexer
62	}
63
64	candidates := chroma.PrioritisedLexers{}
65	// Try file extension.
66	if lexer := Match("filename." + name); lexer != nil {
67		candidates = append(candidates, lexer)
68	}
69	// Try exact filename.
70	if lexer := Match(name); lexer != nil {
71		candidates = append(candidates, lexer)
72	}
73	if len(candidates) == 0 {
74		return nil
75	}
76	sort.Sort(candidates)
77	return candidates[0]
78}
79
80// MatchMimeType attempts to find a lexer for the given MIME type.
81func MatchMimeType(mimeType string) chroma.Lexer {
82	matched := chroma.PrioritisedLexers{}
83	for _, l := range Registry.Lexers {
84		for _, lmt := range l.Config().MimeTypes {
85			if mimeType == lmt {
86				matched = append(matched, l)
87			}
88		}
89	}
90	if len(matched) != 0 {
91		sort.Sort(matched)
92		return matched[0]
93	}
94	return nil
95}
96
97// Match returns the first lexer matching filename.
98func Match(filename string) chroma.Lexer {
99	filename = filepath.Base(filename)
100	matched := chroma.PrioritisedLexers{}
101	// First, try primary filename matches.
102	for _, lexer := range Registry.Lexers {
103		config := lexer.Config()
104		for _, glob := range config.Filenames {
105			ok, err := filepath.Match(glob, filename)
106			if err != nil { // nolint
107				panic(err)
108			} else if ok {
109				matched = append(matched, lexer)
110			} else {
111				for _, suf := range &ignoredSuffixes {
112					ok, err := filepath.Match(glob+suf, filename)
113					if err != nil {
114						panic(err)
115					} else if ok {
116						matched = append(matched, lexer)
117						break
118					}
119				}
120			}
121		}
122	}
123	if len(matched) > 0 {
124		sort.Sort(matched)
125		return matched[0]
126	}
127	matched = nil
128	// Next, try filename aliases.
129	for _, lexer := range Registry.Lexers {
130		config := lexer.Config()
131		for _, glob := range config.AliasFilenames {
132			ok, err := filepath.Match(glob, filename)
133			if err != nil { // nolint
134				panic(err)
135			} else if ok {
136				matched = append(matched, lexer)
137			} else {
138				for _, suf := range &ignoredSuffixes {
139					ok, err := filepath.Match(glob+suf, filename)
140					if err != nil {
141						panic(err)
142					} else if ok {
143						matched = append(matched, lexer)
144						break
145					}
146				}
147			}
148		}
149	}
150	if len(matched) > 0 {
151		sort.Sort(matched)
152		return matched[0]
153	}
154	return nil
155}
156
157// Analyse text content and return the "best" lexer..
158func Analyse(text string) chroma.Lexer {
159	var picked chroma.Lexer
160	highest := float32(0.0)
161	for _, lexer := range Registry.Lexers {
162		if analyser, ok := lexer.(chroma.Analyser); ok {
163			weight := analyser.AnalyseText(text)
164			if weight > highest {
165				picked = lexer
166				highest = weight
167			}
168		}
169	}
170	return picked
171}
172
173// Register a Lexer with the global registry.
174func Register(lexer chroma.Lexer) chroma.Lexer {
175	config := lexer.Config()
176	Registry.byName[config.Name] = lexer
177	Registry.byName[strings.ToLower(config.Name)] = lexer
178	for _, alias := range config.Aliases {
179		Registry.byAlias[alias] = lexer
180		Registry.byAlias[strings.ToLower(alias)] = lexer
181	}
182	Registry.Lexers = append(Registry.Lexers, lexer)
183	return lexer
184}
185
186// PlaintextRules is used for the fallback lexer as well as the explicit
187// plaintext lexer.
188func PlaintextRules() chroma.Rules {
189	return chroma.Rules{
190		"root": []chroma.Rule{
191			{`.+`, chroma.Text, nil},
192			{`\n`, chroma.Text, nil},
193		},
194	}
195}
196
197// Fallback lexer if no other is found.
198var Fallback chroma.Lexer = chroma.MustNewLazyLexer(&chroma.Config{
199	Name:      "fallback",
200	Filenames: []string{"*"},
201}, PlaintextRules)
202