From 8eb432539ca220b1fc884b04c0288e949cc15a9d Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 2 Apr 2025 21:21:19 +0100 Subject: [PATCH 01/13] consolidate `view` and `views` directories --- admin/templates.go | 24 +++++++++---------- templates/templates.go | 37 ++++++++++++++++++------------ {views => view}/404.html | 0 {views => view}/footer.html | 0 {views => view}/header.html | 0 {views => view}/index.html | 0 {views => view}/layout.html | 0 {views => view}/music-gateway.html | 0 {views => view}/music.html | 0 {views => view}/prideflag.html | 0 10 files changed, 34 insertions(+), 27 deletions(-) rename {views => view}/404.html (100%) rename {views => view}/footer.html (100%) rename {views => view}/header.html (100%) rename {views => view}/index.html (100%) rename {views => view}/layout.html (100%) rename {views => view}/music-gateway.html (100%) rename {views => view}/music.html (100%) rename {views => view}/prideflag.html (100%) diff --git a/admin/templates.go b/admin/templates.go index 12cdf08..ab4517e 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -11,44 +11,44 @@ import ( var indexTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "components", "release", "release-list-item.html"), filepath.Join("admin", "views", "index.html"), )) var loginTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "login.html"), )) var loginTOTPTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "login-totp.html"), )) var registerTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "register.html"), )) var logoutTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "logout.html"), )) var accountTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "edit-account.html"), )) var totpSetupTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "totp-setup.html"), )) var totpConfirmTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "totp-confirm.html"), )) @@ -79,23 +79,23 @@ var logsTemplate = template.Must(template.New("layout.html").Funcs(template.Func }, }).ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "logs.html"), )) var releaseTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "edit-release.html"), )) var artistTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "views", "edit-artist.html"), )) var trackTemplate = template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), + filepath.Join("view", "prideflag.html"), filepath.Join("admin", "components", "release", "release-list-item.html"), filepath.Join("admin", "views", "edit-track.html"), )) diff --git a/templates/templates.go b/templates/templates.go index 8d1a5ca..4c5793b 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -6,23 +6,30 @@ import ( ) var IndexTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "index.html"), + filepath.Join("view", "layout.html"), + filepath.Join("view", "header.html"), + filepath.Join("view", "footer.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("view", "index.html"), )) var MusicTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "music.html"), + filepath.Join("view", "layout.html"), + filepath.Join("view", "header.html"), + filepath.Join("view", "footer.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("view", "music.html"), )) var MusicGatewayTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "music-gateway.html"), + filepath.Join("view", "layout.html"), + filepath.Join("view", "header.html"), + filepath.Join("view", "footer.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("view", "music-gateway.html"), +)) +var BlogTemplate = template.Must(template.ParseFiles( + filepath.Join("view", "layout.html"), + filepath.Join("view", "header.html"), + filepath.Join("view", "footer.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("view", "blog.html"), )) diff --git a/views/404.html b/view/404.html similarity index 100% rename from views/404.html rename to view/404.html diff --git a/views/footer.html b/view/footer.html similarity index 100% rename from views/footer.html rename to view/footer.html diff --git a/views/header.html b/view/header.html similarity index 100% rename from views/header.html rename to view/header.html diff --git a/views/index.html b/view/index.html similarity index 100% rename from views/index.html rename to view/index.html diff --git a/views/layout.html b/view/layout.html similarity index 100% rename from views/layout.html rename to view/layout.html diff --git a/views/music-gateway.html b/view/music-gateway.html similarity index 100% rename from views/music-gateway.html rename to view/music-gateway.html diff --git a/views/music.html b/view/music.html similarity index 100% rename from views/music.html rename to view/music.html diff --git a/views/prideflag.html b/view/prideflag.html similarity index 100% rename from views/prideflag.html rename to view/prideflag.html From 1a8dc4d9ce873757e29d048b9dfc6dbceafe93bd Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 2 Apr 2025 23:04:09 +0100 Subject: [PATCH 02/13] basic blog page! --- go.mod | 1 + go.sum | 2 + main.go | 1 + model/blog.go | 39 ++++++++++++++++++ public/img/aridoodle.png | Bin 0 -> 1110 bytes public/script/blog.js | 0 public/style/blog.css | 46 +++++++++++++++++++++ view/blog.go | 86 +++++++++++++++++++++++++++++++++++++++ view/blog.html | 34 ++++++++++++++++ 9 files changed, 209 insertions(+) create mode 100644 model/blog.go create mode 100644 public/img/aridoodle.png create mode 100644 public/script/blog.js create mode 100644 public/style/blog.css create mode 100644 view/blog.go create mode 100644 view/blog.html diff --git a/go.mod b/go.mod index a1c6c76..e9fd53c 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require golang.org/x/crypto v0.27.0 // indirect require ( + github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect diff --git a/go.sum b/go.sum index f2ec7e7..d817720 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= +github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= diff --git a/main.go b/main.go index 2252622..8de29a3 100644 --- a/main.go +++ b/main.go @@ -448,6 +448,7 @@ func createServeMux(app *model.AppState) *http.ServeMux { mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app))) mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app))) + mux.Handle("/blog/", http.StripPrefix("/blog", view.BlogHandler(app))) mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) mux.Handle("/cursor-ws", cursor.Handler(app)) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/model/blog.go b/model/blog.go new file mode 100644 index 0000000..89f37a0 --- /dev/null +++ b/model/blog.go @@ -0,0 +1,39 @@ +package model + +import ( + "fmt" + "html/template" + "regexp" + "strings" + "time" +) + +type ( + Blog struct { + Title string `db:"title"` + Description string `db:"description"` + Visible bool `db:"visible"` + Date time.Time `db:"date"` + AuthorID string `db:"author"` + Markdown string `db:"markdown"` + HTML template.HTML `db:"html"` + } +) + +func (b *Blog) TitleNormalised() string { + rgx := regexp.MustCompile(`[^a-z0-9\-]`) + return rgx.ReplaceAllString( + strings.ReplaceAll( + strings.ToLower(b.Title), " ", "-", + ), + "", + ) +} + +func (b *Blog) GetMonth() string { + return fmt.Sprintf("%02d", int(b.Date.Month())) +} + +func (b *Blog) PrintDate() string { + return b.Date.Format("02 January 2006") +} diff --git a/public/img/aridoodle.png b/public/img/aridoodle.png new file mode 100644 index 0000000000000000000000000000000000000000..c8328b55ab907893ab1ed1e9057a683ab5881059 GIT binary patch literal 1110 zcmV-c1gZOpP)Px#1ZP1_K>z@;j|==^1poj52~bQ_MF0Q*|Ns900048wgK+==00VSVPE-H?0N2V5 zK>z>%32;bRa{vGi!vFvd!vV){sAK>D1II~3K~!i%&63-495Dz5ZU6sAhjWnx8ZF<@zHvXA4RyiRPMq&thr6M6JDN_MwnpZkW5(_T zH?|#TBq=^^WD|TQ9G#%VETit+RG&~@aZZWZjRUN9cnq8zjh+c^J|*@kF|Cn{kBI|H z%rqt?_a1`_O3X4&#+u7lV2={h7{lWt@YxwG(J^qx>lPpOc&9i=twCNIrEYt)AuOuD zF>lS9#Q|;(WGerTd2N-dZsA2BmtKXLB0gj~9L(0tEEMB6kYUHiUVC?bx1YT8R zT>A^g61N|L?Pbg^;gvUyA2hOBjG_M!E{|7odIz z9$kzD?mdS$#duLNBwJhI5M#X6IF+MR8n6r8P8=sTBRBeT;0QRWbiYyMFfw|BX_RLc z0n^1b*$5SEd@Ay7=ME2ljN+qF*RBEkGu*)5*|_jcLt4-$-erVRD{B>$Cfl7Xl*y$x z?Q^5W3*CxBx%94eE(FcpwvkaRm)`4oxpY%Eg4wE&ORu-ndUv_4C>TY&a#@(SQ)x!9 z4^ocL!N{T>=F2ibkHGOXqA5p!p_z$4la-^R5Jog}=Ij2D1rxy>fzn|_y^i?JSHa50 z*D%)=+G^xjuZqQrW+B+FK*H$*tx2`4??@m73k1G_OR1BV+P1}mTqT#UOja1NXp2YRUshB2QJWVh@~)CJRcQQk_M z1?mD7NVJr2h)+T@;>1uCyf$V^f;mL`ZpNx~og=etElr{^y3Wx68957w5z2D(!YF)O z*^OGYkx53giEk^_sKtpiiO94|bh(p_+KWwpPe$*%+DRK`b=D+2zQr3-fYe))nDZN| zQ4NU3mQ3C0DMr-*oh_NV)RT;=1bSNv&y70=;rETFpZGJzSIVQ&ijD6Yac9NOUou*7 z;d{oLpZMS(^4E;MZ}<`8y|S6&#^()~-YVZVzWkBB(Z?+}Moz9nT==->sMMf0zWQ4S z-p(jK7>`a*i4P`6^yMjnU1!{F0GMmv8g; cjbC5?0Q}4$TpJSbz5oCK07*qoM6N<$f`cvjV*mgE literal 0 HcmV?d00001 diff --git a/public/script/blog.js b/public/script/blog.js new file mode 100644 index 0000000..e69de29 diff --git a/public/style/blog.css b/public/style/blog.css new file mode 100644 index 0000000..a333120 --- /dev/null +++ b/public/style/blog.css @@ -0,0 +1,46 @@ +main { + margin: 0 auto 1rem auto; +} + +article.blog { + font-family: 'Lora', serif; +} + +.blog-date { + margin-top: -1em; + opacity: .75; +} + +.blog p { + line-height: 1.5em; +} + +.blog p:hover { + background: inherit; +} + +.blog sub { + opacity: .75; +} + +.blog img { + margin: 0 auto; + max-height: 50%; + max-width: 100%; + + display: block; +} + +.blog i.end-mark { + width: 1.2em; + height: 1.2em; + margin-top: -.2em; + + display: inline-block; + transform: translateY(.2em); + + background: url("/img/aridoodle.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/view/blog.go b/view/blog.go new file mode 100644 index 0000000..85da400 --- /dev/null +++ b/view/blog.go @@ -0,0 +1,86 @@ +package view + +import ( + "html/template" + "net/http" + "time" + + "arimelody-web/model" + "arimelody-web/templates" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +var mdRenderer = html.NewRenderer(html.RendererOptions{ + Flags: html.CommonFlags | html.HrefTargetBlank, +}) + +func BlogHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + blog := model.Blog{ + Title: "hello world!~", + Description: "lorem ipsum yadda yadda something boobies babababababababa", + Visible: true, + Date: time.Now(), + AuthorID: "ari", + Markdown: +` +# hello, world! + +i'm ari! + +she/her 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪 + +i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention! + +you're very welcome to take a look around my little space on the internet here, or explore any of the other parts i inhabit! + +if you're looking to support me financially, that's so cool of you!! if you like, you can buy some of my music over on bandcamp so you can at least get something for your money. thank you very much either way!! 💕 + +for anything else, you can reach me for any and all communications through ari@arimelody.me. if your message contains anything beyond a silly gag, i strongly recommend encrypting your message using my public pgp key, listed below! + +thank you for stopping by- i hope you have a lovely rest of your day! 💫 + +## metadata + +**my colours 🌈** + +- primary: #b7fd49 +- secondary: #f8e05b +- tertiary: #f788fe + +**my keys 🔑** + +- pgp: [[link]](/keys/ari%20melody_0x92678188_public.asc) +- ssh (ed25519): [[link]](/keys/id_ari_ed25519.pub) + +**where to find me 🛰️** + +- youtube +- twitch +- spotify +- bandcamp +- github + +**projects i've worked on 🛠️** + +- catdance +- pride flag +- ipaddrgen +- impact meme +- OpenTerminal +- Silver.js +`, + } + + // blog.Markdown += " " + + mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) + md := mdParser.Parse([]byte(blog.Markdown)) + blog.HTML = template.HTML(markdown.Render(md, mdRenderer)) + + templates.BlogTemplate.Execute(w, &blog) + }) +} diff --git a/view/blog.html b/view/blog.html new file mode 100644 index 0000000..60f3c4b --- /dev/null +++ b/view/blog.html @@ -0,0 +1,34 @@ +{{define "head"}} +{{.Title}} - ari melody 💫 + + + + + + + + + + + + + + +{{end}} + +{{define "content"}} +
+
+

{{.Title}}

+

Posted by {{.AuthorID}} @ {{.PrintDate}}

+ +
+ {{.HTML}} +
+ + +
+ + +
+{{end}} From 835dd344ca9b611d61af222e7b20d7dbd0ba5be0 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 2 Apr 2025 23:49:20 +0100 Subject: [PATCH 03/13] style improvements and bluesky comments! --- controller/bluesky.go | 48 +++++++++++++ model/blog.go | 2 + model/bluesky.go | 82 ++++++++++++++++++++++ public/style/blog.css | 157 ++++++++++++++++++++++++++++++++++++++++-- view/blog.go | 59 ++++++++++++++-- view/blog.html | 56 ++++++++++++++- view/music.go | 2 +- 7 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 controller/bluesky.go create mode 100644 model/bluesky.go diff --git a/controller/bluesky.go b/controller/bluesky.go new file mode 100644 index 0000000..0e382df --- /dev/null +++ b/controller/bluesky.go @@ -0,0 +1,48 @@ +package controller + +import ( + "arimelody-web/model" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" +) + +const BSKY_API_BASE = "https://public.api.bsky.app" + +func FetchThreadViewPost(actorID string, postID string) (*model.ThreadViewPost, error) { + uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", actorID, postID) + + req, err := http.NewRequest( + http.MethodGet, + strings.Join([]string{BSKY_API_BASE, "xrpc", "app.bsky.feed.getPostThread"}, "/"), + nil, + ) + if err != nil { panic(err) } + + req.URL.RawQuery = url.Values{ + "uri": { uri }, + }.Encode() + req.Header.Set("User-Agent", "ari melody [https://arimelody.me]") + req.Header.Set("Accept", "application/json") + + client := &http.Client{} + + res, err := client.Do(req) + if err != nil { + return nil, errors.New(fmt.Sprintf("Failed to call Bluesky API: %v", err)) + } + + type Data struct { + Thread model.ThreadViewPost `json:"thread"` + } + data := Data{} + err = json.NewDecoder(res.Body).Decode(&data) + if err != nil { + return nil, errors.New(fmt.Sprintf("Invalid response from server: %v", err)) + } + + return &data.Thread, nil +} diff --git a/model/blog.go b/model/blog.go index 89f37a0..7f0c87b 100644 --- a/model/blog.go +++ b/model/blog.go @@ -17,6 +17,8 @@ type ( AuthorID string `db:"author"` Markdown string `db:"markdown"` HTML template.HTML `db:"html"` + BlueskyActorID string `db:"bsky_actor"` + BlueskyPostID string `db:"bsky_post"` } ) diff --git a/model/bluesky.go b/model/bluesky.go new file mode 100644 index 0000000..dd8391e --- /dev/null +++ b/model/bluesky.go @@ -0,0 +1,82 @@ +package model + +import ( + "fmt" + "strings" + "time" +) + +type ( + Record struct { + Type string `json:"$type"` + CreatedAt string `json:"createdAt"` + Text string `json:"text"` + } + + Profile struct { + DID string `json:"did"` + Handle string `json:"handle"` + Avatar string `json:"avatar"` + DisplayName string `json:"displayName"` + CreatedAt string `json:"createdAt"` + } + + PostImage struct { + Thumbnail string `json:"thumb"` + Fullsize string `json:"fullsize"` + Alt string `json:"alt"` + } + + EmbedMedia struct { + Images []PostImage `json:"images"` + } + + Embed struct { + Media EmbedMedia `json:"media"` + } + + Post struct { + Author Profile `json:"author"` + Record Record `json:"record"` + ReplyCount int `json:"replyCount"` + RepostCount int `json:"repostCount"` + LikeCount int `json:"likeCount"` + QuoteCount int `json:"quoteCount"` + Embed *Embed `json:"embed"` + URI string `json:"uri"` + } + + ThreadViewPost struct { + Post Post `json:"post"` + Replies []*ThreadViewPost `json:"replies"` + } +) + +func (record *Record) CreatedAtPrint() (string, error) { + t, err := record.CreatedAtTime() + if err != nil { return "", err } + return t.Format("15:04, 2 February 2006"), nil +} + +func (record *Record) CreatedAtTime() (time.Time, error) { + return time.Parse("2006-01-02T15:04:05Z", record.CreatedAt) +} + +func (post *Post) HasImage() bool { + return post.Embed != nil && len(post.Embed.Media.Images) > 0 +} + +func (post *Post) PostID() string { + return strings.TrimPrefix( + post.URI, + fmt.Sprintf("at://%s/app.bsky.feed.post/", post.Author.DID), + ) +} + +func (post *Post) BskyURL() string { + return fmt.Sprintf( + "https://bsky.app/profile/%s/post/%s", + post.Author.DID, + post.PostID(), + ) +} diff --git a/public/style/blog.css b/public/style/blog.css index a333120..7f424d5 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -1,7 +1,20 @@ +:root { + --like: rgb(223, 104, 104); + --repost: rgb(162, 223, 73); + --bluesky: rgb(16, 131, 254); + --mastodon: rgb(86, 58, 204); +} + main { + width: min(calc(100% - 4rem), 900px); margin: 0 auto 1rem auto; } +.blog p:hover, +.comment p:hover { + background: inherit; +} + article.blog { font-family: 'Lora', serif; } @@ -15,16 +28,29 @@ article.blog { line-height: 1.5em; } -.blog p:hover { - background: inherit; -} - .blog sub { opacity: .75; } +.blog pre { + max-height: 15em; + padding: .5em; + font-size: .9em; + border: 1px solid #8884; + border-radius: 2px; + overflow: scroll; + background: #060606; +} + +.blog p code { + padding: .2em .3em; + font-size: .9em; + border: 1px solid #8884; + border-radius: 2px; + background: #060606; +} + .blog img { - margin: 0 auto; max-height: 50%; max-width: 100%; @@ -44,3 +70,124 @@ article.blog { background-repeat: no-repeat; background-position: center; } + + + +/* COMMENTS */ + +.interactions { + margin: 1em 0; + display: flex; + flex-direction: row; + gap: .5em; + flex-wrap: wrap; +} + +.btn { + display: inline-block; + padding: .4em .6em; + border: 1px solid var(--on-background); + border-radius: 2px; + color: inherit; + text-decoration: none; + font-weight: 600; +} + +.interactions .likes, +.interactions .reposts { + padding: 0 .5em; + min-width: fit-content; + font-family: monospace; + text-align: center; + line-height: 2em; + border: 1px solid var(--on-background); + border-radius: 2px; + text-wrap: nowrap; +} + +.interactions .likes { + border-color: var(--on-background); +} +.interactions .reposts { + border-color: var(--on-background); +} + +.btn.bluesky, +.btn.mastodon { + font-family: monospace; + text-wrap: nowrap; +} + +.btn.bluesky { + color: var(--bluesky); + border-color: var(--bluesky); +} + +.btn.mastodon { + color: var(--mastodon); + border-color: var(--mastodon); +} + +.comment { + font-family: 'Inter', 'Arial', sans-serif; + font-size: 1em; +} + +.comment .comment-hover { + padding: 1em; + transition: background-color .1s; +} + +.comment .comment-hover:hover { + background-color: #8881; +} + +.comment .comment-header a { + display: flex; + gap: .5em; + font-weight: 600; + color: var(--primary); + text-decoration: none; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; +} + +.comment .comment-header a .display-name { + overflow: inherit; + text-overflow: inherit; +} + +.comment .comment-header a .handle { + opacity: .5; + font-family: monospace; + font-size: .9em; + overflow: inherit; + text-overflow: inherit; +} + +.comment .comment-header img.avatar { + width: 1.5em; + height: 1.5em; +} + +.comment .comment-body { + color: inherit; + text-decoration: none; +} + +.comment p.comment-text { + margin: .5em 0; + white-space: break-spaces; +} + +.comment .comment-footer .comment-date { + margin: 0; + font-size: .8em; + opacity: .5; +} + +.comment .comment-replies { + margin-left: 1em; + border-left: 2px solid #8884; +} diff --git a/view/blog.go b/view/blog.go index 85da400..9ab52d2 100644 --- a/view/blog.go +++ b/view/blog.go @@ -1,10 +1,13 @@ package view import ( + "fmt" "html/template" "net/http" + "os" "time" + "arimelody-web/controller" "arimelody-web/model" "arimelody-web/templates" @@ -20,16 +23,14 @@ var mdRenderer = html.NewRenderer(html.RendererOptions{ func BlogHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { blog := model.Blog{ - Title: "hello world!~", + Title: "hello world!", Description: "lorem ipsum yadda yadda something boobies babababababababa", Visible: true, Date: time.Now(), AuthorID: "ari", Markdown: ` -# hello, world! - -i'm ari! +**i'm ari!** she/her 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪 @@ -72,7 +73,26 @@ thank you for stopping by- i hope you have a lovely rest of your day! 💫 - impact meme - OpenTerminal - Silver.js + +### code block test + +~~~ c +#include + +int main(int argc, char *argv[]) { + printf("hello world!~\n"); + return 0; +} +~~~ + +### aridoodle + +this is `+"`"+`aridoodle`+"`"+`. please take care of her. + +![aridoodle](/img/aridoodle.png) `, + BlueskyActorID: "did:plc:yct6cvgfipngizry5umzkxr3", + BlueskyPostID: "3llsudsx7pc2u", } // blog.Markdown += " " @@ -81,6 +101,35 @@ thank you for stopping by- i hope you have a lovely rest of your day! 💫 md := mdParser.Parse([]byte(blog.Markdown)) blog.HTML = template.HTML(markdown.Render(md, mdRenderer)) - templates.BlogTemplate.Execute(w, &blog) + comments := []*model.ThreadViewPost{} + blueskyPost, err := controller.FetchThreadViewPost(blog.BlueskyActorID, blog.BlueskyPostID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) + } else { + comments = append(comments, blueskyPost.Replies...) + } + + type BlogView struct { + *model.Blog + Comments []*model.ThreadViewPost + Likes int + Reposts int + BlueskyURL string + MastodonURL string + } + + err = templates.BlogTemplate.Execute(w, BlogView{ + Blog: &blog, + Comments: blueskyPost.Replies, + Likes: 10, + Reposts: 10, + BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID), + MastodonURL: "#", + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Error rendering blog post: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } }) } diff --git a/view/blog.html b/view/blog.html index 60f3c4b..f858252 100644 --- a/view/blog.html +++ b/view/blog.html @@ -24,11 +24,63 @@
{{.HTML}} -
- +
+ +
+ + 🔁 {{.Reposts}} + Bluesky 🦋 + + Mastodon 🐘 +
+ +
+ {{range .Comments}} + {{template "comment" .}} + {{end}} +
+ {{end}} + +{{define "comment"}} + +{{end}} + diff --git a/view/music.go b/view/music.go index 2d40ef0..3c27b6e 100644 --- a/view/music.go +++ b/view/music.go @@ -89,7 +89,7 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler { err := templates.MusicGatewayTemplate.Execute(w, response) if err != nil { - fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) + fmt.Fprintf(os.Stderr, "Error rendering music gateway for %s: %v\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } From 5aa241e4d6a4f6666621f49403ae292654725cf0 Mon Sep 17 00:00:00 2001 From: ari melody Date: Thu, 3 Apr 2025 00:00:16 +0100 Subject: [PATCH 04/13] improve comment callout --- public/style/blog.css | 15 +++++++++++++-- public/style/colours.css | 2 ++ view/blog.html | 11 ++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/style/blog.css b/public/style/blog.css index 7f424d5..9ec8e62 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -39,7 +39,7 @@ article.blog { border: 1px solid #8884; border-radius: 2px; overflow: scroll; - background: #060606; + background: var(--background-alt); } .blog p code { @@ -47,7 +47,7 @@ article.blog { font-size: .9em; border: 1px solid #8884; border-radius: 2px; - background: #060606; + background: var(--background-alt); } .blog img { @@ -112,6 +112,17 @@ article.blog { border-color: var(--on-background); } +.comment-callout { + padding: 1em; + background: var(--background-alt); + border-radius: 2px; + border: 1px solid #8884; + text-align: center; +} +.comment-callout:hover { + background: var(--background-alt); +} + .btn.bluesky, .btn.mastodon { font-family: monospace; diff --git a/public/style/colours.css b/public/style/colours.css index 2bf607d..1407aa8 100644 --- a/public/style/colours.css +++ b/public/style/colours.css @@ -1,5 +1,6 @@ :root { --background: #080808; + --background-alt: #040404; --on-background: #f0f0f0; --primary: #b7fd49; @@ -11,6 +12,7 @@ @media (prefers-color-scheme: light) { :root { --background: #ffffff; + --background-alt: #f0f0f0; --on-background: #101010; --primary: #6d9e23; diff --git a/view/blog.html b/view/blog.html index f858252..a5f8130 100644 --- a/view/blog.html +++ b/view/blog.html @@ -32,11 +32,16 @@
🔁 {{.Reposts}} - Bluesky 🦋 - - Mastodon 🐘
+

+ join the conversation on + Bluesky 🦋 + + or + Mastodon 🐘 +

+
{{range .Comments}} {{template "comment" .}} From 0796ea8fde0bb1c4394fb0bb68847e589c49009a Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 29 Apr 2025 22:04:19 +0100 Subject: [PATCH 05/13] blog: css tweaks --- public/style/blog.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/style/blog.css b/public/style/blog.css index 9ec8e62..ee637bb 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -6,7 +6,7 @@ } main { - width: min(calc(100% - 4rem), 900px); + width: min(calc(100% - 4rem), 1200px); margin: 0 auto 1rem auto; } @@ -17,6 +17,7 @@ main { article.blog { font-family: 'Lora', serif; + font-size: 24px; } .blog-date { @@ -127,17 +128,28 @@ article.blog { .btn.mastodon { font-family: monospace; text-wrap: nowrap; + text-decoration: none; + transition-property: color background-color; + transition-duration: .1s; } .btn.bluesky { color: var(--bluesky); border-color: var(--bluesky); } +.btn.bluesky:hover { + background-color: var(--bluesky); + color: #fff; +} .btn.mastodon { color: var(--mastodon); border-color: var(--mastodon); } +.btn.mastodon:hover { + background-color: var(--mastodon); + color: #fff; +} .comment { font-family: 'Inter', 'Arial', sans-serif; From fece0f5da676c9b78957b530d6d1760567871ef9 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 21 May 2025 18:38:50 +0100 Subject: [PATCH 06/13] blog: display bluesky like and repost counts --- view/blog.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/view/blog.go b/view/blog.go index 9ab52d2..1f3bb1d 100644 --- a/view/blog.go +++ b/view/blog.go @@ -16,6 +16,15 @@ import ( "github.com/gomarkdown/markdown/parser" ) +type BlogView struct { + *model.Blog + Comments []*model.ThreadViewPost + Likes int + Reposts int + BlueskyURL string + MastodonURL string +} + var mdRenderer = html.NewRenderer(html.RendererOptions{ Flags: html.CommonFlags | html.HrefTargetBlank, }) @@ -109,20 +118,11 @@ this is `+"`"+`aridoodle`+"`"+`. please take care of her. comments = append(comments, blueskyPost.Replies...) } - type BlogView struct { - *model.Blog - Comments []*model.ThreadViewPost - Likes int - Reposts int - BlueskyURL string - MastodonURL string - } - err = templates.BlogTemplate.Execute(w, BlogView{ Blog: &blog, Comments: blueskyPost.Replies, - Likes: 10, - Reposts: 10, + Likes: blueskyPost.Post.LikeCount, + Reposts: blueskyPost.Post.RepostCount, BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID), MastodonURL: "#", }) From 0596edc4b2e3e77f3c3f67d898f6d2a3a59fc08f Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 21 May 2025 18:39:33 +0100 Subject: [PATCH 07/13] blog: nice comment tweaks --- public/script/blog.js | 19 +++++++++++++++++++ public/style/blog.css | 2 +- view/blog.html | 12 ++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/public/script/blog.js b/public/script/blog.js index e69de29..6b9fee4 100644 --- a/public/script/blog.js +++ b/public/script/blog.js @@ -0,0 +1,19 @@ +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => { + /** @type {HTMLLinkElement} */ + const commentBody = comment.querySelector('a.comment-body'); + + comment.querySelectorAll('a').forEach((/** @type {HTMLLinkElement} */ element) => { + element.addEventListener('click', event => { + event.stopPropagation(); + }); + }); + + comment.addEventListener('click', () => { + commentBody.click(); + }); + + comment.style.cursor = 'pointer'; + comment.role = 'link'; + }); +}); diff --git a/public/style/blog.css b/public/style/blog.css index ee637bb..19d87b4 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -204,7 +204,7 @@ article.blog { white-space: break-spaces; } -.comment .comment-footer .comment-date { +.comment .comment-footer { margin: 0; font-size: .8em; opacity: .5; diff --git a/view/blog.html b/view/blog.html index a5f8130..eb8c4a1 100644 --- a/view/blog.html +++ b/view/blog.html @@ -38,8 +38,10 @@ join the conversation on Bluesky 🦋 - or - Mastodon 🐘 +

@@ -74,8 +76,10 @@ {{end}}
From 3d64333b4f03fd1af6022dc2d9608eb4f2fc31e3 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 23 Jun 2025 20:38:28 +0100 Subject: [PATCH 08/13] blog sidebar, some cosmetic changes --- public/img/blog/bluesky-dark.svg | 1 + public/img/blog/bluesky-light.svg | 1 + public/img/blog/boost-dark.svg | 1 + public/img/blog/boost-light.svg | 1 + public/img/blog/comment-dark.svg | 1 + public/img/blog/comment-light.svg | 1 + public/img/blog/copy-link-dark.svg | 1 + public/img/blog/copy-link-light.svg | 1 + public/img/blog/like-dark.svg | 1 + public/img/blog/like-light.svg | 1 + public/script/blog.js | 11 ++- public/style/blog.css | 99 ++++++++++++++++-------- public/style/main.css | 23 ++++++ view/blog.go | 49 ++---------- view/blog.html | 114 +++++++++++++++++++++------- 15 files changed, 202 insertions(+), 104 deletions(-) create mode 100644 public/img/blog/bluesky-dark.svg create mode 100644 public/img/blog/bluesky-light.svg create mode 100644 public/img/blog/boost-dark.svg create mode 100644 public/img/blog/boost-light.svg create mode 100644 public/img/blog/comment-dark.svg create mode 100644 public/img/blog/comment-light.svg create mode 100644 public/img/blog/copy-link-dark.svg create mode 100644 public/img/blog/copy-link-light.svg create mode 100644 public/img/blog/like-dark.svg create mode 100644 public/img/blog/like-light.svg diff --git a/public/img/blog/bluesky-dark.svg b/public/img/blog/bluesky-dark.svg new file mode 100644 index 0000000..e3fe1a9 --- /dev/null +++ b/public/img/blog/bluesky-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/bluesky-light.svg b/public/img/blog/bluesky-light.svg new file mode 100644 index 0000000..f0eea97 --- /dev/null +++ b/public/img/blog/bluesky-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/boost-dark.svg b/public/img/blog/boost-dark.svg new file mode 100644 index 0000000..7d0a7bb --- /dev/null +++ b/public/img/blog/boost-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/boost-light.svg b/public/img/blog/boost-light.svg new file mode 100644 index 0000000..be2d14f --- /dev/null +++ b/public/img/blog/boost-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/comment-dark.svg b/public/img/blog/comment-dark.svg new file mode 100644 index 0000000..79f0de5 --- /dev/null +++ b/public/img/blog/comment-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/comment-light.svg b/public/img/blog/comment-light.svg new file mode 100644 index 0000000..2e3e8f7 --- /dev/null +++ b/public/img/blog/comment-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/copy-link-dark.svg b/public/img/blog/copy-link-dark.svg new file mode 100644 index 0000000..5155a70 --- /dev/null +++ b/public/img/blog/copy-link-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/copy-link-light.svg b/public/img/blog/copy-link-light.svg new file mode 100644 index 0000000..b6fa5a6 --- /dev/null +++ b/public/img/blog/copy-link-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/like-dark.svg b/public/img/blog/like-dark.svg new file mode 100644 index 0000000..634bf6f --- /dev/null +++ b/public/img/blog/like-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/blog/like-light.svg b/public/img/blog/like-light.svg new file mode 100644 index 0000000..fb5c7ea --- /dev/null +++ b/public/img/blog/like-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/script/blog.js b/public/script/blog.js index 6b9fee4..4f96ab6 100644 --- a/public/script/blog.js +++ b/public/script/blog.js @@ -1,4 +1,4 @@ -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('readystatechange', () => { document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => { /** @type {HTMLLinkElement} */ const commentBody = comment.querySelector('a.comment-body'); @@ -16,4 +16,13 @@ document.addEventListener('DOMContentLoaded', () => { comment.style.cursor = 'pointer'; comment.role = 'link'; }); + + document.getElementById('blog-copy-link').addEventListener('click', event => { + event.preventDefault(); + if (navigator.clipboard === undefined) { + console.error("clipboard is not supported by this browser!"); + return; + } + navigator.clipboard.writeText(location.protocol + "//" + location.host + location.pathname); + }); }); diff --git a/public/style/blog.css b/public/style/blog.css index 19d87b4..7f57d68 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -10,6 +10,62 @@ main { margin: 0 auto 1rem auto; } +#blog-sidebar { + position: fixed; + width: 3em; + padding: 3em; + transform: translate(-9em, -1em); + overflow: clip; + opacity: .5; + transition: opacity .2s; +} +#blog-sidebar:hover { + opacity: 1; +} + +#blog-sidebar ul { + margin: 0; + padding: .3em; + list-style: none; + display: flex; + flex-direction: column; + gap: .3em; + border-radius: 4px; + border: 1px solid var(--on-background); + box-shadow: 4px 4px 4px #0001; +} + +#blog-sidebar a { + width: 35px; + height: 35px; + display: block; + padding: .2em; + border-radius: 2px; + text-decoration: none; +} +#blog-sidebar a:hover { + background: #0001; +} +#blog-sidebar a:active { + background: #0002; +} + +#blog-sidebar a img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + +#blog-sidebar span { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.5em; +} + .blog p:hover, .comment p:hover { background: inherit; @@ -76,7 +132,7 @@ article.blog { /* COMMENTS */ -.interactions { +#interactions { margin: 1em 0; display: flex; flex-direction: row; @@ -94,8 +150,8 @@ article.blog { font-weight: 600; } -.interactions .likes, -.interactions .reposts { +#interactions .likes, +#interactions .reposts { padding: 0 .5em; min-width: fit-content; font-family: monospace; @@ -106,49 +162,26 @@ article.blog { text-wrap: nowrap; } -.interactions .likes { +#interactions .likes { border-color: var(--on-background); } -.interactions .reposts { +#interactions .reposts { border-color: var(--on-background); } .comment-callout { - padding: 1em; - background: var(--background-alt); - border-radius: 2px; - border: 1px solid #8884; + padding-bottom: 1em; text-align: center; -} -.comment-callout:hover { - background: var(--background-alt); + border-radius: 2px; + border-bottom: 1px solid #8884; } -.btn.bluesky, -.btn.mastodon { - font-family: monospace; - text-wrap: nowrap; - text-decoration: none; - transition-property: color background-color; - transition-duration: .1s; -} - -.btn.bluesky { +.bluesky { color: var(--bluesky); - border-color: var(--bluesky); -} -.btn.bluesky:hover { - background-color: var(--bluesky); - color: #fff; } -.btn.mastodon { +.mastodon { color: var(--mastodon); - border-color: var(--mastodon); -} -.btn.mastodon:hover { - background-color: var(--mastodon); - color: #fff; } .comment { diff --git a/public/style/main.css b/public/style/main.css index 680ee2b..7d73f24 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -141,8 +141,23 @@ a#backtotop:hover { } } +.light-only { + display: none; +} + +.dark-only { + display: inherit; +} @media (prefers-color-scheme: light) { + .light-only { + display: inherit; + } + + .dark-only { + display: none; + } + a.link-button:hover { box-shadow: none; } @@ -161,6 +176,14 @@ a#backtotop:hover { } @media (prefers-color-scheme: dark) { + .light-only { + display: none; + } + + .dark-only { + display: inherit; + } + body.crt { text-shadow: 0 0 3em; } diff --git a/view/blog.go b/view/blog.go index 1f3bb1d..73095b0 100644 --- a/view/blog.go +++ b/view/blog.go @@ -39,62 +39,25 @@ func BlogHandler(app *model.AppState) http.Handler { AuthorID: "ari", Markdown: ` -**i'm ari!** +**i'm ari!** (she/her) 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪 -she/her 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪 +welcome to my blog! i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention! -you're very welcome to take a look around my little space on the internet here, or explore any of the other parts i inhabit! -if you're looking to support me financially, that's so cool of you!! if you like, you can buy some of my music over on bandcamp so you can at least get something for your money. thank you very much either way!! 💕 - -for anything else, you can reach me for any and all communications through ari@arimelody.me. if your message contains anything beyond a silly gag, i strongly recommend encrypting your message using my public pgp key, listed below! - -thank you for stopping by- i hope you have a lovely rest of your day! 💫 - -## metadata - -**my colours 🌈** - -- primary: #b7fd49 -- secondary: #f8e05b -- tertiary: #f788fe - -**my keys 🔑** - -- pgp: [[link]](/keys/ari%20melody_0x92678188_public.asc) -- ssh (ed25519): [[link]](/keys/id_ari_ed25519.pub) - -**where to find me 🛰️** - -- youtube -- twitch -- spotify -- bandcamp -- github - -**projects i've worked on 🛠️** - -- catdance -- pride flag -- ipaddrgen -- impact meme -- OpenTerminal -- Silver.js - -### code block test +## code block test ~~~ c #include int main(int argc, char *argv[]) { - printf("hello world!~\n"); - return 0; + printf("hello world!~\n"); + return 0; } ~~~ -### aridoodle +## aridoodle this is `+"`"+`aridoodle`+"`"+`. please take care of her. diff --git a/view/blog.html b/view/blog.html index eb8c4a1..ea10bbe 100644 --- a/view/blog.html +++ b/view/blog.html @@ -18,39 +18,99 @@ {{define "content"}}
- +
+ + 🔁 {{.Reposts}} +
-
+

+ join the conversation on + Bluesky 🦋 + + +

-
- - 🔁 {{.Reposts}} -
- -

- join the conversation on - Bluesky 🦋 - - -

- -
- {{range .Comments}} - {{template "comment" .}} +
+ {{range .Comments}} + {{template "comment" .}} + {{end}} +
{{end}} -
- + +
{{end}} @@ -59,7 +119,7 @@
- {{.Post.Author.DisplayName}}'s avatar + {{.Post.Author.DisplayName}}'s avatar {{.Post.Author.DisplayName}} @{{.Post.Author.Handle}} From faf6095d1680d4ec3c78e40b7d9a269f396197c4 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 24 Jun 2025 01:32:30 +0100 Subject: [PATCH 09/13] blog visitor frontend (pretty much) done! --- controller/blog.go | 83 +++++++++ controller/migrator.go | 6 +- model/blog.go | 40 +++-- model/bluesky.go | 2 +- public/script/blog.js | 39 ++--- public/script/blogpost.js | 16 ++ public/style/blog.css | 283 ++++++------------------------ public/style/blogpost.css | 314 ++++++++++++++++++++++++++++++++++ public/style/main.css | 4 +- schema-migration/000-init.sql | 22 +++ schema-migration/004-blog.sql | 15 ++ templates/templates.go | 7 + view/blog.go | 187 ++++++++++++++------ view/blog.html | 166 ++++-------------- view/blogpost.html | 178 +++++++++++++++++++ view/header.html | 4 +- 16 files changed, 903 insertions(+), 463 deletions(-) create mode 100644 controller/blog.go create mode 100644 public/script/blogpost.js create mode 100644 public/style/blogpost.css create mode 100644 schema-migration/004-blog.sql create mode 100644 view/blogpost.html diff --git a/controller/blog.go b/controller/blog.go new file mode 100644 index 0000000..7fb201e --- /dev/null +++ b/controller/blog.go @@ -0,0 +1,83 @@ +package controller + +import ( + "arimelody-web/model" + + "github.com/jmoiron/sqlx" +) + +func GetBlogPost(db *sqlx.DB, id string) (*model.BlogPost, error) { + var blog = model.BlogPost{} + + err := db.Get(&blog, "SELECT * FROM blogpost WHERE id=$1", id) + if err != nil { + return nil, err + } + + return &blog, nil +} + +func GetBlogPosts(db *sqlx.DB, onlyVisible bool, limit int, offset int) ([]*model.BlogPost, error) { + var blogs = []*model.BlogPost{} + + query := "SELECT * FROM blogpost ORDER BY created_at" + if onlyVisible { + query = "SELECT * FROM blogpost WHERE visible=true ORDER BY created_at" + } + + var err error + if limit < 0 { + err = db.Select(&blogs, query) + } else { + err = db.Select(&blogs, query + " LIMIT $1 OFFSET $2", limit, offset) + } + if err != nil { + return nil, err + } + + // for range 4 { + // blog := *blogs[len(blogs)-1] + // blog.CreatedAt = blog.CreatedAt.Add(time.Hour * -5000) + // blogs = append(blogs, &blog) + // } + + return blogs, nil +} + +func CreateBlogPost(db *sqlx.DB, post *model.BlogPost) error { + _, err := db.Exec( + "INSERT INTO blogpost (id,title,description,visible,author,markdown,html,bluesky_actor,bluesky_post) " + + "VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)", + post.ID, + post.Title, + post.Description, + post.Visible, + post.AuthorID, + post.Markdown, + post.HTML, + post.BlueskyActorID, + post.BlueskyPostID, + ) + + return err +} + +func UpdateBlogPost(db *sqlx.DB, postID string, post *model.BlogPost) error { + _, err := db.Exec( + "UPDATE blogpost SET " + + "id=$2,title=$3,description=$4,visible=$5,author=$6,markdown=$7,html=$8,bluesky_actor=$9,bluesky_post=$10,modified_at=CURRENT_TIMESTAMP " + + "WHERE id=$1", + postID, + post.ID, + post.Title, + post.Description, + post.Visible, + post.AuthorID, + post.Markdown, + post.HTML, + post.BlueskyActorID, + post.BlueskyPostID, + ) + + return err +} diff --git a/controller/migrator.go b/controller/migrator.go index 4b99b9c..ac3c419 100644 --- a/controller/migrator.go +++ b/controller/migrator.go @@ -8,7 +8,7 @@ import ( "github.com/jmoiron/sqlx" ) -const DB_VERSION int = 4 +const DB_VERSION int = 5 func CheckDBVersionAndMigrate(db *sqlx.DB) { db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody") @@ -49,6 +49,10 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) { ApplyMigration(db, "003-fail-lock") oldDBVersion = 4 + case 4: + ApplyMigration(db, "004-blog") + oldDBVersion = 5 + } } diff --git a/model/blog.go b/model/blog.go index 7f0c87b..d341154 100644 --- a/model/blog.go +++ b/model/blog.go @@ -1,6 +1,7 @@ package model import ( + "database/sql" "fmt" "html/template" "regexp" @@ -9,20 +10,22 @@ import ( ) type ( - Blog struct { - Title string `db:"title"` - Description string `db:"description"` - Visible bool `db:"visible"` - Date time.Time `db:"date"` - AuthorID string `db:"author"` - Markdown string `db:"markdown"` - HTML template.HTML `db:"html"` - BlueskyActorID string `db:"bsky_actor"` - BlueskyPostID string `db:"bsky_post"` + BlogPost struct { + ID string `db:"id"` + Title string `db:"title"` + Description string `db:"description"` + Visible bool `db:"visible"` + CreatedAt time.Time `db:"created_at"` + ModifiedAt sql.NullTime `db:"modified_at"` + AuthorID string `db:"author"` + Markdown string `db:"markdown"` + HTML template.HTML `db:"html"` + BlueskyActorID *string `db:"bluesky_actor"` + BlueskyPostID *string `db:"bluesky_post"` } ) -func (b *Blog) TitleNormalised() string { +func (b *BlogPost) TitleNormalised() string { rgx := regexp.MustCompile(`[^a-z0-9\-]`) return rgx.ReplaceAllString( strings.ReplaceAll( @@ -32,10 +35,17 @@ func (b *Blog) TitleNormalised() string { ) } -func (b *Blog) GetMonth() string { - return fmt.Sprintf("%02d", int(b.Date.Month())) +func (b *BlogPost) GetMonth() string { + return fmt.Sprintf("%02d", int(b.CreatedAt.Month())) } -func (b *Blog) PrintDate() string { - return b.Date.Format("02 January 2006") +func (b *BlogPost) PrintDate() string { + return b.CreatedAt.Format("2 January 2006, 03:04") +} + +func (b *BlogPost) PrintModifiedDate() string { + if !b.ModifiedAt.Valid { + return "" + } + return b.ModifiedAt.Time.Format("2 January 2006, 03:04") } diff --git a/model/bluesky.go b/model/bluesky.go index dd8391e..f04d9db 100644 --- a/model/bluesky.go +++ b/model/bluesky.go @@ -55,7 +55,7 @@ type ( func (record *Record) CreatedAtPrint() (string, error) { t, err := record.CreatedAtTime() if err != nil { return "", err } - return t.Format("15:04, 2 February 2006"), nil + return t.Format("2 Jan 2006, 15:04"), nil } func (record *Record) CreatedAtTime() (time.Time, error) { diff --git a/public/script/blog.js b/public/script/blog.js index 4f96ab6..73d34a3 100644 --- a/public/script/blog.js +++ b/public/script/blog.js @@ -1,28 +1,15 @@ -document.addEventListener('readystatechange', () => { - document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => { - /** @type {HTMLLinkElement} */ - const commentBody = comment.querySelector('a.comment-body'); +import { hijackClickEvent } from "./main.js"; - comment.querySelectorAll('a').forEach((/** @type {HTMLLinkElement} */ element) => { - element.addEventListener('click', event => { - event.stopPropagation(); - }); - }); - - comment.addEventListener('click', () => { - commentBody.click(); - }); - - comment.style.cursor = 'pointer'; - comment.role = 'link'; - }); - - document.getElementById('blog-copy-link').addEventListener('click', event => { - event.preventDefault(); - if (navigator.clipboard === undefined) { - console.error("clipboard is not supported by this browser!"); - return; - } - navigator.clipboard.writeText(location.protocol + "//" + location.host + location.pathname); - }); +document.querySelectorAll('article.blog-post').forEach(element => { + const link = element.querySelector('.blog-title a'); + hijackClickEvent(element, link); +}); +document.querySelectorAll('article.blog-post').forEach(element => { + const link = element.querySelector('.blog-title a'); + hijackClickEvent(element, link); +}); + +document.getElementById('load-more').addEventListener('click', event => { + event.preventDefault(); + alert('ok'); }); diff --git a/public/script/blogpost.js b/public/script/blogpost.js new file mode 100644 index 0000000..fac626e --- /dev/null +++ b/public/script/blogpost.js @@ -0,0 +1,16 @@ +import { hijackClickEvent } from "./main.js"; + +document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} */ comment) => { + /** @type {HTMLLinkElement} */ + const commentDate = comment.querySelector('.comment-date'); + hijackClickEvent(comment, commentDate); +}); + +document.getElementById('blog-copy-link').addEventListener('click', event => { + event.preventDefault(); + if (navigator.clipboard === undefined) { + console.error("clipboard is not supported by this browser!"); + return; + } + navigator.clipboard.writeText(location.protocol + "//" + location.host + location.pathname); +}); diff --git a/public/style/blog.css b/public/style/blog.css index 7f57d68..07b8089 100644 --- a/public/style/blog.css +++ b/public/style/blog.css @@ -1,249 +1,70 @@ -:root { - --like: rgb(223, 104, 104); - --repost: rgb(162, 223, 73); - --bluesky: rgb(16, 131, 254); - --mastodon: rgb(86, 58, 204); -} - -main { - width: min(calc(100% - 4rem), 1200px); - margin: 0 auto 1rem auto; -} - -#blog-sidebar { - position: fixed; - width: 3em; - padding: 3em; - transform: translate(-9em, -1em); - overflow: clip; - opacity: .5; - transition: opacity .2s; -} -#blog-sidebar:hover { - opacity: 1; -} - -#blog-sidebar ul { - margin: 0; - padding: .3em; - list-style: none; - display: flex; - flex-direction: column; - gap: .3em; +article.blog-post { + margin-bottom: 1rem; + padding: 1.5rem; + border: 1px solid #8882; border-radius: 4px; - border: 1px solid var(--on-background); - box-shadow: 4px 4px 4px #0001; -} - -#blog-sidebar a { - width: 35px; - height: 35px; - display: block; - padding: .2em; - border-radius: 2px; + background-color: #ffffff08; + transition: background-color .1s; text-decoration: none; -} -#blog-sidebar a:hover { - background: #0001; -} -#blog-sidebar a:active { - background: #0002; + cursor: pointer; } -#blog-sidebar a img { - display: block; - width: 100%; - height: 100%; - object-fit: contain; +.blog-post h2:hover, +.blog-post p:hover { + background: none; } -#blog-sidebar span { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.5em; +.blog-title { + margin: 0; +} +.blog-title a { + display: inherit; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; } -.blog p:hover, -.comment p:hover { - background: inherit; +.blog-meta { + margin: 0; } -article.blog { - font-family: 'Lora', serif; - font-size: 24px; +.blog-author { + margin: 0; + font-size: .8em; +} +.blog-author img { + width: 1.3em; + height: 1.3em; + display: inline-block; + transform: translate(0, 4px); + border-radius: 4px; } .blog-date { - margin-top: -1em; - opacity: .75; -} - -.blog p { - line-height: 1.5em; -} - -.blog sub { - opacity: .75; -} - -.blog pre { - max-height: 15em; - padding: .5em; - font-size: .9em; - border: 1px solid #8884; - border-radius: 2px; - overflow: scroll; - background: var(--background-alt); -} - -.blog p code { - padding: .2em .3em; - font-size: .9em; - border: 1px solid #8884; - border-radius: 2px; - background: var(--background-alt); -} - -.blog img { - max-height: 50%; - max-width: 100%; - - display: block; -} - -.blog i.end-mark { - width: 1.2em; - height: 1.2em; - margin-top: -.2em; - - display: inline-block; - transform: translateY(.2em); - - background: url("/img/aridoodle.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; -} - - - -/* COMMENTS */ - -#interactions { - margin: 1em 0; - display: flex; - flex-direction: row; - gap: .5em; - flex-wrap: wrap; -} - -.btn { - display: inline-block; - padding: .4em .6em; - border: 1px solid var(--on-background); - border-radius: 2px; - color: inherit; - text-decoration: none; - font-weight: 600; -} - -#interactions .likes, -#interactions .reposts { - padding: 0 .5em; - min-width: fit-content; - font-family: monospace; - text-align: center; - line-height: 2em; - border: 1px solid var(--on-background); - border-radius: 2px; - text-wrap: nowrap; -} - -#interactions .likes { - border-color: var(--on-background); -} -#interactions .reposts { - border-color: var(--on-background); -} - -.comment-callout { - padding-bottom: 1em; - text-align: center; - border-radius: 2px; - border-bottom: 1px solid #8884; -} - -.bluesky { - color: var(--bluesky); -} - -.mastodon { - color: var(--mastodon); -} - -.comment { - font-family: 'Inter', 'Arial', sans-serif; - font-size: 1em; -} - -.comment .comment-hover { - padding: 1em; - transition: background-color .1s; -} - -.comment .comment-hover:hover { - background-color: #8881; -} - -.comment .comment-header a { - display: flex; - gap: .5em; - font-weight: 600; - color: var(--primary); - text-decoration: none; - align-items: center; - overflow: hidden; - text-overflow: ellipsis; -} - -.comment .comment-header a .display-name { - overflow: inherit; - text-overflow: inherit; -} - -.comment .comment-header a .handle { - opacity: .5; - font-family: monospace; - font-size: .9em; - overflow: inherit; - text-overflow: inherit; -} - -.comment .comment-header img.avatar { - width: 1.5em; - height: 1.5em; -} - -.comment .comment-body { - color: inherit; - text-decoration: none; -} - -.comment p.comment-text { - margin: .5em 0; - white-space: break-spaces; -} - -.comment .comment-footer { margin: 0; font-size: .8em; - opacity: .5; + opacity: .75; } -.comment .comment-replies { - margin-left: 1em; - border-left: 2px solid #8884; +.blog-description { + margin: .5em 0 0 0; + display: -webkit-box; + font-size: .8em; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +#load-more { + margin: 0 auto; + padding: .5em 2em; + display: block; + font-family: inherit; + font-size: inherit; +} + +@media screen and (max-width: 1500px) { + #blog-sidebar { + display: none; + } } diff --git a/public/style/blogpost.css b/public/style/blogpost.css new file mode 100644 index 0000000..47145fe --- /dev/null +++ b/public/style/blogpost.css @@ -0,0 +1,314 @@ +:root { + --like: rgb(223, 104, 104); + --boost: rgb(162, 223, 73); + --bluesky: rgb(16, 131, 254); + --mastodon: rgb(86, 58, 204); +} + +main { + width: min(calc(100% - 4rem), 1200px); + margin: 0 auto 1rem auto; +} + +#blog-sidebar { + position: fixed; + width: 3em; + padding: 3em; + transform: translate(-9em, -1em); + overflow: clip; + opacity: .5; + transition: opacity .2s; +} +#blog-sidebar:hover { + opacity: 1; +} + +#blog-sidebar ul { + margin: 0; + padding: .3em; + list-style: none; + display: flex; + flex-direction: column; + gap: .3em; + border-radius: 4px; + border: 1px solid var(--on-background); + box-shadow: 4px 4px 4px #0001; +} + +#blog-sidebar a { + width: 35px; + height: 35px; + display: block; + padding: .2em; + border-radius: 2px; + text-decoration: none; +} +#blog-sidebar a:hover { + background: #0001; +} +#blog-sidebar a:active { + background: #0002; +} + +#blog-sidebar a img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + +#blog-sidebar span { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.5em; +} + +#blog-sidebar hr { + margin: 0; +} + +.blog p:hover, +.comment p:hover { + background: inherit; +} + +article.blog { + font-size: 22px; +} + +.blog h1 { + margin-bottom: 0; + font-size: 1.8em; +} + +.blog-author { + margin: .2em 0; +} +.blog .blog-author img { + width: 1.3em; + height: 1.3em; + display: inline-block; + transform: translate(0, 6px); + border-radius: 4px; +} + +.blog-date { + margin: .5em 0; + font-size: .7em; +} +.blog-modified-date { + font-style: italic; + opacity: .75; +} + +#blog-content { + font-family: 'Lora', serif; +} + +.blog p { + line-height: 1.5em; +} + +.blog sub { + opacity: .75; +} + +.blog pre { + max-height: 15em; + padding: .5em; + font-size: .9em; + border: 1px solid #8884; + border-radius: 2px; + overflow: scroll; + background: var(--background-alt); +} + +.blog p code { + padding: .2em .3em; + font-size: .9em; + border: 1px solid #8884; + border-radius: 2px; + background: var(--background-alt); +} + +.blog img { + max-height: 50%; + max-width: 100%; + + display: block; +} + +.blog i.end-mark { + width: 1.2em; + height: 1.2em; + margin-top: -.2em; + + display: inline-block; + transform: translateY(.2em); + + background: url("/img/aridoodle.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + + + +/* COMMENTS */ + +#interactions { + margin: 1em 0; + display: flex; + flex-direction: row; + gap: .5em; + flex-wrap: wrap; + align-items: center; +} + +.btn { + display: inline-block; + padding: .4em .6em; + border: 1px solid var(--on-background); + border-radius: 2px; + color: inherit; + text-decoration: none; + font-weight: 600; +} + +#interactions button { + min-width: fit-content; + padding: 0 .75em 0 .5em; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: .5em; + + font-family: monospace; + font-size: inherit; + text-align: center; + line-height: 2em; + text-wrap: nowrap; + + color: inherit; + background: none; + border: 1px solid var(--on-background); + border-radius: 4px; +} +#interactions button:hover { + background: #0001; +} +#interactions button:active { + background: #0002; +} + +#interactions img { + width: 1.5em; + height: 1.5em; + display: inline-block; +} + +.comment-callout { + margin: 0 0 0 1em; +} +.comment-callout:hover { + background: none; +} + +.bluesky { + color: var(--bluesky); +} + +.mastodon { + color: var(--mastodon); +} + +.comment { + font-family: 'Inter', 'Arial', sans-serif; + font-size: 1em; +} + +.comment .comment-hover { + padding: 1em; + transition: background-color .1s; + cursor: pointer; +} + +.comment .comment-hover:hover { + background-color: #8881; +} + +.comment .comment-header a { + display: flex; + gap: .5em; + font-weight: 600; + color: var(--primary); + text-decoration: none; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; +} + +.comment .comment-header a .display-name { + overflow: inherit; + text-overflow: inherit; +} + +.comment .comment-header a .handle { + opacity: .5; + font-family: monospace; + font-size: .9em; + overflow: inherit; + text-overflow: inherit; +} + +.comment .comment-header img.avatar { + width: 1.5em; + height: 1.5em; + border-radius: 4px; +} + +.comment .comment-body { + color: inherit; + text-decoration: none; +} + +.comment p.comment-text { + margin: .5em 0; + white-space: break-spaces; +} + +.comment .comment-footer { + margin: 0; + font-size: .8em; +} + +.comment .comment-footer .comment-footer-static { + opacity: .5; +} + +.comment .comment-replies { + margin-left: 1em; + border-left: 2px solid #8884; +} + +.comment .comment-date { + transition: opacity .2s; +} + +@media screen and (prefers-color-scheme: dark) { + #blog-sidebar a:hover { + background: #fff2; + } + #blog-sidebar a:active { + background: #fff4; + } + + .comment-date { + opacity: .5; + } +} diff --git a/public/style/main.css b/public/style/main.css index 7d73f24..0a4bc95 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -38,7 +38,7 @@ a:hover { text-decoration: underline; } -a.link-button { +.link-button { padding: .3em .5em; border: 1px solid var(--links); color: var(--links); @@ -51,7 +51,7 @@ a.link-button { opacity: 0; } -a.link-button:hover { +.link-button:hover { color: #eee; border-color: #eee; background-color: var(--links) !important; diff --git a/schema-migration/000-init.sql b/schema-migration/000-init.sql index 90385ac..42dbd63 100644 --- a/schema-migration/000-init.sql +++ b/schema-migration/000-init.sql @@ -127,17 +127,39 @@ ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIM +CREATE TABLE arimelody.blogpost ( + id TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL, + visible BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, + modified_at TIMESTAMP, + author UUID NOT NULL, + markdown TEXT NOT NULL, + html TEXT NOT NULL, + bluesky_actor TEXT, + bluesky_post TEXT +); +ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id); + + + -- -- Foreign keys -- +-- Account ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; +-- Music ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; ALTER TABLE arimelody.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE; + +-- Blog +ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_author_fk FOREIGN KEY (author) REFERENCES account(id) ON DELETE CASCADE; diff --git a/schema-migration/004-blog.sql b/schema-migration/004-blog.sql new file mode 100644 index 0000000..6e613f0 --- /dev/null +++ b/schema-migration/004-blog.sql @@ -0,0 +1,15 @@ +CREATE TABLE arimelody.blogpost ( + id TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL, + visible BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, + modified_at TIMESTAMP, + author UUID NOT NULL, + markdown TEXT NOT NULL, + html TEXT NOT NULL, + bluesky_actor TEXT, + bluesky_post TEXT +); +ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_pk PRIMARY KEY (id); +ALTER TABLE arimelody.blogpost ADD CONSTRAINT blogpost_author_fk FOREIGN KEY (author) REFERENCES account(id) ON DELETE CASCADE; diff --git a/templates/templates.go b/templates/templates.go index 7e344fb..9cfbd23 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -33,3 +33,10 @@ var BlogTemplate = template.Must(template.ParseFiles( filepath.Join("view", "prideflag.html"), filepath.Join("view", "blog.html"), )) +var BlogPostTemplate = template.Must(template.ParseFiles( + filepath.Join("view", "layout.html"), + filepath.Join("view", "header.html"), + filepath.Join("view", "footer.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("view", "blogpost.html"), +)) diff --git a/view/blog.go b/view/blog.go index 73095b0..3567818 100644 --- a/view/blog.go +++ b/view/blog.go @@ -5,7 +5,9 @@ import ( "html/template" "net/http" "os" - "time" + "slices" + "strconv" + "strings" "arimelody-web/controller" "arimelody-web/model" @@ -16,14 +18,26 @@ import ( "github.com/gomarkdown/markdown/parser" ) -type BlogView struct { - *model.Blog - Comments []*model.ThreadViewPost - Likes int - Reposts int - BlueskyURL string - MastodonURL string -} +type ( + BlogView struct { + Collections []*BlogViewPostCollection + } + + BlogViewPostCollection struct { + Name string + Posts []*BlogPostView + } + + BlogPostView struct { + *model.BlogPost + Author *model.Account + Comments []*model.ThreadViewPost + Likes int + Boosts int + BlueskyURL string + MastodonURL string + } +) var mdRenderer = html.NewRenderer(html.RendererOptions{ Flags: html.CommonFlags | html.HrefTargetBlank, @@ -31,41 +45,105 @@ var mdRenderer = html.NewRenderer(html.RendererOptions{ func BlogHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - blog := model.Blog{ - Title: "hello world!", - Description: "lorem ipsum yadda yadda something boobies babababababababa", - Visible: true, - Date: time.Now(), - AuthorID: "ari", - Markdown: -` -**i'm ari!** (she/her) 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪 + if strings.Count(r.URL.Path, "/") > 1 { + http.NotFound(w, r) + return + } -welcome to my blog! + if len(r.URL.Path) > 1 { + ServeBlogPost(app, r.URL.Path[1:]).ServeHTTP(w, r) + return + } -i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention! + dbPosts, err := controller.GetBlogPosts(app.DB, true, -1, 0) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + collections := []*BlogViewPostCollection{} + posts := []*BlogPostView{} + collectionYear := 0 + for i, post := range dbPosts { + author, err := controller.GetAccountByID(app.DB, post.AuthorID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err) + continue + } -## code block test + if i == 0 { + collectionYear = post.CreatedAt.Year() + } -~~~ c -#include + if post.CreatedAt.Year() != collectionYear || i == len(dbPosts) - 1 { + if i == len(dbPosts) - 1 { + posts = append(posts, &BlogPostView{ + BlogPost: post, + Author: author, + }) + } + postsCopy := slices.Clone(posts) + collections = append(collections, &BlogViewPostCollection{ + Name: strconv.Itoa(collectionYear), + Posts: postsCopy, + }) + posts = []*BlogPostView{} + collectionYear = post.CreatedAt.Year() + } -int main(int argc, char *argv[]) { - printf("hello world!~\n"); - return 0; -} -~~~ + posts = append(posts, &BlogPostView{ + BlogPost: post, + Author: author, + }) + } -## aridoodle - -this is `+"`"+`aridoodle`+"`"+`. please take care of her. - -![aridoodle](/img/aridoodle.png) -`, - BlueskyActorID: "did:plc:yct6cvgfipngizry5umzkxr3", - BlueskyPostID: "3llsudsx7pc2u", + err = templates.BlogTemplate.Execute(w, BlogView{ + Collections: collections, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } + }) +} + +func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + blog, err := controller.GetBlogPost(app.DB, blogPostID) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if !blog.Visible { + session, err := controller.GetSessionFromRequest(app, r) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if session == nil || session.Account == nil { + http.NotFound(w, r) + return + } + } + + author, err := controller.GetAccountByID(app.DB, blog.AuthorID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", blog.ID, err) + } // blog.Markdown += " " @@ -74,23 +152,32 @@ this is `+"`"+`aridoodle`+"`"+`. please take care of her. blog.HTML = template.HTML(markdown.Render(md, mdRenderer)) comments := []*model.ThreadViewPost{} - blueskyPost, err := controller.FetchThreadViewPost(blog.BlueskyActorID, blog.BlueskyPostID) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) - } else { - comments = append(comments, blueskyPost.Replies...) - } + likeCount := 0 + boostCount := 0 + var blueskyURL string + var blueskyPost *model.ThreadViewPost + if blog.BlueskyActorID != nil && blog.BlueskyPostID != nil { + blueskyPost, err = controller.FetchThreadViewPost(*blog.BlueskyActorID, *blog.BlueskyPostID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) + } else { + comments = append(comments, blueskyPost.Replies...) + likeCount += blueskyPost.Post.LikeCount + boostCount += blueskyPost.Post.RepostCount + blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, *blog.BlueskyPostID) + } + } - err = templates.BlogTemplate.Execute(w, BlogView{ - Blog: &blog, - Comments: blueskyPost.Replies, - Likes: blueskyPost.Post.LikeCount, - Reposts: blueskyPost.Post.RepostCount, - BlueskyURL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blog.BlueskyActorID, blog.BlueskyPostID), - MastodonURL: "#", + err = templates.BlogPostTemplate.Execute(w, BlogPostView{ + BlogPost: blog, + Author: author, + Comments: comments, + Likes: likeCount, + Boosts: boostCount, + BlueskyURL: blueskyURL, }) if err != nil { - fmt.Fprintf(os.Stderr, "Error rendering blog post: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/view/blog.html b/view/blog.html index ea10bbe..d1bf56d 100644 --- a/view/blog.html +++ b/view/blog.html @@ -1,15 +1,15 @@ {{define "head"}} -{{.Title}} - ari melody 💫 +blog - ari melody 💫 - + - + - - + + - + @@ -18,138 +18,34 @@ {{define "content"}}
-
- -
-
-
-

{{.Title}}

-

Posted by {{.AuthorID}} @ {{.PrintDate}}

+

# blog

+

thoughts from your local SPACEGIRL 💫

-
- {{.HTML}} +
-
- - {{if ne .BlueskyURL ""}} -
- -
- - 🔁 {{.Reposts}} -
- -

- join the conversation on - Bluesky 🦋 - - -

- -
- {{range .Comments}} - {{template "comment" .}} - {{end}} -
+
+ {{if eq (len .Collections) 0}} +

there are no posts! 🍃

+ {{end}} + {{range .Collections}} +

{{.Name}}

+ {{range .Posts}} +
+

{{.Title}}

+

+ {{.Author.Username}}'s avatar {{.Author.Username}} + • {{.PrintDate}} +

+ {{if ne .Description ""}} +

{{.Description}}

+ {{end}} +
+ {{end}} {{end}} - -
+ + + +
{{end}} - -{{define "comment"}} - -{{end}} - diff --git a/view/blogpost.html b/view/blogpost.html new file mode 100644 index 0000000..6126eda --- /dev/null +++ b/view/blogpost.html @@ -0,0 +1,178 @@ +{{define "head"}} +{{.Title}} - ari melody 💫 + + + + + + + + + + + + + + +{{end}} + +{{define "content"}} +
+
+ +
+
+
+
+

# {{.Title}}

+

by {{.Author.Username}} {{.Author.Username}}'s avatar

+

posted {{.PrintDate}}{{if .ModifiedAt.Valid}} • updated {{.PrintModifiedDate}}{{end}}

+
+ +
+ +
+ {{.HTML}} +
+
+ + {{if ne .BlueskyURL ""}} +
+ +
+ + + +

+ join the conversation on + Bluesky 🦋 + + +

+
+ +
+ {{range .Comments}} + {{template "comment" .}} + {{end}} +
+ {{end}} + + +
+
+{{end}} + +{{define "comment"}} + +{{end}} diff --git a/view/header.html b/view/header.html index 03a8384..2457197 100644 --- a/view/header.html +++ b/view/header.html @@ -21,11 +21,11 @@ home
  • - music + music
  • - blog + blog
  • From ddbf3444eb03a28ce91c7f080d71556745c19edc Mon Sep 17 00:00:00 2001 From: ari melody Date: Sat, 5 Jul 2025 15:11:51 +0100 Subject: [PATCH 10/13] blog: reading mode fixes, add highight.js for codeblocks --- public/script/blogpost.js | 4 + public/style/blogpost.css | 46 +- public/vendor/highlight/.gitignore | 6 + public/vendor/highlight/LICENSE | 29 + public/vendor/highlight/README.md | 45 + public/vendor/highlight/highlight.min.js | 912 ++++++++++++++++++ public/vendor/highlight/languages/bash.min.js | 21 + public/vendor/highlight/languages/c.min.js | 41 + public/vendor/highlight/languages/cpp.min.js | 47 + public/vendor/highlight/languages/css.min.js | 31 + public/vendor/highlight/languages/go.min.js | 20 + public/vendor/highlight/languages/http.min.js | 14 + public/vendor/highlight/languages/ini.min.js | 15 + public/vendor/highlight/languages/java.min.js | 38 + .../highlight/languages/javascript.min.js | 81 ++ public/vendor/highlight/languages/json.min.js | 8 + public/vendor/highlight/languages/lua.min.js | 15 + .../highlight/languages/markdown.min.js | 32 + .../vendor/highlight/languages/nginx.min.js | 21 + .../vendor/highlight/languages/pgsql.min.js | 69 ++ .../vendor/highlight/languages/python.min.js | 42 + public/vendor/highlight/languages/rust.min.js | 27 + .../highlight/languages/typescript.min.js | 99 ++ public/vendor/highlight/styles/.gitignore | 5 + public/vendor/highlight/styles/ari.css | 109 +++ view/blogpost.html | 25 +- 26 files changed, 1777 insertions(+), 25 deletions(-) create mode 100644 public/vendor/highlight/.gitignore create mode 100644 public/vendor/highlight/LICENSE create mode 100644 public/vendor/highlight/README.md create mode 100644 public/vendor/highlight/highlight.min.js create mode 100644 public/vendor/highlight/languages/bash.min.js create mode 100644 public/vendor/highlight/languages/c.min.js create mode 100644 public/vendor/highlight/languages/cpp.min.js create mode 100644 public/vendor/highlight/languages/css.min.js create mode 100644 public/vendor/highlight/languages/go.min.js create mode 100644 public/vendor/highlight/languages/http.min.js create mode 100644 public/vendor/highlight/languages/ini.min.js create mode 100644 public/vendor/highlight/languages/java.min.js create mode 100644 public/vendor/highlight/languages/javascript.min.js create mode 100644 public/vendor/highlight/languages/json.min.js create mode 100644 public/vendor/highlight/languages/lua.min.js create mode 100644 public/vendor/highlight/languages/markdown.min.js create mode 100644 public/vendor/highlight/languages/nginx.min.js create mode 100644 public/vendor/highlight/languages/pgsql.min.js create mode 100644 public/vendor/highlight/languages/python.min.js create mode 100644 public/vendor/highlight/languages/rust.min.js create mode 100644 public/vendor/highlight/languages/typescript.min.js create mode 100644 public/vendor/highlight/styles/.gitignore create mode 100644 public/vendor/highlight/styles/ari.css diff --git a/public/script/blogpost.js b/public/script/blogpost.js index fac626e..dbe44ff 100644 --- a/public/script/blogpost.js +++ b/public/script/blogpost.js @@ -6,6 +6,7 @@ document.querySelectorAll('.comment-hover').forEach((/** @type {HTMLDivElement} hijackClickEvent(comment, commentDate); }); +/* document.getElementById('blog-copy-link').addEventListener('click', event => { event.preventDefault(); if (navigator.clipboard === undefined) { @@ -14,3 +15,6 @@ document.getElementById('blog-copy-link').addEventListener('click', event => { } navigator.clipboard.writeText(location.protocol + "//" + location.host + location.pathname); }); +*/ + +hljs.highlightAll(); diff --git a/public/style/blogpost.css b/public/style/blogpost.css index 47145fe..3a09a7a 100644 --- a/public/style/blogpost.css +++ b/public/style/blogpost.css @@ -70,16 +70,16 @@ main { margin: 0; } -.blog p:hover, +article#blog p:hover, .comment p:hover { background: inherit; } -article.blog { +article#blog { font-size: 22px; } -.blog h1 { +article#blog h1 { margin-bottom: 0; font-size: 1.8em; } @@ -87,7 +87,7 @@ article.blog { .blog-author { margin: .2em 0; } -.blog .blog-author img { +article#blog .blog-author img { width: 1.3em; height: 1.3em; display: inline-block; @@ -104,19 +104,28 @@ article.blog { opacity: .75; } -#blog-content { +article#blog { font-family: 'Lora', serif; } -.blog p { +article#blog header { + position: relative; + width: auto; + font-family: 'Monaspace Argon', monospace; + border: none; + background: none; + z-index: 0; +} + +article#blog p { line-height: 1.5em; } -.blog sub { +article#blog sub { opacity: .75; } -.blog pre { +article#blog pre { max-height: 15em; padding: .5em; font-size: .9em; @@ -126,7 +135,7 @@ article.blog { background: var(--background-alt); } -.blog p code { +article#blog p code { padding: .2em .3em; font-size: .9em; border: 1px solid #8884; @@ -134,14 +143,20 @@ article.blog { background: var(--background-alt); } -.blog img { +article#blog blockquote { + margin: 1em 0; + padding: 0 0 0 1em; + border-left: .2em solid #8888; +} + +article#blog img { max-height: 50%; max-width: 100%; display: block; } -.blog i.end-mark { +article#blog i.end-mark { width: 1.2em; height: 1.2em; margin-top: -.2em; @@ -178,7 +193,7 @@ article.blog { font-weight: 600; } -#interactions button { +#interactions .button { min-width: fit-content; padding: 0 .75em 0 .5em; @@ -198,11 +213,14 @@ article.blog { background: none; border: 1px solid var(--on-background); border-radius: 4px; + + cursor: pointer; + text-decoration: none; } -#interactions button:hover { +#interactions .button:hover { background: #0001; } -#interactions button:active { +#interactions .button:active { background: #0002; } diff --git a/public/vendor/highlight/.gitignore b/public/vendor/highlight/.gitignore new file mode 100644 index 0000000..6f30158 --- /dev/null +++ b/public/vendor/highlight/.gitignore @@ -0,0 +1,6 @@ +es +package.json +DIGESTS.md +highlight.js +languages/*.js +!languages/*.min.js diff --git a/public/vendor/highlight/LICENSE b/public/vendor/highlight/LICENSE new file mode 100644 index 0000000..2250cc7 --- /dev/null +++ b/public/vendor/highlight/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2006, Ivan Sagalaev. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/vendor/highlight/README.md b/public/vendor/highlight/README.md new file mode 100644 index 0000000..30d84b9 --- /dev/null +++ b/public/vendor/highlight/README.md @@ -0,0 +1,45 @@ +# Highlight.js CDN Assets + +[![install size](https://packagephobia.now.sh/badge?p=highlight.js)](https://packagephobia.now.sh/result?p=highlight.js) + +**This package contains only the CDN build assets of highlight.js.** + +This may be what you want if you'd like to install the pre-built distributable highlight.js client-side assets via NPM. If you're wanting to use highlight.js mainly on the server-side you likely want the [highlight.js][1] package instead. + +To access these files via CDN:
    +https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/ + +**If you just want a single .js file with the common languages built-in: +** + +--- + +## Highlight.js + +Highlight.js is a syntax highlighter written in JavaScript. It works in +the browser as well as on the server. It works with pretty much any +markup, doesn’t depend on any framework, and has automatic language +detection. + +If you'd like to read the full README:
    + + +## License + +Highlight.js is released under the BSD License. See [LICENSE][7] file +for details. + +## Links + +The official site for the library is at . + +The Github project may be found at: + +Further in-depth documentation for the API and other topics is at +. + +A list of the Core Team and contributors can be found in the [CONTRIBUTORS.md][8] file. + +[1]: https://www.npmjs.com/package/highlight.js +[7]: https://github.com/highlightjs/highlight.js/blob/main/LICENSE +[8]: https://github.com/highlightjs/highlight.js/blob/main/CONTRIBUTORS.md diff --git a/public/vendor/highlight/highlight.min.js b/public/vendor/highlight/highlight.min.js new file mode 100644 index 0000000..ba781b1 --- /dev/null +++ b/public/vendor/highlight/highlight.min.js @@ -0,0 +1,912 @@ +/*! + Highlight.js v11.11.1 (git: 08cb242e7d) + (c) 2006-2025 Josh Goebel and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";function e(t){ +return t instanceof Map?t.clear=t.delete=t.set=()=>{ +throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ +const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) +})),t}class t{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope +;class r{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ +if(e.startsWith("language:"))return e.replace("language:","language-") +;if(e.includes(".")){const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} +closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const o=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class a{constructor(){ +this.rootNode=o(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=o({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} +addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ +this.closeNode()}__addSublanguage(e,t){const n=e.root +;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ +return new r(this,this.options).value()}finalize(){ +return this.closeAllNodes(),!0}}function l(e){ +return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} +function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} +function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} +function p(e){return RegExp(e.toString()+"|").exec("").length-1} +const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} +s+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], +"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} +const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",O={ +begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, +contains:[]},n);s.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return s.contains.push({begin:h(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s +},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({ +__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ +scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:N, +C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},C_NUMBER_RE:_,END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, +MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, +PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, +end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function L(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function P(e,t){ +void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},H=["of","and","for","in","not","or","if","then","parent","list","value"] +;function C(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,C(e[n],t,n))})),i;function s(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,$(n[0],n[1])]}))}}function $(e,t){ +return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const U={},z=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +U[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),U[`${e}/${t}`]=!0) +},G=Error();function K(e,t,{key:n}){let i=0;const s=e[n],r={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=s[e],r[e+i]=!0,i+=p(t[e-1]) +;e[n]=o,e[n]._emit=r,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw z("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw z("beginScope must be object"), +G;K(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw z("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw z("endScope must be object"), +G;K(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function Z(e){ +function t(t,n){ +return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=i(e.classNameAliases||{}),function n(r,o){const a=r +;if(r.isCompiled)return a +;[I,B,F,D].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))), +r.__beforeBegin=null,[T,L,P].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +c=r.keywords.$pattern, +delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=C(r.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(a.endRe=t(a.end)), +a.terminatorEnd=l(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)), +r.illegal&&(a.illegalRe=t(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:V(e)?i(e,{ +starts:e.starts?i(e.starts):null +}):Object.isFrozen(e)?i(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a) +})),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new s +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function V(e){ +return!!e&&(e.endsWithParent||V(e.starts))}class q extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const J=n,Y=i,Q=Symbol("nomatch"),ee=n=>{ +const i=Object.create(null),s=Object.create(null),r=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ +disableAutodetect:!0,name:"Plain text",contains:[]};let p={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:c};function b(e){ +return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,s=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +s=e,i=t),void 0===n&&(n=!0);const r={code:i,language:s};N("before:highlight",r) +;const o=r.result?r.result:E(r.language,r.code,n) +;return o.code=r.code,N("after:highlight",o),o}function E(e,n,s,r){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" +;for(;t;){n+=R.substring(e,t.index) +;const s=w.case_insensitive?t[0].toLowerCase():t[0],r=(i=s,N.keywords[i]);if(r){ +const[e,i]=r +;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{ +const n=w.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i +;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ +if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(R) +;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top +}else e=x(R,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language) +})():l(),R=""}function u(e,t){ +""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 +;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} +const i=w.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} +function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(w.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(u(R,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ +value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) +;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ +return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ +const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return Q;const r=N +;N.endScope&&N.endScope._wrap?(g(), +u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), +d(N.endScope,e)):r.skip?R+=t:(r.returnEnd||r.excludeEnd||(R+=t), +g(),r.excludeEnd&&(R=t));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent +}while(N!==s.parent);return s.starts&&h(s.starts,e),r.returnEnd?0:t.length} +let y={};function _(i,r){const a=r&&r[0];if(R+=i,null==a)return g(),0 +;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===a){ +if(R+=n.slice(r.index,r.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=y.rule,t}return 1} +if(y=r,"begin"===r.type)return(e=>{ +const n=e[0],i=e.rule,s=new t(i),r=[i.__beforeBegin,i["on:begin"]] +;for(const t of r)if(t&&(t(e,s),s.isMatchIgnored))return b(n) +;return i.skip?R+=n:(i.excludeBegin&&(R+=n), +g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(r) +;if("illegal"===r.type&&!s){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') +;throw e.mode=N,e}if("end"===r.type){const e=m(r);if(e!==Q)return e} +if("illegal"===r.type&&""===a)return R+="\n",1 +;if(I>1e5&&I>3*r.index)throw Error("potential infinite loop, way more iterations than matches") +;return R+=a,a.length}const w=O(e) +;if(!w)throw z(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const v=Z(w);let k="",N=r||v;const S={},M=new p.__emitter(p);(()=>{const e=[] +;for(let t=N;t!==w;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{ +if(w.__emitTokens)w.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ +I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A +;const e=N.matcher.exec(n);if(!e)break;const t=_(n.substring(A,e.index),e) +;A=e.index+t}_(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e, +value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){ +if(t.message&&t.message.includes("Illegal"))return{language:e,value:J(n), +illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A, +context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(o)return{ +language:e,value:J(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} +;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ +const t={value:J(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} +;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1))) +;s.unshift(n);const r=s.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=r,c=o +;return c.secondBest=a,c}function y(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(N("before:highlightElement",{el:e,language:n +}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) +;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),p.throwUnescapedHTML))throw new q("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,r=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,r.language),e.result={language:r.language,re:r.relevance, +relevance:r.relevance},r.secondBest&&(e.secondBest={ +language:r.secondBest.language,relevance:r.secondBest.relevance +}),N("after:highlightElement",{el:e,result:r,text:i})}let _=!1;function w(){ +if("loading"===document.readyState)return _||window.addEventListener("DOMContentLoaded",(()=>{ +w()}),!1),void(_=!0);document.querySelectorAll(p.cssSelector).forEach(y)} +function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +s[e.toLowerCase()]=t}))}function k(e){const t=O(e) +;return t&&!t.disableAutodetect}function N(e,t){const n=e;r.forEach((e=>{ +e[n]&&e[n](t)}))}Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:w, +highlightElement:y, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, +initHighlighting:()=>{ +w(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +w(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ +if(z("Language definition for '{}' could not be registered.".replace("{}",e)), +!o)throw t;z(t),s=l} +s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),r.push(e)}, +removePlugin:e=>{const t=r.indexOf(e);-1!==t&&r.splice(t,1)}}),n.debugMode=()=>{ +o=!1},n.safeMode=()=>{o=!0},n.versionString="11.11.1",n.regex={concat:h, +lookahead:g,either:f,optional:d,anyNumberOfTimes:u} +;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n +},te=ee({});return te.newInstance=()=>ee({}),te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `bash` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/, +end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{ +className:"variable",variants:[{ +begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE] +},i=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),c={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(o);const r={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),m={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","time","for","while","until","in","do","done","case","esac","coproc","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),m,r,i,c,{match:/(\/[a-z._-]+)+/},o,{match:/\\"/},{ +className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}})() +;hljs.registerLanguage("bash",e)})();/*! `c` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,n=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",s="[a-zA-Z_]\\w*::",r="("+a+"|"+t.optional(s)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",i={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},l={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{match:/\b(0b[01']+)/},{ +match:/(-?)\b([\d']+(\.[\d']*)?|\.[\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)/ +},{ +match:/(-?)\b(0[xX][a-fA-F0-9]+(?:'[a-fA-F0-9]+)*(?:\.[a-fA-F0-9]*(?:'[a-fA-F0-9]*)*)?(?:[pP][-+]?[0-9]+)?(l|L)?(u|U)?)/ +},{match:/(-?)\b\d+(?:'\d+)*(?:\.\d*(?:'\d*)*)?(?:[eE][-+]?\d+)?/}],relevance:0 +},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef elifdef elifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(l,{className:"string"}),{ +className:"string",begin:/<.*?>/},n,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:t.optional(s)+e.IDENT_RE,relevance:0 +},_=t.optional(s)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","typeof","typeof_unqual","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_BitInt","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal96","_Decimal128","_Decimal64x","_Decimal128x","_Float16","_Float32","_Float64","_Float128","_Float32x","_Float64x","_Float128x","const","static","constexpr","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},m=[c,i,n,e.C_BLOCK_COMMENT_MODE,o,l],g={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+_,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:_,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[n,e.C_BLOCK_COMMENT_MODE,l,o,i,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",n,e.C_BLOCK_COMMENT_MODE,l,o,i] +}]},i,n,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:l,keywords:u}}}})();hljs.registerLanguage("c",e)})();/*! `cpp` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,a=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),n="decltype\\(auto\\)",r="[a-zA-Z_]\\w*::",i="(?!struct)("+n+"|"+t.optional(r)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},c={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{ +begin:"[+-]?(?:(?:[0-9](?:'?[0-9])*\\.(?:[0-9](?:'?[0-9])*)?|\\.[0-9](?:'?[0-9])*)(?:[Ee][+-]?[0-9](?:'?[0-9])*)?|[0-9](?:'?[0-9])*[Ee][+-]?[0-9](?:'?[0-9])*|0[Xx](?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*(?:\\.(?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)?)?|\\.[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)[Pp][+-]?[0-9](?:'?[0-9])*)(?:[Ff](?:16|32|64|128)?|(BF|bf)16|[Ll]|)" +},{ +begin:"[+-]?\\b(?:0[Bb][01](?:'?[01])*|0[Xx][0-9A-Fa-f](?:'?[0-9A-Fa-f])*|0(?:'?[0-7])*|[1-9](?:'?[0-9])*)(?:[Uu](?:LL?|ll?)|[Uu][Zz]?|(?:LL?|ll?)[Uu]?|[Zz][Uu]|)" +}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(c,{className:"string"}),{ +className:"string",begin:/<.*?>/},a,e.C_BLOCK_COMMENT_MODE]},u={ +className:"title",begin:t.optional(r)+e.IDENT_RE,relevance:0 +},d=t.optional(r)+e.IDENT_RE+"\\s*\\(",p={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","flat_map","flat_set","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},_={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:t.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,t.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[_,l,s,a,e.C_BLOCK_COMMENT_MODE,o,c],f={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:p,contains:m.concat([{begin:/\(/,end:/\)/,keywords:p, +contains:m.concat(["self"]),relevance:0}]),relevance:0},g={className:"function", +begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:p,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:n,keywords:p,relevance:0},{ +begin:d,returnBegin:!0,contains:[u],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[c,o]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:p,relevance:0, +contains:[a,e.C_BLOCK_COMMENT_MODE,c,o,s,{begin:/\(/,end:/\)/,keywords:p, +relevance:0,contains:["self",a,e.C_BLOCK_COMMENT_MODE,c,o,s]}] +},s,a,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:p,illegal:"",keywords:p,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:p},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}}})();hljs.registerLanguage("cpp",e) +})();/*! `css` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video","defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),t=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),r=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-align","box-decoration-break","box-direction","box-flex","box-flex-group","box-lines","box-ordinal-group","box-orient","box-pack","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","color-scheme","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","contain-intrinsic-block-size","contain-intrinsic-height","contain-intrinsic-inline-size","contain-intrinsic-size","contain-intrinsic-width","container","container-name","container-type","content","content-visibility","counter-increment","counter-reset","counter-set","cue","cue-after","cue-before","cursor","cx","cy","direction","display","dominant-baseline","empty-cells","enable-background","field-sizing","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flood-color","flood-opacity","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-optical-sizing","font-palette","font-size","font-size-adjust","font-smooth","font-smoothing","font-stretch","font-style","font-synthesis","font-synthesis-position","font-synthesis-small-caps","font-synthesis-style","font-synthesis-weight","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-emoji","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","forced-color-adjust","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphenate-character","hyphenate-limit-chars","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","initial-letter","initial-letter-align","inline-size","inset","inset-area","inset-block","inset-block-end","inset-block-start","inset-inline","inset-inline-end","inset-inline-start","isolation","justify-content","justify-items","justify-self","kerning","left","letter-spacing","lighting-color","line-break","line-height","line-height-step","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","margin-trim","marker","marker-end","marker-mid","marker-start","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","masonry-auto-flow","math-depth","math-shift","math-style","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","offset","offset-anchor","offset-distance","offset-path","offset-position","offset-rotate","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-anchor","overflow-block","overflow-clip-margin","overflow-inline","overflow-wrap","overflow-x","overflow-y","overlay","overscroll-behavior","overscroll-behavior-block","overscroll-behavior-inline","overscroll-behavior-x","overscroll-behavior-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","paint-order","pause","pause-after","pause-before","perspective","perspective-origin","place-content","place-items","place-self","pointer-events","position","position-anchor","position-visibility","print-color-adjust","quotes","r","resize","rest","rest-after","rest-before","right","rotate","row-gap","ruby-align","ruby-position","scale","scroll-behavior","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scroll-timeline","scroll-timeline-axis","scroll-timeline-name","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","speak","speak-as","src","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","tab-size","table-layout","text-align","text-align-all","text-align-last","text-anchor","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-skip-ink","text-decoration-style","text-decoration-thickness","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-size-adjust","text-transform","text-underline-offset","text-underline-position","text-wrap","text-wrap-mode","text-wrap-style","timeline-scope","top","touch-action","transform","transform-box","transform-origin","transform-style","transition","transition-behavior","transition-delay","transition-duration","transition-property","transition-timing-function","translate","unicode-bidi","user-modify","user-select","vector-effect","vertical-align","view-timeline","view-timeline-axis","view-timeline-inset","view-timeline-name","view-transition-name","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","white-space-collapse","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index","zoom"].sort().reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+t.join("|")+")"},{begin:":(:)?("+o.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+r.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})();/*! `go` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const a={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:a,illegal:"{var e=(()=>{"use strict";return e=>{const n="HTTP/([32]|1\\.[01])",a={ +className:"attribute", +begin:e.regex.concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{ +contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$", +relevance:0}}]}},s=[a,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0} +}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{ +begin:"^(?="+n+" \\d{3})",end:/$/,contains:[{className:"meta",begin:n},{ +className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/, +contains:s}},{begin:"(?=^[A-Z]+ (.*?) "+n+"$)",end:/$/,contains:[{ +className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{ +className:"meta",begin:n},{className:"keyword",begin:"[A-Z]+"}],starts:{ +end:/\b\B/,illegal:/\S/,contains:s}},e.inherit(a,{relevance:0})]}}})() +;hljs.registerLanguage("http",e)})();/*! `ini` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={className:"number", +relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{begin:e.NUMBER_RE}] +},s=e.COMMENT();s.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];const i={ +className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/ +}]},t={className:"literal",begin:/\bon|off|true|false|yes|no\b/},r={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''", +end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"' +},{begin:"'",end:"'"}]},l={begin:/\[/,end:/\]/,contains:[s,t,i,r,a,"self"], +relevance:0},c=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[s,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(c,"(\\s*\\.\\s*",c,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[s,l,t,i,r,a]}}]}}})() +;hljs.registerLanguage("ini",e)})();/*! `java` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;var e="[0-9](_*[0-9])*",a=`\\.(${e})`,n="[0-9a-fA-F](_*[0-9a-fA-F])*",s={ +className:"number",variants:[{ +begin:`(\\b(${e})((${a})|\\.)?|(${a}))[eE][+-]?(${e})[fFdD]?\\b`},{ +begin:`\\b(${e})((${a})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${a})[fFdD]?\\b` +},{begin:`\\b(${e})[fFdD]\\b`},{ +begin:`\\b0[xX]((${n})\\.?|(${n})?\\.(${n}))[pP][+-]?(${e})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${n})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function t(e,a,n){return-1===n?"":e.replace(a,(s=>t(e,a,n-1)))} +return e=>{ +const a=e.regex,n="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",i=n+t("(?:<"+n+"~~~(?:\\s*,\\s*"+n+"~~~)*>)?",/~~~/g,2),r={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits","goto","when"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},l={className:"meta",begin:"@"+n,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},c={className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:r,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,n],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[a.concat(/(?!else)/,n),/\s+/,n,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,n],className:{1:"keyword", +3:"title.class"},contains:[c,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+i+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:r,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0, +contains:[l,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,s,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},s,l]}}})() +;hljs.registerLanguage("java",e)})();/*! `javascript` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="e+"\\s*\\(")), +l.concat("(?!",T.join("|"),")")),d,l.lookahead(/\s*\(/)), +className:"title.function",relevance:0};var T;const C={ +begin:l.concat(/\./,l.lookahead(l.concat(d,/(?![0-9A-Za-z$_(])/))),end:d, +excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},M={ +match:[/get|set/,/\s+/,d,/(?=\()/],className:{1:"keyword",3:"title.function"}, +contains:[{begin:/\(\)/},R] +},B="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+o.UNDERSCORE_IDENT_RE+")\\s*=>",$={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:k},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,h,_,N,f,p,{match:/\$\d+/},A,k,{ +scope:"attr",match:d+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[p,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0 +},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},I,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},C,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},x,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},O,M,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `json` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],s={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",aliases:["jsonc"], +keywords:{literal:a},contains:[{className:"attr", +begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/, +className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,s,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `lua` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t="\\[=*\\[",a="\\]=*\\]",n={ +begin:t,end:a,contains:["self"] +},o=[e.COMMENT("--(?!"+t+")","$"),e.COMMENT("--"+t,a,{contains:[n],relevance:10 +})];return{name:"Lua",aliases:["pluto"],keywords:{ +$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:o.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:o}].concat(o) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:t,end:a,contains:[n],relevance:5}])}}})();hljs.registerLanguage("lua",e) +})();/*! `markdown` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={begin:/<\/?[A-Za-z_]/, +end:">",subLanguage:"xml",relevance:0},a={variants:[{begin:/\[.+?\]\[.*?\]/, +relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},i={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},s={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},c=e.inherit(i,{contains:[] +}),t=e.inherit(s,{contains:[]});i.contains.push(t),s.contains.push(c) +;let g=[n,a];return[i,s,c,t].forEach((e=>{e.contains=e.contains.concat(g) +})),g=g.concat(i,s),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:g},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:g}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:g, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]},{scope:"literal", +match:/&([a-zA-Z0-9]+|#[0-9]{1,7}|#[Xx][0-9a-fA-F]{1,6});/}]}}})() +;hljs.registerLanguage("markdown",e)})();/*! `nginx` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={ +className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{\w+\}/},{ +begin:n.concat(/[$@]/,e.UNDERSCORE_IDENT_RE)}]},s={endsWithParent:!0,keywords:{ +$pattern:/[a-z_]{2,}|\/dev\/poll/, +literal:["on","off","yes","no","true","false","none","blocked","debug","info","notice","warn","error","crit","select","break","last","permanent","redirect","kqueue","rtsig","epoll","poll","/dev/poll"] +},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string", +contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/ +}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[a] +},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:"\\s\\^", +end:"\\s|\\{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|\\{|;",returnEnd:!0},{ +begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number", +begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{ +className:"number",begin:"\\b\\d+[kKmMgGdshdwy]?\\b",relevance:0},a]};return{ +name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{ +beginKeywords:"upstream location",end:/;|\{/,contains:s.contains,keywords:{ +section:"upstream location"}},{className:"section", +begin:n.concat(e.UNDERSCORE_IDENT_RE+n.lookahead(/\s+\{/)),relevance:0},{ +begin:n.lookahead(e.UNDERSCORE_IDENT_RE+"\\s"),end:";|\\{",contains:[{ +className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:s}],relevance:0}], +illegal:"[^\\s\\}\\{]"}}})();hljs.registerLanguage("nginx",e)})();/*! `pgsql` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var E=(()=>{"use strict";return E=>{ +const T=E.COMMENT("--","$"),N="\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",A="BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ",R=A.trim().split(" ").map((E=>E.split("|")[0])).join("|"),I="ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP PERCENTILE_CONT PERCENTILE_DISC ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE NUM_NONNULLS NUM_NULLS ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT TRUNC WIDTH_BUCKET RANDOM SETSEED ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAP LEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR TO_ASCII TO_HEX TRANSLATE OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 TIMEOFDAY TRANSACTION_TIMESTAMP|10 ENUM_FIRST ENUM_LAST ENUM_RANGE AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILY INET_MERGE MACADDR8_SET7BIT ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA CURSOR_TO_XML CURSOR_TO_XMLSCHEMA SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA XMLATTRIBUTES TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY CURRVAL LASTVAL NEXTVAL SETVAL COALESCE NULLIF GREATEST LEAST ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY STRING_TO_ARRAY UNNEST ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE GENERATE_SERIES GENERATE_SUBSCRIPTS CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE GIN_CLEAN_PENDING_LIST SUPPRESS_REDUNDANT_UPDATES_TRIGGER LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE GROUPING CAST".split(" ").map((E=>E.split("|")[0])).join("|") +;return{name:"PostgreSQL",aliases:["postgres","postgresql"],supersetOf:"sql", +case_insensitive:!0,keywords:{ +keyword:"ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION INDEX PROCEDURE ASSERTION ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS DEFERRABLE RANGE DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED OF NOTHING NONE EXCLUDE ATTRIBUTE USAGE ROUTINES TRUE FALSE NAN INFINITY ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT OPEN SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS ", +built_in:"CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 SQLSTATE SQLERRM|10 SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED INDEX_CORRUPTED " +},illegal:/:==|\W\s*\(\*|(^|\s)\$[a-z]|\{\{|[a-z]:\s*$|\.\.\.|TO:|DO:/, +contains:[{className:"keyword",variants:[{begin:/\bTEXT\s*SEARCH\b/},{ +begin:/\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/},{ +begin:/\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/},{ +begin:/\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/},{ +begin:/\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/},{begin:/\bNULLS\s+(FIRST|LAST)\b/},{ +begin:/\bEVENT\s+TRIGGER\b/},{begin:/\b(MAPPING|OR)\s+REPLACE\b/},{ +begin:/\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/},{ +begin:/\b(SHARE|EXCLUSIVE)\s+MODE\b/},{ +begin:/\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/},{ +begin:/\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/ +},{begin:/\bPRESERVE\s+ROWS\b/},{begin:/\bDISCARD\s+PLANS\b/},{ +begin:/\bREFERENCING\s+(OLD|NEW)\b/},{begin:/\bSKIP\s+LOCKED\b/},{ +begin:/\bGROUPING\s+SETS\b/},{ +begin:/\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/},{ +begin:/\b(WITH|WITHOUT)\s+HOLD\b/},{ +begin:/\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/},{ +begin:/\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/},{ +begin:/\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/},{ +begin:/\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/},{begin:/\bIS\s+(NOT\s+)?UNKNOWN\b/ +},{begin:/\bSECURITY\s+LABEL\b/},{begin:/\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/ +},{begin:/\bWITH\s+(NO\s+)?DATA\b/},{begin:/\b(FOREIGN|SET)\s+DATA\b/},{ +begin:/\bSET\s+(CATALOG|CONSTRAINTS)\b/},{begin:/\b(WITH|FOR)\s+ORDINALITY\b/},{ +begin:/\bIS\s+(NOT\s+)?DOCUMENT\b/},{ +begin:/\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/},{ +begin:/\b(STRIP|PRESERVE)\s+WHITESPACE\b/},{ +begin:/\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/},{ +begin:/\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/},{begin:/\bAT\s+TIME\s+ZONE\b/},{ +begin:/\bGRANTED\s+BY\b/},{begin:/\bRETURN\s+(QUERY|NEXT)\b/},{ +begin:/\b(ATTACH|DETACH)\s+PARTITION\b/},{ +begin:/\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/},{ +begin:/\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/ +},{begin:/\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/}]},{ +begin:/\b(FORMAT|FAMILY|VERSION)\s*\(/},{begin:/\bINCLUDE\s*\(/, +keywords:"INCLUDE"},{begin:/\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/ +},{ +begin:/\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/ +},{begin:/\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/,relevance:10},{ +begin:/\bEXTRACT\s*\(/,end:/\bFROM\b/,returnEnd:!0,keywords:{ +type:"CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR TIMEZONE_MINUTE WEEK YEAR" +}},{begin:/\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/,keywords:{keyword:"NAME"}},{ +begin:/\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/,keywords:{ +keyword:"DOCUMENT CONTENT"}},{beginKeywords:"CACHE INCREMENT MAXVALUE MINVALUE", +end:E.C_NUMBER_RE,returnEnd:!0,keywords:"BY CACHE INCREMENT MAXVALUE MINVALUE" +},{className:"type",begin:/\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/},{className:"type", +begin:/\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/ +},{ +begin:/\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/, +keywords:{keyword:"RETURNS", +type:"LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER" +}},{begin:"\\b("+I+")\\s*\\("},{begin:"\\.("+R+")\\b"},{ +begin:"\\b("+R+")\\s+PATH\\b",keywords:{keyword:"PATH", +type:A.replace("PATH ","")}},{className:"type",begin:"\\b("+R+")\\b"},{ +className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{ +className:"string",begin:"(e|E|u&|U&)'",end:"'",contains:[{begin:"\\\\."}], +relevance:10},E.END_SAME_AS_BEGIN({begin:N,end:N,contains:[{ +subLanguage:["pgsql","perl","python","tcl","r","lua","java","php","ruby","bash","scheme","xml","json"], +endsWithParent:!0}]}),{begin:'"',end:'"',contains:[{begin:'""'}] +},E.C_NUMBER_MODE,E.C_BLOCK_COMMENT_MODE,T,{className:"meta",variants:[{ +begin:"%(ROW)?TYPE",relevance:10},{begin:"\\$\\d+"},{begin:"^#\\w",end:"$"}]},{ +className:"symbol",begin:"<<\\s*[a-zA-Z_][a-zA-Z_0-9$]*\\s*>>",relevance:10}]}} +})();hljs.registerLanguage("pgsql",E)})();/*! `python` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a=/[\p{XID_Start}_]\p{XID_Continue}*/u,s=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],t={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:s, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},i={className:"meta",begin:/^(>>>|\.\.\.) /},r={className:"subst",begin:/\{/, +end:/\}/,keywords:t,illegal:/#/},l={begin:/\{\{/,relevance:0},o={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,l,r]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},b="[0-9](_?[0-9])*",c=`(\\b(${b}))?\\.(${b})|\\b(${b})\\.`,d="\\b|"+s.join("|"),g={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${b})|(${c}))[eE][+-]?(${b})[jJ]?(?=${d})`},{begin:`(${c})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${d})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${d})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${d})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${d})`},{begin:`\\b(${b})[jJ](?=${d})` +}]},p={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:t, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t, +contains:["self",i,g,o,e.HASH_COMMENT_MODE]}]};return r.contains=[o,g,i],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:t, +illegal:/(<\/|\?)|=>/,contains:[i,g,{scope:"variable.language",match:/\bself\b/ +},{beginKeywords:"if",relevance:0},{match:/\bor\b/,scope:"keyword" +},o,p,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,a],scope:{1:"keyword", +3:"title.function"},contains:[m]},{variants:[{ +match:[/\bclass/,/\s+/,a,/\s*/,/\(\s*/,a,/\s*\)/]},{match:[/\bclass/,/\s+/,a]}], +scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ +className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[g,m,o]}]}}})() +;hljs.registerLanguage("python",e)})();/*! `rust` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const t=e.regex,n=/(r#)?/,a=t.concat(n,e.UNDERSCORE_IDENT_RE),i=t.concat(n,e.IDENT_RE),s={ +className:"title.function.invoke",relevance:0, +begin:t.concat(/\b/,/(?!let|for|while|if|else|match\b)/,i,t.lookahead(/\s*\(/)) +},r="([ui](8|16|32|64|128|size)|f(32|64))?",l=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],o=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:o, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","union","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:l},illegal:""},s]}}})() +;hljs.registerLanguage("rust",e)})();/*! `typescript` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],c=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],r=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(c,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="e+"\\s*\\(")), +l.concat("(?!",C.join("|"),")")),d,l.lookahead(/\s*\(/)), +className:"title.function",relevance:0};var C;const T={ +begin:l.concat(/\./,l.lookahead(l.concat(d,/(?![0-9A-Za-z$_(])/))),end:d, +excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},M={ +match:[/get|set/,/\s+/,d,/(?=\()/],className:{1:"keyword",3:"title.function"}, +contains:[{begin:/\(\)/},R] +},B="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+o.UNDERSCORE_IDENT_RE+")\\s*=>",$={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:x},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,p,N,f,_,h,{match:/\$\d+/},A,x,{ +scope:"attr",match:d+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0 +},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},T,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},I,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},k,M,{match:/\$[(.]/}]}}return t=>{ +const s=t.regex,c=o(t),l=e,d=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],b={ +begin:[/namespace/,/\s+/,t.IDENT_RE],beginScope:{1:"keyword",3:"title.class"} +},g={beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:{ +keyword:"interface extends",built_in:d},contains:[c.exports.CLASS_REFERENCE] +},u={$pattern:e, +keyword:n.concat(["type","interface","public","private","protected","implements","declare","abstract","readonly","enum","override","satisfies"]), +literal:a,built_in:i.concat(d),"variable.language":r},m={className:"meta", +begin:"@"+l},E=(e,n,a)=>{const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;Object.assign(c.keywords,u),c.exports.PARAMS_CONTAINS.push(m) +;const A=c.contains.find((e=>"attr"===e.scope)),y=Object.assign({},A,{ +match:s.concat(l,s.lookahead(/\s*\?:/))}) +;return c.exports.PARAMS_CONTAINS.push([c.exports.CLASS_REFERENCE,A,y]), +c.contains=c.contains.concat([m,b,g,y]), +E(c,"shebang",t.SHEBANG()),E(c,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),c.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(c,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),c}})() +;hljs.registerLanguage("typescript",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/bash.min.js b/public/vendor/highlight/languages/bash.min.js new file mode 100644 index 0000000..226aca0 --- /dev/null +++ b/public/vendor/highlight/languages/bash.min.js @@ -0,0 +1,21 @@ +/*! `bash` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/, +end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{ +className:"variable",variants:[{ +begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE] +},i=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),c={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(o);const r={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),m={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","time","for","while","until","in","do","done","case","esac","coproc","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),m,r,i,c,{match:/(\/[a-z._-]+)+/},o,{match:/\\"/},{ +className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}})() +;hljs.registerLanguage("bash",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/c.min.js b/public/vendor/highlight/languages/c.min.js new file mode 100644 index 0000000..cf59786 --- /dev/null +++ b/public/vendor/highlight/languages/c.min.js @@ -0,0 +1,41 @@ +/*! `c` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,n=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",s="[a-zA-Z_]\\w*::",r="("+a+"|"+t.optional(s)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",i={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},l={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{match:/\b(0b[01']+)/},{ +match:/(-?)\b([\d']+(\.[\d']*)?|\.[\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)/ +},{ +match:/(-?)\b(0[xX][a-fA-F0-9]+(?:'[a-fA-F0-9]+)*(?:\.[a-fA-F0-9]*(?:'[a-fA-F0-9]*)*)?(?:[pP][-+]?[0-9]+)?(l|L)?(u|U)?)/ +},{match:/(-?)\b\d+(?:'\d+)*(?:\.\d*(?:'\d*)*)?(?:[eE][-+]?\d+)?/}],relevance:0 +},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef elifdef elifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(l,{className:"string"}),{ +className:"string",begin:/<.*?>/},n,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:t.optional(s)+e.IDENT_RE,relevance:0 +},_=t.optional(s)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","typeof","typeof_unqual","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_BitInt","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal96","_Decimal128","_Decimal64x","_Decimal128x","_Float16","_Float32","_Float64","_Float128","_Float32x","_Float64x","_Float128x","const","static","constexpr","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},m=[c,i,n,e.C_BLOCK_COMMENT_MODE,o,l],g={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+_,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:_,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[n,e.C_BLOCK_COMMENT_MODE,l,o,i,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",n,e.C_BLOCK_COMMENT_MODE,l,o,i] +}]},i,n,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:l,keywords:u}}}})();hljs.registerLanguage("c",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/cpp.min.js b/public/vendor/highlight/languages/cpp.min.js new file mode 100644 index 0000000..ec11d3b --- /dev/null +++ b/public/vendor/highlight/languages/cpp.min.js @@ -0,0 +1,47 @@ +/*! `cpp` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,a=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),n="decltype\\(auto\\)",r="[a-zA-Z_]\\w*::",i="(?!struct)("+n+"|"+t.optional(r)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},c={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{ +begin:"[+-]?(?:(?:[0-9](?:'?[0-9])*\\.(?:[0-9](?:'?[0-9])*)?|\\.[0-9](?:'?[0-9])*)(?:[Ee][+-]?[0-9](?:'?[0-9])*)?|[0-9](?:'?[0-9])*[Ee][+-]?[0-9](?:'?[0-9])*|0[Xx](?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*(?:\\.(?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)?)?|\\.[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)[Pp][+-]?[0-9](?:'?[0-9])*)(?:[Ff](?:16|32|64|128)?|(BF|bf)16|[Ll]|)" +},{ +begin:"[+-]?\\b(?:0[Bb][01](?:'?[01])*|0[Xx][0-9A-Fa-f](?:'?[0-9A-Fa-f])*|0(?:'?[0-7])*|[1-9](?:'?[0-9])*)(?:[Uu](?:LL?|ll?)|[Uu][Zz]?|(?:LL?|ll?)[Uu]?|[Zz][Uu]|)" +}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(c,{className:"string"}),{ +className:"string",begin:/<.*?>/},a,e.C_BLOCK_COMMENT_MODE]},u={ +className:"title",begin:t.optional(r)+e.IDENT_RE,relevance:0 +},d=t.optional(r)+e.IDENT_RE+"\\s*\\(",p={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","flat_map","flat_set","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},_={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:t.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,t.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[_,l,s,a,e.C_BLOCK_COMMENT_MODE,o,c],f={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:p,contains:m.concat([{begin:/\(/,end:/\)/,keywords:p, +contains:m.concat(["self"]),relevance:0}]),relevance:0},g={className:"function", +begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:p,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:n,keywords:p,relevance:0},{ +begin:d,returnBegin:!0,contains:[u],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[c,o]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:p,relevance:0, +contains:[a,e.C_BLOCK_COMMENT_MODE,c,o,s,{begin:/\(/,end:/\)/,keywords:p, +relevance:0,contains:["self",a,e.C_BLOCK_COMMENT_MODE,c,o,s]}] +},s,a,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:p,illegal:"",keywords:p,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:p},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}}})();hljs.registerLanguage("cpp",e) +})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/css.min.js b/public/vendor/highlight/languages/css.min.js new file mode 100644 index 0000000..dd99135 --- /dev/null +++ b/public/vendor/highlight/languages/css.min.js @@ -0,0 +1,31 @@ +/*! `css` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video","defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),t=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),o=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),r=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-align","box-decoration-break","box-direction","box-flex","box-flex-group","box-lines","box-ordinal-group","box-orient","box-pack","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","color-scheme","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","contain-intrinsic-block-size","contain-intrinsic-height","contain-intrinsic-inline-size","contain-intrinsic-size","contain-intrinsic-width","container","container-name","container-type","content","content-visibility","counter-increment","counter-reset","counter-set","cue","cue-after","cue-before","cursor","cx","cy","direction","display","dominant-baseline","empty-cells","enable-background","field-sizing","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flood-color","flood-opacity","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-optical-sizing","font-palette","font-size","font-size-adjust","font-smooth","font-smoothing","font-stretch","font-style","font-synthesis","font-synthesis-position","font-synthesis-small-caps","font-synthesis-style","font-synthesis-weight","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-emoji","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","forced-color-adjust","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphenate-character","hyphenate-limit-chars","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","initial-letter","initial-letter-align","inline-size","inset","inset-area","inset-block","inset-block-end","inset-block-start","inset-inline","inset-inline-end","inset-inline-start","isolation","justify-content","justify-items","justify-self","kerning","left","letter-spacing","lighting-color","line-break","line-height","line-height-step","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","margin-trim","marker","marker-end","marker-mid","marker-start","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","masonry-auto-flow","math-depth","math-shift","math-style","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","offset","offset-anchor","offset-distance","offset-path","offset-position","offset-rotate","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-anchor","overflow-block","overflow-clip-margin","overflow-inline","overflow-wrap","overflow-x","overflow-y","overlay","overscroll-behavior","overscroll-behavior-block","overscroll-behavior-inline","overscroll-behavior-x","overscroll-behavior-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","paint-order","pause","pause-after","pause-before","perspective","perspective-origin","place-content","place-items","place-self","pointer-events","position","position-anchor","position-visibility","print-color-adjust","quotes","r","resize","rest","rest-after","rest-before","right","rotate","row-gap","ruby-align","ruby-position","scale","scroll-behavior","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scroll-timeline","scroll-timeline-axis","scroll-timeline-name","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","speak","speak-as","src","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","tab-size","table-layout","text-align","text-align-all","text-align-last","text-anchor","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-skip-ink","text-decoration-style","text-decoration-thickness","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-size-adjust","text-transform","text-underline-offset","text-underline-position","text-wrap","text-wrap-mode","text-wrap-style","timeline-scope","top","touch-action","transform","transform-box","transform-origin","transform-style","transition","transition-behavior","transition-delay","transition-duration","transition-property","transition-timing-function","translate","unicode-bidi","user-modify","user-select","vector-effect","vertical-align","view-timeline","view-timeline-axis","view-timeline-inset","view-timeline-name","view-transition-name","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","white-space-collapse","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index","zoom"].sort().reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+t.join("|")+")"},{begin:":(:)?("+o.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+r.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/go.min.js b/public/vendor/highlight/languages/go.min.js new file mode 100644 index 0000000..fdb009a --- /dev/null +++ b/public/vendor/highlight/languages/go.min.js @@ -0,0 +1,20 @@ +/*! `go` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const a={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:a,illegal:"{var e=(()=>{"use strict";return e=>{const n="HTTP/([32]|1\\.[01])",a={ +className:"attribute", +begin:e.regex.concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{ +contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$", +relevance:0}}]}},s=[a,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0} +}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{ +begin:"^(?="+n+" \\d{3})",end:/$/,contains:[{className:"meta",begin:n},{ +className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/, +contains:s}},{begin:"(?=^[A-Z]+ (.*?) "+n+"$)",end:/$/,contains:[{ +className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{ +className:"meta",begin:n},{className:"keyword",begin:"[A-Z]+"}],starts:{ +end:/\b\B/,illegal:/\S/,contains:s}},e.inherit(a,{relevance:0})]}}})() +;hljs.registerLanguage("http",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/ini.min.js b/public/vendor/highlight/languages/ini.min.js new file mode 100644 index 0000000..ed4b960 --- /dev/null +++ b/public/vendor/highlight/languages/ini.min.js @@ -0,0 +1,15 @@ +/*! `ini` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={className:"number", +relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{begin:e.NUMBER_RE}] +},s=e.COMMENT();s.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];const i={ +className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/ +}]},t={className:"literal",begin:/\bon|off|true|false|yes|no\b/},r={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''", +end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"' +},{begin:"'",end:"'"}]},l={begin:/\[/,end:/\]/,contains:[s,t,i,r,a,"self"], +relevance:0},c=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[s,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(c,"(\\s*\\.\\s*",c,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[s,l,t,i,r,a]}}]}}})() +;hljs.registerLanguage("ini",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/java.min.js b/public/vendor/highlight/languages/java.min.js new file mode 100644 index 0000000..25f67df --- /dev/null +++ b/public/vendor/highlight/languages/java.min.js @@ -0,0 +1,38 @@ +/*! `java` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;var e="[0-9](_*[0-9])*",a=`\\.(${e})`,n="[0-9a-fA-F](_*[0-9a-fA-F])*",s={ +className:"number",variants:[{ +begin:`(\\b(${e})((${a})|\\.)?|(${a}))[eE][+-]?(${e})[fFdD]?\\b`},{ +begin:`\\b(${e})((${a})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${a})[fFdD]?\\b` +},{begin:`\\b(${e})[fFdD]\\b`},{ +begin:`\\b0[xX]((${n})\\.?|(${n})?\\.(${n}))[pP][+-]?(${e})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${n})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function t(e,a,n){return-1===n?"":e.replace(a,(s=>t(e,a,n-1)))} +return e=>{ +const a=e.regex,n="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",i=n+t("(?:<"+n+"~~~(?:\\s*,\\s*"+n+"~~~)*>)?",/~~~/g,2),r={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits","goto","when"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},l={className:"meta",begin:"@"+n,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},c={className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:r,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,n],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[a.concat(/(?!else)/,n),/\s+/,n,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,n],className:{1:"keyword", +3:"title.class"},contains:[c,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+i+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:r,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0, +contains:[l,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,s,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},s,l]}}})() +;hljs.registerLanguage("java",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/javascript.min.js b/public/vendor/highlight/languages/javascript.min.js new file mode 100644 index 0000000..7df9939 --- /dev/null +++ b/public/vendor/highlight/languages/javascript.min.js @@ -0,0 +1,81 @@ +/*! `javascript` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="e+"\\s*\\(")), +l.concat("(?!",T.join("|"),")")),d,l.lookahead(/\s*\(/)), +className:"title.function",relevance:0};var T;const C={ +begin:l.concat(/\./,l.lookahead(l.concat(d,/(?![0-9A-Za-z$_(])/))),end:d, +excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},M={ +match:[/get|set/,/\s+/,d,/(?=\()/],className:{1:"keyword",3:"title.function"}, +contains:[{begin:/\(\)/},R] +},B="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+o.UNDERSCORE_IDENT_RE+")\\s*=>",$={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:k},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,h,_,N,f,p,{match:/\$\d+/},A,k,{ +scope:"attr",match:d+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[p,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0 +},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},I,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},C,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},x,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},O,M,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/json.min.js b/public/vendor/highlight/languages/json.min.js new file mode 100644 index 0000000..48edba3 --- /dev/null +++ b/public/vendor/highlight/languages/json.min.js @@ -0,0 +1,8 @@ +/*! `json` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],s={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",aliases:["jsonc"], +keywords:{literal:a},contains:[{className:"attr", +begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/, +className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,s,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/lua.min.js b/public/vendor/highlight/languages/lua.min.js new file mode 100644 index 0000000..e722cfb --- /dev/null +++ b/public/vendor/highlight/languages/lua.min.js @@ -0,0 +1,15 @@ +/*! `lua` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const t="\\[=*\\[",a="\\]=*\\]",n={ +begin:t,end:a,contains:["self"] +},o=[e.COMMENT("--(?!"+t+")","$"),e.COMMENT("--"+t,a,{contains:[n],relevance:10 +})];return{name:"Lua",aliases:["pluto"],keywords:{ +$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:o.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:o}].concat(o) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:t,end:a,contains:[n],relevance:5}])}}})();hljs.registerLanguage("lua",e) +})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/markdown.min.js b/public/vendor/highlight/languages/markdown.min.js new file mode 100644 index 0000000..f223e36 --- /dev/null +++ b/public/vendor/highlight/languages/markdown.min.js @@ -0,0 +1,32 @@ +/*! `markdown` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={begin:/<\/?[A-Za-z_]/, +end:">",subLanguage:"xml",relevance:0},a={variants:[{begin:/\[.+?\]\[.*?\]/, +relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},i={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},s={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},c=e.inherit(i,{contains:[] +}),t=e.inherit(s,{contains:[]});i.contains.push(t),s.contains.push(c) +;let g=[n,a];return[i,s,c,t].forEach((e=>{e.contains=e.contains.concat(g) +})),g=g.concat(i,s),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:g},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:g}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:g, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]},{scope:"literal", +match:/&([a-zA-Z0-9]+|#[0-9]{1,7}|#[Xx][0-9a-fA-F]{1,6});/}]}}})() +;hljs.registerLanguage("markdown",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/nginx.min.js b/public/vendor/highlight/languages/nginx.min.js new file mode 100644 index 0000000..bdd39e7 --- /dev/null +++ b/public/vendor/highlight/languages/nginx.min.js @@ -0,0 +1,21 @@ +/*! `nginx` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={ +className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{\w+\}/},{ +begin:n.concat(/[$@]/,e.UNDERSCORE_IDENT_RE)}]},s={endsWithParent:!0,keywords:{ +$pattern:/[a-z_]{2,}|\/dev\/poll/, +literal:["on","off","yes","no","true","false","none","blocked","debug","info","notice","warn","error","crit","select","break","last","permanent","redirect","kqueue","rtsig","epoll","poll","/dev/poll"] +},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string", +contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/ +}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[a] +},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,a],variants:[{begin:"\\s\\^", +end:"\\s|\\{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|\\{|;",returnEnd:!0},{ +begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number", +begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{ +className:"number",begin:"\\b\\d+[kKmMgGdshdwy]?\\b",relevance:0},a]};return{ +name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{ +beginKeywords:"upstream location",end:/;|\{/,contains:s.contains,keywords:{ +section:"upstream location"}},{className:"section", +begin:n.concat(e.UNDERSCORE_IDENT_RE+n.lookahead(/\s+\{/)),relevance:0},{ +begin:n.lookahead(e.UNDERSCORE_IDENT_RE+"\\s"),end:";|\\{",contains:[{ +className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:s}],relevance:0}], +illegal:"[^\\s\\}\\{]"}}})();hljs.registerLanguage("nginx",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/pgsql.min.js b/public/vendor/highlight/languages/pgsql.min.js new file mode 100644 index 0000000..3a44c8e --- /dev/null +++ b/public/vendor/highlight/languages/pgsql.min.js @@ -0,0 +1,69 @@ +/*! `pgsql` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var E=(()=>{"use strict";return E=>{ +const T=E.COMMENT("--","$"),N="\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$",A="BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR NAME OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ",R=A.trim().split(" ").map((E=>E.split("|")[0])).join("|"),I="ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP PERCENTILE_CONT PERCENTILE_DISC ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE NUM_NONNULLS NUM_NULLS ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT TRUNC WIDTH_BUCKET RANDOM SETSEED ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAP LEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR TO_ASCII TO_HEX TRANSLATE OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 TIMEOFDAY TRANSACTION_TIMESTAMP|10 ENUM_FIRST ENUM_LAST ENUM_RANGE AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILY INET_MERGE MACADDR8_SET7BIT ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA CURSOR_TO_XML CURSOR_TO_XMLSCHEMA SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA XMLATTRIBUTES TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY CURRVAL LASTVAL NEXTVAL SETVAL COALESCE NULLIF GREATEST LEAST ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY STRING_TO_ARRAY UNNEST ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE GENERATE_SERIES GENERATE_SUBSCRIPTS CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE GIN_CLEAN_PENDING_LIST SUPPRESS_REDUNDANT_UPDATES_TRIGGER LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE GROUPING CAST".split(" ").map((E=>E.split("|")[0])).join("|") +;return{name:"PostgreSQL",aliases:["postgres","postgresql"],supersetOf:"sql", +case_insensitive:!0,keywords:{ +keyword:"ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION INDEX PROCEDURE ASSERTION ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS DEFERRABLE RANGE DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED OF NOTHING NONE EXCLUDE ATTRIBUTE USAGE ROUTINES TRUE FALSE NAN INFINITY ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT OPEN SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS ", +built_in:"CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 SQLSTATE SQLERRM|10 SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED INDEX_CORRUPTED " +},illegal:/:==|\W\s*\(\*|(^|\s)\$[a-z]|\{\{|[a-z]:\s*$|\.\.\.|TO:|DO:/, +contains:[{className:"keyword",variants:[{begin:/\bTEXT\s*SEARCH\b/},{ +begin:/\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/},{ +begin:/\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/},{ +begin:/\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/},{ +begin:/\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/},{begin:/\bNULLS\s+(FIRST|LAST)\b/},{ +begin:/\bEVENT\s+TRIGGER\b/},{begin:/\b(MAPPING|OR)\s+REPLACE\b/},{ +begin:/\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/},{ +begin:/\b(SHARE|EXCLUSIVE)\s+MODE\b/},{ +begin:/\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/},{ +begin:/\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/ +},{begin:/\bPRESERVE\s+ROWS\b/},{begin:/\bDISCARD\s+PLANS\b/},{ +begin:/\bREFERENCING\s+(OLD|NEW)\b/},{begin:/\bSKIP\s+LOCKED\b/},{ +begin:/\bGROUPING\s+SETS\b/},{ +begin:/\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/},{ +begin:/\b(WITH|WITHOUT)\s+HOLD\b/},{ +begin:/\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/},{ +begin:/\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/},{ +begin:/\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/},{ +begin:/\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/},{begin:/\bIS\s+(NOT\s+)?UNKNOWN\b/ +},{begin:/\bSECURITY\s+LABEL\b/},{begin:/\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/ +},{begin:/\bWITH\s+(NO\s+)?DATA\b/},{begin:/\b(FOREIGN|SET)\s+DATA\b/},{ +begin:/\bSET\s+(CATALOG|CONSTRAINTS)\b/},{begin:/\b(WITH|FOR)\s+ORDINALITY\b/},{ +begin:/\bIS\s+(NOT\s+)?DOCUMENT\b/},{ +begin:/\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/},{ +begin:/\b(STRIP|PRESERVE)\s+WHITESPACE\b/},{ +begin:/\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/},{ +begin:/\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/},{begin:/\bAT\s+TIME\s+ZONE\b/},{ +begin:/\bGRANTED\s+BY\b/},{begin:/\bRETURN\s+(QUERY|NEXT)\b/},{ +begin:/\b(ATTACH|DETACH)\s+PARTITION\b/},{ +begin:/\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/},{ +begin:/\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/ +},{begin:/\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/}]},{ +begin:/\b(FORMAT|FAMILY|VERSION)\s*\(/},{begin:/\bINCLUDE\s*\(/, +keywords:"INCLUDE"},{begin:/\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/ +},{ +begin:/\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/ +},{begin:/\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/,relevance:10},{ +begin:/\bEXTRACT\s*\(/,end:/\bFROM\b/,returnEnd:!0,keywords:{ +type:"CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR TIMEZONE_MINUTE WEEK YEAR" +}},{begin:/\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/,keywords:{keyword:"NAME"}},{ +begin:/\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/,keywords:{ +keyword:"DOCUMENT CONTENT"}},{beginKeywords:"CACHE INCREMENT MAXVALUE MINVALUE", +end:E.C_NUMBER_RE,returnEnd:!0,keywords:"BY CACHE INCREMENT MAXVALUE MINVALUE" +},{className:"type",begin:/\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/},{className:"type", +begin:/\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/ +},{ +begin:/\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/, +keywords:{keyword:"RETURNS", +type:"LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER" +}},{begin:"\\b("+I+")\\s*\\("},{begin:"\\.("+R+")\\b"},{ +begin:"\\b("+R+")\\s+PATH\\b",keywords:{keyword:"PATH", +type:A.replace("PATH ","")}},{className:"type",begin:"\\b("+R+")\\b"},{ +className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{ +className:"string",begin:"(e|E|u&|U&)'",end:"'",contains:[{begin:"\\\\."}], +relevance:10},E.END_SAME_AS_BEGIN({begin:N,end:N,contains:[{ +subLanguage:["pgsql","perl","python","tcl","r","lua","java","php","ruby","bash","scheme","xml","json"], +endsWithParent:!0}]}),{begin:'"',end:'"',contains:[{begin:'""'}] +},E.C_NUMBER_MODE,E.C_BLOCK_COMMENT_MODE,T,{className:"meta",variants:[{ +begin:"%(ROW)?TYPE",relevance:10},{begin:"\\$\\d+"},{begin:"^#\\w",end:"$"}]},{ +className:"symbol",begin:"<<\\s*[a-zA-Z_][a-zA-Z_0-9$]*\\s*>>",relevance:10}]}} +})();hljs.registerLanguage("pgsql",E)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/python.min.js b/public/vendor/highlight/languages/python.min.js new file mode 100644 index 0000000..57337c4 --- /dev/null +++ b/public/vendor/highlight/languages/python.min.js @@ -0,0 +1,42 @@ +/*! `python` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a=/[\p{XID_Start}_]\p{XID_Continue}*/u,s=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],t={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:s, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},i={className:"meta",begin:/^(>>>|\.\.\.) /},r={className:"subst",begin:/\{/, +end:/\}/,keywords:t,illegal:/#/},l={begin:/\{\{/,relevance:0},o={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,i],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,i,l,r]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,l,r]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},b="[0-9](_?[0-9])*",c=`(\\b(${b}))?\\.(${b})|\\b(${b})\\.`,d="\\b|"+s.join("|"),g={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${b})|(${c}))[eE][+-]?(${b})[jJ]?(?=${d})`},{begin:`(${c})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${d})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${d})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${d})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${d})`},{begin:`\\b(${b})[jJ](?=${d})` +}]},p={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:t, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t, +contains:["self",i,g,o,e.HASH_COMMENT_MODE]}]};return r.contains=[o,g,i],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:t, +illegal:/(<\/|\?)|=>/,contains:[i,g,{scope:"variable.language",match:/\bself\b/ +},{beginKeywords:"if",relevance:0},{match:/\bor\b/,scope:"keyword" +},o,p,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,a],scope:{1:"keyword", +3:"title.function"},contains:[m]},{variants:[{ +match:[/\bclass/,/\s+/,a,/\s*/,/\(\s*/,a,/\s*\)/]},{match:[/\bclass/,/\s+/,a]}], +scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ +className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[g,m,o]}]}}})() +;hljs.registerLanguage("python",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/rust.min.js b/public/vendor/highlight/languages/rust.min.js new file mode 100644 index 0000000..3b83991 --- /dev/null +++ b/public/vendor/highlight/languages/rust.min.js @@ -0,0 +1,27 @@ +/*! `rust` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const t=e.regex,n=/(r#)?/,a=t.concat(n,e.UNDERSCORE_IDENT_RE),i=t.concat(n,e.IDENT_RE),s={ +className:"title.function.invoke",relevance:0, +begin:t.concat(/\b/,/(?!let|for|while|if|else|match\b)/,i,t.lookahead(/\s*\(/)) +},r="([ui](8|16|32|64|128|size)|f(32|64))?",l=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],o=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:o, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","union","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:l},illegal:""},s]}}})() +;hljs.registerLanguage("rust",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/languages/typescript.min.js b/public/vendor/highlight/languages/typescript.min.js new file mode 100644 index 0000000..075d577 --- /dev/null +++ b/public/vendor/highlight/languages/typescript.min.js @@ -0,0 +1,99 @@ +/*! `typescript` grammar compiled for Highlight.js 11.11.1 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],c=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],r=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(c,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="e+"\\s*\\(")), +l.concat("(?!",C.join("|"),")")),d,l.lookahead(/\s*\(/)), +className:"title.function",relevance:0};var C;const T={ +begin:l.concat(/\./,l.lookahead(l.concat(d,/(?![0-9A-Za-z$_(])/))),end:d, +excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},M={ +match:[/get|set/,/\s+/,d,/(?=\()/],className:{1:"keyword",3:"title.function"}, +contains:[{begin:/\(\)/},R] +},B="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+o.UNDERSCORE_IDENT_RE+")\\s*=>",$={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:x},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,p,N,f,_,h,{match:/\$\d+/},A,x,{ +scope:"attr",match:d+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0 +},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},T,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},I,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},k,M,{match:/\$[(.]/}]}}return t=>{ +const s=t.regex,c=o(t),l=e,d=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],b={ +begin:[/namespace/,/\s+/,t.IDENT_RE],beginScope:{1:"keyword",3:"title.class"} +},g={beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:{ +keyword:"interface extends",built_in:d},contains:[c.exports.CLASS_REFERENCE] +},u={$pattern:e, +keyword:n.concat(["type","interface","public","private","protected","implements","declare","abstract","readonly","enum","override","satisfies"]), +literal:a,built_in:i.concat(d),"variable.language":r},m={className:"meta", +begin:"@"+l},E=(e,n,a)=>{const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;Object.assign(c.keywords,u),c.exports.PARAMS_CONTAINS.push(m) +;const A=c.contains.find((e=>"attr"===e.scope)),y=Object.assign({},A,{ +match:s.concat(l,s.lookahead(/\s*\?:/))}) +;return c.exports.PARAMS_CONTAINS.push([c.exports.CLASS_REFERENCE,A,y]), +c.contains=c.contains.concat([m,b,g,y]), +E(c,"shebang",t.SHEBANG()),E(c,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),c.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(c,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),c}})() +;hljs.registerLanguage("typescript",e)})(); \ No newline at end of file diff --git a/public/vendor/highlight/styles/.gitignore b/public/vendor/highlight/styles/.gitignore new file mode 100644 index 0000000..c9edab3 --- /dev/null +++ b/public/vendor/highlight/styles/.gitignore @@ -0,0 +1,5 @@ +*.css +*.jpg +*.png +base16 +!ari.css diff --git a/public/vendor/highlight/styles/ari.css b/public/vendor/highlight/styles/ari.css new file mode 100644 index 0000000..5322091 --- /dev/null +++ b/public/vendor/highlight/styles/ari.css @@ -0,0 +1,109 @@ +/*! + Theme: Default + Description: Original highlight.js style + Author: (c) Ivan Sagalaev + Maintainer: @highlightjs/core-team + Website: https://highlightjs.org/ + License: see project LICENSE + Touched: 2021 +*/ +/* +This is left on purpose making default.css the single file that can be lifted +as-is from the repository directly without the need for a build step + +Typically this "required" baseline CSS is added by `makestuff.js` during build. +*/ +pre code.hljs { + display: block; + overflow-x: auto; + /* padding: 1em */ +} +code.hljs { + padding: 3px 5px +} +/* end baseline CSS */ +.hljs { + background: #F3F3F3; + color: #444 +} +/* Base color: saturation 0; */ +.hljs-subst { + /* default */ + +} +/* purposely ignored */ +.hljs-formula, +.hljs-attr, +.hljs-property, +.hljs-params { + +} +.hljs-comment { + color: #697070 +} +.hljs-tag, +.hljs-punctuation { + color: #444a +} +.hljs-tag .hljs-name, +.hljs-tag .hljs-attr { + color: #444 +} +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta .hljs-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold +} +/* User color: hue: 0 */ +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000 +} +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold +} +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-operator, +.hljs-selector-pseudo { + color: #ab5656 +} +/* Language color: hue: 90; */ +.hljs-literal { + color: #695 +} +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300 +} +/* Meta color: hue: 200 */ +.hljs-meta { + color: #1f7199 +} +.hljs-meta .hljs-string { + color: #38a +} +/* Misc effects */ +.hljs-emphasis { + font-style: italic +} +.hljs-strong { + font-weight: bold +} diff --git a/view/blogpost.html b/view/blogpost.html index 6126eda..2818c38 100644 --- a/view/blogpost.html +++ b/view/blogpost.html @@ -14,10 +14,14 @@ + + + {{end}} {{define "content"}}
    +
    - {{if ne .BlueskyURL ""}}
    {{.Boosts}} - +

    join the conversation on From bd2dc806d53a5d181c6762aa88683eff27d94eff Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 15 Jul 2025 16:40:15 +0100 Subject: [PATCH 11/13] refactor: move music admin to /admin/music; keep /admin generic --- admin/{ => account}/accounthttp.go | 59 +- admin/auth/authhttp.go | 386 +++++++++++++ admin/components/credits/addcredit.html | 2 +- admin/components/credits/editcredits.html | 4 +- .../components/release/release-list-item.html | 6 +- admin/components/tracks/addtrack.html | 2 +- admin/components/tracks/edittracks.html | 4 +- admin/core/funcs.go | 56 ++ admin/http.go | 506 +----------------- admin/{ => logs}/logshttp.go | 19 +- admin/{ => music}/artisthttp.go | 15 +- admin/music/musichttp.go | 69 +++ admin/{ => music}/releasehttp.go | 29 +- admin/{ => music}/trackhttp.go | 15 +- admin/static/admin.css | 9 +- admin/static/index.css | 81 --- admin/static/index.js | 74 --- admin/static/music.css | 82 +++ admin/static/music.js | 74 +++ admin/templates.go | 125 ----- admin/templates/index.go | 13 + admin/templates/login.go | 42 ++ admin/templates/logs.go | 41 ++ admin/templates/music.go | 54 ++ admin/views/edit-artist.html | 2 +- admin/views/edit-release.html | 16 +- admin/views/index.html | 62 +-- admin/views/layout.html | 13 +- admin/views/music.html | 73 +++ controller/release.go | 50 +- public/style/index.css | 2 +- 31 files changed, 1079 insertions(+), 906 deletions(-) rename admin/{ => account}/accounthttp.go (89%) create mode 100644 admin/auth/authhttp.go create mode 100644 admin/core/funcs.go rename admin/{ => logs}/logshttp.go (88%) rename admin/{ => music}/artisthttp.go (88%) create mode 100644 admin/music/musichttp.go rename admin/{ => music}/releasehttp.go (91%) rename admin/{ => music}/trackhttp.go (88%) create mode 100644 admin/static/music.css create mode 100644 admin/static/music.js delete mode 100644 admin/templates.go create mode 100644 admin/templates/index.go create mode 100644 admin/templates/login.go create mode 100644 admin/templates/logs.go create mode 100644 admin/templates/music.go create mode 100644 admin/views/music.html diff --git a/admin/accounthttp.go b/admin/account/accounthttp.go similarity index 89% rename from admin/accounthttp.go rename to admin/account/accounthttp.go index 945a507..5601a2e 100644 --- a/admin/accounthttp.go +++ b/admin/account/accounthttp.go @@ -1,22 +1,25 @@ -package admin +package account import ( - "database/sql" - "fmt" - "net/http" - "net/url" - "os" + "database/sql" + "fmt" + "net/http" + "net/url" + "os" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) -func accountHandler(app *model.AppState) http.Handler { +func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() + mux.Handle("/", accountIndexHandler(app)) + mux.Handle("/totp-setup", totpSetupHandler(app)) mux.Handle("/totp-confirm", totpConfirmHandler(app)) mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) @@ -64,7 +67,7 @@ func accountIndexHandler(app *model.AppState) http.Handler { session.Message = sessionMessage session.Error = sessionError - err = accountTemplate.Execute(w, accountResponse{ + err = templates.AccountTemplate.Execute(w, accountResponse{ Session: session, TOTPs: totps, }) @@ -92,7 +95,7 @@ func changePasswordHandler(app *model.AppState) http.Handler { currentPassword := r.Form.Get("current-password") if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(currentPassword)); err != nil { controller.SetSessionError(app.DB, session, "Incorrect password.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -102,7 +105,7 @@ func changePasswordHandler(app *model.AppState) http.Handler { if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -111,7 +114,7 @@ func changePasswordHandler(app *model.AppState) http.Handler { if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -119,7 +122,7 @@ func changePasswordHandler(app *model.AppState) http.Handler { controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, "Password updated successfully.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) }) } @@ -147,7 +150,7 @@ func deleteAccountHandler(app *model.AppState) http.Handler { if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil { app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r)) controller.SetSessionError(app.DB, session, "Incorrect password.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -155,7 +158,7 @@ func deleteAccountHandler(app *model.AppState) http.Handler { if err != nil { fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -184,7 +187,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) + err := templates.TotpSetupTemplate.Execute(w, totpSetupData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -221,7 +224,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) + err := templates.TotpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -235,7 +238,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) } - err = totpConfirmTemplate.Execute(w, totpConfirmData{ + err = templates.TotpConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: &totp, NameEscaped: url.PathEscape(totp.Name), @@ -277,7 +280,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to fetch TOTP method: %v\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } if totp == nil { @@ -296,7 +299,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) if code != confirmCodeOffset { session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } - err = totpConfirmTemplate.Execute(w, totpConfirmData{ + err = templates.TotpConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: totp, NameEscaped: url.PathEscape(totp.Name), @@ -314,7 +317,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to confirm TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -322,7 +325,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler { controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name)) - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) }) } @@ -345,7 +348,7 @@ func totpDeleteHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to fetch TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } if totp == nil { @@ -357,7 +360,7 @@ func totpDeleteHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to delete TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) return } @@ -365,6 +368,6 @@ func totpDeleteHandler(app *model.AppState) http.Handler { controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name)) - http.Redirect(w, r, "/admin/account", http.StatusFound) + http.Redirect(w, r, "/admin/account/", http.StatusFound) }) } diff --git a/admin/auth/authhttp.go b/admin/auth/authhttp.go new file mode 100644 index 0000000..aba4074 --- /dev/null +++ b/admin/auth/authhttp.go @@ -0,0 +1,386 @@ +package auth + +import ( + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" + "database/sql" + "fmt" + "net/http" + "os" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func RegisterAccountHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + + if session.Account != nil { + // user is already logged in + http.Redirect(w, r, "/admin", http.StatusFound) + return + } + + type registerData struct { + Session *model.Session + } + + render := func() { + err := templates.RegisterTemplate.Execute(w, registerData{ Session: session }) + if err != nil { + fmt.Printf("WARN: Error rendering create account page: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + } + + if r.Method == http.MethodGet { + render() + return + } + + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + type RegisterRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Invite string `json:"invite"` + } + credentials := RegisterRequest{ + Username: r.Form.Get("username"), + Email: r.Form.Get("email"), + Password: r.Form.Get("password"), + Invite: r.Form.Get("invite"), + } + + // make sure invite code exists in DB + invite, err := controller.GetInvite(app.DB, credentials.Invite) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + if invite == nil || time.Now().After(invite.ExpiresAt) { + if invite != nil { + err := controller.DeleteInvite(app.DB, invite.Code) + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } + } + controller.SetSessionError(app.DB, session, "Invalid invite code.") + render() + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + + account := model.Account{ + Username: credentials.Username, + Password: string(hashedPassword), + Email: sql.NullString{ String: credentials.Email, Valid: true }, + AvatarURL: sql.NullString{ String: "/img/default-avatar.png", Valid: true }, + } + err = controller.CreateAccount(app.DB, &account) + if err != nil { + if strings.HasPrefix(err.Error(), "pq: duplicate key") { + controller.SetSessionError(app.DB, session, "An account with that username already exists.") + render() + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + + app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(app, r)) + + err = controller.DeleteInvite(app.DB, invite.Code) + if err != nil { + app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err) + } + + // registration success! + controller.SetSessionAccount(app.DB, session, &account) + controller.SetSessionMessage(app.DB, session, "") + controller.SetSessionError(app.DB, session, "") + http.Redirect(w, r, "/admin", http.StatusFound) + }) +} + +func LoginHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + session := r.Context().Value("session").(*model.Session) + + type loginData struct { + Session *model.Session + } + + render := func() { + err := templates.LoginTemplate.Execute(w, loginData{ Session: session }) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } + + if r.Method == http.MethodGet { + if session.Account != nil { + // user is already logged in + http.Redirect(w, r, "/admin", http.StatusFound) + return + } + render() + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if !r.Form.Has("username") || !r.Form.Has("password") { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + account, err := controller.GetAccountByUsername(app.DB, username) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err) + controller.SetSessionError(app.DB, session, "Invalid username or password.") + render() + return + } + if account == nil { + controller.SetSessionError(app.DB, session, "Invalid username or password.") + render() + return + } + if account.Locked { + controller.SetSessionError(app.DB, session, "This account is locked.") + render() + return + } + + err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) + if err != nil { + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r)) + if locked := handleFailedLogin(app, account, r); locked { + controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") + } else { + controller.SetSessionError(app.DB, session, "Invalid username or password.") + } + render() + return + } + + totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + + if len(totps) > 0 { + err = controller.SetSessionAttemptAccount(app.DB, session, account) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to set attempt session: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + controller.SetSessionMessage(app.DB, session, "") + controller.SetSessionError(app.DB, session, "") + http.Redirect(w, r, "/admin/totp", http.StatusFound) + return + } + + // login success! + // TODO: log login activity to user + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r)) + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) + + err = controller.SetSessionAccount(app.DB, session, account) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + controller.SetSessionMessage(app.DB, session, "") + controller.SetSessionError(app.DB, session, "") + http.Redirect(w, r, "/admin", http.StatusFound) + }) +} + +func LoginTOTPHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + + if session.AttemptAccount == nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + type loginTOTPData struct { + Session *model.Session + } + + render := func() { + err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } + + if r.Method == http.MethodGet { + render() + return + } + + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + r.ParseForm() + + if !r.Form.Has("totp") { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + totpCode := r.FormValue("totp") + + if len(totpCode) != controller.TOTP_CODE_LENGTH { + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) + controller.SetSessionError(app.DB, session, "Invalid TOTP.") + render() + return + } + + totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.AttemptAccount.ID, totpCode) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to check TOTPs: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + if totpMethod == nil { + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) + if locked := handleFailedLogin(app, session.AttemptAccount, r); locked { + controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") + controller.SetSessionAttemptAccount(app.DB, session, nil) + http.Redirect(w, r, "/admin", http.StatusFound) + } else { + controller.SetSessionError(app.DB, session, "Incorrect TOTP.") + } + render() + return + } + + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r)) + + err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + render() + return + } + err = controller.SetSessionAttemptAccount(app.DB, session, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to clear attempt session: %v\n", err) + } + controller.SetSessionMessage(app.DB, session, "") + controller.SetSessionError(app.DB, session, "") + http.Redirect(w, r, "/admin", http.StatusFound) + }) +} + +func LogoutHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } + + session := r.Context().Value("session").(*model.Session) + err := controller.DeleteSession(app.DB, session.Token) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to delete session: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: model.COOKIE_TOKEN, + Expires: time.Now(), + Path: "/", + }) + + err = templates.LogoutTemplate.Execute(w, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) +} + +func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool { + locked, err := controller.IncrementAccountFails(app.DB, account.ID) + if err != nil { + fmt.Fprintf( + os.Stderr, + "WARN: Failed to increment login failures for \"%s\": %v\n", + account.Username, + err, + ) + app.Log.Warn( + log.TYPE_ACCOUNT, + "Failed to increment login failures for \"%s\"", + account.Username, + ) + } + if locked { + app.Log.Warn( + log.TYPE_ACCOUNT, + "Account \"%s\" was locked: %d failed login attempts (IP: %s)", + account.Username, + model.MAX_LOGIN_FAIL_ATTEMPTS, + controller.ResolveIP(app, r), + ) + } + return locked +} diff --git a/admin/components/credits/addcredit.html b/admin/components/credits/addcredit.html index c09a550..debc660 100644 --- a/admin/components/credits/addcredit.html +++ b/admin/components/credits/addcredit.html @@ -7,7 +7,7 @@ {{range $Artist := .Artists}}

  • diff --git a/admin/components/credits/editcredits.html b/admin/components/credits/editcredits.html index 94dc268..0c50fac 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -3,8 +3,8 @@

    Editing: Credits

    Add diff --git a/admin/components/release/release-list-item.html b/admin/components/release/release-list-item.html index 4b8f41e..142c26a 100644 --- a/admin/components/release/release-list-item.html +++ b/admin/components/release/release-list-item.html @@ -5,7 +5,7 @@
  • - {{.Title}} + {{.Title}} {{.ReleaseDate.Year}} {{if not .Visible}}(hidden){{end}} @@ -13,9 +13,9 @@

    {{.PrintArtists true true}}

    {{.ReleaseType}} - ({{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}})

    + ({{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}})

    diff --git a/admin/components/tracks/addtrack.html b/admin/components/tracks/addtrack.html index a6dd433..f62f369 100644 --- a/admin/components/tracks/addtrack.html +++ b/admin/components/tracks/addtrack.html @@ -8,7 +8,7 @@
  • diff --git a/admin/components/tracks/edittracks.html b/admin/components/tracks/edittracks.html index d03f80a..3b8368d 100644 --- a/admin/components/tracks/edittracks.html +++ b/admin/components/tracks/edittracks.html @@ -3,8 +3,8 @@

    Editing: Tracks

    Add diff --git a/admin/core/funcs.go b/admin/core/funcs.go new file mode 100644 index 0000000..c471f8a --- /dev/null +++ b/admin/core/funcs.go @@ -0,0 +1,56 @@ +package core + +import ( + "arimelody-web/controller" + "arimelody-web/model" + "context" + "fmt" + "net/http" + "os" + "strings" +) + +func RequireAccount(next http.Handler) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + if session.Account == nil { + // TODO: include context in redirect + http.Redirect(w, r, "/admin/login", http.StatusFound) + return + } + next.ServeHTTP(w, r) + }) +} + +func EnforceSession(app *model.AppState, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session, err := controller.GetSessionFromRequest(app, r) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if session == nil { + // create a new session + session, err = controller.CreateSession(app.DB, r.UserAgent()) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to create session: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: model.COOKIE_TOKEN, + Value: session.Token, + Expires: session.ExpiresAt, + Secure: strings.HasPrefix(app.Config.BaseUrl, "https"), + HttpOnly: true, + Path: "/", + }) + } + + ctx := context.WithValue(r.Context(), "session", session) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/admin/http.go b/admin/http.go index 245a152..e04c636 100644 --- a/admin/http.go +++ b/admin/http.go @@ -1,57 +1,38 @@ package admin import ( - "context" - "database/sql" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "time" + "fmt" + "net/http" + "os" + "path/filepath" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" - - "golang.org/x/crypto/bcrypt" + "arimelody-web/admin/account" + "arimelody-web/admin/auth" + "arimelody-web/admin/core" + "arimelody-web/admin/logs" + "arimelody-web/admin/music" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() - mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - qrB64Img, err := controller.GenerateQRCode("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family") - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + mux.Handle("/register", auth.RegisterAccountHandler(app)) + mux.Handle("/login", auth.LoginHandler(app)) + mux.Handle("/totp", auth.LoginTOTPHandler(app)) + mux.Handle("/logout", core.RequireAccount(auth.LogoutHandler(app))) - w.Write([]byte("")) - })) - - mux.Handle("/login", loginHandler(app)) - mux.Handle("/totp", loginTOTPHandler(app)) - mux.Handle("/logout", requireAccount(logoutHandler(app))) - - mux.Handle("/register", registerAccountHandler(app)) - - mux.Handle("/account", requireAccount(accountIndexHandler(app))) - mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) - - mux.Handle("/logs", requireAccount(logsHandler(app))) - - mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) - mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) - mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app)))) + mux.Handle("/music/", core.RequireAccount(http.StripPrefix("/music", music.Handler(app)))) + mux.Handle("/logs", core.RequireAccount(logs.Handler(app))) + mux.Handle("/account/", core.RequireAccount(http.StripPrefix("/account", account.Handler(app)))) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) - - mux.Handle("/", requireAccount(AdminIndexHandler(app))) + mux.Handle("/", core.RequireAccount(AdminIndexHandler(app))) // response wrapper to make sure a session cookie exists - return enforceSession(app, mux) + return core.EnforceSession(app, mux) } func AdminIndexHandler(app *model.AppState) http.Handler { @@ -63,39 +44,21 @@ func AdminIndexHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - releases, err := controller.GetAllReleases(app.DB, false, 0, true) + latestRelease, err := controller.GetLatestRelease(app.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - artists, err := controller.GetAllArtists(app.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - tracks, err := controller.GetOrphanTracks(app.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to pull latest release: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type IndexData struct { - Session *model.Session - Releases []*model.Release - Artists []*model.Artist - Tracks []*model.Track + Session *model.Session + LatestRelease *model.Release } - err = indexTemplate.Execute(w, IndexData{ + err = templates.IndexTemplate.Execute(w, IndexData{ Session: session, - Releases: releases, - Artists: artists, - Tracks: tracks, + LatestRelease: latestRelease, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) @@ -105,361 +68,6 @@ func AdminIndexHandler(app *model.AppState) http.Handler { }) } -func registerAccountHandler(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - if session.Account != nil { - // user is already logged in - http.Redirect(w, r, "/admin", http.StatusFound) - return - } - - type registerData struct { - Session *model.Session - } - - render := func() { - err := registerTemplate.Execute(w, registerData{ Session: session }) - if err != nil { - fmt.Printf("WARN: Error rendering create account page: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - } - - if r.Method == http.MethodGet { - render() - return - } - - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - err := r.ParseForm() - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - Invite string `json:"invite"` - } - credentials := RegisterRequest{ - Username: r.Form.Get("username"), - Email: r.Form.Get("email"), - Password: r.Form.Get("password"), - Invite: r.Form.Get("invite"), - } - - // make sure invite code exists in DB - invite, err := controller.GetInvite(app.DB, credentials.Invite) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - if invite == nil || time.Now().After(invite.ExpiresAt) { - if invite != nil { - err := controller.DeleteInvite(app.DB, invite.Code) - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } - } - controller.SetSessionError(app.DB, session, "Invalid invite code.") - render() - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - - account := model.Account{ - Username: credentials.Username, - Password: string(hashedPassword), - Email: sql.NullString{ String: credentials.Email, Valid: true }, - AvatarURL: sql.NullString{ String: "/img/default-avatar.png", Valid: true }, - } - err = controller.CreateAccount(app.DB, &account) - if err != nil { - if strings.HasPrefix(err.Error(), "pq: duplicate key") { - controller.SetSessionError(app.DB, session, "An account with that username already exists.") - render() - return - } - fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - - app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(app, r)) - - err = controller.DeleteInvite(app.DB, invite.Code) - if err != nil { - app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err) - } - - // registration success! - controller.SetSessionAccount(app.DB, session, &account) - controller.SetSessionMessage(app.DB, session, "") - controller.SetSessionError(app.DB, session, "") - http.Redirect(w, r, "/admin", http.StatusFound) - }) -} - -func loginHandler(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet && r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - session := r.Context().Value("session").(*model.Session) - - type loginData struct { - Session *model.Session - } - - render := func() { - err := loginTemplate.Execute(w, loginData{ Session: session }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - } - - if r.Method == http.MethodGet { - if session.Account != nil { - // user is already logged in - http.Redirect(w, r, "/admin", http.StatusFound) - return - } - render() - return - } - - err := r.ParseForm() - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - if !r.Form.Has("username") || !r.Form.Has("password") { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - username := r.FormValue("username") - password := r.FormValue("password") - - account, err := controller.GetAccountByUsername(app.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err) - controller.SetSessionError(app.DB, session, "Invalid username or password.") - render() - return - } - if account == nil { - controller.SetSessionError(app.DB, session, "Invalid username or password.") - render() - return - } - if account.Locked { - controller.SetSessionError(app.DB, session, "This account is locked.") - render() - return - } - - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) - if err != nil { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r)) - if locked := handleFailedLogin(app, account, r); locked { - controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") - } else { - controller.SetSessionError(app.DB, session, "Invalid username or password.") - } - render() - return - } - - totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - - if len(totps) > 0 { - err = controller.SetSessionAttemptAccount(app.DB, session, account) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to set attempt session: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - controller.SetSessionMessage(app.DB, session, "") - controller.SetSessionError(app.DB, session, "") - http.Redirect(w, r, "/admin/totp", http.StatusFound) - return - } - - // login success! - // TODO: log login activity to user - app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r)) - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) - - err = controller.SetSessionAccount(app.DB, session, account) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - controller.SetSessionMessage(app.DB, session, "") - controller.SetSessionError(app.DB, session, "") - http.Redirect(w, r, "/admin", http.StatusFound) - }) -} - -func loginTOTPHandler(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - if session.AttemptAccount == nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - type loginTOTPData struct { - Session *model.Session - } - - render := func() { - err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - } - - if r.Method == http.MethodGet { - render() - return - } - - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - r.ParseForm() - - if !r.Form.Has("totp") { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - totpCode := r.FormValue("totp") - - if len(totpCode) != controller.TOTP_CODE_LENGTH { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) - controller.SetSessionError(app.DB, session, "Invalid TOTP.") - render() - return - } - - totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.AttemptAccount.ID, totpCode) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to check TOTPs: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - if totpMethod == nil { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) - if locked := handleFailedLogin(app, session.AttemptAccount, r); locked { - controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") - controller.SetSessionAttemptAccount(app.DB, session, nil) - http.Redirect(w, r, "/admin", http.StatusFound) - } else { - controller.SetSessionError(app.DB, session, "Incorrect TOTP.") - } - render() - return - } - - app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r)) - - err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - render() - return - } - err = controller.SetSessionAttemptAccount(app.DB, session, nil) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to clear attempt session: %v\n", err) - } - controller.SetSessionMessage(app.DB, session, "") - controller.SetSessionError(app.DB, session, "") - http.Redirect(w, r, "/admin", http.StatusFound) - }) -} - -func logoutHandler(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.NotFound(w, r) - return - } - - session := r.Context().Value("session").(*model.Session) - err := controller.DeleteSession(app.DB, session.Token) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to delete session: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - http.SetCookie(w, &http.Cookie{ - Name: model.COOKIE_TOKEN, - Expires: time.Now(), - Path: "/", - }) - - err = logoutTemplate.Execute(w, nil) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func requireAccount(next http.Handler) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - if session.Account == nil { - // TODO: include context in redirect - http.Redirect(w, r, "/admin/login", http.StatusFound) - return - } - next.ServeHTTP(w, r) - }) -} - func staticHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) @@ -480,63 +88,3 @@ func staticHandler() http.Handler { http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) }) } - -func enforceSession(app *model.AppState, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session, err := controller.GetSessionFromRequest(app, r) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - if session == nil { - // create a new session - session, err = controller.CreateSession(app.DB, r.UserAgent()) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to create session: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - http.SetCookie(w, &http.Cookie{ - Name: model.COOKIE_TOKEN, - Value: session.Token, - Expires: session.ExpiresAt, - Secure: strings.HasPrefix(app.Config.BaseUrl, "https"), - HttpOnly: true, - Path: "/", - }) - } - - ctx := context.WithValue(r.Context(), "session", session) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool { - locked, err := controller.IncrementAccountFails(app.DB, account.ID) - if err != nil { - fmt.Fprintf( - os.Stderr, - "WARN: Failed to increment login failures for \"%s\": %v\n", - account.Username, - err, - ) - app.Log.Warn( - log.TYPE_ACCOUNT, - "Failed to increment login failures for \"%s\"", - account.Username, - ) - } - if locked { - app.Log.Warn( - log.TYPE_ACCOUNT, - "Account \"%s\" was locked: %d failed login attempts (IP: %s)", - account.Username, - model.MAX_LOGIN_FAIL_ATTEMPTS, - controller.ResolveIP(app, r), - ) - } - return locked -} diff --git a/admin/logshttp.go b/admin/logs/logshttp.go similarity index 88% rename from admin/logshttp.go rename to admin/logs/logshttp.go index 7249b16..5c5c5c9 100644 --- a/admin/logshttp.go +++ b/admin/logs/logshttp.go @@ -1,15 +1,16 @@ -package admin +package logs import ( - "arimelody-web/log" - "arimelody-web/model" - "fmt" - "net/http" - "os" - "strings" + "arimelody-web/admin/templates" + "arimelody-web/log" + "arimelody-web/model" + "fmt" + "net/http" + "os" + "strings" ) -func logsHandler(app *model.AppState) http.Handler { +func Handler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.NotFound(w, r) @@ -54,7 +55,7 @@ func logsHandler(app *model.AppState) http.Handler { Logs []*log.Log } - err = logsTemplate.Execute(w, LogsResponse{ + err = templates.LogsTemplate.Execute(w, LogsResponse{ Session: session, Logs: logs, }) diff --git a/admin/artisthttp.go b/admin/music/artisthttp.go similarity index 88% rename from admin/artisthttp.go rename to admin/music/artisthttp.go index 9fa6bb2..a936d05 100644 --- a/admin/artisthttp.go +++ b/admin/music/artisthttp.go @@ -1,12 +1,13 @@ -package admin +package music import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/model" - "arimelody-web/controller" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveArtist(app *model.AppState) http.Handler { @@ -39,7 +40,7 @@ func serveArtist(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = artistTemplate.Execute(w, ArtistResponse{ + err = templates.ArtistTemplate.Execute(w, ArtistResponse{ Session: session, Artist: artist, Credits: credits, diff --git a/admin/music/musichttp.go b/admin/music/musichttp.go new file mode 100644 index 0000000..c212248 --- /dev/null +++ b/admin/music/musichttp.go @@ -0,0 +1,69 @@ +package music + +import ( + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" + "fmt" + "net/http" + "os" +) + +func Handler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux := http.NewServeMux() + + mux.Handle("/release/", http.StripPrefix("/release", serveRelease(app))) + mux.Handle("/artist/", http.StripPrefix("/artist", serveArtist(app))) + mux.Handle("/track/", http.StripPrefix("/track", serveTrack(app))) + mux.Handle("/", musicHandler(app)) + + mux.ServeHTTP(w, r) + }) +} + +func musicHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + + releases, err := controller.GetAllReleases(app.DB, false, 0, true) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + artists, err := controller.GetAllArtists(app.DB) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + tracks, err := controller.GetOrphanTracks(app.DB) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + type MusicData struct { + Session *model.Session + Releases []*model.Release + Artists []*model.Artist + Tracks []*model.Track + } + + err = templates.MusicTemplate.Execute(w, MusicData{ + Session: session, + Releases: releases, + Artists: artists, + Tracks: tracks, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} diff --git a/admin/releasehttp.go b/admin/music/releasehttp.go similarity index 91% rename from admin/releasehttp.go rename to admin/music/releasehttp.go index c6b68ab..6716b1a 100644 --- a/admin/releasehttp.go +++ b/admin/music/releasehttp.go @@ -1,12 +1,13 @@ -package admin +package music import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveRelease(app *model.AppState) http.Handler { @@ -60,7 +61,7 @@ func serveRelease(app *model.AppState) http.Handler { Release *model.Release } - err = releaseTemplate.Execute(w, ReleaseResponse{ + err = templates.ReleaseTemplate.Execute(w, ReleaseResponse{ Session: session, Release: release, }) @@ -74,7 +75,7 @@ func serveRelease(app *model.AppState) http.Handler { func serveEditCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := editCreditsTemplate.Execute(w, release) + err := templates.EditCreditsTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -97,7 +98,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = addCreditTemplate.Execute(w, response{ + err = templates.AddCreditTemplate.Execute(w, response{ ReleaseID: release.ID, Artists: artists, }) @@ -123,7 +124,7 @@ func serveNewCredit(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = newCreditTemplate.Execute(w, artist) + err = templates.NewCreditTemplate.Execute(w, artist) if err != nil { fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -134,7 +135,7 @@ func serveNewCredit(app *model.AppState) http.Handler { func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := editLinksTemplate.Execute(w, release) + err := templates.EditLinksTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -151,7 +152,7 @@ func serveEditTracks(release *model.Release) http.Handler { Add func(a int, b int) int } - err := editTracksTemplate.Execute(w, editTracksData{ + err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release, Add: func(a, b int) int { return a + b }, }) @@ -177,7 +178,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = addTrackTemplate.Execute(w, response{ + err = templates.AddTrackTemplate.Execute(w, response{ ReleaseID: release.ID, Tracks: tracks, }) @@ -204,7 +205,7 @@ func serveNewTrack(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = newTrackTemplate.Execute(w, track) + err = templates.NewTrackTemplate.Execute(w, track) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/trackhttp.go b/admin/music/trackhttp.go similarity index 88% rename from admin/trackhttp.go rename to admin/music/trackhttp.go index 93eacdb..5f67a83 100644 --- a/admin/trackhttp.go +++ b/admin/music/trackhttp.go @@ -1,12 +1,13 @@ -package admin +package music import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/model" - "arimelody-web/controller" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveTrack(app *model.AppState) http.Handler { @@ -39,7 +40,7 @@ func serveTrack(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = trackTemplate.Execute(w, TrackResponse{ + err = templates.TrackTemplate.Execute(w, TrackResponse{ Session: session, Track: track, Releases: releases, diff --git a/admin/static/admin.css b/admin/static/admin.css index 877b5da..99dc276 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -27,7 +27,10 @@ nav { border-radius: 4px; border: 1px solid #808080; } -nav .icon { +.nav-item.icon { + padding: 0; +} +.nav-item.icon img { height: 100%; } nav .title { @@ -92,6 +95,10 @@ code { border-radius: 4px; } +h1 { + margin: 0 0 .5em 0; +} + .card { diff --git a/admin/static/index.css b/admin/static/index.css index 9fcd731..0e5121f 100644 --- a/admin/static/index.css +++ b/admin/static/index.css @@ -1,82 +1 @@ @import url("/admin/static/release-list-item.css"); - -.artist { - margin-bottom: .5em; - padding: .5em; - display: flex; - flex-direction: row; - align-items: center; - gap: .5em; - - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.artist:hover { - text-decoration: hover; -} - -.artist-avatar { - width: 32px; - height: 32px; - object-fit: cover; - border-radius: 100%; -} - -.track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.track p { - margin: 0; -} - -.card h2.track-title { - margin: 0; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.track-id { - width: fit-content; - font-family: "Monaspace Argon", monospace; - font-size: .8em; - font-style: italic; - line-height: 1em; - user-select: all; -} - -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; -} diff --git a/admin/static/index.js b/admin/static/index.js index e251802..e69de29 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -1,74 +0,0 @@ -const newReleaseBtn = document.getElementById("create-release"); -const newArtistBtn = document.getElementById("create-artist"); -const newTrackBtn = document.getElementById("create-track"); - -newReleaseBtn.addEventListener("click", event => { - event.preventDefault(); - const id = prompt("Enter an ID for this release:"); - if (id == null || id == "") return; - - fetch("/api/v1/music", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({id}) - }).then(res => { - if (res.ok) location = "/admin/release/" + id; - else { - res.text().then(err => { - alert("Request failed: " + err); - console.error(err); - }); - } - }).catch(err => { - alert("Failed to create release. Check the console for details."); - console.error(err); - }); -}); - -newArtistBtn.addEventListener("click", event => { - event.preventDefault(); - const id = prompt("Enter an ID for this artist:"); - if (id == null || id == "") return; - - fetch("/api/v1/artist", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({id}) - }).then(res => { - res.text().then(text => { - if (res.ok) { - location = "/admin/artist/" + id; - } else { - alert("Request failed: " + text); - console.error(text); - } - }) - }).catch(err => { - alert("Failed to create artist. Check the console for details."); - console.error(err); - }); -}); - -newTrackBtn.addEventListener("click", event => { - event.preventDefault(); - const title = prompt("Enter an title for this track:"); - if (title == null || title == "") return; - - fetch("/api/v1/track", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({title}) - }).then(res => { - res.text().then(text => { - if (res.ok) { - location = "/admin/track/" + text; - } else { - alert("Request failed: " + text); - console.error(text); - } - }) - }).catch(err => { - alert("Failed to create track. Check the console for details."); - console.error(err); - }); -}); diff --git a/admin/static/music.css b/admin/static/music.css new file mode 100644 index 0000000..9fcd731 --- /dev/null +++ b/admin/static/music.css @@ -0,0 +1,82 @@ +@import url("/admin/static/release-list-item.css"); + +.artist { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: .5em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.artist:hover { + text-decoration: hover; +} + +.artist-avatar { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 100%; +} + +.track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.track p { + margin: 0; +} + +.card h2.track-title { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.track-id { + width: fit-content; + font-family: "Monaspace Argon", monospace; + font-size: .8em; + font-style: italic; + line-height: 1em; + user-select: all; +} + +.track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.track-album.empty { + color: #ff2020; + opacity: 1; +} + +.track-description { + font-style: italic; +} + +.track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.track .empty { + opacity: 0.75; +} diff --git a/admin/static/music.js b/admin/static/music.js new file mode 100644 index 0000000..6e4a7a9 --- /dev/null +++ b/admin/static/music.js @@ -0,0 +1,74 @@ +const newReleaseBtn = document.getElementById("create-release"); +const newArtistBtn = document.getElementById("create-artist"); +const newTrackBtn = document.getElementById("create-track"); + +newReleaseBtn.addEventListener("click", event => { + event.preventDefault(); + const id = prompt("Enter an ID for this release:"); + if (id == null || id == "") return; + + fetch("/api/v1/music", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({id}) + }).then(res => { + if (res.ok) location = "/admin/music/release/" + id; + else { + res.text().then(err => { + alert("Request failed: " + err); + console.error(err); + }); + } + }).catch(err => { + alert("Failed to create release. Check the console for details."); + console.error(err); + }); +}); + +newArtistBtn.addEventListener("click", event => { + event.preventDefault(); + const id = prompt("Enter an ID for this artist:"); + if (id == null || id == "") return; + + fetch("/api/v1/artist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({id}) + }).then(res => { + res.text().then(text => { + if (res.ok) { + location = "/admin/music/artist/" + id; + } else { + alert("Request failed: " + text); + console.error(text); + } + }) + }).catch(err => { + alert("Failed to create artist. Check the console for details."); + console.error(err); + }); +}); + +newTrackBtn.addEventListener("click", event => { + event.preventDefault(); + const title = prompt("Enter an title for this track:"); + if (title == null || title == "") return; + + fetch("/api/v1/track", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({title}) + }).then(res => { + res.text().then(text => { + if (res.ok) { + location = "/admin/music/track/" + text; + } else { + alert("Request failed: " + text); + console.error(text); + } + }) + }).catch(err => { + alert("Failed to create track. Check the console for details."); + console.error(err); + }); +}); diff --git a/admin/templates.go b/admin/templates.go deleted file mode 100644 index 4b7c10c..0000000 --- a/admin/templates.go +++ /dev/null @@ -1,125 +0,0 @@ -package admin - -import ( - "arimelody-web/log" - "fmt" - "html/template" - "path/filepath" - "strings" - "time" -) - -var indexTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "index.html"), -)) - -var loginTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "login.html"), -)) -var loginTOTPTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "login-totp.html"), -)) -var registerTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "register.html"), -)) -var logoutTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "logout.html"), -)) -var accountTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "edit-account.html"), -)) -var totpSetupTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "totp-setup.html"), -)) -var totpConfirmTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "totp-confirm.html"), -)) - -var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{ - "parseLevel": func(level log.LogLevel) string { - switch level { - case log.LEVEL_INFO: - return "INFO" - case log.LEVEL_WARN: - return "WARN" - } - return fmt.Sprintf("%d?", level) - }, - "titleCase": func(logType string) string { - runes := []rune(logType) - for i, r := range runes { - if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { - runes[i] = r + ('A' - 'a') - } - } - return string(runes) - }, - "lower": func(str string) string { return strings.ToLower(str) }, - "prettyTime": func(t time.Time) string { - // return t.Format("2006-01-02 15:04:05") - // return t.Format("15:04:05, 2 Jan 2006") - return t.Format("02 Jan 2006, 15:04:05") - }, -}).ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "logs.html"), -)) - -var releaseTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "edit-release.html"), -)) -var artistTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "views", "edit-artist.html"), -)) -var trackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("view", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "edit-track.html"), -)) - -var editCreditsTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "editcredits.html"), -)) -var addCreditTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "addcredit.html"), -)) -var newCreditTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "newcredit.html"), -)) - -var editLinksTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "links", "editlinks.html"), -)) - -var editTracksTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "edittracks.html"), -)) -var addTrackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "addtrack.html"), -)) -var newTrackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "newtrack.html"), -)) diff --git a/admin/templates/index.go b/admin/templates/index.go new file mode 100644 index 0000000..e66c6cd --- /dev/null +++ b/admin/templates/index.go @@ -0,0 +1,13 @@ +package templates + +import ( + "html/template" + "path/filepath" +) + +var IndexTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "index.html"), +)) diff --git a/admin/templates/login.go b/admin/templates/login.go new file mode 100644 index 0000000..55e7a57 --- /dev/null +++ b/admin/templates/login.go @@ -0,0 +1,42 @@ +package templates + +import ( + "html/template" + "path/filepath" +) + +var LoginTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "login.html"), +)) +var LoginTOTPTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "login-totp.html"), +)) +var RegisterTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "register.html"), +)) +var LogoutTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "logout.html"), +)) +var AccountTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "edit-account.html"), +)) +var TotpSetupTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "totp-setup.html"), +)) +var TotpConfirmTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "totp-confirm.html"), +)) diff --git a/admin/templates/logs.go b/admin/templates/logs.go new file mode 100644 index 0000000..7d4eccb --- /dev/null +++ b/admin/templates/logs.go @@ -0,0 +1,41 @@ +package templates + +import ( + "arimelody-web/log" + "fmt" + "html/template" + "path/filepath" + "strings" + "time" +) + +var LogsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{ + "parseLevel": func(level log.LogLevel) string { + switch level { + case log.LEVEL_INFO: + return "INFO" + case log.LEVEL_WARN: + return "WARN" + } + return fmt.Sprintf("%d?", level) + }, + "titleCase": func(logType string) string { + runes := []rune(logType) + for i, r := range runes { + if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { + runes[i] = r + ('A' - 'a') + } + } + return string(runes) + }, + "lower": func(str string) string { return strings.ToLower(str) }, + "prettyTime": func(t time.Time) string { + // return t.Format("2006-01-02 15:04:05") + // return t.Format("15:04:05, 2 Jan 2006") + return t.Format("02 Jan 2006, 15:04:05") + }, +}).ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "logs.html"), +)) diff --git a/admin/templates/music.go b/admin/templates/music.go new file mode 100644 index 0000000..3e9a772 --- /dev/null +++ b/admin/templates/music.go @@ -0,0 +1,54 @@ +package templates + +import ( + "html/template" + "path/filepath" +) + +var MusicTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "music.html"), +)) + +var ReleaseTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "edit-release.html"), +)) +var ArtistTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "views", "edit-artist.html"), +)) +var TrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("view", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "edit-track.html"), +)) + +var EditCreditsTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "editcredits.html"), +)) +var AddCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "addcredit.html"), +)) +var NewCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "newcredit.html"), +)) + +var EditLinksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "links", "editlinks.html"), +)) + +var EditTracksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "edittracks.html"), +)) +var AddTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "addtrack.html"), +)) +var NewTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "newtrack.html"), +)) diff --git a/admin/views/edit-artist.html b/admin/views/edit-artist.html index b0cfb41..f0e03dc 100644 --- a/admin/views/edit-artist.html +++ b/admin/views/edit-artist.html @@ -38,7 +38,7 @@
    -

    {{.Release.Title}}

    +

    {{.Release.Title}}

    {{.Release.PrintArtists true true}}

    Role: {{.Role}} diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html index 02447e1..34ee86e 100644 --- a/admin/views/edit-release.html +++ b/admin/views/edit-release.html @@ -100,8 +100,8 @@

    Credits ({{len .Release.Credits}})

    Edit @@ -111,7 +111,7 @@
    -

    {{.Artist.Name}}

    +

    {{.Artist.Name}}

    {{.Role}} {{if .Primary}} @@ -129,8 +129,8 @@

    Links ({{len .Release.Links}})

    Edit @@ -144,8 +144,8 @@

    Tracklist ({{len .Release.Tracks}})

    Edit @@ -155,7 +155,7 @@

    {{.Add $i 1}} - {{$track.Title}} + {{$track.Title}}

    Description

    diff --git a/admin/views/index.html b/admin/views/index.html index 2b9c897..6032849 100644 --- a/admin/views/index.html +++ b/admin/views/index.html @@ -6,65 +6,19 @@ {{define "content"}}
    - +

    Admin Dashboard

    -

    Releases

    - Create New +

    Music

    + Browse All
    -
    - {{range .Releases}} - {{block "release" .}}{{end}} - {{end}} - {{if not .Releases}} +
    + {{if .LatestRelease}} +

    Latest Release

    + {{block "release" .LatestRelease}}{{end}} + {{else}}

    There are no releases.

    {{end}}
    - -
    -

    Artists

    - Create New -
    -
    - {{range $Artist := .Artists}} - - {{end}} - {{if not .Artists}} -

    There are no artists.

    - {{end}} -
    - -
    -

    Tracks

    - Create New -
    -
    -

    "Orphaned" tracks that have not yet been bound to a release.

    -
    - {{range $Track := .Tracks}} -
    -

    - {{$Track.Title}} -

    - {{if $Track.Description}} -

    {{$Track.GetDescriptionHTML}}

    - {{else}} -

    No description provided.

    - {{end}} - {{if $Track.Lyrics}} -

    {{$Track.GetLyricsHTML}}

    - {{else}} -

    There are no lyrics.

    - {{end}} -
    - {{end}} - {{if not .Artists}} -

    There are no artists.

    - {{end}} -
    -
    diff --git a/admin/views/layout.html b/admin/views/layout.html index 52b0620..ac46263 100644 --- a/admin/views/layout.html +++ b/admin/views/layout.html @@ -16,14 +16,19 @@