diff --git a/.goreleaser.yml b/.goreleaser.yml index a49bb32e8..8be51ded4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -27,6 +27,8 @@ builds: - static_build - kvformat - timetzdata + - >- + {{ if and (index .Env "DEBUG") (.Env.DEBUG) }}debugenv{{ end }} env: - CGO_ENABLED=0 goos: diff --git a/internal/api/auth/authorize.go b/internal/api/auth/authorize.go index 4977ae4f2..e4694de57 100644 --- a/internal/api/auth/authorize.go +++ b/internal/api/auth/authorize.go @@ -144,17 +144,25 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { return } - // the authorize template will display a form to the user where they can get some information - // about the app that's trying to authorize, and the scope of the request. - // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler - c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ - "appname": app.Name, - "appwebsite": app.Website, - "redirect": redirect, - "scope": scope, - "user": acct.Username, - "instance": instance, - }) + // The authorize template will display a form + // to the user where they can see some info + // about the app that's trying to authorize, + // and the scope of the request. They can then + // approve it if it looks OK to them, which + // will POST to the AuthorizePOSTHandler. + page := apiutil.WebPage{ + Template: "authorize.tmpl", + Instance: instance, + Extra: map[string]any{ + "appname": app.Name, + "appwebsite": app.Website, + "redirect": redirect, + "scope": scope, + "user": acct.Username, + }, + } + + apiutil.TemplateWebPage(c, page) } // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize diff --git a/internal/api/auth/callback.go b/internal/api/auth/callback.go index 97b3ae279..d0fa78322 100644 --- a/internal/api/auth/callback.go +++ b/internal/api/auth/callback.go @@ -143,11 +143,17 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "finalize.tmpl", gin.H{ - "instance": instance, - "name": claims.Name, - "preferredUsername": claims.PreferredUsername, - }) + + page := apiutil.WebPage{ + Template: "finalize.tmpl", + Instance: instance, + Extra: map[string]any{ + "name": claims.Name, + "preferredUsername": claims.PreferredUsername, + }, + } + + apiutil.TemplateWebPage(c, page) return } s.Set(sessionUserID, user.ID) @@ -177,12 +183,18 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "finalize.tmpl", gin.H{ - "instance": instance, - "name": form.Name, - "preferredUsername": form.Username, - "error": err, - }) + + page := apiutil.WebPage{ + Template: "finalize.tmpl", + Instance: instance, + Extra: map[string]any{ + "name": form.Name, + "preferredUsername": form.Username, + "error": err, + }, + } + + apiutil.TemplateWebPage(c, page) } // check if the username conforms to the spec diff --git a/internal/api/auth/oob.go b/internal/api/auth/oob.go index 5953524ab..8c7b1f2a5 100644 --- a/internal/api/auth/oob.go +++ b/internal/api/auth/oob.go @@ -21,7 +21,6 @@ "context" "errors" "fmt" - "net/http" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -101,10 +100,15 @@ func (m *Module) OobHandler(c *gin.Context) { // we're done with the session now, so just clear it out m.clearSession(s) - c.HTML(http.StatusOK, "oob.tmpl", gin.H{ - "instance": instance, - "user": acct.Username, - "oobToken": oobToken, - "scope": scope, - }) + page := apiutil.WebPage{ + Template: "oob.tmpl", + Instance: instance, + Extra: map[string]any{ + "user": acct.Username, + "oobToken": oobToken, + "scope": scope, + }, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/api/auth/signin.go b/internal/api/auth/signin.go index a6b503a83..a8713d05f 100644 --- a/internal/api/auth/signin.go +++ b/internal/api/auth/signin.go @@ -32,8 +32,8 @@ "golang.org/x/crypto/bcrypt" ) -// login just wraps a form-submitted username (we want an email) and password -type login struct { +// signIn just wraps a form-submitted username (we want an email) and password +type signIn struct { Email string `form:"username"` Password string `form:"password"` } @@ -55,10 +55,12 @@ func (m *Module) SignInGETHandler(c *gin.Context) { return } - // no idp provider, use our own funky little sign in page - c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{ - "instance": instance, - }) + page := apiutil.WebPage{ + Template: "sign-in.tmpl", + Instance: instance, + } + + apiutil.TemplateWebPage(c, page) return } @@ -83,7 +85,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) { func (m *Module) SignInPOSTHandler(c *gin.Context) { s := sessions.Default(c) - form := &login{} + form := &signIn{} if err := c.ShouldBind(form); err != nil { m.clearSession(s) apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) @@ -129,7 +131,7 @@ func (m *Module) ValidatePassword(ctx context.Context, email string, password st } if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil { - err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err) + err := fmt.Errorf("password hash didn't match for user %s during sign in attempt: %s", user.Email, err) return incorrectPassword(err) } diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 128cd65bb..8ca41c767 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -116,6 +116,12 @@ type Status struct { // // swagger:ignore WebPollOptions []WebPollOption `json:"-"` + + // Status is from a local account. + // Always false for non-web statuses. + // + // swagger:ignore + Local bool `json:"-"` } /* diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go index 8bb251040..848beff5b 100644 --- a/internal/api/util/errorhandling.go +++ b/internal/api/util/errorhandling.go @@ -50,10 +50,10 @@ func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*api panic(err) } - c.HTML(http.StatusNotFound, "404.tmpl", gin.H{ - "instance": instance, - "requestID": gtscontext.RequestID(ctx), - }) + template404Page(c, + instance, + gtscontext.RequestID(ctx), + ) default: JSON(c, http.StatusNotFound, map[string]string{ "error": errWithCode.Safe(), @@ -73,12 +73,12 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) ( panic(err) } - c.HTML(errWithCode.Code(), "error.tmpl", gin.H{ - "instance": instance, - "code": errWithCode.Code(), - "error": errWithCode.Safe(), - "requestID": gtscontext.RequestID(ctx), - }) + templateErrorPage(c, + instance, + errWithCode.Code(), + errWithCode.Safe(), + gtscontext.RequestID(ctx), + ) default: JSON(c, errWithCode.Code(), map[string]string{ "error": errWithCode.Safe(), diff --git a/internal/web/opengraph.go b/internal/api/util/opengraph.go similarity index 74% rename from internal/web/opengraph.go rename to internal/api/util/opengraph.go index 66b6c6eea..185dc8132 100644 --- a/internal/web/opengraph.go +++ b/internal/api/util/opengraph.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package web +package util import ( "html" @@ -28,10 +28,10 @@ const maxOGDescriptionLength = 300 -// ogMeta represents supported OpenGraph Meta tags +// OGMeta represents supported OpenGraph Meta tags // // see eg https://ogp.me/ -type ogMeta struct { +type OGMeta struct { // vanilla og tags Title string // og:title Type string // og:type @@ -56,23 +56,23 @@ type ogMeta struct { ProfileUsername string // profile:username } -// ogBase returns an *ogMeta suitable for serving at +// OGBase returns an *ogMeta suitable for serving at // the base root of an instance. It also serves as a // foundation for building account / status ogMeta on // top of. -func ogBase(instance *apimodel.InstanceV1) *ogMeta { +func OGBase(instance *apimodel.InstanceV1) *OGMeta { var locale string if len(instance.Languages) > 0 { locale = instance.Languages[0] } - og := &ogMeta{ + og := &OGMeta{ Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial", Type: "website", Locale: locale, URL: instance.URI, SiteName: instance.AccountDomain, - Description: parseDescription(instance.ShortDescription), + Description: ParseDescription(instance.ShortDescription), Image: instance.Thumbnail, ImageAlt: instance.ThumbnailDescription, @@ -81,15 +81,15 @@ func ogBase(instance *apimodel.InstanceV1) *ogMeta { return og } -// withAccount uses the given account to build an ogMeta +// WithAccount uses the given account to build an ogMeta // struct specific to that account. It's suitable for serving // at account profile pages. -func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta { - og.Title = parseTitle(account, og.SiteName) +func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta { + og.Title = AccountTitle(account, og.SiteName) og.Type = "profile" og.URL = account.URL if account.Note != "" { - og.Description = parseDescription(account.Note) + og.Description = ParseDescription(account.Note) } else { og.Description = `content="This GoToSocial user hasn't written a bio yet!"` } @@ -102,11 +102,11 @@ func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta { return og } -// withStatus uses the given status to build an ogMeta +// WithStatus uses the given status to build an ogMeta // struct specific to that status. It's suitable for serving // at status pages. -func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta { - og.Title = "Post by " + parseTitle(status.Account, og.SiteName) +func (og *OGMeta) WithStatus(status *apimodel.Status) *OGMeta { + og.Title = "Post by " + AccountTitle(status.Account, og.SiteName) og.Type = "article" if status.Language != nil { og.Locale = *status.Language @@ -114,9 +114,9 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta { og.URL = status.URL switch { case status.SpoilerText != "": - og.Description = parseDescription("CW: " + status.SpoilerText) + og.Description = ParseDescription("CW: " + status.SpoilerText) case status.Text != "": - og.Description = parseDescription(status.Text) + og.Description = ParseDescription(status.Text) default: og.Description = og.Title } @@ -147,34 +147,38 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta { return og } -// parseTitle parses a page title from account and accountDomain -func parseTitle(account *apimodel.Account, accountDomain string) string { +// AccountTitle parses a page title from account and accountDomain +func AccountTitle(account *apimodel.Account, accountDomain string) string { user := "@" + account.Acct + "@" + accountDomain if len(account.DisplayName) == 0 { return user } - return account.DisplayName + " (" + user + ")" + return account.DisplayName + ", " + user } -// parseDescription returns a string description which is +// ParseDescription returns a string description which is // safe to use as a template.HTMLAttr inside templates. -func parseDescription(in string) string { +func ParseDescription(in string) string { i := text.SanitizeToPlaintext(in) i = strings.ReplaceAll(i, "\n", " ") i = strings.Join(strings.Fields(i), " ") i = html.EscapeString(i) i = strings.ReplaceAll(i, `\`, "\") - i = trim(i, maxOGDescriptionLength) + i = truncate(i, maxOGDescriptionLength) return `content="` + i + `"` } -// trim strings trim s to specified length -func trim(s string, length int) string { - if len(s) < length { +// truncate trims given string to +// specified length (in runes). +func truncate(s string, l int) string { + r := []rune(s) + if len(r) < l { + // No need + // to trim. return s } - return s[:length] + return string(r[:l]) + "..." } diff --git a/internal/web/opengraph_test.go b/internal/api/util/opengraph_test.go similarity index 87% rename from internal/web/opengraph_test.go rename to internal/api/util/opengraph_test.go index 06e97cdce..2ecd6a740 100644 --- a/internal/web/opengraph_test.go +++ b/internal/api/util/opengraph_test.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package web +package util import ( "fmt" @@ -40,18 +40,18 @@ func (suite *OpenGraphTestSuite) TestParseDescription() { for _, tt := range tests { tt := tt suite.Run(tt.name, func() { - suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), parseDescription(tt.in)) + suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), ParseDescription(tt.in)) }) } } func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { - baseMeta := ogBase(&apimodel.InstanceV1{ + baseMeta := OGBase(&apimodel.InstanceV1{ AccountDomain: "example.org", Languages: []string{"en"}, }) - accountMeta := baseMeta.withAccount(&apimodel.Account{ + accountMeta := baseMeta.WithAccount(&apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", @@ -59,8 +59,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { Username: "example_account", }) - suite.EqualValues(ogMeta{ - Title: "example person!! (@example_account@example.org)", + suite.EqualValues(OGMeta{ + Title: "example person!!, @example_account@example.org", Type: "profile", Locale: "en", URL: "https://example.org/@example_account", @@ -79,12 +79,12 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { } func (suite *OpenGraphTestSuite) TestWithAccountNoNote() { - baseMeta := ogBase(&apimodel.InstanceV1{ + baseMeta := OGBase(&apimodel.InstanceV1{ AccountDomain: "example.org", Languages: []string{"en"}, }) - accountMeta := baseMeta.withAccount(&apimodel.Account{ + accountMeta := baseMeta.WithAccount(&apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", @@ -92,8 +92,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() { Username: "example_account", }) - suite.EqualValues(ogMeta{ - Title: "example person!! (@example_account@example.org)", + suite.EqualValues(OGMeta{ + Title: "example person!!, @example_account@example.org", Type: "profile", Locale: "en", URL: "https://example.org/@example_account", diff --git a/internal/api/util/template.go b/internal/api/util/template.go new file mode 100644 index 000000000..b8c710c3c --- /dev/null +++ b/internal/api/util/template.go @@ -0,0 +1,135 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +) + +// WebPage encapsulates variables for +// rendering an HTML template within +// a standard GtS "page" template. +type WebPage struct { + // Name of the template for rendering + // the page. Eg., "example.tmpl". + Template string + + // Instance model for rendering header, + // footer, and "about" information. + Instance *apimodel.InstanceV1 + + // OGMeta for rendering page + // "meta:og*" tags. Can be nil. + OGMeta *OGMeta + + // Paths to CSS files to add to + // the page as "stylesheet" entries. + // Can be nil. + Stylesheets []string + + // Paths to JS files to add to + // the page as "script" entries. + // Can be nil. + Javascript []string + + // Extra parameters to pass to + // the template for rendering, + // eg., "account": *Account etc. + // Can be nil. + Extra map[string]any +} + +// TemplateWebPage renders the given HTML template and +// page params within the standard GtS "page" template. +// +// ogMeta, stylesheets, javascript, and any extra +// properties will be provided to the template if +// set, but can all be nil. +func TemplateWebPage( + c *gin.Context, + page WebPage, +) { + obj := map[string]any{ + "instance": page.Instance, + "ogMeta": page.OGMeta, + "stylesheets": page.Stylesheets, + "javascript": page.Javascript, + } + + for k, v := range page.Extra { + obj[k] = v + } + + templatePage(c, page.Template, http.StatusOK, obj) +} + +// templateErrorPage renders the given +// HTTP code, error, and request ID +// within the standard error template. +func templateErrorPage( + c *gin.Context, + instance *apimodel.InstanceV1, + code int, + err string, + requestID string, +) { + const errorTmpl = "error.tmpl" + + obj := map[string]any{ + "instance": instance, + "code": code, + "error": err, + "requestID": requestID, + } + + templatePage(c, errorTmpl, code, obj) +} + +// template404Page renders +// a standard 404 page. +func template404Page( + c *gin.Context, + instance *apimodel.InstanceV1, + requestID string, +) { + const notFoundTmpl = "404.tmpl" + + obj := map[string]any{ + "instance": instance, + "requestID": requestID, + } + + templatePage(c, notFoundTmpl, http.StatusNotFound, obj) +} + +// render the given template inside +// "page.tmpl" with the provided +// code and template object. +func templatePage( + c *gin.Context, + template string, + code int, + obj map[string]any, +) { + const pageTmpl = "page.tmpl" + obj["pageContent"] = template + c.HTML(code, pageTmpl, obj) +} diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 97e6812c5..3e4519479 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -56,8 +56,8 @@ OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning // HelpfulAdvice is a handy hint to users; // particularly important during the login flow - HelpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials" - HelpfulAdviceGrant = "If you arrived at this error during a login/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client" + HelpfulAdvice = "If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials" + HelpfulAdviceGrant = "If you arrived at this error during a sign in/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client" ) // Server wraps some oauth2 server functions in an interface, exposing only what is needed diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go index e528c27e0..6ae285f9e 100644 --- a/internal/processing/account/rss_test.go +++ b/internal/processing/account/rss_test.go @@ -39,7 +39,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { fmt.Println(feed) - suite.Equal("\n \n Posts from @admin@localhost:8080\n http://localhost:8080/@admin\n Posts from @admin@localhost:8080\n Wed, 20 Oct 2021 12:36:45 +0000\n Wed, 20 Oct 2021 12:36:45 +0000\n \n open to see some puppies\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"\n \n @admin@localhost:8080\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n Wed, 20 Oct 2021 12:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n hello world! #welcome ! first post on the instance :rainbow: !\n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"\n !]]>\n @admin@localhost:8080\n \n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n Wed, 20 Oct 2021 11:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n", feed) + suite.Equal("\n \n Posts from @admin@localhost:8080\n http://localhost:8080/@admin\n Posts from @admin@localhost:8080\n Wed, 20 Oct 2021 12:36:45 +0000\n Wed, 20 Oct 2021 12:36:45 +0000\n \n open to see some puppies\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"\n \n @admin@localhost:8080\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n Wed, 20 Oct 2021 12:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n hello world! #welcome ! first post on the instance :rainbow: !\n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"\n !]]>\n @admin@localhost:8080\n \n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n Wed, 20 Oct 2021 11:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n", feed) } func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { diff --git a/internal/router/router.go b/internal/router/router.go index f71dc97ef..b2fb7418e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -83,7 +83,6 @@ func New(ctx context.Context) (*Router, error) { // Attach functions used by HTML templating, // and load HTML templates into the engine. - LoadTemplateFunctions(engine) if err := LoadTemplates(engine); err != nil { return nil, err } diff --git a/internal/router/template.go b/internal/router/template.go index d1f6f297c..981c3fcf4 100644 --- a/internal/router/template.go +++ b/internal/router/template.go @@ -18,52 +18,121 @@ package router import ( + "bytes" "fmt" "html/template" "os" "path/filepath" "reflect" + "regexp" "strings" "time" "unsafe" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/render" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/regexes" "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/util" ) -const ( - justTime = "15:04" - dateYear = "Jan 02, 2006" - dateTime = "Jan 02, 15:04" - dateYearTime = "Jan 02, 2006, 15:04" - monthYear = "Jan, 2006" - badTimestamp = "bad timestamp" -) - -// LoadTemplates loads html templates for use by the given engine +// LoadTemplates loads templates found at `web-template-base-dir` +// into the Gin engine, or errors if templates cannot be loaded. +// +// The special functions "include" and "includeAttr" will be added +// to the template funcMap for use in any template. Use these "include" +// functions when you need to pass a template through a pipeline. +// Otherwise, prefer the built-in "template" function. func LoadTemplates(engine *gin.Engine) error { templateBaseDir := config.GetWebTemplateBaseDir() if templateBaseDir == "" { - return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebTemplateBaseDirFlag()) + return gtserror.Newf( + "%s cannot be empty and must be a relative or absolute path", + config.WebTemplateBaseDirFlag(), + ) } - templateBaseDir, err := filepath.Abs(templateBaseDir) + templateDirAbs, err := filepath.Abs(templateBaseDir) if err != nil { - return fmt.Errorf("error getting absolute path of %s: %s", templateBaseDir, err) + return gtserror.Newf( + "error getting absolute path of web-template-base-dir %s: %w", + templateBaseDir, err, + ) } - if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil { - return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err) + indexTmplPath := filepath.Join(templateDirAbs, "index.tmpl") + if _, err := os.Stat(indexTmplPath); err != nil { + return gtserror.Newf( + "cannot find index.tmpl in web template directory %s: %w", + templateDirAbs, err, + ) } - engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*")) + // Bring base template into scope. + tmpl := template.New("base") + + // Set additional "include" functions to render + // provided template name using the base template. + funcMap["include"] = func(name string, data any) (template.HTML, error) { + var buf strings.Builder + err := tmpl.ExecuteTemplate(&buf, name, data) + + // Template was already escaped by + // ExecuteTemplate so we can trust it. + return noescape(buf.String()), err + } + + funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) { + var buf strings.Builder + err := tmpl.ExecuteTemplate(&buf, name, data) + + // Template was already escaped by + // ExecuteTemplate so we can trust it. + return noescapeAttr(buf.String()), err + } + + // Load functions into the base template, and + // associate other templates with base template. + templateGlob := filepath.Join(templateDirAbs, "*") + tmpl, err = tmpl.Funcs(funcMap).ParseGlob(templateGlob) + if err != nil { + return gtserror.Newf("error loading templates: %w", err) + } + + // Almost done; teach the + // engine how to render. + engine.SetFuncMap(funcMap) + engine.HTMLRender = render.HTMLProduction{Template: tmpl} + return nil } +var funcMap = template.FuncMap{ + "add": add, + "acctInstance": acctInstance, + "demojify": demojify, + "deref": deref, + "emojify": emojify, + "escape": escape, + "increment": increment, + "indent": indent, + "indentAttr": indentAttr, + "isNil": isNil, + "outdentPre": outdentPre, + "noescapeAttr": noescapeAttr, + "noescape": noescape, + "oddOrEven": oddOrEven, + "subtract": subtract, + "timestampPrecise": timestampPrecise, + "timestamp": timestamp, + "timestampVague": timestampVague, + "visibilityIcon": visibilityIcon, +} + func oddOrEven(n int) string { if n%2 == 0 { return "even" @@ -71,21 +140,40 @@ func oddOrEven(n int) string { return "odd" } +// escape HTML escapes the given string, +// returning a trusted template. func escape(str string) template.HTML { /* #nosec G203 */ return template.HTML(template.HTMLEscapeString(str)) } +// noescape marks the given string as a +// trusted template. The provided string +// MUST have already passed through a +// template or escaping function. func noescape(str string) template.HTML { /* #nosec G203 */ return template.HTML(str) } +// noescapeAttr marks the given string as a +// trusted HTML attribute. The provided string +// MUST have already passed through a template +// or escaping function. func noescapeAttr(str string) template.HTMLAttr { /* #nosec G203 */ return template.HTMLAttr(str) } +const ( + justTime = "15:04" + dateYear = "Jan 02, 2006" + dateTime = "Jan 02, 15:04" + dateYearTime = "Jan 02, 2006, 15:04" + monthYear = "Jan, 2006" + badTimestamp = "bad timestamp" +) + func timestamp(stamp string) string { t, err := util.ParseISO8601(stamp) if err != nil { @@ -127,38 +215,55 @@ func timestampVague(stamp string) string { return t.Format(monthYear) } -type iconWithLabel struct { - faIcon string - label string -} - func visibilityIcon(visibility apimodel.Visibility) template.HTML { - var icon iconWithLabel + var ( + label string + icon string + ) switch visibility { case apimodel.VisibilityPublic: - icon = iconWithLabel{"globe", "public"} + label = "public" + icon = "globe" case apimodel.VisibilityUnlisted: - icon = iconWithLabel{"unlock", "unlisted"} + label = "unlisted" + icon = "unlock" case apimodel.VisibilityPrivate: - icon = iconWithLabel{"lock", "private"} + label = "private" + icon = "lock" case apimodel.VisibilityMutualsOnly: - icon = iconWithLabel{"handshake-o", "mutuals only"} + label = "mutuals-only" + icon = "handshake-o" case apimodel.VisibilityDirect: - icon = iconWithLabel{"envelope", "direct"} + label = "direct" + icon = "envelope" } /* #nosec G203 */ - return template.HTML(fmt.Sprintf(``, icon.label, icon.faIcon)) + return template.HTML(fmt.Sprintf( + ``, + label, icon, + )) } -// text is a template.HTML to affirm that the input of this function is already escaped -func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML { - out := text.Emojify(emojis, string(inputText)) +// emojify replaces emojis in the given +// html fragment with suitable tags. +// +// The provided input must have been +// escaped / templated already! +func emojify( + emojis []apimodel.Emoji, + html template.HTML, +) template.HTML { + return text.EmojifyWeb(emojis, html) +} - /* #nosec G203 */ - // (this is escaped above) - return template.HTML(out) +// demojify replaces emoji shortcodes in +// the given fragment with empty strings. +// +// Output must then be escaped as appropriate. +func demojify(input string) string { + return text.Demojify(input) } func acctInstance(acct string) string { @@ -170,10 +275,79 @@ func acctInstance(acct string) string { return "" } +// increment adds 1 +// to the given int. func increment(i int) int { return i + 1 } +// add adds n2 to n1. +func add(n1 int, n2 int) int { + return n1 + n2 +} + +// subtract subtracts n2 from n1. +func subtract(n1 int, n2 int) int { + return n1 - n2 +} + +var ( + indentRegex = regexp.MustCompile(`(?m)^`) + indentStr = " " + indentStrLen = len(indentStr) + indents = strings.Repeat(indentStr, 12) + indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)^((?:%s)+)
.*
`, indentStr)) +) + +// indent appropriately indents the given html +// by prepending each line with the indentStr. +func indent(n int, html template.HTML) template.HTML { + out := indentRegex.ReplaceAllString( + string(html), + indents[:n*indentStrLen], + ) + return noescape(out) +} + +// indentAttr appropriately indents the given html +// attribute by prepending each line with the indentStr. +func indentAttr(n int, html template.HTMLAttr) template.HTMLAttr { + out := indentRegex.ReplaceAllString( + string(html), + indents[:n*indentStrLen], + ) + return noescapeAttr(out) +} + +// outdentPre outdents all `
` tags in the
+// given HTML so that they render correctly in code
+// blocks, even if they were indented before.
+func outdentPre(html template.HTML) template.HTML {
+	input := string(html)
+	output := regexes.ReplaceAllStringFunc(indentPre, input,
+		func(match string, buf *bytes.Buffer) string {
+			// Reuse the regex to pull out submatches.
+			matches := indentPre.FindAllStringSubmatch(match, -1)
+			if len(matches) != 1 {
+				return match
+			}
+
+			var (
+				indented = matches[0][0]
+				indent   = matches[0][1]
+			)
+
+			// Outdent everything in the inner match, add
+			// a newline at the end to make it a bit neater.
+			outdented := strings.ReplaceAll(indented, indent, "")
+
+			// Replace original match with the outdented version.
+			return strings.ReplaceAll(match, indented, outdented)
+		},
+	)
+	return noescape(output)
+}
+
 // isNil will safely check if 'v' is nil without
 // dealing with weird Go interface nil bullshit.
 func isNil(i interface{}) bool {
@@ -193,21 +367,3 @@ func deref(i any) any {
 
 	return vOf.Elem()
 }
-
-func LoadTemplateFunctions(engine *gin.Engine) {
-	engine.SetFuncMap(template.FuncMap{
-		"escape":           escape,
-		"noescape":         noescape,
-		"noescapeAttr":     noescapeAttr,
-		"oddOrEven":        oddOrEven,
-		"visibilityIcon":   visibilityIcon,
-		"timestamp":        timestamp,
-		"timestampVague":   timestampVague,
-		"timestampPrecise": timestampPrecise,
-		"emojify":          emojify,
-		"acctInstance":     acctInstance,
-		"increment":        increment,
-		"isNil":            isNil,
-		"deref":            deref,
-	})
-}
diff --git a/internal/router/template_test.go b/internal/router/template_test.go
new file mode 100644
index 000000000..19bf759e0
--- /dev/null
+++ b/internal/router/template_test.go
@@ -0,0 +1,204 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see .
+
+package router
+
+import (
+	"html/template"
+	"testing"
+)
+
+func TestOutdentPre(t *testing.T) {
+	const html = template.HTML(`
+        
+
+

Here's a bunch of HTML, read it and weep, weep then!

+
<section class="about-user">
+                    <div class="col-header">
+                        <h2>About</h2>
+                    </div>            
+                    <div class="fields">
+                        <h3 class="sr-only">Fields</h3>
+                        <dl>
+                            <div class="field">
+                                <dt>should you follow me?</dt>
+                                <dd>maybe!</dd>
+                            </div>
+                            <div class="field">
+                                <dt>age</dt>
+                                <dd>120</dd>
+                            </div>
+                        </dl>
+                    </div>
+                    <div class="bio">
+                        <h3 class="sr-only">Bio</h3>
+                        <p>i post about things that concern me</p>
+                    </div>
+                    <div class="sr-only" role="group">
+                        <h3 class="sr-only">Stats</h3>
+                        <span>Joined in Jun, 2022.</span>
+                        <span>8 posts.</span>
+                        <span>Followed by 1.</span>
+                        <span>Following 1.</span>
+                    </div>
+                    <div class="accountstats" aria-hidden="true">
+                        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
+                        <b>Posts</b><span>8</span>
+                        <b>Followed by</b><span>1</span>
+                        <b>Following</b><span>1</span>
+                    </div>
+                </section>
+                
+

There, hope you liked that!

+
+
+
+
+

Here's a bunch of HTML, read it and weep, weep then!

+
<section class="about-user">
+                    <div class="col-header">
+                        <h2>About</h2>
+                    </div>            
+                    <div class="fields">
+                        <h3 class="sr-only">Fields</h3>
+                        <dl>
+                            <div class="field">
+                                <dt>should you follow me?</dt>
+                                <dd>maybe!</dd>
+                            </div>
+                            <div class="field">
+                                <dt>age</dt>
+                                <dd>120</dd>
+                            </div>
+                        </dl>
+                    </div>
+                    <div class="bio">
+                        <h3 class="sr-only">Bio</h3>
+                        <p>i post about things that concern me</p>
+                    </div>
+                    <div class="sr-only" role="group">
+                        <h3 class="sr-only">Stats</h3>
+                        <span>Joined in Jun, 2022.</span>
+                        <span>8 posts.</span>
+                        <span>Followed by 1.</span>
+                        <span>Following 1.</span>
+                    </div>
+                    <div class="accountstats" aria-hidden="true">
+                        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
+                        <b>Posts</b><span>8</span>
+                        <b>Followed by</b><span>1</span>
+                        <b>Following</b><span>1</span>
+                    </div>
+                </section>
+                
+

There, hope you liked that!

+
+
+`) + + const expected = template.HTML(` +
+
+

Here's a bunch of HTML, read it and weep, weep then!

+
<section class="about-user">
+    <div class="col-header">
+        <h2>About</h2>
+    </div>            
+    <div class="fields">
+        <h3 class="sr-only">Fields</h3>
+        <dl>
+            <div class="field">
+<dt>should you follow me?</dt>
+<dd>maybe!</dd>
+            </div>
+            <div class="field">
+<dt>age</dt>
+<dd>120</dd>
+            </div>
+        </dl>
+    </div>
+    <div class="bio">
+        <h3 class="sr-only">Bio</h3>
+        <p>i post about things that concern me</p>
+    </div>
+    <div class="sr-only" role="group">
+        <h3 class="sr-only">Stats</h3>
+        <span>Joined in Jun, 2022.</span>
+        <span>8 posts.</span>
+        <span>Followed by 1.</span>
+        <span>Following 1.</span>
+    </div>
+    <div class="accountstats" aria-hidden="true">
+        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
+        <b>Posts</b><span>8</span>
+        <b>Followed by</b><span>1</span>
+        <b>Following</b><span>1</span>
+    </div>
+</section>
+
+

There, hope you liked that!

+
+
+
+
+

Here's a bunch of HTML, read it and weep, weep then!

+
<section class="about-user">
+    <div class="col-header">
+        <h2>About</h2>
+    </div>            
+    <div class="fields">
+        <h3 class="sr-only">Fields</h3>
+        <dl>
+            <div class="field">
+<dt>should you follow me?</dt>
+<dd>maybe!</dd>
+            </div>
+            <div class="field">
+<dt>age</dt>
+<dd>120</dd>
+            </div>
+        </dl>
+    </div>
+    <div class="bio">
+        <h3 class="sr-only">Bio</h3>
+        <p>i post about things that concern me</p>
+    </div>
+    <div class="sr-only" role="group">
+        <h3 class="sr-only">Stats</h3>
+        <span>Joined in Jun, 2022.</span>
+        <span>8 posts.</span>
+        <span>Followed by 1.</span>
+        <span>Following 1.</span>
+    </div>
+    <div class="accountstats" aria-hidden="true">
+        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
+        <b>Posts</b><span>8</span>
+        <b>Followed by</b><span>1</span>
+        <b>Following</b><span>1</span>
+    </div>
+</section>
+
+

There, hope you liked that!

+
+
+`) + + out := outdentPre(html) + if out != expected { + t.Fatalf("unexpected output:\n`%s`\n", out) + } +} diff --git a/internal/text/emojify.go b/internal/text/emojify.go index 2100d5e81..23730eaf9 100644 --- a/internal/text/emojify.go +++ b/internal/text/emojify.go @@ -20,18 +20,76 @@ import ( "bytes" "html" + "html/template" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/regexes" ) -// Emojify replaces shortcodes in `inputText` with the emoji in `emojis`. -// -// Callers should ensure that inputText and resulting text are escaped -// appropriately depending on what they're used for. -func Emojify(emojis []apimodel.Emoji, inputText string) string { - emojisMap := make(map[string]apimodel.Emoji, len(emojis)) +// EmojifyWeb replaces emoji shortcodes like `:example:` in the given HTML +// fragment with `` tags suitable for rendering on the web frontend. +func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML { + out := emojify( + emojis, + string(html), + func(url, code string, buf *bytes.Buffer) { + buf.WriteString(`:`)
+			buf.WriteString(code)
+			buf.WriteString(`:`) + }, + ) + // If input was safe, + // we can trust output. + return template.HTML(out) // #nosec G203 +} + +// EmojifyRSS replaces emoji shortcodes like `:example:` in the given text +// fragment with `` tags suitable for rendering as RSS content. +func EmojifyRSS(emojis []apimodel.Emoji, text string) string { + return emojify( + emojis, + text, + func(url, code string, buf *bytes.Buffer) { + buf.WriteString(`:`)
+			buf.WriteString(code)
+			buf.WriteString(`:`) + }, + ) +} + +// Demojify replaces emoji shortcodes like `:example:` in the given text +// fragment with empty strings, essentially stripping them from the text. +// This is useful for text used in OG Meta headers. +func Demojify(text string) string { + return regexes.EmojiFinder.ReplaceAllString(text, "") +} + +func emojify( + emojis []apimodel.Emoji, + input string, + write func(url, code string, buf *bytes.Buffer), +) string { + // Build map of shortcodes. Normalize each + // shortcode by readding closing colons. + emojisMap := make(map[string]apimodel.Emoji, len(emojis)) for _, emoji := range emojis { shortcode := ":" + emoji.Shortcode + ":" emojisMap[shortcode] = emoji @@ -39,27 +97,20 @@ func Emojify(emojis []apimodel.Emoji, inputText string) string { return regexes.ReplaceAllStringFunc( regexes.EmojiFinder, - inputText, + input, func(shortcode string, buf *bytes.Buffer) string { - // Look for emoji according to this shortcode + // Look for emoji with this shortcode. emoji, ok := emojisMap[shortcode] if !ok { return shortcode } - // Escape raw emoji content - safeURL := html.EscapeString(emoji.URL) - safeCode := html.EscapeString(emoji.Shortcode) - - // Write HTML emoji repr to buffer - buf.WriteString(`:`)
-			buf.WriteString(safeCode)
-			buf.WriteString(`:`) + // Escape raw emoji content. + url := html.EscapeString(emoji.URL) + code := html.EscapeString(emoji.Shortcode) + // Write emoji repr to buffer. + write(url, code, buf) return buf.String() }, ) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 09d538a3d..9d1f7edeb 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -662,6 +662,10 @@ func (c *Converter) StatusToWebStatus( return nil, err } + // Whack a newline before and after each "pre" to make it easier to outdent it. + webStatus.Content = strings.ReplaceAll(webStatus.Content, "
", "\n
")
+	webStatus.Content = strings.ReplaceAll(webStatus.Content, "
", "
\n") + // Add additional information for template. // Assume empty langs, hope for not empty language. webStatus.LanguageTag = new(language.Language) @@ -727,6 +731,8 @@ func (c *Converter) StatusToWebStatus( a.Sensitive = webStatus.Sensitive } + webStatus.Local = *s.Local + return webStatus, nil } diff --git a/internal/typeutils/internaltorss.go b/internal/typeutils/internaltorss.go index e70b11aae..e1174caf6 100644 --- a/internal/typeutils/internaltorss.go +++ b/internal/typeutils/internaltorss.go @@ -151,7 +151,7 @@ func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*f apiEmojis = append(apiEmojis, apiEmoji) } } - content := text.Emojify(apiEmojis, s.Content) + content := text.EmojifyRSS(apiEmojis, s.Content) return &feeds.Item{ Title: title, diff --git a/internal/typeutils/internaltorss_test.go b/internal/typeutils/internaltorss_test.go index ea6f3cc93..305015d28 100644 --- a/internal/typeutils/internaltorss_test.go +++ b/internal/typeutils/internaltorss_test.go @@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() { suite.Equal("62529", item.Enclosure.Length) suite.Equal("image/jpeg", item.Enclosure.Type) suite.Equal("http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", item.Enclosure.Url) - suite.Equal("hello world! #welcome ! first post on the instance \":rainbow:\" !", item.Content) + suite.Equal("hello world! #welcome ! first post on the instance \":rainbow:\" !", item.Content) } func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() { diff --git a/internal/web/about.go b/internal/web/about.go index 89bb13f0d..2bc558962 100644 --- a/internal/web/about.go +++ b/internal/web/about.go @@ -18,9 +18,10 @@ package web import ( - "net/http" + "context" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -31,20 +32,35 @@ ) func (m *Module) aboutGETHandler(c *gin.Context) { - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "about.tmpl", gin.H{ - "instance": instance, - "languages": config.GetInstanceLanguages().DisplayStrs(), - "ogMeta": ogBase(instance), - "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "about.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssAbout}, + Extra: map[string]any{ + "showStrap": true, + "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), + "languages": config.GetInstanceLanguages().DisplayStrs(), }, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index b0ece58cd..f15252bf7 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -18,39 +18,58 @@ package web import ( + "context" "errors" "net/http" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) func (m *Module) confirmEmailGETHandler(c *gin.Context) { - ctx := c.Request.Context() - - // if there's no token in the query, just serve the 404 web handler - token := c.Query(tokenParam) - if token == "" { - apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGetV1) - return - } - - user, errWithCode := m.processor.User().EmailConfirm(ctx, token) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - instance, err := m.processor.InstanceGetV1(ctx) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) return } - c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{ - "instance": instance, - "email": user.Email, - "username": user.Account.Username, - }) + // If there's no token in the query, + // just serve the 404 web handler. + token := c.Query("token") + if token == "" { + errWithCode := gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "confirmed.tmpl", + Instance: instance, + Extra: map[string]any{ + "email": user.Email, + "username": user.Account.Username, + }, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/customcss.go b/internal/web/customcss.go index ef57e0033..b23ebce8e 100644 --- a/internal/web/customcss.go +++ b/internal/web/customcss.go @@ -18,9 +18,7 @@ package web import ( - "errors" "net/http" - "strings" "github.com/gin-gonic/gin" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" @@ -31,31 +29,29 @@ const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8") func (m *Module) customCSSGETHandler(c *gin.Context) { - if !config.GetAccountsAllowCustomCSS() { - err := errors.New("accounts-allow-custom-css is not enabled on this instance") - apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGetV1) - return - } - if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil { apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) return } - // usernames on our instance will always be lowercase - username := strings.ToLower(c.Param(usernameKey)) - if username == "" { - err := errors.New("no account username specified") - apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - return - } - - customCSS, errWithCode := m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), username) + targetUsername, errWithCode := apiutil.ParseWebUsername(c.Param(apiutil.WebUsernameKey)) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } + // Retrieve customCSS if enabled on the instance. + // Else use an empty string, to help with caching + // when custom CSS gets toggled on or off. + var customCSS string + if config.GetAccountsAllowCustomCSS() { + customCSS, errWithCode = m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), targetUsername) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + } + c.Header(cacheControlHeader, cacheControlNoCache) c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS)) } diff --git a/internal/web/domain-blocklist.go b/internal/web/domain-blocklist.go index 8a88a0932..5d631e0f7 100644 --- a/internal/web/domain-blocklist.go +++ b/internal/web/domain-blocklist.go @@ -18,14 +18,14 @@ package web import ( + "context" "fmt" - "net/http" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" ) const ( @@ -33,37 +33,44 @@ ) func (m *Module) domainBlockListGETHandler(c *gin.Context) { - authed, err := oauth.Authed(c, false, false, false, false) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) - return - } - - if !config.GetInstanceExposeSuspendedWeb() && (authed.Account == nil || authed.User == nil) { - err := fmt.Errorf("this instance does not expose the list of suspended domains publicly") - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) - return - } - - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) - return - } - - domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "domain-blocklist.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "blocklist": domainBlocks, - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - }, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + if !config.GetInstanceExposeSuspendedWeb() { + err := fmt.Errorf("this instance does not publicy expose its blocklist") + apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), instanceGet) + return + } + + domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "domain-blocklist.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA}, + Javascript: []string{jsFrontend}, + Extra: map[string]any{"blocklist": domainBlocks}, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/base.go b/internal/web/index.go similarity index 52% rename from internal/web/base.go rename to internal/web/index.go index 5bc3c536a..25960cf7f 100644 --- a/internal/web/base.go +++ b/internal/web/index.go @@ -18,33 +18,50 @@ package web import ( + "context" "net/http" "strings" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -func (m *Module) baseHandler(c *gin.Context) { - // if a landingPageUser is set in the config, redirect to that user's profile +func (m *Module) indexHandler(c *gin.Context) { + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + // If a landingPageUser is set in the config, redirect to + // that user's profile instead of rendering landing/index page. if landingPageUser := config.GetLandingPageUser(); landingPageUser != "" { c.Redirect(http.StatusFound, "/@"+strings.ToLower(landingPageUser)) return } - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) - return + page := apiutil.WebPage{ + Template: "index.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssAbout, cssIndex}, + Extra: map[string]any{"showStrap": true}, } - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "stylesheets": []string{ - distPathPrefix + "/index.css", - }, - }) + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/profile.go b/internal/web/profile.go index b2c3bb944..b629c98b9 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -27,7 +27,6 @@ "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -141,28 +140,28 @@ func (m *Module) profileGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - distPathPrefix + "/profile.css", - } - if config.GetAccountsAllowCustomCSS() { - stylesheets = append(stylesheets, "/@"+targetAccount.Username+"/custom.css") + page := apiutil.WebPage{ + Template: "profile.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount), + Stylesheets: []string{ + cssFA, cssStatus, cssThread, cssProfile, + // Custom CSS for this user last in cascade. + "/@" + targetAccount.Username + "/custom.css", + }, + Javascript: []string{jsFrontend}, + Extra: map[string]any{ + "account": targetAccount, + "rssFeed": rssFeed, + "robotsMeta": robotsMeta, + "statuses": statusResp.Items, + "statuses_next": statusResp.NextLink, + "pinned_statuses": pinnedStatuses, + "show_back_to_top": paging, + }, } - c.HTML(http.StatusOK, "profile.tmpl", gin.H{ - "instance": instance, - "account": targetAccount, - "ogMeta": ogBase(instance).withAccount(targetAccount), - "rssFeed": rssFeed, - "robotsMeta": robotsMeta, - "statuses": statusResp.Items, - "statuses_next": statusResp.NextLink, - "pinned_statuses": pinnedStatuses, - "show_back_to_top": paging, - "stylesheets": stylesheets, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + apiutil.TemplateWebPage(c, page) } // returnAPAccount returns an ActivityPub representation of diff --git a/internal/web/robots.go b/internal/web/robots.go index aee4d1a55..92bd33014 100644 --- a/internal/web/robots.go +++ b/internal/web/robots.go @@ -71,7 +71,7 @@ # API endpoints. Disallow: /api/ -# Auth/login endpoints. +# Auth/Sign in endpoints. Disallow: /auth/ Disallow: /oauth/ Disallow: /check_your_email diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index 615f2d265..9de30d1f1 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -18,30 +18,44 @@ package web import ( - "net/http" + "context" "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) func (m *Module) SettingsPanelHandler(c *gin.Context) { - instance, err := m.processor.InstanceGetV1(c.Request.Context()) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - c.HTML(http.StatusOK, "frontend.tmpl", gin.H{ - "instance": instance, - "stylesheets": []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/_colors.css", - distPathPrefix + "/base.css", - distPathPrefix + "/profile.css", - distPathPrefix + "/status.css", - distPathPrefix + "/settings-style.css", + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "frontend.tmpl", + Instance: instance, + Stylesheets: []string{ + cssFA, + cssProfile, // Used for rendering stub/fake profiles. + cssStatus, // Used for rendering stub/fake statuses. + cssSettings, }, - "javascript": []string{distPathPrefix + "/settings.js"}, - }) + Javascript: []string{jsSettings}, + } + + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/tag.go b/internal/web/tag.go index 69591f114..5c3cd31a6 100644 --- a/internal/web/tag.go +++ b/internal/web/tag.go @@ -19,7 +19,6 @@ import ( "context" - "net/http" "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -56,16 +55,13 @@ func (m *Module) tagGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - distPathPrefix + "/tag.css", + page := apiutil.WebPage{ + Template: "tag.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA, cssThread, cssTag}, + Extra: map[string]any{"tagName": tagName}, } - c.HTML(http.StatusOK, "tag.tmpl", gin.H{ - "instance": instance, - "ogMeta": ogBase(instance), - "tagName": tagName, - "stylesheets": stylesheets, - }) + apiutil.TemplateWebPage(c, page) } diff --git a/internal/web/thread.go b/internal/web/thread.go index 20145cfcd..4dcd1d221 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -28,7 +28,6 @@ "github.com/gin-gonic/gin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -139,22 +138,23 @@ func (m *Module) threadGETHandler(c *gin.Context) { return } - stylesheets := []string{ - assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", - distPathPrefix + "/status.css", - } - if config.GetAccountsAllowCustomCSS() { - stylesheets = append(stylesheets, "/@"+targetUsername+"/custom.css") + page := apiutil.WebPage{ + Template: "thread.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance).WithStatus(status), + Stylesheets: []string{ + cssFA, cssStatus, cssThread, + // Custom CSS for this user last in cascade. + "/@" + targetUsername + "/custom.css", + }, + Javascript: []string{jsFrontend}, + Extra: map[string]any{ + "status": status, + "context": context, + }, } - c.HTML(http.StatusOK, "thread.tmpl", gin.H{ - "instance": instance, - "status": status, - "context": context, - "ogMeta": ogBase(instance).withStatus(status), - "stylesheets": stylesheets, - "javascript": []string{distPathPrefix + "/frontend.js"}, - }) + apiutil.TemplateWebPage(c, page) } // returnAPStatus returns an ActivityPub representation of target status, diff --git a/internal/web/web.go b/internal/web/web.go index 86e74d6f8..5e57d08c6 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -37,7 +37,7 @@ const ( confirmEmailPath = "/" + uris.ConfirmEmailPath - profileGroupPath = "/@:" + usernameKey + profileGroupPath = "/@:username" statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group tagsPath = "/tags/:" + apiutil.TagNameKey customCSSPath = profileGroupPath + "/custom.css" @@ -49,15 +49,24 @@ userPanelPath = settingsPathPrefix + "/user" adminPanelPath = settingsPathPrefix + "/admin" - tokenParam = "token" - usernameKey = "username" - cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives ifModifiedSinceHeader = "If-Modified-Since" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since ifNoneMatchHeader = "If-None-Match" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match eTagHeader = "ETag" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified + + cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css" + cssAbout = distPathPrefix + "/about.css" + cssIndex = distPathPrefix + "/index.css" + cssStatus = distPathPrefix + "/status.css" + cssThread = distPathPrefix + "/thread.css" + cssProfile = distPathPrefix + "/profile.css" + cssSettings = distPathPrefix + "/settings-style.css" + cssTag = distPathPrefix + "/tag.css" + + jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS. + jsSettings = distPathPrefix + "/settings.js" // Settings panel React application. ) type Module struct { @@ -99,7 +108,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler) // Attach individual web handlers which require no specific middlewares - r.AttachHandler(http.MethodGet, "/", m.baseHandler) // front-page + r.AttachHandler(http.MethodGet, "/", m.indexHandler) // front-page r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) diff --git a/testrig/gin.go b/testrig/gin.go index c14f79984..cd32654e8 100644 --- a/testrig/gin.go +++ b/testrig/gin.go @@ -27,7 +27,6 @@ // CreateGinTextContext creates a new gin.Context suitable for a test, with an instantiated gin.Engine. func CreateGinTestContext(rw http.ResponseWriter, r *http.Request) (*gin.Context, *gin.Engine) { ctx, eng := gin.CreateTestContext(rw) - router.LoadTemplateFunctions(eng) if err := router.LoadTemplates(eng); err != nil { panic(err) } diff --git a/testrig/router.go b/testrig/router.go index 64b3842de..20317dae6 100644 --- a/testrig/router.go +++ b/testrig/router.go @@ -59,6 +59,5 @@ func NewTestRouter(db db.DB) *router.Router { // ConfigureTemplatesWithGin will panic on any errors related to template loading during tests func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) { - router.LoadTemplateFunctions(engine) engine.LoadHTMLGlob(filepath.Join(templatePath, "*")) } diff --git a/web/source/.eslintignore b/web/source/.eslintignore index b512c09d4..46d39d131 100644 --- a/web/source/.eslintignore +++ b/web/source/.eslintignore @@ -1 +1,3 @@ -node_modules \ No newline at end of file +node_modules +prism.js +prism.css \ No newline at end of file diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css index 79acf988c..87022c559 100644 --- a/web/source/css/_colors.css +++ b/web/source/css/_colors.css @@ -82,11 +82,11 @@ $button-danger-bg: $error3; $button-danger-fg: $white1; $button-danger-hover-bg: $error2; -$toot-bg: $gray3; -$toot-info-bg: $gray2; +$status-bg: $gray3; +$status-info-bg: $gray2; -$toot-focus-bg: $gray5; -$toot-focus-info-bg: $gray4; +$status-focus-bg: $gray5; +$status-focus-info-bg: $gray4; $no-img-desc-bg: $orange1; $no-img-desc-fg: $gray1; diff --git a/web/source/css/about.css b/web/source/css/about.css new file mode 100644 index 000000000..55318572c --- /dev/null +++ b/web/source/css/about.css @@ -0,0 +1,39 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +.about { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 2rem; + + background: $bg-accent; + box-shadow: $boxshadow; + border: $boxshadow-border; + border-radius: $br; + + .about-section { + ul, ol { + margin-top: 0; + } + + h3, h4 { + margin-top: 0; + } + } +} diff --git a/web/source/css/base.css b/web/source/css/base.css index 5cd2cd047..0d3c436c8 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -20,35 +20,52 @@ /* noto-sans-regular - latin */ @font-face { - font-family: "Noto Sans"; - font-weight: 400; - font-display: swap; - font-style: normal; - src: url('../fonts/noto-sans-v27-latin-regular.woff2') format('woff2'), - url('../fonts/noto-sans-v27-latin-regular.woff') format('woff'); + font-family: "Noto Sans"; + font-weight: 400; + font-display: swap; + font-style: normal; + src: url('../fonts/noto-sans-v27-latin-regular.woff2') format('woff2'), + url('../fonts/noto-sans-v27-latin-regular.woff') format('woff'); } /* noto-sans-700 - latin */ @font-face { - font-family: "Noto Sans"; - font-weight: 700; - font-display: swap; - font-style: normal; - src: url('../fonts/noto-sans-v27-latin-700.woff2') format('woff2'), - url('../fonts/noto-sans-v27-latin-700.woff') format('woff'); + font-family: "Noto Sans"; + font-weight: 700; + font-display: swap; + font-style: normal; + src: url('../fonts/noto-sans-v27-latin-700.woff2') format('woff2'), + url('../fonts/noto-sans-v27-latin-700.woff') format('woff'); } -/* standard border radius for nice squircles */ +/************************************* +***** SECTION 1: HANDY VARIABLES ***** +**************************************/ + +/* + Standard border radius + for nice squircles. +*/ $br: 0.4rem; -/* border radius for items that are framed/bordered - inside something with $br, eg avatar, header img */ + +/* + Border radius for items that + are framed/bordered inside + something with $br, eg avatar, + header img, etc. +*/ $br-inner: 0.2rem; -/* Fork-Awesome 'fa-fw' fixed icon width - keep in sync with https://github.com/ForkAwesome/Fork-Awesome/blob/a99579ae3e735ee70e51ed62dfcee3172b5b2db7/css/fork-awesome.css#L50 +/* + Fork-Awesome 'fa-fw' fixed icon width; + keep in sync with https://github.com/ForkAwesome/Fork-Awesome/blob/a99579ae3e735ee70e51ed62dfcee3172b5b2db7/css/fork-awesome.css#L50 */ $fa-fw: 1.28571429em; +/****************************************** +***** SECTION 2: BASIC GLOBAL STYLING ***** +*******************************************/ + html, body { padding: 0; margin: 0; @@ -63,90 +80,28 @@ body { position: relative; } -.hidden { - display: none; -} - -.page { - display: grid; - min-height: 100vh; - - grid-template-columns: 1fr minmax(auto, 50rem) 1fr; - grid-template-columns: 1fr min(92%, 50rem) 1fr; - grid-template-rows: auto 1fr auto; -} - -h1 { - margin: 0; - line-height: 2.4rem; -} - a { color: $link-fg; } -header, footer { - grid-column: 1 / span 3; -} - -.content { - grid-column: 2; - align-self: start; -} - -header { - display: flex; - justify-content: center; -} - -header a { - display: flex; - flex-wrap: wrap; - margin: 1.5rem; - gap: 1rem; - justify-content: center; - - img { - align-self: center; - height: 3rem; - } - - h1 { - flex-grow: 1; - align-self: center; - text-align: center; - - font-size: 1.5rem; - word-wrap: anywhere; - color: $fg; - } -} - -.excerpt-top { - margin-bottom: 2rem; - font-style: italic; - font-weight: normal; - text-align: center; - font-size: 1.2rem; - - .count { - font-weight: bold; - color: $fg-accent; - } -} - +/* + Normalize margins of first and last children. + We generally don't want to open a paragraph or + paragraph-like element with a top margin or + close it with a bottom margin. +*/ main { - p:first-child { + p:first-child, ol:first-child, ul:first-child { margin-top: 0; } - p:last-child { + p:last-child, ol:last-child, ul:last-child { margin-bottom: 0; } } .button, button { - border-radius: 0.2rem; + border-radius: $br-inner; color: $button-fg; background: $button-bg; box-shadow: $boxshadow; @@ -184,6 +139,166 @@ main { } } +/* + Form styling - used in settings frontend as well. +*/ +input, select, textarea, .input { + box-sizing: border-box; + border: 0.15rem solid $input-border; + border-radius: 0.1rem; + color: $fg; + background: $input-bg; + width: 100%; + font-family: 'Noto Sans', sans-serif; + font-size: 1rem; + padding: 0.3rem; + + &:focus, &:active { + border-color: $input-focus-border; + } + + &:invalid, .invalid & { + border-color: $input-error-border; + } + + &:disabled { + background: transparent; + } + + &::placeholder { + opacity: 1; + color: $fg-reduced + } +} + +/* + Squeeze emojis so they fit inline in text. +*/ +.emoji { + width: 1.45em; + height: 1.45em; + margin: -0.2em 0.02em 0; + object-fit: contain; + vertical-align: middle; + transition: 0.1s; + + /* + Enlarge emojis on hover to give + viewer a good look at them. + */ + &:hover, &:active { + transform: scale(2); + background-color: $bg; + box-shadow: $boxshadow; + border: $boxshadow-border; + border-radius: $br-inner; + } +} + +/* + Restyle unordered lists; outdent + and replace dot with orange dot. +*/ +ul { + padding-left: 2.5rem; + list-style: none; + + li::before { + content: "\2022"; + color: $border-accent; + font-weight: bold; + display: inline-block; + width: 1.5rem; + margin-left: -1.5rem; + } +} + +/* + Mirror the same styling a little bit + for ordered lists by making marker bold. +*/ +ol { + padding-left: 2.5rem; + + li::marker { + font-weight: bold; + } +} + +/* + Outdent block quotes a bit; use + orange stripe for left border. +*/ +blockquote { + padding: 0.5rem 0 0.5rem 0.5rem; + border-left: 0.2rem solid $border-accent; + margin: 0; + font-style: italic; +} + +/* + Nice dashed orange line + for horizontal rules. +*/ +hr { + border: 0; + border-top: 1px dashed $border-accent; +} + +/* + Don't indent definition + lists and definitions. +*/ +dl { + margin: 0; + + dd { + margin-left: 0; + } +} + +label { + cursor: pointer; +} + +/************************************* +***** SECTION 3: UTILITY CLASSES ***** +**************************************/ + +/* + Column header that appears at the top + of threads, at the top of sections of + profiles (About, Pinned Posts, etc). +*/ +.col-header { + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + + justify-content: start; + align-items: center; + + margin: 0; + background: $profile-bg; + border-top-left-radius: $br; + border-top-right-radius: $br; + padding: 0.75rem; + + a { + justify-self: end; + } + + h1, h2, h3, h4 { + font-size: 1.2rem; + line-height: 1.3rem; + margin: 0; + } +} + +.hidden { + display: none; +} + .nounderline { text-decoration: none; } @@ -192,57 +307,37 @@ main { color: $acc1; } -.logo { - justify-self: center; - img { - height: 30vh; +.text-cutoff { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +/* + Class for lists that don't + want the orange dot. +*/ +.nodot { + li::before { + content: initial; } } -section.apps { - align-self: start; +/*********************************** +***** SECTION 4: SHAMEFUL MESS ***** +************************************/ - .applist { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 0.5rem; - align-content: start; +/* + EVERYTHING BELOW THIS POINT: + Should be moved somewhere else + to avoid cluttering up this file. +*/ - .entry { - display: grid; - grid-template-columns: 25% 1fr; - gap: 1.5rem; - padding: 0.5rem; - background: $bg-accent; - border-radius: 0.5rem; - - .logo { - align-self: center; - width: 100%; - object-fit: contain; - flex: 1 1 auto; - } - - .logo.redraw { - fill: $fg; - stroke: $fg; - } - - a { - font-weight: bold; - } - - div { - padding: 0; - h3 { - margin-top: 0; - } - } - } - } -} - -section.login { +/* + Below section stylings are used + in transient/error templates. +*/ +section.sign-in { form { display: flex; flex-direction: column; @@ -291,98 +386,11 @@ section.oob-token { } } -.error-text { - color: $error1; - background: $error2; - border-radius: 0.1rem; - font-weight: bold; -} - -input, select, textarea, .input { - box-sizing: border-box; - border: 0.15rem solid $input-border; - border-radius: 0.1rem; - color: $fg; - background: $input-bg; - width: 100%; - font-family: 'Noto Sans', sans-serif; - font-size: 1rem; - padding: 0.3rem; - - &:focus, &:active { - border-color: $input-focus-border; - } - - &:invalid, .invalid & { - border-color: $input-error-border; - } - - &:disabled { - background: transparent; - } -} - -::placeholder { - opacity: 1; - color: $fg-reduced -} - -hr { - color: transparent; - width: 100%; - border-bottom: 0.02rem solid $border-accent; -} - -footer { - align-self: end; - padding: 2rem 0 1rem 0; - - display: flex; - flex-wrap: wrap; - justify-content: center; - - div { - text-align: center; - padding: 1rem; - flex-grow: 1; - } - - a { - font-weight: bold; - } -} - -@media screen and (max-width: 600px) { - header { - text-align: center; - } - - footer { - grid-template-columns: 1fr; - - div { - text-align: initial; - width: 100%; - } - } - - section.apps .applist { - grid-template-columns: 1fr; - } -} - -.emoji { - width: 1.45em; - height: 1.45em; - margin: -0.2em 0.02em 0; - object-fit: contain; - vertical-align: middle; -} - -.monospace { - font-family: monospace; -} - +/* + TODO: This is only used in the "finalize" + template for new signups; move this elsewhere + when that stuff is finished up. +*/ .callout { margin: 1.5rem 0; border: .05rem solid $border-accent; @@ -397,22 +405,11 @@ footer { } } -label { - cursor: pointer; -} - -@media (prefers-reduced-motion) { - .fa-spin { - animation: none; - } -} - -.text-cutoff { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - +/* + TODO: list and blocklist are only used + in settings panel and on blocklist page; + consider moving them somewhere else. +*/ .list { display: flex; flex-direction: column; @@ -495,21 +492,18 @@ label { } } -.about { - display: flex; - flex-direction: column; - gap: 1rem; - - h2 { - margin: 0.5rem 0; +@media screen and (max-width: 30rem) { + .domain-blocklist .entry { + grid-template-columns: 1fr; + gap: 0; } - - ul { - margin-bottom: 0; - } - } +/* + TODO: this is only used on About + page and in settings application; + consider moving it somewhere else. +*/ .account-card { display: inline-grid; grid-template-columns: auto 1fr; @@ -541,61 +535,3 @@ label { grid-row: 1 / span 2; } } - -.instance-rules { - list-style-position: inside; - margin: 0; - padding: 0; - - a.rule { - display: grid; - grid-template-columns: 1fr auto; - align-items: center; - color: $fg; - text-decoration: none; - background: $toot-bg; - padding: 1rem; - margin: 0.5rem 0; - border-radius: $br; - line-height: 2rem; - position: relative; - - &:hover { - color: $fg-accent; - - .edit-icon { - display: inline; - } - } - - .edit-icon { - display: none; - font-size: 1rem; - line-height: 1.5rem; - } - - li { - font-size: 1.75rem; - padding: 0; - margin: 0; - - h2 { - margin: 0; - margin-top: 0 !important; - display: inline-block; - font-size: 1.5rem; - } - } - - span { - color: $fg-reduced; - } - } -} - -@media screen and (max-width: 30rem) { - .domain-blocklist .entry { - grid-template-columns: 1fr; - gap: 0; - } -} \ No newline at end of file diff --git a/web/source/css/index.css b/web/source/css/index.css index b4ad1bd1c..4ea3b78e1 100644 --- a/web/source/css/index.css +++ b/web/source/css/index.css @@ -16,26 +16,85 @@ along with this program. If not, see . */ -header a { - margin: 2rem; - gap: 2rem; - - img { - height: 6rem; - } - - h1 { - font-size: 2rem; - } +/* + Render instance title a + bit bigger on index page. +*/ +.page-header a h1 { + font-size: 2rem; + line-height: 2rem; } -main { - section { +/* + Reuse about styling, but rework it + to separate sections a bit more. +*/ +.about { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 0; + + background: initial; + box-shadow: initial; + border: initial; + border-radius: initial; + + .about-section { + padding: 2rem; background: $bg-accent; box-shadow: $boxshadow; border: $boxshadow-border; border-radius: $br; - padding: 2rem; - margin-bottom: 2rem; } -} \ No newline at end of file +} + +.apps { + align-self: start; + + .applist { + margin: 0; + padding: 0; + + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 0.5rem; + align-content: start; + + .applist-entry { + display: grid; + grid-template-columns: 25% 1fr; + grid-template-areas: "logo text"; + gap: 1.5rem; + padding: 0.5rem; + + .applist-logo { + grid-area: logo; + align-self: center; + justify-self: center; + width: 100%; + object-fit: contain; + flex: 1 1 auto; + } + + .applist-logo.redraw { + fill: $fg; + stroke: $fg; + } + + .applist-text { + grid-area: text; + + a { + font-weight: bold; + } + } + } + } +} + +@media screen and (max-width: 600px) { + .apps .applist { + grid-template-columns: 1fr; + } +} diff --git a/web/source/css/page.css b/web/source/css/page.css new file mode 100644 index 000000000..d60dbe9c8 --- /dev/null +++ b/web/source/css/page.css @@ -0,0 +1,107 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +.page { + display: grid; + min-height: 100vh; + + grid-template-columns: 1fr minmax(auto, 50rem) 1fr; + grid-template-columns: 1fr min(92%, 50rem) 1fr; + grid-template-rows: auto 1fr auto; +} + +.page-header, .page-footer { + grid-column: 1 / span 3; +} + +.page-content { + grid-column: 2; + align-self: start; +} + +.page-header { + display: flex; + flex-direction: column; + justify-content: center; + padding: 1.5rem; + gap: 1rem; + + a { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + + img { + align-self: center; + } + + h1 { + align-self: center; + text-align: center; + + font-size: 1.5rem; + line-height: 1.5rem; + word-wrap: anywhere; + color: $fg; + } + } + + aside { + margin: 0; + font-style: italic; + font-weight: normal; + text-align: center; + font-size: 1.2rem; + + .count { + font-weight: bold; + color: $fg-accent; + } + } +} + +.page-footer { + align-self: end; + + nav ul { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + + /* Override list styling */ + list-style-type: none; + padding-left: 0; + + li { + text-align: center; + padding: 1rem; + flex-grow: 1; + + a { + font-weight: bold; + } + } + } +} + +@media screen and (max-width: 600px) { + .page-header { + text-align: center; + } +} diff --git a/web/source/css/prism.css b/web/source/css/prism.css new file mode 100644 index 000000000..c1d369a9c --- /dev/null +++ b/web/source/css/prism.css @@ -0,0 +1,5 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+bash+c+csharp+cpp+docker+elixir+erlang+go+go-module+ini+java+json+kotlin+lua+makefile+markup-templating+nginx+nix+perl+php+promql+python+r+jsx+tsx+ruby+rust+scala+sql+swift+typescript&plugins=show-invisibles+show-language+toolbar+copy-to-clipboard */ +code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} +.token.cr,.token.lf,.token.space,.token.tab:not(:empty){position:relative}.token.cr:before,.token.lf:before,.token.space:before,.token.tab:not(:empty):before{color:grey;opacity:.6;position:absolute}.token.tab:not(:empty):before{content:'\21E5'}.token.cr:before{content:'\240D'}.token.crlf:before{content:'\240D\240A'}.token.lf:before{content:'\240A'}.token.space:before{content:'\00B7'} +div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none} diff --git a/web/source/css/profile.css b/web/source/css/profile.css index bc1a32a5d..06c93e6e0 100644 --- a/web/source/css/profile.css +++ b/web/source/css/profile.css @@ -17,28 +17,27 @@ */ .page { - grid-template-columns: 1fr minmax(auto, 60rem) 1fr; /* fallback for lack of min() support */ + /* + Profile page can be a little wider than default + page, since we're using a side-by-side column view. + */ + grid-template-columns: 1fr minmax(auto, 60rem) 1fr; grid-template-columns: 1fr min(92%, 65rem) 1fr; } -.profile { - padding: 0.5rem; - border-radius: $br; - - .column-split { - display: flex; - flex-wrap: wrap; - gap: 1rem; - } +.profile .column-split { + display: flex; + flex-wrap: wrap; + gap: 1rem; } -.profile .header { +.profile .profile-header { background: $profile-bg; border-radius: $br; overflow: hidden; margin-bottom: 1rem; - .header-image { + .header-image-wrapper { position: relative; padding-top: 33.33%; /* aspect-ratio 1/3 */ @@ -55,12 +54,11 @@ /* Basic info container has the user's avatar, display- and username, and role - It's partially overlapped over the header image, by a negative margin-top + It's partially overlapped over the header image, by a negative margin-top. */ $avatar-size: 8.5rem; $name-size: 3rem; $username-size: 2rem; - $overlap: calc($avatar-size - $name-size - $username-size); .basic-info { @@ -71,8 +69,8 @@ grid-template-rows: $overlap $name-size auto; grid-template-areas: "avatar . ." - "avatar displayname displayname" - "avatar username role"; + "avatar namerole namerole" + "avatar namerole namerole"; margin: 1rem; margin-top: calc(-1 * $overlap); @@ -93,131 +91,119 @@ } } - .displayname { - grid-area: displayname; - line-height: $name-size; - font-size: 1.5rem; - font-weight: bold; - } + .namerole { + grid-area: namerole; - .username { - min-width: 0; - grid-area: username; - line-height: $username-size; + display: grid; + gap: 0 1rem; + box-sizing: border-box; + grid-template-columns: auto 1fr; + grid-template-rows: $name-size auto; + grid-template-areas: + "displayname displayname" + "username role"; - font-size: 1rem; - font-weight: bold; - color: $fg-accent; - user-select: all; - } - - .role { - background: $bg; - color: $fg; - border: 0.13rem solid $bg; - - grid-area: role; - align-self: center; - justify-self: start; - border-radius: $br; - padding: 0.3rem; - - line-height: 1.1rem; - font-size: 0.9rem; - font-variant: small-caps; - font-weight: bold; - - &.admin { - color: $role-admin; - border-color: $role-admin; + .displayname { + grid-area: displayname; + line-height: $name-size; + font-size: 1.5rem; + font-weight: bold; } - - &.moderator { - color: $role-mod; - border-color: $role-mod; + + .username { + min-width: 0; + grid-area: username; + line-height: $username-size; + + font-size: 1rem; + font-weight: bold; + color: $fg-accent; + user-select: all; + } + + .role { + background: $bg; + color: $fg; + border: 0.13rem solid $bg; + + grid-area: role; + align-self: center; + justify-self: start; + border-radius: $br; + padding: 0.3rem; + + line-height: 1.1rem; + font-size: 0.9rem; + font-variant: small-caps; + font-weight: bold; + + &.admin { + color: $role-admin; + border-color: $role-admin; + } + + &.moderator { + color: $role-mod; + border-color: $role-mod; + } } } } } @media screen and (max-width: 750px) { - .profile .header { + .profile .profile-header { .basic-info { grid-template-columns: auto 1fr; grid-template-rows: $avatar-size $name-size auto; grid-template-areas: "avatar avatar" - "displayname displayname" - "username role"; - - .displayname { - font-size: 1.4rem; + "namerole namerole" + "namerole namerole"; + + .namerole { + grid-template-columns: 1fr; + grid-template-rows: $name-size auto; + grid-template-areas: + "displayname displayname" + "username role"; + + .displayname { + font-size: 1.4rem; + } } } } } -.profile .col-header { - display: flex; - justify-content: start; - gap: 2rem; - align-items: center; - - margin: 0; - background: $profile-bg; - border-top-left-radius: $br; - border-top-right-radius: $br; - padding: 0.75rem; - - h1, h2 { - font-size: 1.2rem; - line-height: 1.3rem; - margin: 0; - } -} - -.profile .toots { +.profile .statuses-wrapper { flex: 65 25rem; display: flex; flex-direction: column; gap: 0.4rem; min-width: 0%; +} - .col-header { - display: grid; - grid-template-columns: auto 1fr; - gap: 1rem; +.profile .statuses { + display: flex; + flex-direction: column; + gap: 0.4rem; - a { - justify-self: end; - } - - .rss-icon { - display: block; - margin: -0.25rem 0; - - .fa { - font-size: 2rem; - object-fit: contain; - vertical-align: middle; - color: $orange2; - /* can't size a single-color background, so we use a linear-gradient that's effectively white */ - background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center; - background-size: 1.2rem 1.4rem; - } - } - } - - .toot { - border-radius: 0; - - .info { - padding: 0.3rem 0.75rem; - } - - &:last-child { - border-bottom-left-radius: $br; - border-bottom-right-radius: $br; + .rss-icon { + display: block; + margin: -0.25rem 0; + + .fa { + font-size: 2rem; + object-fit: contain; + vertical-align: middle; + color: $orange2; + /* + Can't size a single-color background, so we use + a linear-gradient that's effectively white. + */ + background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center; + background-size: 1.2rem 1.4rem; } } @@ -240,6 +226,10 @@ margin-bottom: -0.25rem; } + dt { + font-weight: bold; + } + .fields { background: $profile-bg; display: flex; diff --git a/web/source/css/status.css b/web/source/css/status.css index cbe8c04d2..019fbd0b4 100644 --- a/web/source/css/status.css +++ b/web/source/css/status.css @@ -19,25 +19,19 @@ @import "photoswipe/dist/photoswipe.css"; @import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css"; @import "plyr/dist/plyr.css"; +@import "./prism.css"; main { background: transparent; grid-auto-rows: auto; } -.thread { - display: flex; - flex-direction: column; - border-radius: $br; -} - -.toot { - background: $toot-bg; +.status { + background: $status-bg; box-shadow: $boxshadow; border: $boxshadow-border; border-radius: $br; position: relative; - margin-bottom: $br; padding-top: 0.75rem; a { @@ -47,66 +41,75 @@ main { text-decoration: none; } - .author > a { - padding: 0 0.75rem; - display: grid; - grid-template-columns: 3.5rem 1fr auto; - grid-template-rows: auto auto; - grid-template-areas: - "avatar display date" - "avatar user ."; - gap: 0 0.5rem; + .status-header > address { + /* + Avoid stretching so wide that user + can't click on open thread link that's + behind the status header link. + */ + width: fit-content; - .avatar { - grid-area: avatar; - height: 3.5rem; - width: 3.5rem; - object-fit: cover; - - border: 0.15rem solid $avatar-border; - border-radius: $br; - overflow: hidden; /* hides corners from img overflowing */ - - img { - height: 100%; - width: 100%; - object-fit: cover; - background: $bg; - } - } - - .displayname, .username { - justify-self: start; - align-self: start; - - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + > a { + padding: 0 0.75rem; + display: grid; + grid-template-columns: 3.5rem 1fr auto; + grid-template-rows: auto auto; + grid-template-areas: + "avatar author-strap author-strap" + "avatar author-strap author-strap"; + gap: 0 0.5rem; + font-style: normal; - .displayname { - grid-area: display; - font-weight: bold; - font-size: 1rem; - line-height: 1.3rem; - /* margin-top: -0.5rem; */ - } - - .username { - grid-area: user; - color: $link-fg; - font-size: 1rem; - line-height: 1.3rem; - } - - .timestamp { - grid-area: date; - color: $fg-reduced; + .avatar { + grid-area: avatar; + height: 3.5rem; + width: 3.5rem; + object-fit: cover; + + border: 0.15rem solid $avatar-border; + border-radius: $br; + overflow: hidden; /* hides corners from img overflowing */ + + img { + height: 100%; + width: 100%; + object-fit: cover; + background: $bg; + } + } + + .author-strap { + grid-area: author-strap; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto; + grid-template-areas: + "display display" + "user user"; + gap: 0 0.5rem; + + .displayname, .username { + justify-self: start; + align-self: start; + max-width: 100%; + font-size: 1rem; + line-height: 1.3rem; + } + + .displayname { + grid-area: display; + font-weight: bold; + } + + .username { + grid-area: user; + color: $link-fg; + } + } } } - .body { + .status-body { padding: 0.5rem 0.75rem; display: flex; flex-direction: column; @@ -157,6 +160,10 @@ main { line-height: 1.6rem; width: 100%; + /* + Normalize header sizes to fit better + with the line-height we use for statuses. + */ h1 { margin: 0; font-size: 1.8rem; @@ -187,35 +194,63 @@ main { line-height: initial; } - blockquote { - padding: 0.5rem 0 0.5rem 0.5rem; - border-left: 0.2rem solid $border-accent; - margin: 0; - font-style: italic; - } - - hr { - border: 1px dashed $border-accent; - } - pre, code { background-color: $gray2; } + /* + Just code on its own inside status + content, ie, `here is some code`. + */ code { padding: 0.25rem; border-radius: $br-inner; + white-space: pre-wrap; } - pre { + /* + Restyle Prism code highlighting toolbar + plugin buttons to our own button style. + */ + .code-toolbar .toolbar { + margin-right: 0.5rem; display: flex; + gap: 0.25rem; + + .toolbar-item { + span, button { + color: $button-fg; + background: $button-bg; + font-weight: bold; + } + + .copy-to-clipboard-button, span { + box-shadow: $boxshadow; + } + + .copy-to-clipboard-button:hover, .copy-to-clipboard-button:hover span { + background: $button-hover-bg; + } + } + } + + pre, pre[class*="language-"] { border-radius: $br; padding: 0.5rem; + white-space: pre; + overflow-x: auto; - code { - padding: 0.5rem; + /* + Code inside a pre block, ie., + + ``` + here is some code + ``` + */ + code { + width: 100%; + padding: 0; white-space: pre; - border-radius: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; } @@ -230,18 +265,6 @@ main { } } - .emoji { - transition: 0.1s; - } - - .emoji:hover, .emoji:active { - transform: scale(2); - background-color: $bg; - box-shadow: $boxshadow; - border: $boxshadow-border; - border-radius: $br-inner; - } - .poll { background-color: $gray2; z-index: 2; @@ -451,41 +474,41 @@ main { } } - .info { - display: flex; - background: $toot-info-bg; + .status-info { + background: $status-info-bg; color: $fg-reduced; - border-top: 0.15rem solid $toot-info-border; + border-top: 0.15rem solid $status-info-border; padding: 0.5rem 0.75rem; - time { - padding-right: 1rem; - } - - .stats { - display: inline-flex; - flex: 1; + .status-stats { + display: flex; gap: 1rem; - + + .stats-grouping { + display: flex; + flex-wrap: wrap; + column-gap: 1rem; + } + .stats-item { - span { - white-space: nowrap; - } + display: flex; + gap: 0.4rem; + } + + .stats-item:not(.published-at) { + z-index: 1; + user-select: none; } .language { margin-left: auto; - z-index: 1; - cursor: pointer; - user-select: none; } } grid-column: span 3; - flex-wrap: wrap; } - .toot-link { + .status-link { top: 0; right: 0; bottom: 0; @@ -508,15 +531,12 @@ main { /* bottom left, bottom right */ border-bottom-left-radius: $br; border-bottom-right-radius: $br; - margin-bottom: 0; } &.expanded { - background: $toot-focus-bg; - padding-bottom: 0; - - .info { - background: $toot-focus-info-bg; + background: $status-focus-bg; + .status-info { + background: $status-focus-info-bg; } } } diff --git a/web/source/css/thread.css b/web/source/css/thread.css new file mode 100644 index 000000000..fca998b3c --- /dev/null +++ b/web/source/css/thread.css @@ -0,0 +1,56 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +.thread { + display: flex; + flex-direction: column; + gap: 0.4rem; + + /* + This column header might contain + quite some info, so let it wrap. + */ + .col-header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + column-gap: 1rem; + row-gap: 0.5rem; + + box-shadow: $boxshadow; + border: $boxshadow-border; + + h2 { + margin-right: auto; + } + } + + .status { + border-radius: 0; + + &:last-child { + border-bottom-left-radius: $br; + border-bottom-right-radius: $br; + + .status-info { + border-bottom-left-radius: $br; + border-bottom-right-radius: $br; + } + } + } +} \ No newline at end of file diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js index faeb35ce4..74cb28460 100644 --- a/web/source/frontend/index.js +++ b/web/source/frontend/index.js @@ -21,6 +21,10 @@ const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js"); const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js"); const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default; const Plyr = require("plyr"); +const Prism = require("./prism.js"); + +Prism.manual = true; +Prism.highlightAll(); let [_, _user, type, id] = window.location.pathname.split("/"); if (type == "statuses") { diff --git a/web/source/frontend/prism.js b/web/source/frontend/prism.js new file mode 100644 index 000000000..b1eb83385 --- /dev/null +++ b/web/source/frontend/prism.js @@ -0,0 +1,42 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+bash+c+csharp+cpp+docker+elixir+erlang+go+go-module+ini+java+json+kotlin+lua+makefile+markup-templating+nginx+nix+perl+php+promql+python+r+jsx+tsx+ruby+rust+scala+sql+swift+typescript&plugins=show-invisibles+show-language+toolbar+copy-to-clipboard */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; +!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; +!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),Prism.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],char:Prism.languages.c.char,comment:Prism.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}}}),Prism.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete Prism.languages.c.boolean; +!function(e){function n(e,n){return e.replace(/<<(\d+)>>/g,(function(e,s){return"(?:"+n[+s]+")"}))}function s(e,s,a){return RegExp(n(e,s),a||"")}function a(e,n){for(var s=0;s>/g,(function(){return"(?:"+e+")"}));return e.replace(/<>/g,"[^\\s\\S]")}var t="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",r="class enum interface record struct",i="add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",o="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function l(e){return"\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var d=l(r),p=RegExp(l(t+" "+r+" "+i+" "+o)),c=l(r+" "+i+" "+o),u=l(t+" "+r+" "+o),g=a("<(?:[^<>;=+\\-*/%&|^]|<>)*>",2),b=a("\\((?:[^()]|<>)*\\)",2),h="@?\\b[A-Za-z_]\\w*\\b",f=n("<<0>>(?:\\s*<<1>>)?",[h,g]),m=n("(?!<<0>>)<<1>>(?:\\s*\\.\\s*<<1>>)*",[c,f]),k="\\[\\s*(?:,\\s*)*\\]",y=n("<<0>>(?:\\s*(?:\\?\\s*)?<<1>>)*(?:\\s*\\?)?",[m,k]),w=n("[^,()<>[\\];=+\\-*/%&|^]|<<0>>|<<1>>|<<2>>",[g,b,k]),v=n("\\(<<0>>+(?:,<<0>>+)+\\)",[w]),x=n("(?:<<0>>|<<1>>)(?:\\s*(?:\\?\\s*)?<<2>>)*(?:\\s*\\?)?",[v,m,k]),$={keyword:p,punctuation:/[<>()?,.:[\]]/},_="'(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'",B='"(?:\\\\.|[^\\\\"\r\n])*"';e.languages.csharp=e.languages.extend("clike",{string:[{pattern:s("(^|[^$\\\\])<<0>>",['@"(?:""|\\\\[^]|[^\\\\"])*"(?!")']),lookbehind:!0,greedy:!0},{pattern:s("(^|[^@$\\\\])<<0>>",[B]),lookbehind:!0,greedy:!0}],"class-name":[{pattern:s("(\\busing\\s+static\\s+)<<0>>(?=\\s*;)",[m]),lookbehind:!0,inside:$},{pattern:s("(\\busing\\s+<<0>>\\s*=\\s*)<<1>>(?=\\s*;)",[h,x]),lookbehind:!0,inside:$},{pattern:s("(\\busing\\s+)<<0>>(?=\\s*=)",[h]),lookbehind:!0},{pattern:s("(\\b<<0>>\\s+)<<1>>",[d,f]),lookbehind:!0,inside:$},{pattern:s("(\\bcatch\\s*\\(\\s*)<<0>>",[m]),lookbehind:!0,inside:$},{pattern:s("(\\bwhere\\s+)<<0>>",[h]),lookbehind:!0},{pattern:s("(\\b(?:is(?:\\s+not)?|as)\\s+)<<0>>",[y]),lookbehind:!0,inside:$},{pattern:s("\\b<<0>>(?=\\s+(?!<<1>>|with\\s*\\{)<<2>>(?:\\s*[=,;:{)\\]]|\\s+(?:in|when)\\b))",[x,u,h]),inside:$}],keyword:p,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),e.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),e.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:s("([(,]\\s*)<<0>>(?=\\s*:)",[h]),lookbehind:!0,alias:"punctuation"}}),e.languages.insertBefore("csharp","class-name",{namespace:{pattern:s("(\\b(?:namespace|using)\\s+)<<0>>(?:\\s*\\.\\s*<<0>>)*(?=\\s*[;{])",[h]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:s("(\\b(?:default|sizeof|typeof)\\s*\\(\\s*(?!\\s))(?:[^()\\s]|\\s(?!\\s)|<<0>>)*(?=\\s*\\))",[b]),lookbehind:!0,alias:"class-name",inside:$},"return-type":{pattern:s("<<0>>(?=\\s+(?:<<1>>\\s*(?:=>|[({]|\\.\\s*this\\s*\\[)|this\\s*\\[))",[x,m]),inside:$,alias:"class-name"},"constructor-invocation":{pattern:s("(\\bnew\\s+)<<0>>(?=\\s*[[({])",[x]),lookbehind:!0,inside:$,alias:"class-name"},"generic-method":{pattern:s("<<0>>\\s*<<1>>(?=\\s*\\()",[h,g]),inside:{function:s("^<<0>>",[h]),generic:{pattern:RegExp(g),alias:"class-name",inside:$}}},"type-list":{pattern:s("\\b((?:<<0>>\\s+<<1>>|record\\s+<<1>>\\s*<<5>>|where\\s+<<2>>)\\s*:\\s*)(?:<<3>>|<<4>>|<<1>>\\s*<<5>>|<<6>>)(?:\\s*,\\s*(?:<<3>>|<<4>>|<<6>>))*(?=\\s*(?:where|[{;]|=>|$))",[d,f,h,x,p.source,b,"\\bnew\\s*\\(\\s*\\)"]),lookbehind:!0,inside:{"record-arguments":{pattern:s("(^(?!new\\s*\\()<<0>>\\s*)<<1>>",[f,b]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:p,"class-name":{pattern:RegExp(x),greedy:!0,inside:$},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var E=B+"|"+_,R=n("/(?![*/])|//[^\r\n]*[\r\n]|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>",[E]),z=a(n("[^\"'/()]|<<0>>|\\(<>*\\)",[R]),2),S="\\b(?:assembly|event|field|method|module|param|property|return|type)\\b",j=n("<<0>>(?:\\s*\\(<<1>>*\\))?",[m,z]);e.languages.insertBefore("csharp","class-name",{attribute:{pattern:s("((?:^|[^\\s\\w>)?])\\s*\\[\\s*)(?:<<0>>\\s*:\\s*)?<<1>>(?:\\s*,\\s*<<1>>)*(?=\\s*\\])",[S,j]),lookbehind:!0,greedy:!0,inside:{target:{pattern:s("^<<0>>(?=\\s*:)",[S]),alias:"keyword"},"attribute-arguments":{pattern:s("\\(<<0>>*\\)",[z]),inside:e.languages.csharp},"class-name":{pattern:RegExp(m),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var A=":[^}\r\n]+",F=a(n("[^\"'/()]|<<0>>|\\(<>*\\)",[R]),2),P=n("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}",[F,A]),U=a(n("[^\"'/()]|/(?!\\*)|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>|\\(<>*\\)",[E]),2),Z=n("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}",[U,A]);function q(n,a){return{interpolation:{pattern:s("((?:^|[^{])(?:\\{\\{)*)<<0>>",[n]),lookbehind:!0,inside:{"format-string":{pattern:s("(^\\{(?:(?![}:])<<0>>)*)<<1>>(?=\\}$)",[a,A]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:e.languages.csharp}}},string:/[\s\S]+/}}e.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:s('(^|[^\\\\])(?:\\$@|@\\$)"(?:""|\\\\[^]|\\{\\{|<<0>>|[^\\\\{"])*"',[P]),lookbehind:!0,greedy:!0,inside:q(P,F)},{pattern:s('(^|[^@\\\\])\\$"(?:\\\\.|\\{\\{|<<0>>|[^\\\\"{])*"',[Z]),lookbehind:!0,greedy:!0,inside:q(Z,U)}],char:{pattern:RegExp(_),greedy:!0}}),e.languages.dotnet=e.languages.cs=e.languages.csharp}(Prism); +!function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n="\\b(?!)\\w+(?:\\s*\\.\\s*\\w+)*\\b".replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp("(\\b(?:class|concept|enum|struct|typename)\\s+)(?!)\\w+".replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp('(\\b(?:import|module)\\s+)(?:"(?:\\\\(?:\r\n|[^])|[^"\\\\\r\n])*"|<[^<>\r\n]*>|'+"(?:\\s*:\\s*)?|:\\s*".replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(Prism); +!function(e){var n="(?:[ \t]+(?![ \t])(?:)?|)".replace(//g,(function(){return"\\\\[\r\n](?:\\s|\\\\[\r\n]|#.*(?!.))*(?![\\s#]|\\\\[\r\n])"})),r="\"(?:[^\"\\\\\r\n]|\\\\(?:\r\n|[^]))*\"|'(?:[^'\\\\\r\n]|\\\\(?:\r\n|[^]))*'",t="--[\\w-]+=(?:|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)".replace(//g,(function(){return r})),o={pattern:RegExp(r),greedy:!0},i={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function a(e,r){return e=e.replace(//g,(function(){return t})).replace(//g,(function(){return n})),RegExp(e,r)}e.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:a("(^(?:ONBUILD)?\\w+)(?:)*","i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[o,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:a("(^(?:ONBUILD)?HEALTHCHECK(?:)*)(?:CMD|NONE)\\b","i"),lookbehind:!0,greedy:!0},{pattern:a("(^(?:ONBUILD)?FROM(?:)*(?!--)[^ \t\\\\]+)AS","i"),lookbehind:!0,greedy:!0},{pattern:a("(^ONBUILD)\\w+","i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:i,string:o,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,operator:/\\$/m}},comment:i},e.languages.dockerfile=e.languages.docker}(Prism); +Prism.languages.elixir={doc:{pattern:/@(?:doc|moduledoc)\s+(?:("""|''')[\s\S]*?\1|("|')(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2)/,inside:{attribute:/^@\w+/,string:/['"][\s\S]+/}},comment:{pattern:/#.*/,greedy:!0},regex:{pattern:/~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/,greedy:!0},string:[{pattern:/~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|#(?!\{)|[^#\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/,greedy:!0,inside:{}},{pattern:/("""|''')[\s\S]*?\1/,greedy:!0,inside:{}},{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{}}],atom:{pattern:/(^|[^:]):\w+/,lookbehind:!0,alias:"symbol"},module:{pattern:/\b[A-Z]\w*\b/,alias:"class-name"},"attr-name":/\b\w+\??:(?!:)/,argument:{pattern:/(^|[^&])&\d+/,lookbehind:!0,alias:"variable"},attribute:{pattern:/@\w+/,alias:"variable"},function:/\b[_a-zA-Z]\w*[?!]?(?:(?=\s*(?:\.\s*)?\()|(?=\/\d))/,number:/\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i,keyword:/\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\b/,boolean:/\b(?:false|nil|true)\b/,operator:[/\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/,{pattern:/([^<])<(?!<)/,lookbehind:!0},{pattern:/([^>])>(?!>)/,lookbehind:!0}],punctuation:/<<|>>|[.,%\[\]{}()]/},Prism.languages.elixir.string.forEach((function(e){e.inside={interpolation:{pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"},rest:Prism.languages.elixir}}}})); +Prism.languages.erlang={comment:/%.+/,string:{pattern:/"(?:\\.|[^\\"\r\n])*"/,greedy:!0},"quoted-function":{pattern:/'(?:\\.|[^\\'\r\n])+'(?=\()/,alias:"function"},"quoted-atom":{pattern:/'(?:\\.|[^\\'\r\n])+'/,alias:"atom"},boolean:/\b(?:false|true)\b/,keyword:/\b(?:after|begin|case|catch|end|fun|if|of|receive|try|when)\b/,number:[/\$\\?./,/\b\d+#[a-z0-9]+/i,/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i],function:/\b[a-z][\w@]*(?=\()/,variable:{pattern:/(^|[^@])(?:\b|\?)[A-Z_][\w@]*/,lookbehind:!0},operator:[/[=\/<>:]=|=[:\/]=|\+\+?|--?|[=*\/!]|\b(?:and|andalso|band|bnot|bor|bsl|bsr|bxor|div|not|or|orelse|rem|xor)\b/,{pattern:/(^|[^<])<(?!<)/,lookbehind:!0},{pattern:/(^|[^>])>(?!>)/,lookbehind:!0}],atom:/\b[a-z][\w@]*/,punctuation:/[()[\]{}:;,.#|]|<<|>>/}; +Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),Prism.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go["class-name"]; +Prism.languages["go-mod"]=Prism.languages["go-module"]={comment:{pattern:/\/\/.*/,greedy:!0},version:{pattern:/(^|[\s()[\],])v\d+\.\d+\.\d+(?:[+-][-+.\w]*)?(?![^\s()[\],])/,lookbehind:!0,alias:"number"},"go-version":{pattern:/((?:^|\s)go\s+)\d+(?:\.\d+){1,2}/,lookbehind:!0,alias:"number"},keyword:{pattern:/^([ \t]*)(?:exclude|go|module|replace|require|retract)\b/m,lookbehind:!0},operator:/=>/,punctuation:/[()[\],]/}; +Prism.languages.ini={comment:{pattern:/(^[ \f\t\v]*)[#;][^\n\r]*/m,lookbehind:!0},section:{pattern:/(^[ \f\t\v]*)\[[^\n\r\]]*\]?/m,lookbehind:!0,inside:{"section-name":{pattern:/(^\[[ \f\t\v]*)[^ \f\t\v\]]+(?:[ \f\t\v]+[^ \f\t\v\]]+)*/,lookbehind:!0,alias:"selector"},punctuation:/\[|\]/}},key:{pattern:/(^[ \f\t\v]*)[^ \f\n\r\t\v=]+(?:[ \f\t\v]+[^ \f\n\r\t\v=]+)*(?=[ \f\t\v]*=)/m,lookbehind:!0,alias:"attr-name"},value:{pattern:/(=[ \f\t\v]*)[^ \f\n\r\t\v]+(?:[ \f\t\v]+[^ \f\n\r\t\v]+)*/,lookbehind:!0,alias:"attr-value",inside:{"inner-value":{pattern:/^("|').+(?=\1$)/,lookbehind:!0}}},punctuation:/=/}; +!function(e){var n=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,t="(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*",s={pattern:RegExp("(^|[^\\w.])"+t+"[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[s,{pattern:RegExp("(^|[^\\w.])"+t+"[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()]|\\s*(?:\\[[\\s,]*\\]\\s*)?::\\s*new\\b)"),lookbehind:!0,inside:s.inside},{pattern:RegExp("(\\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\\s+)"+t+"[A-Z]\\w*\\b"),lookbehind:!0,inside:s.inside}],keyword:n,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":s,keyword:n,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp("(\\bimport\\s+)"+t+"(?:[A-Z]\\w*|\\*)(?=\\s*;)"),lookbehind:!0,inside:{namespace:s.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp("(\\bimport\\s+static\\s+)"+t+"(?:\\w+|\\*)(?=\\s*;)"),lookbehind:!0,alias:"static",inside:{namespace:s.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp("(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)(?!)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?".replace(//g,(function(){return n.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism); +Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; +!function(n){n.languages.kotlin=n.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,lookbehind:!0},function:[{pattern:/(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/,greedy:!0},{pattern:/(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,lookbehind:!0,greedy:!0}],number:/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete n.languages.kotlin["class-name"];var e={"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:n.languages.kotlin}};n.languages.insertBefore("kotlin","string",{"string-literal":[{pattern:/"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,alias:"multiline",inside:{interpolation:{pattern:/\$(?:[a-z_]\w*|\{[^{}]*\})/i,inside:e},string:/[\s\S]+/}},{pattern:/"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,alias:"singleline",inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,lookbehind:!0,inside:e},string:/[\s\S]+/}}],char:{pattern:/'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete n.languages.kotlin.string,n.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),n.languages.insertBefore("kotlin","function",{label:{pattern:/\b\w+@|@\w+\b/,alias:"symbol"}}),n.languages.kt=n.languages.kotlin,n.languages.kts=n.languages.kotlin}(Prism); +Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/}; +Prism.languages.makefile={comment:{pattern:/(^|[^\\])#(?:\\(?:\r\n|[\s\S])|[^\\\r\n])*/,lookbehind:!0},string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"builtin-target":{pattern:/\.[A-Z][^:#=\s]+(?=\s*:(?!=))/,alias:"builtin"},target:{pattern:/^(?:[^:=\s]|[ \t]+(?![\s:]))+(?=\s*:(?!=))/m,alias:"symbol",inside:{variable:/\$+(?:(?!\$)[^(){}:#=\s]+|(?=[({]))/}},variable:/\$+(?:(?!\$)[^(){}:#=\s]+|\([@*%<^+?][DF]\)|(?=[({]))/,keyword:/-include\b|\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\b/,function:{pattern:/(\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \t])/,lookbehind:!0},operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/}; +!function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism); +!function(e){var n=/\$(?:\w[a-z\d]*(?:_[^\x00-\x1F\s"'\\()$]*)?|\{[^}\s"'\\]+\})/i;e.languages.nginx={comment:{pattern:/(^|[\s{};])#.*/,lookbehind:!0,greedy:!0},directive:{pattern:/(^|\s)\w(?:[^;{}"'\\\s]|\\.|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\s+(?:#.*(?!.)|(?![#\s])))*?(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:{string:{pattern:/((?:^|[^\\])(?:\\\\)*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,lookbehind:!0,greedy:!0,inside:{escape:{pattern:/\\["'\\nrt]/,alias:"entity"},variable:n}},comment:{pattern:/(\s)#.*/,lookbehind:!0,greedy:!0},keyword:{pattern:/^\S+/,greedy:!0},boolean:{pattern:/(\s)(?:off|on)(?!\S)/,lookbehind:!0},number:{pattern:/(\s)\d+[a-z]*(?!\S)/i,lookbehind:!0},variable:n}},punctuation:/[{};]/}}(Prism); +Prism.languages.nix={comment:{pattern:/\/\*[\s\S]*?\*\/|#.*/,greedy:!0},string:{pattern:/"(?:[^"\\]|\\[\s\S])*"|''(?:(?!'')[\s\S]|''(?:'|\\|\$\{))*''/,greedy:!0,inside:{interpolation:{pattern:/(^|(?:^|(?!'').)[^\\])\$\{(?:[^{}]|\{[^}]*\})*\}/,lookbehind:!0,inside:null}}},url:[/\b(?:[a-z]{3,7}:\/\/)[\w\-+%~\/.:#=?&]+/,{pattern:/([^\/])(?:[\w\-+%~.:#=?&]*(?!\/\/)[\w\-+%~\/.:#=?&])?(?!\/\/)\/[\w\-+%~\/.:#=?&]*/,lookbehind:!0}],antiquotation:{pattern:/\$(?=\{)/,alias:"important"},number:/\b\d+\b/,keyword:/\b(?:assert|builtins|else|if|in|inherit|let|null|or|then|with)\b/,function:/\b(?:abort|add|all|any|attrNames|attrValues|baseNameOf|compareVersions|concatLists|currentSystem|deepSeq|derivation|dirOf|div|elem(?:At)?|fetch(?:Tarball|url)|filter(?:Source)?|fromJSON|genList|getAttr|getEnv|hasAttr|hashString|head|import|intersectAttrs|is(?:Attrs|Bool|Function|Int|List|Null|String)|length|lessThan|listToAttrs|map|mul|parseDrvName|pathExists|read(?:Dir|File)|removeAttrs|replaceStrings|seq|sort|stringLength|sub(?:string)?|tail|throw|to(?:File|JSON|Path|String|XML)|trace|typeOf)\b|\bfoldl'\B/,boolean:/\b(?:false|true)\b/,operator:/[=!<>]=?|\+\+?|\|\||&&|\/\/|->?|[?@]/,punctuation:/[{}()[\].,:;]/},Prism.languages.nix.string.inside.interpolation.inside=Prism.languages.nix; +!function(e){var n="(?:\\((?:[^()\\\\]|\\\\[^])*\\)|\\{(?:[^{}\\\\]|\\\\[^])*\\}|\\[(?:[^[\\]\\\\]|\\\\[^])*\\]|<(?:[^<>\\\\]|\\\\[^])*>)";e.languages.perl={comment:[{pattern:/(^\s*)=\w[\s\S]*?=cut.*/m,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\$])#.*/,lookbehind:!0,greedy:!0}],string:[{pattern:RegExp("\\b(?:q|qq|qw|qx)(?![a-zA-Z0-9])\\s*(?:"+["([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[^])*\\1","([a-zA-Z0-9])(?:(?!\\2)[^\\\\]|\\\\[^])*\\2",n].join("|")+")"),greedy:!0},{pattern:/("|`)(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/'(?:[^'\\\r\n]|\\.)*'/,greedy:!0}],regex:[{pattern:RegExp("\\b(?:m|qr)(?![a-zA-Z0-9])\\s*(?:"+["([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[^])*\\1","([a-zA-Z0-9])(?:(?!\\2)[^\\\\]|\\\\[^])*\\2",n].join("|")+")[msixpodualngc]*"),greedy:!0},{pattern:RegExp("(^|[^-])\\b(?:s|tr|y)(?![a-zA-Z0-9])\\s*(?:"+["([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\2)[^\\\\]|\\\\[^])*\\2(?:(?!\\2)[^\\\\]|\\\\[^])*\\2","([a-zA-Z0-9])(?:(?!\\3)[^\\\\]|\\\\[^])*\\3(?:(?!\\3)[^\\\\]|\\\\[^])*\\3",n+"\\s*"+n].join("|")+")[msixpodualngcer]*"),lookbehind:!0,greedy:!0},{pattern:/\/(?:[^\/\\\r\n]|\\.)*\/[msixpodualngc]*(?=\s*(?:$|[\r\n,.;})&|\-+*~<>!?^]|(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|x|xor)\b))/,greedy:!0}],variable:[/[&*$@%]\{\^[A-Z]+\}/,/[&*$@%]\^[A-Z_]/,/[&*$@%]#?(?=\{)/,/[&*$@%]#?(?:(?:::)*'?(?!\d)[\w$]+(?![\w$]))+(?:::)*/,/[&*$@%]\d+/,/(?!%=)[$@%][!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~]/],filehandle:{pattern:/<(?![<=])\S*?>|\b_\b/,alias:"symbol"},"v-string":{pattern:/v\d+(?:\.\d+)*|\d+(?:\.\d+){2,}/,alias:"string"},function:{pattern:/(\bsub[ \t]+)\w+/,lookbehind:!0},keyword:/\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while)\b/,number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)\b/,operator:/-[rwxoRWXOezsfdlpSbctugkTBMAC]\b|\+[+=]?|-[-=>]?|\*\*?=?|\/\/?=?|=[=~>]?|~[~=]?|\|\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\.(?:=|\.\.?)?|[\\?]|\bx(?:=|\b)|\b(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|xor)\b/,punctuation:/[{}[\];(),:]/}}(Prism); +!function(e){var a=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,t=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],i=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,n=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,s=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:a,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:t,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:i,operator:n,punctuation:s};var l={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},r=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:l}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:l}}];e.languages.insertBefore("php","variable",{string:r,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:a,string:r,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:t,number:i,operator:n,punctuation:s}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(a){/<\?/.test(a.code)&&e.languages["markup-templating"].buildPlaceholders(a,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g)})),e.hooks.add("after-tokenize",(function(a){e.languages["markup-templating"].tokenizePlaceholders(a,"php")}))}(Prism); +!function(t){var n=["on","ignoring","group_right","group_left","by","without"],a=["sum","min","max","avg","group","stddev","stdvar","count","count_values","bottomk","topk","quantile"].concat(n,["offset"]);t.languages.promql={comment:{pattern:/(^[ \t]*)#.*/m,lookbehind:!0},"vector-match":{pattern:new RegExp("((?:"+n.join("|")+")\\s*)\\([^)]*\\)"),lookbehind:!0,inside:{"label-key":{pattern:/\b[^,]+\b/,alias:"attr-name"},punctuation:/[(),]/}},"context-labels":{pattern:/\{[^{}]*\}/,inside:{"label-key":{pattern:/\b[a-z_]\w*(?=\s*(?:=|![=~]))/,alias:"attr-name"},"label-value":{pattern:/(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0,alias:"attr-value"},punctuation:/\{|\}|=~?|![=~]|,/}},"context-range":[{pattern:/\[[\w\s:]+\]/,inside:{punctuation:/\[|\]|:/,"range-duration":{pattern:/\b(?:\d+(?:[smhdwy]|ms))+\b/i,alias:"number"}}},{pattern:/(\boffset\s+)\w+/,lookbehind:!0,inside:{"range-duration":{pattern:/\b(?:\d+(?:[smhdwy]|ms))+\b/i,alias:"number"}}}],keyword:new RegExp("\\b(?:"+a.join("|")+")\\b","i"),function:/\b[a-z_]\w*(?=\s*\()/i,number:/[-+]?(?:(?:\b\d+(?:\.\d+)?|\B\.\d+)(?:e[-+]?\d+)?\b|\b(?:0x[0-9a-f]+|nan|inf)\b)/i,operator:/[\^*/%+-]|==|!=|<=|<|>=|>|\b(?:and|or|unless)\b/i,punctuation:/[{};()`,.[\]]/}}(Prism); +Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; +Prism.languages.r={comment:/#.*/,string:{pattern:/(['"])(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},"percent-operator":{pattern:/%[^%\s]*%/,alias:"operator"},boolean:/\b(?:FALSE|TRUE)\b/,ellipsis:/\.\.(?:\.|\d+)/,number:[/\b(?:Inf|NaN)\b/,/(?:\b0x[\dA-Fa-f]+(?:\.\d*)?|\b\d+(?:\.\d*)?|\B\.\d+)(?:[EePp][+-]?\d+)?[iL]?/],keyword:/\b(?:NA|NA_character_|NA_complex_|NA_integer_|NA_real_|NULL|break|else|for|function|if|in|next|repeat|while)\b/,operator:/->?>?|<(?:=|=!]=?|::?|&&?|\|\|?|[+*\/^$@~]/,punctuation:/[(){}\[\],;]/}; +!function(t){var n=t.util.clone(t.languages.javascript),e="(?:\\{*\\.{3}(?:[^{}]|)*\\})";function a(t,n){return t=t.replace(//g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(//g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(//g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend("markup",n),t.languages.jsx.tag.pattern=a("+(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+|))?|))**/?)?>"),t.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,t.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,t.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore("inside","attr-name",{spread:{pattern:a(""),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("="),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?"string"==typeof t?t:"string"==typeof t.content?t.content:t.content.map(s).join(""):""},g=function(n){for(var e=[],a=0;a0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():"/>"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&"punctuation"===o.type&&"{"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&"punctuation"===o.type&&"}"===o.content?e[e.length-1].openedBraces--:i=!0),(i||"string"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a0&&("string"==typeof n[a-1]||"plain-text"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token("plain-text",r,null,r)}o.content&&"string"!=typeof o.content&&g(o.content)}};t.hooks.add("after-tokenize",(function(t){"jsx"!==t.language&&"tsx"!==t.language||g(t.tokens)}))}(Prism); +!function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism); +!function(e){var a=e.util.clone(e.languages.typescript);e.languages.tsx=e.languages.extend("jsx",a),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"];var t=e.languages.tsx.tag;t.pattern=RegExp("(^|[^\\w$]|(?=|[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\].,;]/}),e.languages.insertBefore("ruby","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}});var n={pattern:/((?:^|[^\\])(?:\\{2})*)#\{(?:[^{}]|\{[^{}]*\})*\}/,lookbehind:!0,inside:{content:{pattern:/^(#\{)[\s\S]+(?=\}$)/,lookbehind:!0,inside:e.languages.ruby},delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"}}};delete e.languages.ruby.function;var t="(?:"+["([^a-zA-Z0-9\\s{(\\[<=])(?:(?!\\1)[^\\\\]|\\\\[^])*\\1","\\((?:[^()\\\\]|\\\\[^]|\\((?:[^()\\\\]|\\\\[^])*\\))*\\)","\\{(?:[^{}\\\\]|\\\\[^]|\\{(?:[^{}\\\\]|\\\\[^])*\\})*\\}","\\[(?:[^\\[\\]\\\\]|\\\\[^]|\\[(?:[^\\[\\]\\\\]|\\\\[^])*\\])*\\]","<(?:[^<>\\\\]|\\\\[^]|<(?:[^<>\\\\]|\\\\[^])*>)*>"].join("|")+")",i='(?:"(?:\\\\.|[^"\\\\\r\n])*"|(?:\\b[a-zA-Z_]\\w*|[^\\s\0-\\x7F]+)[?!]?|\\$.)';e.languages.insertBefore("ruby","keyword",{"regex-literal":[{pattern:RegExp("%r"+t+"[egimnosux]{0,6}"),greedy:!0,inside:{interpolation:n,regex:/[\s\S]+/}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:n,regex:/[\s\S]+/}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:[{pattern:RegExp("(^|[^:]):"+i),lookbehind:!0,greedy:!0},{pattern:RegExp("([\r\n{(,][ \t]*)"+i+"(?=:(?!:))"),lookbehind:!0,greedy:!0}],"method-definition":{pattern:/(\bdef\s+)\w+(?:\s*\.\s*\w+)?/,lookbehind:!0,inside:{function:/\b\w+$/,keyword:/^self\b/,"class-name":/^\w+/,punctuation:/\./}}}),e.languages.insertBefore("ruby","string",{"string-literal":[{pattern:RegExp("%[qQiIwWs]?"+t),greedy:!0,inside:{interpolation:n,string:/[\s\S]+/}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:n,string:/[\s\S]+/}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?/}},interpolation:n,string:/[\s\S]+/}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\s\S]+/}}],"command-literal":[{pattern:RegExp("%x"+t),greedy:!0,inside:{interpolation:n,command:{pattern:/[\s\S]+/,alias:"string"}}},{pattern:/`(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|[^\\`#\r\n])*`/,greedy:!0,inside:{interpolation:n,command:{pattern:/[\s\S]+/,alias:"string"}}}]}),delete e.languages.ruby.string,e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\b/,constant:/\b[A-Z][A-Z0-9_]*(?:[?!]|\b)/}),e.languages.rb=e.languages.ruby}(Prism); +!function(e){for(var a="/\\*(?:[^*/]|\\*(?!/)|/(?!\\*)|)*\\*/",t=0;t<2;t++)a=a.replace(//g,(function(){return a}));a=a.replace(//g,(function(){return"[^\\s\\S]"})),e.languages.rust={comment:[{pattern:RegExp("(^|[^\\\\])"+a),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?"(?:\\[\s\S]|[^\\"])*"|b?r(#*)"(?:[^"]|"(?!\1))*"\1/,greedy:!0},char:{pattern:/b?'(?:\\(?:x[0-7][\da-fA-F]|u\{(?:[\da-fA-F]_*){1,6}\}|.)|[^\\\r\n\t'])'/,greedy:!0},attribute:{pattern:/#!?\[(?:[^\[\]"]|"(?:\\[\s\S]|[^\\"])*")*\]/,greedy:!0,alias:"attr-name",inside:{string:null}},"closure-params":{pattern:/([=(,:]\s*|\bmove\s*)\|[^|]*\||\|[^|]*\|(?=\s*(?:\{|->))/,lookbehind:!0,greedy:!0,inside:{"closure-punctuation":{pattern:/^\||\|$/,alias:"punctuation"},rest:null}},"lifetime-annotation":{pattern:/'\w+/,alias:"symbol"},"fragment-specifier":{pattern:/(\$\w+:)[a-z]+/,lookbehind:!0,alias:"punctuation"},variable:/\$\w+/,"function-definition":{pattern:/(\bfn\s+)\w+/,lookbehind:!0,alias:"function"},"type-definition":{pattern:/(\b(?:enum|struct|trait|type|union)\s+)\w+/,lookbehind:!0,alias:"class-name"},"module-declaration":[{pattern:/(\b(?:crate|mod)\s+)[a-z][a-z_\d]*/,lookbehind:!0,alias:"namespace"},{pattern:/(\b(?:crate|self|super)\s*)::\s*[a-z][a-z_\d]*\b(?:\s*::(?:\s*[a-z][a-z_\d]*\s*::)*)?/,lookbehind:!0,alias:"namespace",inside:{punctuation:/::/}}],keyword:[/\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,/\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\b/],function:/\b[a-z_]\w*(?=\s*(?:::\s*<|\())/,macro:{pattern:/\b\w+!/,alias:"property"},constant:/\b[A-Z_][A-Z_\d]+\b/,"class-name":/\b[A-Z]\w*\b/,namespace:{pattern:/(?:\b[a-z][a-z_\d]*\s*::\s*)*\b[a-z][a-z_\d]*\s*::(?!\s*<)/,inside:{punctuation:/::/}},number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\b/,boolean:/\b(?:false|true)\b/,punctuation:/->|\.\.=|\.{1,3}|::|[{}[\];(),:]/,operator:/[-+*\/%!^]=?|=[=>]?|&[&=]?|\|[|=]?|<>?=?|[@?]/},e.languages.rust["closure-params"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(Prism); +Prism.languages.scala=Prism.languages.extend("java",{"triple-quoted-string":{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},keyword:/<-|=>|\b(?:abstract|case|catch|class|def|derives|do|else|enum|extends|extension|final|finally|for|forSome|given|if|implicit|import|infix|inline|lazy|match|new|null|object|opaque|open|override|package|private|protected|return|sealed|self|super|this|throw|trait|transparent|try|type|using|val|var|while|with|yield)\b/,number:/\b0x(?:[\da-f]*\.)?[\da-f]+|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e\d+)?[dfl]?/i,builtin:/\b(?:Any|AnyRef|AnyVal|Boolean|Byte|Char|Double|Float|Int|Long|Nothing|Short|String|Unit)\b/,symbol:/'[^\d\s\\]\w*/}),Prism.languages.insertBefore("scala","triple-quoted-string",{"string-interpolation":{pattern:/\b[a-z]\w*(?:"""(?:[^$]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*?"""|"(?:[^$"\r\n]|\$(?:[^{]|\{(?:[^{}]|\{[^{}]*\})*\}))*")/i,greedy:!0,inside:{id:{pattern:/^\w+/,greedy:!0,alias:"function"},escape:{pattern:/\\\$"|\$[$"]/,greedy:!0,alias:"symbol"},interpolation:{pattern:/\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,greedy:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:Prism.languages.scala}}},string:/[\s\S]+/}}}),delete Prism.languages.scala["class-name"],delete Prism.languages.scala.function,delete Prism.languages.scala.constant; +Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:FALSE|NULL|TRUE)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/}; +Prism.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp('(^|[^"#])(?:"(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^(])|[^\\\\\r\n"])*"|"""(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|[^(])|[^\\\\"]|"(?!""))*""")(?!["#])'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp('(^|[^"#])(#+)(?:"(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^#])|[^\\\\\r\n])*?"|"""(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|[^#])|[^\\\\])*?""")\\2'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp("#(?:(?:elseif|if)\\b(?:[ \t]*(?:![ \t]*)?(?:\\b\\w+\\b(?:[ \t]*\\((?:[^()]|\\([^()]*\\))*\\))?|\\((?:[^()]|\\([^()]*\\))*\\))(?:[ \t]*(?:&&|\\|\\|))?)+|(?:else|endif)\\b)"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:false|true)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},Prism.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift})); +!function(){if("undefined"!=typeof Prism){var r={tab:/\t/,crlf:/\r\n/,lf:/\n/,cr:/\r/,space:/ /};Prism.hooks.add("before-highlight",(function(r){i(r.grammar)}))}function e(r,a){var n=r[a];switch(Prism.util.type(n)){case"RegExp":var t={};r[a]={pattern:n,inside:t},i(t);break;case"Array":for(var f=0,s=n.length;f -
- - - - {account.display_name.trim().length > 0 ? account.display_name : account.username} - . - - @{account.username} - -
-
+ ); diff --git a/web/source/settings/admin/reports/index.jsx b/web/source/settings/admin/reports/index.jsx index 5ffbfd3a0..58fca998d 100644 --- a/web/source/settings/admin/reports/index.jsx +++ b/web/source/settings/admin/reports/index.jsx @@ -83,7 +83,7 @@ function ReportEntry({ report }) {
reported
-

+

{report.action_taken ? "Resolved" : "Open"}

diff --git a/web/source/settings/components/fake-profile.jsx b/web/source/settings/components/fake-profile.jsx index d1f28f36c..6cb8d0187 100644 --- a/web/source/settings/components/fake-profile.jsx +++ b/web/source/settings/components/fake-profile.jsx @@ -22,24 +22,29 @@ const React = require("react"); module.exports = function FakeProfile({ avatar, header, display_name, username, role }) { return ( // Keep in sync with web/template/profile.tmpl
-
-
+
+
{header
diff --git a/web/source/settings/components/fake-toot.jsx b/web/source/settings/components/fake-toot.jsx index 7c2e40454..08f806008 100644 --- a/web/source/settings/components/fake-toot.jsx +++ b/web/source/settings/components/fake-toot.jsx @@ -29,20 +29,27 @@ module.exports = function FakeToot({ children }) { } } = query.useVerifyCredentialsQuery(); return ( -
-
- - - - {account.display_name.trim().length > 0 ? account.display_name : account.username} - . - - @{account.username} - -
-
+ diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 524f5e4ab..13642dd0c 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -20,26 +20,14 @@ body { grid-template-rows: auto 1fr; } -.content { +.page-content { grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */ width: 100%; } -header { - justify-content: start; - - a { - margin: 1.5rem; - gap: 1rem; - - h1 { - font-size: 1.5rem; - } - - img { - height: 3rem; - } - } +/* Don't inherit orange dot from base.css. */ +ul li::before { + content: initial; } #root { @@ -1007,7 +995,7 @@ button.with-padding { grid-template-columns: 1fr auto; gap: 0.5rem; - .status { + .report-status { color: $border-accent; } } @@ -1029,7 +1017,7 @@ button.with-padding { color: $fg-reduced; border-left: 0.4rem solid $bg; - .byline .status { + .byline .report-status { color: $fg-reduced; } @@ -1141,11 +1129,62 @@ button.with-padding { } } +.instance-rules { + list-style-position: inside; + margin: 0; + padding: 0; + + a.rule { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + color: $fg; + text-decoration: none; + background: $status-bg; + padding: 1rem; + margin: 0.5rem 0; + border-radius: $br; + line-height: 2rem; + position: relative; + + &:hover { + color: $fg-accent; + + .edit-icon { + display: inline; + } + } + + .edit-icon { + display: none; + font-size: 1rem; + line-height: 1.5rem; + } + + li { + font-size: 1.75rem; + padding: 0; + margin: 0; + + h2 { + margin: 0; + margin-top: 0 !important; + display: inline-block; + font-size: 1.5rem; + } + } + + span { + color: $fg-reduced; + } + } +} + @media screen and (orientation: portrait) { .reports .report .byline { grid-template-columns: 1fr; - .status { + .report-status { grid-row: 1; } } @@ -1162,4 +1201,14 @@ button.with-padding { to { opacity: 0; } -} \ No newline at end of file +} + +@media (prefers-reduced-motion) { + .fa-spin { + animation: none; + } +} + +.monospace { + font-family: monospace; +} diff --git a/web/template/404.tmpl b/web/template/404.tmpl index 2269d667b..cbad36091 100644 --- a/web/template/404.tmpl +++ b/web/template/404.tmpl @@ -17,23 +17,27 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-

404: Page Not Found

-

- GoToSocial only serves Public statuses via the web. - If you reached this page by clicking on a status link, - it's possible that the status is not Public, has been - deleted by the author, you don't have permission to see - it, or it just doesn't exist at all. -

-

- If you believe this 404 was an error, you can contact - the instance admin. Provide them with the following request - Request ID: {{.requestID}}. -

-
+
+

404: Not Found

+

+ GoToSocial only serves Public statuses via the web. +

+

+ If you reached this page by clicking on a status link, + it's likely that the status is not Public. You can try + entering the status URL in your client's search bar, + to view the status from your account. If that doesn't + work, it's possible that the status has been deleted by + the author, you don't have permission to view it, or it + doesn't exist at all. +

+

+ If you believe this 404 was an error, you can contact + the instance admin. Provide them with the following + request ID: {{- .requestID -}}. +

+
- -{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/about.tmpl b/web/template/about.tmpl index 6579f492f..a23dfa953 100644 --- a/web/template/about.tmpl +++ b/web/template/about.tmpl @@ -17,105 +17,133 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} -
-
-

About

-
- {{.instance.Description |noescape}} -
+{{- define "description" -}} +{{- if .instance.Description }} +{{ .instance.Description | noescape }} +{{- else }} +

No description has yet been set for this instance.

+{{- end }} +{{- end -}} -

-

Languages

-

- {{ if .languages }} - This instance prefers the following languages: -

    - {{range .languages}} -
  1. {{.}}
  2. - {{end}} -
- {{ else }} - This instance does not have any preferred languages. - {{ end }} -

-
+{{- define "registrationLimits" -}} +{{- if .instance.Registrations -}} + Registration is enabled; new signups can be submitted to this instance.
+ {{- if .instance.ApprovalRequired -}} + Admin approval is required for new registrations. + {{- else -}} + Admin approval is not required for registrations; new signups will be automatically approved (pending email confirmation). + {{- end -}} +{{- else -}} + Registration is disabled; new signups are currently closed for this instance. +{{- end -}} +{{- end -}} - +{{- define "customCSSLimits" -}} +{{- if .instance.Configuration.Accounts.AllowCustomCSS -}} +Users are allowed to set Custom CSS for their profiles. +{{- else -}} +Custom CSS is not enabled for user profiles. +{{- end -}} +{{- end -}} -
-

Rules

-
    - {{range .instance.Rules}} -
  1. {{.Text}}
  2. - {{end}} -
-
+{{- define "statusLimits" -}} +Statuses can contain up to  +{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and  +{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments. +{{- end -}} -
-

Features

-
    -
  • - Registration is - {{if .instance.Registrations}} - enabled{{if .instance.ApprovalRequired}}, but requires admin approval{{end}}. - {{else}} - disabled. - {{end}} -
  • - {{if .instance.Configuration.Accounts.AllowCustomCSS}} -
  • - Users are allowed to set Custom CSS for their profiles. -
  • - {{end}} -
  • - Toots can contain up to {{.instance.Configuration.Statuses.MaxCharacters}} characters and - {{.instance.Configuration.Statuses.MaxMediaAttachments}} media attachments. -
  • -
  • - Polls can have up to {{.instance.Configuration.Polls.MaxOptions}} options, with - {{.instance.Configuration.Polls.MaxCharactersPerOption}} characters each. -
  • -
-
+{{- define "pollLimits" -}} +Polls can have up to  +{{- .instance.Configuration.Polls.MaxOptions }} options, with  +{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option. +{{- end -}} -
-

Moderated servers

-

- ActivityPub instances exchange (federate) data with other instances, including accounts and toots. - This can be prevented for specific domains by suspending them. None of their content is stored, - and interaction with their users is blocked both ways.
- {{if .blocklistExposed}} - View the list of suspended domains - {{else}} - This instance does not publically share this list. - {{end}} -

-
- -
-

Instance Statistics

-
    -
  • Users: {{.instance.Stats.user_count}}
  • -
  • Posts: {{.instance.Stats.status_count}}
  • -
  • Federates with: {{.instance.Stats.domain_count}} instances
  • -
-
-
+{{- with . }} +
+
+

About {{ .instance.Title -}}

+ {{- with . }} + {{- include "description" . | indent 2 }} + {{- end }} +
+
+

Admin Contact

+ {{- if .instance.ContactAccount }} + + {{- else }} +

This instance has not yet set a contact account.

+ {{- end }} + {{- if .instance.Email }} +

Email: {{- .instance.Email -}}

+ {{- else }} +

This instance has not yet set a contact email address.

+ {{- end }} +
+
+

Languages

+ {{- if .languages }} +

This instance prefers the following languages:

+
    + {{- range .languages }} +
  1. {{- . -}}
  2. + {{- end }} +
+ {{- else }} +

This instance does not have any preferred languages.

+ {{- end }} +
+
+

Instance Rules

+

This instance has the following rules:

+ {{- if .instance.Rules }} +
    + {{- range .instance.Rules }} +
  1. {{- .Text -}}
  2. + {{- end }} +
+ {{- else }} +

This instance has not yet set any rules.

+ {{- end }} +
+
+

Instance Features

+
    +
  • {{- template "registrationLimits" . -}}
  • +
  • {{- template "customCSSLimits" . -}}
  • +
  • {{- template "statusLimits" . -}}
  • +
  • {{- template "pollLimits" . -}}
  • +
+
+
+

Moderated servers

+

+ ActivityPub instances federate with other instances by exchanging data with them over the network. + Exchanged data includes things like accounts, statuses, likes, boosts, and media attachments. + This exchange of data can prevented for instances on specific domains via a domain block created + by an instance admin. When an instance is domain blocked by another instance: +

+
    +
  • Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.
  • +
  • Interaction between the two instances is cut off in both directions; neither instance can interact with the other.
  • +
  • No new data from the blocked instance will be created on the instance that blocks it.
  • +
+

+ {{- if .blocklistExposed }} + View the list of domains blocked by this instance + {{- else }} + This instance does not publically share their list of blocked domains. + {{- end }} +

+
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/authorize.tmpl b/web/template/authorize.tmpl index ada078968..9be094137 100644 --- a/web/template/authorize.tmpl +++ b/web/template/authorize.tmpl @@ -17,26 +17,24 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} -
-
-

Hi {{.user}}!

-

- Application {{.appname}} - {{if len .appwebsite | eq 0 | not}} - ({{.appwebsite}}) - {{end}} - would like to perform actions on your behalf, with scope {{.scope}}. -

-

The application will redirect to {{.redirect}} to continue.

-

- -

-
-
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- with . }} +
+
+

Hi {{ .user -}}!

+

+ Application + {{- if .appwebsite }} + {{- .appname -}} + {{- else }} + {{- .appname -}} + {{- end }} + would like to perform actions on your behalf, with scope + {{- .scope -}}. +

+

+ To continue, the application will redirect to: {{- .redirect -}} +

+ +
+
+{{- end }} \ No newline at end of file diff --git a/web/template/confirmed.tmpl b/web/template/confirmed.tmpl index 3cf5b7ac9..c1633a8fb 100644 --- a/web/template/confirmed.tmpl +++ b/web/template/confirmed.tmpl @@ -17,12 +17,11 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-

Email Address Confirmed

-

Thanks {{.username}}! Your email address {{.email}} has been confirmed.

-

+
+

Email Address Confirmed

+

Thanks {{ .username -}}! Your email address {{- .email -}} has been confirmed.

+

- -{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/domain-blocklist.tmpl b/web/template/domain-blocklist.tmpl index def1b990e..9a21796f9 100644 --- a/web/template/domain-blocklist.tmpl +++ b/web/template/domain-blocklist.tmpl @@ -17,36 +17,36 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-

Suspended Instances

-

- The following list of domains have been suspended by the administrator(s) of this server. -

-

- All current and future accounts on these instances are blocked, and no more data is federated to the remote - servers. - This extends to subdomains, so an entry for 'example.com' includes 'social.example.com' as well. -

-
-
-
Domain
-
Public comment
-
- {{range .blocklist}} -
- -
-

- {{.PublicComment}} -

-
-
- {{end}} -
-
+
+

Suspended Instances

+

+ The following list of domains have been suspended + by the administrator(s) of this server. +

+

+ All current and future accounts on these instances are + blocked, and no more data is federated to the remote servers. + This extends to subdomains, so an entry for 'example.com' + includes 'social.example.com' as well. +

+
+
+
Domain
+
Public comment
+
+ {{- range .blocklist }} +
+ +
+

{{- .PublicComment -}}

+
+
+ {{- end }} +
+
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/error.tmpl b/web/template/error.tmpl index dc0713e43..816062e27 100644 --- a/web/template/error.tmpl +++ b/web/template/error.tmpl @@ -17,16 +17,16 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-

An error occured:

-
{{.error}}
- {{if .requestID}} -
- Request ID: {{.requestID}} -
- {{end}} -
+
+

An error occured:

+
{{- .error -}}
+ {{- if .requestID }} +
+ Request ID: {{- .requestID -}} +
+ {{- end }} +
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/finalize.tmpl b/web/template/finalize.tmpl index e0d880db7..56ab677e5 100644 --- a/web/template/finalize.tmpl +++ b/web/template/finalize.tmpl @@ -17,34 +17,31 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} -
-
-

Hi {{.name}}!

-

- You are about to sign-up to {{ .instance.Title }} ({{ .instance.URI }}) -
- To ensure the best experience for you, we need you to provide some additional details. -

- {{if .error}} -
-
{{.error}}
-
- {{end}} -
-

Important

-

Due to the way the ActivityPub standard works, you cannot change your username after it has been set.

-
-
- - -
- - -
-
-{{ template "footer.tmpl" .}} +{{- with . }} +
+
+

Hi {{ .name -}}!

+

+ You are about to sign-up to {{ .instance.Title -}}. + To ensure the best experience for you, we need you to provide some additional details. +

+
+

Important

+

Due to the way the ActivityPub standard works, you cannot change your username after it has been set.

+
+
+ + +
+ + +
+
+{{- end }} \ No newline at end of file diff --git a/web/template/frontend.tmpl b/web/template/frontend.tmpl index 977a7fab4..8e5267f4a 100644 --- a/web/template/frontend.tmpl +++ b/web/template/frontend.tmpl @@ -17,9 +17,8 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-
+
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/header.tmpl b/web/template/header.tmpl deleted file mode 100644 index 2759ab804..000000000 --- a/web/template/header.tmpl +++ /dev/null @@ -1,122 +0,0 @@ -{{- /* -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -*/ -}} - - -{{- /* - NESTED TEMPLATE DECLARATIONS - If some if/else macro is used multiple times, declare it once here instead. - When invoking these nested templates, remember to pass in the values passed - to the executing template, ie., use '{{ template "example" . }}' not - '{{ template "example" }}', otherwise you'll end up with empty variables. -*/ -}} -{{ define "thumbnailType" }}{{ if .instance.ThumbnailType }}{{ .instance.ThumbnailType }}{{ else }}image/png{{ end }}{{ end }} -{{ define "instanceTitle" }}{{ if .ogMeta }}{{ .ogMeta.Title }}{{ else }}{{ .instance.Title }} - GoToSocial{{ end }}{{ end }} - -{{- /* - BOILERPLATE GOES HERE -*/ -}} - - - - - - - - - {{- /* - ROBOTS META TAGS - If this template was provided with a specific robots meta policy, use that. - Otherwise, fall back to a default restrictive policy. - See: https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag - */ -}} - - - {{- /* - OPEN GRAPH META TAGS - To enable fancy previews of links to GtS posts/profiles shared via instant - messaging, or other social media, parse out provided Open Graph meta tags. - */ -}} - {{ if .ogMeta -}} - {{ if .ogMeta.Locale }}{{ end }} - - - - - - {{ if .ogMeta.ArticlePublisher }} - - - - - {{ end }} - {{ if .ogMeta.ProfileUsername }}{{ end }} - - {{ if .ogMeta.ImageAlt }}{{ end }} - {{ if .ogMeta.ImageWidth }} - - - {{ end }} - {{- end }} - - {{- /* - ICON - For icon, provide a link to the instance thumbnail. If the instance admin has - set a custom thumbnail, use the type they uploaded, else assume image/png. - See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#icon - */ -}} - - - - - {{- /* - RSS FEED - To enable automatic rss feed discovery for feed readers, provide the 'alternate' - link only if rss is enabled. - See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#alternate - */ -}} - {{ if .rssFeed -}} - - {{- end }} - - {{- /* - STYLESHEET STUFF - To try to speed up rendering a little bit, offer a preload for each stylesheet. - See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload. - */ -}} - - - {{ range .stylesheets }}{{ end }} - - - {{ range .stylesheets }}{{ end }} - {{ template "instanceTitle" . }} - - - -
-
- - {{ if .instance.ThumbnailDescription }}{{ .instance.ThumbnailDescription }}{{ else }}Instance Logo{{ end }} -

- {{ .instance.Title }} -

-
-
-
\ No newline at end of file diff --git a/web/template/index.tmpl b/web/template/index.tmpl index 665ce7a7e..f27cf8570 100644 --- a/web/template/index.tmpl +++ b/web/template/index.tmpl @@ -17,61 +17,21 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} -
- home to {{.instance.Stats.user_count}} users - who posted {{.instance.Stats.status_count}} statuses, - federating with {{.instance.Stats.domain_count}} other instances. -
-
-
-
- {{.instance.ShortDescription |noescape}} -
-
-
-

- GoToSocial does not provide its own webclient, but implements the Mastodon client API. - You can use this server through a variety of other clients: -

-
-
- -
-

Semaphore

-

Semaphore is a web client designed for speed and simplicity.

- Use Semaphore -
-
-
- -
-

Tusky

-

Tusky is a lightweight mobile client for Android.

- Get Tusky -
-
-
- -
-

Feditext

-

Feditext (beta) is a beautiful client for iOS, iPadOS and macOS.

- Get Feditext -
-
-
- -
-

More clients

-

Or try one of the clients listed on the official Mastodon page.

- Get Mastodon apps -
-
-
-
+{{- define "shortDescription" -}} +{{- if .instance.ShortDescription }} +{{ .instance.ShortDescription | noescape }} +{{- else }} +

No short description has yet been set for this instance.

+{{- end }} +{{- end -}} + +{{- with . }} +

+
+

About this instance

+ {{- include "shortDescription" . | indent 2 }} + See more details +
+ {{- include "index_apps.tmpl" . | indent 1 }}
-{{ template "footer.tmpl" .}} +{{- end }} \ No newline at end of file diff --git a/web/template/index_apps.tmpl b/web/template/index_apps.tmpl new file mode 100644 index 000000000..05a4a9517 --- /dev/null +++ b/web/template/index_apps.tmpl @@ -0,0 +1,115 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- with . }} +
+

Client applications

+

+ GoToSocial does not provide its own webclient, but implements the Mastodon client API. + You can use this server through a variety of other clients: +

+
    +
  • +
    +

    Semaphore is a web client designed for speed and simplicity.

    + + Use Semaphore + +
    + +
  • +
  • +
    +

    Tusky is a lightweight mobile client for Android.

    + + Get Tusky + +
    + +
  • +
  • +
    +

    Feditext (beta) is a beautiful client for iOS, iPadOS and macOS.

    + + Get Feditext + +
    + +
  • +
  • +
    +

    Or try one of the Mastodon clients listed on the official Mastodon page.

    + + Get Mastodon apps + +
    + +
  • +
+
+{{- end }} \ No newline at end of file diff --git a/web/template/oob.tmpl b/web/template/oob.tmpl index 0f183c350..ff36582e7 100644 --- a/web/template/oob.tmpl +++ b/web/template/oob.tmpl @@ -17,12 +17,12 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-

Hi {{ .user }}!

-

Here's your out-of-band token with scope "{{.scope}}", use it wisely:

- {{ .oobToken }} -
+
+

Hi {{ .user -}}!

+

Here's your out-of-band token with scope "{{- .scope -}}", use it wisely:

+ {{- .oobToken -}} +
-{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/page.tmpl b/web/template/page.tmpl new file mode 100644 index 000000000..347caf33e --- /dev/null +++ b/web/template/page.tmpl @@ -0,0 +1,85 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- /* + NESTED TEMPLATE DECLARATIONS + If some if/else macro is used multiple times, declare it once here instead. + When invoking these nested templates, remember to pass in the values passed + to the executing template, ie., use '{{ template "example" . }}' not + '{{ template "example" }}', otherwise you'll end up with empty variables. +*/ -}} + +{{- define "thumbnailType" -}} +{{- if .instance.ThumbnailType -}} +{{- .instance.ThumbnailType -}} +{{- else -}} +image/png +{{- end -}} +{{- end -}} + +{{- define "instanceTitle" -}} +{{- if .ogMeta -}} +{{- demojify .ogMeta.Title | noescape -}} +{{- else -}} +{{- .instance.Title }} - GoToSocial +{{- end -}} +{{- end -}} + + + + + + + + + {{- if .ogMeta }} + {{- include "page_ogmeta.tmpl" . | indent 2 }} + {{- else }} + {{- end }} + {{- if .rssFeed }} + + {{- else }} + {{- end }} + {{- if .account }} + + {{- else if .status }} + + {{- else }} + {{- end }} + + + + {{- include "page_stylesheets.tmpl" . | indent 2 }} + {{- range .javascript }} + + {{- end }} + {{- template "instanceTitle" . -}} + + + +
+ {{- include .pageContent . | indent 3 | outdentPre }} +
+
+ {{- include "page_footer.tmpl" . | indent 3 }} +
+ + \ No newline at end of file diff --git a/web/template/page_footer.tmpl b/web/template/page_footer.tmpl new file mode 100644 index 000000000..a00f4dfde --- /dev/null +++ b/web/template/page_footer.tmpl @@ -0,0 +1,67 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- with . }} + +{{- end }} \ No newline at end of file diff --git a/web/template/page_header.tmpl b/web/template/page_header.tmpl new file mode 100644 index 000000000..dc727d144 --- /dev/null +++ b/web/template/page_header.tmpl @@ -0,0 +1,72 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- define "thumbnailDescription" -}} +{{- if .instance.ThumbnailDescription -}} +{{- .instance.ThumbnailDescription -}} +{{- else -}} +Instance Logo +{{- end -}} +{{- end -}} + +{{- define "strapUsers" -}} +{{- with .instance.Stats.user_count -}} + {{- if eq . 1 -}} + {{- . -}} user + {{- else -}} + {{- . -}} users + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "strapPosts" -}} +{{- with .instance.Stats.status_count -}} + {{- if eq . 1 -}} + {{- . -}} post + {{- else -}} + {{- . -}} posts + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "strapInstances" -}} +{{- with .instance.Stats.domain_count -}} + {{- if eq . 1 -}} + {{- . -}} other instance + {{- else -}} + {{- . -}} other instances + {{- end -}} +{{- end -}} +{{- end -}} + +{{- with . }} + + {{- template +

{{- .instance.Title -}}

+
+{{- if .showStrap }} + +{{- end }} +{{- end }} \ No newline at end of file diff --git a/web/template/page_ogmeta.tmpl b/web/template/page_ogmeta.tmpl new file mode 100644 index 000000000..82bb4bbfb --- /dev/null +++ b/web/template/page_ogmeta.tmpl @@ -0,0 +1,57 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- /* + OPEN GRAPH META TAGS + To enable fancy previews of links to GtS posts/profiles shared via instant + messaging, or other social media, parse out provided Open Graph meta tags. +*/ -}} + +{{- with .ogMeta }} +{{- if .Locale }} + +{{- else }} +{{- end }} + + + + + +{{- if .ArticlePublisher }} + + + + +{{- else }} +{{- end }} +{{- if .ProfileUsername }} + +{{- else }} +{{- end }} + +{{- if .ImageAlt }} + +{{- else }} +{{- end }} +{{- if .ImageWidth }} + + +{{- else }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/web/template/page_stylesheets.tmpl b/web/template/page_stylesheets.tmpl new file mode 100644 index 000000000..9234607f8 --- /dev/null +++ b/web/template/page_stylesheets.tmpl @@ -0,0 +1,41 @@ +{{- /* +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ -}} + +{{- /* + Order of stylesheet loading is important: _colors and base should always be loaded + before any other provided sheets, since the latter cascade from the former. + + To try to speed up rendering a little bit, offer a preload for each stylesheet. + See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload. +*/ -}} + +{{- with . }} + + + +{{- range .stylesheets }} + +{{- end }} + + + +{{- range .stylesheets }} + +{{- end }} +{{- end }} \ No newline at end of file diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl index 4207b39e8..0b079db10 100644 --- a/web/template/profile.tmpl +++ b/web/template/profile.tmpl @@ -17,129 +17,123 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} - +{{- with . }}
-
-
- {{ if .account.Header }} - - {{ end }} -
- -
- Profile for - {{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}. - Username @{{.account.Username}}, {{.instance.AccountDomain}}. - {{ if and (.account.Role) (ne .account.Role.Name "user") }} - Role: {{ .account.Role.Name }} - {{ end }} -
-
- -
- -
-
-

About

-
- -
- {{ range .account.Fields }} -
- {{emojify $.account.Emojis (noescape .Name)}} - {{emojify $.account.Emojis (noescape .Value)}} -
- {{ end }} -
- -
- {{ if .account.Note }} - {{emojify .account.Emojis (noescape .account.Note)}} - {{else}} - This GoToSocial user hasn't written a bio yet! - {{end}} -
- -
- Joined on {{.account.CreatedAt | timestampVague}}. - {{.account.StatusesCount}} post{{if .account.StatusesCount | eq 1 | not}}s{{end}}. - Followed by {{.account.FollowersCount}}. - Following {{.account.FollowingCount}}. -
- - -
- -
- {{ if .pinned_statuses }} -
-

Pinned posts

- jump to recent -
-
- {{ range .pinned_statuses }} -
- {{ template "status.tmpl" .}} -
- {{ end }} -
- {{ end }} - -
-

Recent posts

- {{ if .rssFeed }} - - - - {{ end }} -
- -
- {{ if not .statuses }} -
Nothing here!
- {{ else }} - {{ range .statuses }} -
- {{ template "status.tmpl" .}} -
- {{ end }} - {{ end }} -
- - -
-
+

Profile for {{ .account.Username -}}

+
+
+ Header for {{ .account.Username -}} +
+
+ + Avatar for {{ .account.Username -}} + +
+
Display name
+
+ {{- if .account.DisplayName -}} + {{- emojify .account.Emojis (escape .account.DisplayName) -}} + {{- else -}} + {{- .account.Username -}} + {{- end -}} +
+
Username
+
@{{- .account.Username -}}@{{- .instance.AccountDomain -}}
+ {{- if and (.account.Role) (ne .account.Role.Name "user") }} +
Role
+
{{- .account.Role.Name -}}
+ {{- end }} +
+
+
+
+
+
+

About {{- .account.Username -}}

+
+ {{- if .account.Fields }} + {{- include "profile_fields.tmpl" . | indent 3 }} + {{- end }} +

Bio

+
+ {{- if .account.Note }} + {{ emojify .account.Emojis (noescape .account.Note) }} + {{- else }} +

This GoToSocial user hasn't written a bio yet!

+ {{- end }} +
+

Stats

+
+
Joined
+
+
Posts
+
{{- .account.StatusesCount -}}
+
Followed by
+
{{- .account.FollowersCount -}}
+
Following
+
{{- .account.FollowingCount -}}
+
+
+
+ {{- if .pinned_statuses }} +
+
+

Pinned posts

+ jump to recent +
+
+ {{- range .pinned_statuses }} +
+ {{- include "status.tmpl" . | indent 6 }} +
+ {{- end }} +
+
+ {{- end }} +
+
+

Recent posts

+ {{- if .rssFeed }} + + + + {{- end }} +
+
+ {{- if not .statuses }} +
Nothing here!
+ {{- else }} + {{- range .statuses }} +
+ {{- include "status.tmpl" . | indent 6 }} +
+ {{- end }} + {{- end }} +
+ +
+
+
- -{{ template "footer.tmpl" .}} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/web/template/footer.tmpl b/web/template/profile_fields.tmpl similarity index 51% rename from web/template/footer.tmpl rename to web/template/profile_fields.tmpl index 028a27ffb..e9005d4c9 100644 --- a/web/template/footer.tmpl +++ b/web/template/profile_fields.tmpl @@ -17,30 +17,16 @@ // along with this program. If not, see . */ -}} - -
- -
- {{ if .javascript }} - {{ range .javascript }} - - {{ end }} - {{ end }} - - \ No newline at end of file +{{- with . }} +
+

Fields

+
+ {{- range .account.Fields }} +
+
{{- emojify $.account.Emojis (noescape .Name) -}}
+
{{- emojify $.account.Emojis (noescape .Value) -}}
+
+ {{- end }} +
+
+{{- end }} \ No newline at end of file diff --git a/web/template/sign-in.tmpl b/web/template/sign-in.tmpl index e2a985b9c..916d6942f 100644 --- a/web/template/sign-in.tmpl +++ b/web/template/sign-in.tmpl @@ -17,10 +17,10 @@ // along with this program. If not, see . */ -}} -{{ template "header.tmpl" .}} +{{- with . }}
-
-{{ template "footer.tmpl" .}} +{{- end }} \ No newline at end of file diff --git a/web/template/status.tmpl b/web/template/status.tmpl index eb6c6a6c6..de2167d88 100644 --- a/web/template/status.tmpl +++ b/web/template/status.tmpl @@ -17,88 +17,74 @@ // along with this program. If not, see . */ -}} -Open thread -
- - - - - - {{- if .Account.DisplayName -}} - {{- emojify .Account.Emojis (escape .Account.DisplayName) -}}. Username: @{{ .Account.Acct -}}. - {{- else -}} - @{{- .Account.Acct -}}. - {{- end -}} - - -
-
- {{- if .SpoilerText }} -
- - {{- emojify .Emojis (escape .SpoilerText) -}} - Toggle visibility - -
-
- {{ emojify .Emojis (noescape .Content) }} -
- {{- if .Poll }} - {{ template "status_poll.tmpl" . }} - {{- end }} -
-
- {{- else }} -
-
- {{ emojify .Emojis (noescape .Content) }} -
- {{- if .Poll }} - {{ template "status_poll.tmpl" . }} - {{- end }} -
- {{- end }} - {{- if .MediaAttachments }} - {{ template "status_attachments.tmpl" . }} - {{- end }} -
-