forked from benrowe/books
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsource_files.go
279 lines (252 loc) · 7.54 KB
/
source_files.go
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
package main
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/kjk/u"
)
/*
FileDirective describes reulst of parsing a line like:
// no output, no playground
*/
type FileDirective struct {
FileName string // :file foo.txt
NoOutput bool // "no output"
AllowError bool // "allow error"
LineLimit int // limit ${n}
NoPlayground bool // no playground
RunCmd string // :run ${cmd}
Glot bool // :glot, use glot.io to execute the code snippet
GoPlayground bool // :goplay, use go playground to execute the snippet
DoOutput bool // :output
}
// SourceFile represents source file. It comes from a code block in
// Notion, replit, file in repository etc.
type SourceFile struct {
// for debugging, url of page on notion from which this
// snippet comes from
NotionOriginURL string
EmbedURL string
// full path of the file
Path string
// name of the file
FileName string
SnippetName string
// URL on GitHub for this file
GitHubURL string
// language of the file, detected from name
Lang string
// for Go files, this is playground id
GoPlaygroundID string
// for some files, this is glot.io snippet id
GlotPlaygroundID string
PlaygroundURI string
// optional, extracted from first line of the file
// allows providing meta-data instruction for this file
Directive *FileDirective
// raw content of the code snippet with line endings normalized to '\n'
// it can either come from code block in Notion or a file on disk
// or replit etc.
CodeFull string
// CodeFull after extracting directive, run cmd at the top
// and removing :show annotation lines
// This is the content to execute
CodeToRun string
// the part that we want to show i.e. the parts inside
// :show start, :show end blocks
LinesToShow []string
// output of running a file via glot.io
GlotOutput string
}
// Output returns the output of the execution of the code snippet
func (f *SourceFile) Output() string {
if f.Directive.NoOutput {
return ""
}
return f.GlotOutput
}
// Sha1 returns sha1 (in hex) of the code snippet
func (f *SourceFile) Sha1() string {
return u.Sha1HexOfBytes([]byte(f.CodeFull))
}
// CodeToShow returns part of the file tbat we want to show
func (f *SourceFile) CodeToShow() []byte {
s := strings.Join(f.LinesToShow, "\n")
return []byte(s)
}
// strip "//" or "#" comment mark from line and return string
// after removing the mark
func stripComment(line string) (string, bool) {
line = strings.TrimSpace(line)
s := strings.TrimPrefix(line, "//")
if s != line {
return s, true
}
s = strings.TrimPrefix(line, "#")
if s != line {
return s, true
}
return "", false
}
/* Parses a line like:
// no output, no playground, line ${n}, allow error
*/
func parseFileDirective(res *FileDirective, line string) (bool, error) {
s, ok := stripComment(line)
if !ok {
// doesn't start with a comment, so is not a file directive
return false, nil
}
parts := strings.Split(s, ",")
for _, s := range parts {
s = strings.TrimSpace(s)
// directives can also start with ":", to make them more distinct
startsWithColon := strings.HasPrefix(s, ":")
s = strings.TrimPrefix(s, ":")
if s == "glot" {
res.Glot = true
} else if s == "output" {
res.DoOutput = true
} else if s == "goplay" {
res.GoPlayground = true
} else if s == "no output" || s == "nooutput" {
res.NoOutput = true
} else if s == "no playground" || s == "noplayground" {
res.NoPlayground = true
} else if s == "allow error" || s == "allow_error" || s == "allowerror" {
res.AllowError = true
} else if strings.HasPrefix(s, "name ") {
// expect: name foo.txt
rest := strings.TrimSpace(strings.TrimPrefix(s, "name "))
if len(rest) == 0 {
return false, fmt.Errorf("parseFileDirective: invalid line '%s'", line)
}
res.FileName = rest
} else if strings.HasPrefix(s, "file ") {
// expect: file foo.txt
rest := strings.TrimSpace(strings.TrimPrefix(s, "file "))
if len(rest) == 0 {
return false, fmt.Errorf("parseFileDirective: invalid line '%s'", line)
}
res.FileName = rest
} else if strings.HasPrefix(s, "line ") {
rest := strings.TrimSpace(strings.TrimPrefix(s, "line "))
n, err := strconv.Atoi(rest)
if err != nil {
return false, fmt.Errorf("parseFileDirective: invalid line '%s'", line)
}
res.LineLimit = n
} else if strings.HasPrefix(s, "run ") {
rest := strings.TrimSpace(strings.TrimPrefix(s, "run "))
res.RunCmd = rest
// fmt.Printf(" run:: '%s'\n", res.RunCmd)
} else {
// if started with ":" we assume it was meant to be a directive
// but there was a typo
if startsWithColon {
return false, fmt.Errorf("parseFileDirective: invalid line '%s'", line)
}
// otherwise we assume this is just a comment
return false, nil
}
}
return true, nil
}
func extractFileDirectives(lines []string) (*FileDirective, []string, error) {
directive := &FileDirective{}
for len(lines) > 0 {
isDirectiveLine, err := parseFileDirective(directive, lines[0])
if err != nil {
return directive, lines, err
}
if !isDirectiveLine {
return directive, lines, nil
}
lines = lines[1:]
}
return directive, lines, nil
}
// https://www.onlinetool.io/gitoembed/widget?url=https%3A%2F%2Fgithub.com%2Fessentialbooks%2Fbooks%2Fblob%2Fmaster%2Fbooks%2Fgo%2F0020-basic-types%2Fbooleans.go
// to:
// books/go/0020-basic-types/booleans.go
// returns empty string if doesn't conform to what we expect
func gitoembedToRelativePath(uri string) string {
parsed, err := url.Parse(uri)
if err != nil {
return ""
}
switch parsed.Host {
case "www.onlinetool.io", "onlinetool.io":
// do nothing
default:
return ""
}
path := parsed.Path
if path != "/gitoembed/widget" {
return ""
}
uri = parsed.Query().Get("url")
// https://github.com/essentialbooks/books/blob/master/books/go/0020-basic-types/booleans.go
parsed, err = url.Parse(uri)
if parsed.Host != "github.com" {
return ""
}
path = strings.TrimPrefix(parsed.Path, "/essentialbooks/books/")
if path == parsed.Path {
return ""
}
// blob/master/books/go/0020-basic-types/booleans.go
path = strings.TrimPrefix(path, "blob/")
// master/books/go/0020-basic-types/booleans.go
// those are branch names. Should I just strip first 2 elements from the path?
path = strings.TrimPrefix(path, "master/")
path = strings.TrimPrefix(path, "notion/")
// books/go/0020-basic-types/booleans.go
return path
}
// we don't want to show our // :show annotations in snippets
func removeAnnotationLines(lines []string) []string {
var res []string
prevWasEmpty := false
for _, l := range lines {
if strings.Contains(l, "// :show ") {
continue
}
if len(l) == 0 && prevWasEmpty {
continue
}
prevWasEmpty = len(l) == 0
res = append(res, l)
}
return res
}
// convert local path like books/go/foo.go into path to the file in a github repo
func getGitHubPathForFile(path string) string {
return "https://github.com/essentialbooks/books/blob/master/" + toUnixPath(path)
}
func setGoPlaygroundID(b *Book, sf *SourceFile) error {
if sf.Lang != "go" {
return nil
}
if sf.Directive.NoPlayground {
return nil
}
id, err := getSha1ToGoPlaygroundIDCached(b, sf.CodeToRun)
if err != nil {
return err
}
sf.GoPlaygroundID = id
sf.PlaygroundURI = "https://goplay.space/#" + sf.GoPlaygroundID
return nil
}
func setSourceFileData(sf *SourceFile, data []byte) error {
sf.CodeFull = string(data)
lines := dataToLines(data)
directive, lines, err := extractFileDirectives(lines)
sf.Directive = directive
linesToRun := removeAnnotationLines(lines)
sf.CodeToRun = strings.Join(linesToRun, "\n")
sf.LinesToShow, err = extractCodeSnippets(lines)
return err
}