turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

dockerfile.go · 163 lines · 5.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1package syntax
2
3import "strings"
4
5// highlightDockerfile colours a Dockerfile, one slice of spans per line.
6//
7// Nothing in a Dockerfile crosses a line break as far as colouring is
8// concerned. A backslash continuation joins two lines for Docker, but each half
9// still reads as shell and each is coloured on its own — which is what a reader
10// wants, since the halves are usually a command and its arguments.
11func highlightDockerfile(src string) [][]Span {
12 return ScanLines(src, scanDockerfileLine)
13}
14
15// scanDockerfileLine colours one line. Dockerfiles need no carried state, so
16// the state parameter is the empty struct.
17func scanDockerfileLine(line []rune, carry struct{}) ([]Span, struct{}) {
18 s := &LineScanner{line: line}
19
20 s.SkipSpaces()
21 switch {
22 case s.AtEnd():
23 return s.spans, carry
24 case s.Peek(0) == '#':
25 // This covers the parser directives too — `# syntax=docker/…` and
26 // `# escape=\` are comments to everything but Docker's own front end,
27 // and colouring them as instructions would suggest they are ones.
28 s.TakeRest(ClassComment)
29 return s.spans, carry
30 }
31
32 takeDockerfileInstruction(s)
33 for !s.AtEnd() {
34 stepDockerfileArgument(s)
35 }
36 return s.spans, carry
37}
38
39// takeDockerfileInstruction colours the instruction a line opens with.
40//
41// Instructions are matched case-insensitively because Docker accepts any case
42// and real files are inconsistent about it, though the convention is capitals.
43// A first word that is not an instruction is left as plain text rather than
44// guessed at: a continuation line's first word is an ordinary argument.
45func takeDockerfileInstruction(s *LineScanner) {
46 start := s.Pos()
47 for !s.AtEnd() && IsLetter(s.Peek(0)) {
48 s.Advance(1)
49 }
50 if s.Pos() == start {
51 return
52 }
53
54 if !dockerfileInstructions[strings.ToUpper(string(s.line[start:s.Pos()]))] {
55 // Not an instruction. Put the position back so the word is coloured by
56 // the argument rules, which is where a continuation line's first word
57 // belongs.
58 s.pos = start
59 return
60 }
61 s.Emit(start, s.Pos(), ClassKeyword)
62}
63
64// stepDockerfileArgument colours one thing after the instruction.
65func stepDockerfileArgument(s *LineScanner) {
66 switch r := s.Peek(0); {
67 case r == ' ' || r == '\t':
68 s.SkipSpaces()
69 case r == '#':
70 s.TakeRest(ClassComment)
71 case r == '"' || r == '\'':
72 TakeQuoted(s, r, ClassString)
73 case r == '$':
74 takeDockerfileVariable(s)
75 case s.HasPrefix(0, "--"):
76 // A flag such as --from=builder or --chown=me:me. The name is the part
77 // that carries the meaning, so the value after `=` is coloured as one.
78 s.TakeWhile(ClassAttribute, func(c rune) bool { return c != '=' && c != ' ' })
79 case r == '\\' && s.Peek(1) == 0:
80 // The line continuation, which is structure rather than an argument.
81 s.Take(1, ClassOperator)
82 case IsDigit(r):
83 s.TakeWhile(ClassNumber, func(c rune) bool { return IsDigit(c) || c == '.' })
84 case IsLetter(r) || r == '_' || r == '/' || r == '.':
85 takeDockerfileWord(s)
86 case IsPunctuationRune(r) || r == '=' || r == ':':
87 s.Take(1, ClassPunctuation)
88 default:
89 s.Advance(1)
90 }
91}
92
93// takeDockerfileVariable colours $NAME and ${NAME:-default}.
94func takeDockerfileVariable(s *LineScanner) {
95 start := s.Pos()
96 s.Advance(1)
97
98 if s.Peek(0) == '{' {
99 for !s.AtEnd() && s.Peek(0) != '}' {
100 s.Advance(1)
101 }
102 s.Advance(1) // the closing brace
103 s.Emit(start, s.Pos(), ClassBuiltin)
104 return
105 }
106
107 for !s.AtEnd() && IsWordRune(s.Peek(0)) {
108 s.Advance(1)
109 }
110 s.Emit(start, s.Pos(), ClassBuiltin)
111}
112
113// takeDockerfileWord colours a bare word: `AS`, `as` and the stage name after
114// them are what a reader looks for in a multi-stage build.
115func takeDockerfileWord(s *LineScanner) {
116 start := s.Pos()
117 for !s.AtEnd() && isDockerfileWordRune(s.Peek(0)) {
118 s.Advance(1)
119 }
120
121 class := ClassIdentifier
122 if dockerfileModifiers[strings.ToUpper(string(s.line[start:s.Pos()]))] {
123 class = ClassKeyword
124 }
125 s.Emit(start, s.Pos(), class)
126}
127
128// isDockerfileWordRune reports whether a rune belongs to a bare word.
129//
130// Paths and image references are words: `/usr/local/bin` and
131// `golang:1.26-alpine` each read as one thing, and splitting them would be
132// three colours for one argument. The colon is in the list for the image
133// reference's sake, which is the same reasoning the YAML scanner uses for
134// `image: nginx:1.27`.
135func isDockerfileWordRune(r rune) bool {
136 return IsWordRune(r) || strings.ContainsRune(dockerfileWordRunes, r)
137}
138
139// dockerfileWordRunes are the characters that join a bare word beyond the
140// letters, digits and underscores every language shares.
141const dockerfileWordRunes = "-./@:"
142
143// dockerfileInstructions are the words that may open a line. The list is the
144// whole of the Dockerfile language: there is no user-extensible instruction, so
145// a word that is not here is an argument.
146var dockerfileInstructions = wordSet(
147 "ADD", "ARG", "CMD", "COPY", "ENTRYPOINT", "ENV", "EXPOSE", "FROM",
148 "HEALTHCHECK", "LABEL", "MAINTAINER", "ONBUILD", "RUN", "SHELL",
149 "STOPSIGNAL", "USER", "VOLUME", "WORKDIR",
150)
151
152// dockerfileModifiers are words that carry meaning inside an instruction rather
153// than opening one.
154var dockerfileModifiers = wordSet("AS", "NONE")
155
156// wordSet builds a lookup from a list of words.
157func wordSet(words ...string) map[string]bool {
158 set := make(map[string]bool, len(words))
159 for _, word := range words {
160 set[word] = true
161 }
162 return set
163}