dis: Rework styling and build procedure
This commit is contained in:
parent
1e1d1a3cad
commit
cdc7d69ad9
51 changed files with 1251 additions and 339 deletions
94
pkg/fsx/filter.go
Normal file
94
pkg/fsx/filter.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package fsx
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
)
|
||||
|
||||
// Censor returns a new filesystem censors files for which the censor function returns true.
|
||||
//
|
||||
// A censored file cannot be opened by the filesystem and return [fs.ErrNotExist].
|
||||
// Hard and Soft Links pointing to the file might still read it.
|
||||
func Censor(fsys fs.FS, censor func(name string) bool) fs.FS {
|
||||
return &censorFS{
|
||||
fsys: fsys,
|
||||
censor: censor,
|
||||
}
|
||||
}
|
||||
|
||||
type censorFS struct {
|
||||
censor func(path string) bool
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (cf *censorFS) Sub(path string) (fs.FS, error) {
|
||||
sub, err := fs.Sub(cf.fsys, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &censorFS{
|
||||
censor: func(name string) bool {
|
||||
return cf.censor(filepath.Join(path, name))
|
||||
},
|
||||
fsys: sub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ef *censorFS) Open(name string) (fs.File, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
file, err := ef.fsys.Open(name)
|
||||
|
||||
// we need to also censor the ReadDir function of the returned file
|
||||
// this is to prevent the file from appearing in directory listings.
|
||||
if rdf, ok := file.(fs.ReadDirFile); ok {
|
||||
return &censorFSFile{
|
||||
ReadDirFile: rdf,
|
||||
|
||||
name: name,
|
||||
censor: ef,
|
||||
}, err
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
type censorFSFile struct {
|
||||
fs.ReadDirFile
|
||||
|
||||
name string
|
||||
censor *censorFS
|
||||
}
|
||||
|
||||
func (f *censorFSFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
entries, err := f.ReadDirFile.ReadDir(n)
|
||||
return f.censor.handleReadDir(f.name, entries, err)
|
||||
}
|
||||
|
||||
func (ef *censorFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
// censor ReadDir() entries too
|
||||
entries, err := fs.ReadDir(ef.fsys, name)
|
||||
return ef.handleReadDir(name, entries, err)
|
||||
}
|
||||
|
||||
// handleReadDir censors a ReadDir call
|
||||
func (ef *censorFS) handleReadDir(base string, entries []fs.DirEntry, err error) ([]fs.DirEntry, error) {
|
||||
entries = collection.Filter(entries, func(entry fs.DirEntry) bool {
|
||||
return !ef.censor(filepath.Join(base, entry.Name()))
|
||||
})
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (ef *censorFS) ReadFile(name string) ([]byte, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
return fs.ReadFile(ef.fsys, name)
|
||||
}
|
||||
113
pkg/resources/resources.go
Normal file
113
pkg/resources/resources.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// Package resources provides Resources
|
||||
package resources
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// Resources represents resources found inside a "html" file
|
||||
type Resources struct {
|
||||
JSModules []string // <script type="module">
|
||||
JSRegular []string // <script>
|
||||
CSS []string // <link rel="stylesheet">
|
||||
}
|
||||
|
||||
var attributeEscaper = strings.NewReplacer("<", "<", "&", "&", "\"", """)
|
||||
|
||||
const attributeShouldQuote = "<&\" "
|
||||
const quoteString = "\""
|
||||
|
||||
func attributeValue(value string) string {
|
||||
value = attributeEscaper.Replace(value)
|
||||
if strings.ContainsAny(value, attributeShouldQuote) {
|
||||
return quoteString + value + quoteString
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
var openLinkBytes = []byte("<link rel=stylesheet href=")
|
||||
var closeLinkBytes = []byte(">")
|
||||
var openModuleBytes = []byte("<script type=module src=")
|
||||
var openRegularBytes = []byte("<script src=")
|
||||
var closeScriptBytes = []byte("></script>")
|
||||
|
||||
// WriteCSS writes all link tags to writer
|
||||
func (resources *Resources) WriteCSS(writer io.Writer) {
|
||||
for _, href := range resources.CSS {
|
||||
writer.Write(openLinkBytes)
|
||||
writer.Write([]byte(attributeValue(href)))
|
||||
writer.Write(closeLinkBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (resources *Resources) CSSTemplate() template.HTML {
|
||||
var buffer strings.Builder
|
||||
resources.WriteCSS(&buffer)
|
||||
return template.HTML(buffer.String())
|
||||
}
|
||||
|
||||
// WriteJS writes all JavaScript tags to writer
|
||||
func (resources *Resources) WriteJS(writer io.Writer) {
|
||||
for _, href := range resources.JSModules {
|
||||
writer.Write(openModuleBytes)
|
||||
writer.Write([]byte(attributeValue(href)))
|
||||
writer.Write(closeScriptBytes)
|
||||
}
|
||||
for _, href := range resources.JSRegular {
|
||||
writer.Write(openRegularBytes)
|
||||
writer.Write([]byte(attributeValue(href)))
|
||||
writer.Write(closeScriptBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (resources *Resources) JSTemplate() template.HTML {
|
||||
var buffer strings.Builder
|
||||
resources.WriteJS(&buffer)
|
||||
return template.HTML(buffer.String())
|
||||
}
|
||||
|
||||
// Parse parses resources from reader
|
||||
func Parse(r io.Reader) (src Resources) {
|
||||
z := html.NewTokenizer(r)
|
||||
for {
|
||||
// read the next token
|
||||
z.Next()
|
||||
token := z.Token()
|
||||
|
||||
switch {
|
||||
case token.Type == html.ErrorToken:
|
||||
return
|
||||
|
||||
case token.Type == html.StartTagToken && token.Data == "script":
|
||||
// <script src="...">
|
||||
js := getAttributeValue(token, "src")
|
||||
if js != "" {
|
||||
if getAttributeValue(token, "type") == "module" {
|
||||
src.JSModules = append(src.JSModules, js)
|
||||
} else {
|
||||
src.JSRegular = append(src.JSRegular, js)
|
||||
}
|
||||
}
|
||||
}
|
||||
if token.Type == html.StartTagToken && token.Data == "link" && getAttributeValue(token, "rel") == "stylesheet" {
|
||||
// <link rel="stylesheet" href="...">
|
||||
css := getAttributeValue(token, "href")
|
||||
if css != "" {
|
||||
src.CSS = append(src.CSS, css)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// getAttributeValue returns the value of the given attribute, or the empty string if it is unset
|
||||
func getAttributeValue(token html.Token, attr string) string {
|
||||
return collection.First(token.Attr, func(a html.Attribute) bool {
|
||||
return a.Key == attr
|
||||
}).Val
|
||||
}
|
||||
34
pkg/resources/resources_test.go
Normal file
34
pkg/resources/resources_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExampleParse() {
|
||||
resources := Parse(strings.NewReader(`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/some/sheet1.css">
|
||||
<link rel="stylesheet" href="/some/sheet2.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/some/module1.js"></script>
|
||||
<script type="module" src="/some/module2.js"></script>
|
||||
<script src="/some/nonmodule1.js"></script>
|
||||
<script src="/some/nonmodule2.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("css: ")
|
||||
resources.WriteCSS(&builder)
|
||||
|
||||
builder.WriteString("\njs: ")
|
||||
resources.WriteJS(&builder)
|
||||
fmt.Println(builder.String())
|
||||
|
||||
// Output: css: <link rel=stylesheet href="/some/sheet1.css"><link rel=stylesheet href="/some/sheet2.css">
|
||||
// js: <script type=module src="/some/module1.js"><script type=module src="/some/module2.js"><script src="/some/nonmodule1.js"><script src="/some/nonmodule2.js">
|
||||
}
|
||||
29
pkg/resources/template.go
Normal file
29
pkg/resources/template.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MustParse parses a new "html/template" from the given value, and registers the given functions with it.
|
||||
// When something goes wrong, calls panic()
|
||||
func (resources *Resources) MustParse(value string) *template.Template {
|
||||
return template.Must(resources.RegisterFuncs(template.New("")).Parse(value))
|
||||
}
|
||||
|
||||
// RegisterFuncs registers two new template functions with t.
|
||||
// "JS" and "CSS" that return the appropriate resources to insert into the template.
|
||||
func (resources *Resources) RegisterFuncs(t *template.Template) *template.Template {
|
||||
var builder strings.Builder
|
||||
resources.WriteCSS(&builder)
|
||||
css := template.HTML(builder.String())
|
||||
|
||||
builder.Reset()
|
||||
resources.WriteJS(&builder)
|
||||
js := template.HTML(builder.String())
|
||||
|
||||
return t.Funcs(template.FuncMap{
|
||||
"JS": func() template.HTML { return js },
|
||||
"CSS": func() template.HTML { return css },
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue