summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.goreleaser.yml2
-rw-r--r--internal/api/auth/authorize.go30
-rw-r--r--internal/api/auth/callback.go34
-rw-r--r--internal/api/auth/oob.go18
-rw-r--r--internal/api/auth/signin.go18
-rw-r--r--internal/api/model/status.go6
-rw-r--r--internal/api/util/errorhandling.go20
-rw-r--r--internal/api/util/opengraph.go (renamed from internal/web/opengraph.go)56
-rw-r--r--internal/api/util/opengraph_test.go (renamed from internal/web/opengraph_test.go)20
-rw-r--r--internal/api/util/template.go135
-rw-r--r--internal/oauth/server.go4
-rw-r--r--internal/processing/account/rss_test.go2
-rw-r--r--internal/router/router.go1
-rw-r--r--internal/router/template.go260
-rw-r--r--internal/router/template_test.go204
-rw-r--r--internal/text/emojify.go91
-rw-r--r--internal/typeutils/internaltofrontend.go6
-rw-r--r--internal/typeutils/internaltorss.go2
-rw-r--r--internal/typeutils/internaltorss_test.go2
-rw-r--r--internal/web/about.go42
-rw-r--r--internal/web/confirmemail.go49
-rw-r--r--internal/web/customcss.go30
-rw-r--r--internal/web/domain-blocklist.go49
-rw-r--r--internal/web/index.go (renamed from internal/web/base.go)43
-rw-r--r--internal/web/profile.go45
-rw-r--r--internal/web/robots.go2
-rw-r--r--internal/web/settings-panel.go44
-rw-r--r--internal/web/tag.go18
-rw-r--r--internal/web/thread.go30
-rw-r--r--internal/web/web.go19
-rw-r--r--testrig/gin.go1
-rw-r--r--testrig/router.go1
-rw-r--r--web/source/.eslintignore4
-rw-r--r--web/source/css/_colors.css8
-rw-r--r--web/source/css/about.css39
-rw-r--r--web/source/css/base.css560
-rw-r--r--web/source/css/index.css89
-rw-r--r--web/source/css/page.css107
-rw-r--r--web/source/css/prism.css5
-rw-r--r--web/source/css/profile.css220
-rw-r--r--web/source/css/status.css252
-rw-r--r--web/source/css/thread.css56
-rw-r--r--web/source/frontend/index.js4
-rw-r--r--web/source/frontend/prism.js42
-rw-r--r--web/source/settings/admin/reports/detail.jsx42
-rw-r--r--web/source/settings/admin/reports/index.jsx2
-rw-r--r--web/source/settings/components/fake-profile.jsx29
-rw-r--r--web/source/settings/components/fake-toot.jsx33
-rw-r--r--web/source/settings/style.css89
-rw-r--r--web/template/404.tmpl40
-rw-r--r--web/template/about.tmpl220
-rw-r--r--web/template/authorize.tmpl44
-rw-r--r--web/template/confirmed.tmpl13
-rw-r--r--web/template/domain-blocklist.tmpl62
-rw-r--r--web/template/error.tmpl22
-rw-r--r--web/template/finalize.tmpl59
-rw-r--r--web/template/frontend.tmpl7
-rw-r--r--web/template/header.tmpl122
-rw-r--r--web/template/index.tmpl74
-rw-r--r--web/template/index_apps.tmpl115
-rw-r--r--web/template/oob.tmpl14
-rw-r--r--web/template/page.tmpl85
-rw-r--r--web/template/page_footer.tmpl67
-rw-r--r--web/template/page_header.tmpl72
-rw-r--r--web/template/page_ogmeta.tmpl57
-rw-r--r--web/template/page_stylesheets.tmpl41
-rw-r--r--web/template/profile.tmpl242
-rw-r--r--web/template/profile_fields.tmpl (renamed from web/template/footer.tmpl)40
-rw-r--r--web/template/sign-in.tmpl10
-rw-r--r--web/template/status.tmpl154
-rw-r--r--web/template/status_attachments.tmpl184
-rw-r--r--web/template/status_attributes.tmpl55
-rw-r--r--web/template/status_header.tmpl56
-rw-r--r--web/template/status_info.tmpl74
-rw-r--r--web/template/status_poll.tmpl105
-rw-r--r--web/template/tag.tmpl16
-rw-r--r--web/template/thread.tmpl59
77 files changed, 3250 insertions, 1724 deletions
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 @@ import (
"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 @@ import (
"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
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 <http://www.gnu.org/licenses/>.
-package web
+package util
import (
"html"
@@ -28,10 +28,10 @@ import (
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, `\`, "&bsol;")
- 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
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 <http://www.gnu.org/licenses/>.
-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 <http://www.gnu.org/licenses/>.
+
+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 @@ const (
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
+ suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", 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,
+ )
+ }
+
+ 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,
+ )
+ }
+
+ // 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
}
- 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)
+ 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
}
- engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*"))
+ // 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(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))
+ return template.HTML(fmt.Sprintf(
+ `<i aria-label="Visibility: %s" class="fa fa-%s"></i>`,
+ 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 <img> 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)+)<pre>.*</pre>`, 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 `<pre></pre>` 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 <http://www.gnu.org/licenses/>.
+
+package router
+
+import (
+ "html/template"
+ "testing"
+)
+
+func TestOutdentPre(t *testing.T) {
+ const html = template.HTML(`
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+ <pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;should you follow me?&lt;/dt&gt;
+ &lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;age&lt;/dt&gt;
+ &lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;/section&gt;
+ </code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+ <pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;should you follow me?&lt;/dt&gt;
+ &lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;age&lt;/dt&gt;
+ &lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;/section&gt;
+ </code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+`)
+
+ const expected = template.HTML(`
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+<pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;should you follow me?&lt;/dt&gt;
+&lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;age&lt;/dt&gt;
+&lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+&lt;/section&gt;
+</code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+ <div class="text">
+ <div class="content" lang="en">
+ <p>Here's a bunch of HTML, read it and weep, weep then!</p>
+<pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;should you follow me?&lt;/dt&gt;
+&lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+&lt;dt&gt;age&lt;/dt&gt;
+&lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+&lt;/section&gt;
+</code></pre>
+ <p>There, hope you liked that!</p>
+ </div>
+ </div>
+`)
+
+ 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 @@ package text
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 `<img>` 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(`<img src="`)
+ buf.WriteString(url)
+ buf.WriteString(`" title=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" alt=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" class="emoji" `)
+ // Lazy load emojis when
+ // they scroll into view.
+ buf.WriteString(`loading="lazy" `)
+ // Limit size to avoid showing
+ // huge emojis when unstyled.
+ buf.WriteString(`width="25" height="25"/>`)
+ },
+ )
+
+ // 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 `<img>` 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(`<img src="`)
+ buf.WriteString(url)
+ buf.WriteString(`" title=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" alt=":`)
+ buf.WriteString(code)
+ buf.WriteString(`:" `)
+ // Limit size to avoid showing
+ // huge emojis in RSS readers.
+ buf.WriteString(`width="25" height="25"/>`)
+ },
+ )
+}
+
+// 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(`<img src="`)
- buf.WriteString(safeURL)
- buf.WriteString(`" title=":`)
- buf.WriteString(safeCode)
- buf.WriteString(`:" alt=":`)
- buf.WriteString(safeCode)
- buf.WriteString(`:" class="emoji"/>`)
+ // 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, "<pre>", "\n<pre>")
+ webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\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 <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !", item.Content)
+ suite.Equal("hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !", 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 @@ const (
)
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()
+ 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
+ }
- // if there's no token in the query, just serve the 404 web handler
- token := c.Query(tokenParam)
+ // 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 there's no token in the query,
+ // just serve the 404 web handler.
+ token := c.Query("token")
if token == "" {
- apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGetV1)
+ errWithCode := gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound)))
+ apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
}
- user, errWithCode := m.processor.User().EmailConfirm(ctx, token)
+ user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token)
if errWithCode != nil {
- apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
}
- instance, err := m.processor.InstanceGetV1(ctx)
- if err != nil {
- apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
- return
+ page := apiutil.WebPage{
+ Template: "confirmed.tmpl",
+ Instance: instance,
+ Extra: map[string]any{
+ "email": user.Email,
+ "username": user.Account.Username,
+ },
}
- c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{
- "instance": instance,
- "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 @@ import (
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 @@ const (
)
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)
+ instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
+ if errWithCode != nil {
+ apiutil.WebErrorHandler(c, errWithCode, 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 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
}
- instance, err := m.processor.InstanceGetV1(c.Request.Context())
- if err != nil {
- apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
+ 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, m.processor.InstanceGetV1)
+ apiutil.WebErrorHandler(c, errWithCode, instanceGet)
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"},
- })
+ 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
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 @@ import (
"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")
- }
-
- 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"},
- })
+ 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,
+ },
+ }
+
+ 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 @@ Crawl-delay: 500
# 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 @@ package web
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 @@ import (
"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 @@ import (
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 @@ const (
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 @@ import (
// 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 <http://www.gnu.org/licenses/>.
+*/
+
+.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;
}
-section.apps {
- align-self: start;
-
- .applist {
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-gap: 0.5rem;
- align-content: start;
-
- .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;
- }
- }
- }
+/*
+ Class for lists that don't
+ want the orange dot.
+*/
+.nodot {
+ li::before {
+ content: initial;
}
}
-section.login {
+/***********************************
+***** SECTION 4: SHAMEFUL MESS *****
+************************************/
+
+/*
+ EVERYTHING BELOW THIS POINT:
+ Should be moved somewhere else
+ to avoid cluttering up this file.
+*/
+
+/*
+ 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;
- }
-
- ul {
- margin-bottom: 0;
+@media screen and (max-width: 30rem) {
+ .domain-blocklist .entry {
+ grid-template-columns: 1fr;
+ gap: 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 <http://www.gnu.org/licenses/>.
*/
-header a {
- margin: 2rem;
+/*
+ Render instance title a
+ bit bigger on index page.
+*/
+.page-header a h1 {
+ font-size: 2rem;
+ line-height: 2rem;
+}
+
+/*
+ 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;
- img {
- height: 6rem;
+ .about-section {
+ padding: 2rem;
+ background: $bg-accent;
+ box-shadow: $boxshadow;
+ border: $boxshadow-border;
+ border-radius: $br;
}
+}
- h1 {
- font-size: 2rem;
+.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;
+ }
+ }
+ }
}
}
-main {
- section {
- background: $bg-accent;
- box-shadow: $boxshadow;
- border: $boxshadow-border;
- border-radius: $br;
- padding: 2rem;
- margin-bottom: 2rem;
+@media screen and (max-width: 600px) {
+ .apps .applist {
+ grid-template-columns: 1fr;
}
-} \ No newline at end of file
+}
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 <http://www.gnu.org/licenses/>.
+*/
+
+.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;
- }
-
- .username {
- min-width: 0;
- grid-area: username;
- line-height: $username-size;
+ .namerole {
+ grid-area: namerole;
- 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;
+ 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";
- &.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;
-
- 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;
- }
+.profile .statuses {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
- &: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;
-
- .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%;
+ .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;
+
+ > 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;
+
+ .avatar {
+ grid-area: avatar;
+ height: 3.5rem;
+ width: 3.5rem;
object-fit: cover;
- background: $bg;
+
+ 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;
- }
- .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;
+ .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;
}
- .language {
- margin-left: auto;
+ .stats-item:not(.published-at) {
z-index: 1;
- cursor: pointer;
user-select: none;
}
+
+ .language {
+ margin-left: auto;
+ }
}
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 <http://www.gnu.org/licenses/>.
+*/
+
+.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,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function e(n,t){var r,i;switch(t=t||{},a.util.type(n)){case"Object":if(i=a.util.objId(n),t[i])return t[i];for(var l in r={},t[i]=r,n)n.hasOwnProperty(l)&&(r[l]=e(n[l],t));return r;case"Array":return i=a.util.objId(n),t[i]?t[i]:(r=[],t[i]=r,n.forEach((function(n,a){r[a]=e(n,t)})),r);default:return n}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,"gi"),""),e.classList.add("language-"+t)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(r){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(r.stack)||[])[1];if(e){var n=document.getElementsByTagName("script");for(var t in n)if(n[t].src==e)return n[t]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:r,plaintext:r,text:r,txt:r,extend:function(e,n){var t=a.util.clone(a.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(e,n,t,r){var i=(r=r||a.languages)[e],l={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var s in t)t.hasOwnProperty(s)&&(l[s]=t[s]);t.hasOwnProperty(o)||(l[o]=i[o])}var u=r[e];return r[e]=l,a.languages.DFS(a.languages,(function(n,t){t===u&&n!=e&&(this[n]=l)})),l},DFS:function e(n,t,r,i){i=i||{};var l=a.util.objId;for(var o in n)if(n.hasOwnProperty(o)){t.call(n,o,n[o],r||o);var s=n[o],u=a.util.type(s);"Object"!==u||i[l(s)]?"Array"!==u||i[l(s)]||(i[l(s)]=!0,e(s,t,o,i)):(i[l(s)]=!0,e(s,t,null,i))}}},plugins:{},highlightAll:function(e,n){a.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};a.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),a.hooks.run("before-all-elements-highlight",r);for(var i,l=0;i=r.elements[l++];)a.highlightElement(i,!0===n,r.callback)},highlightElement:function(n,t,r){var i=a.util.getLanguage(n),l=a.languages[i];a.util.setLanguage(n,i);var o=n.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&a.util.setLanguage(o,i);var s={element:n,language:i,grammar:l,code:n.textContent};function u(e){s.highlightedCode=e,a.hooks.run("before-insert",s),s.element.innerHTML=s.highlightedCode,a.hooks.run("after-highlight",s),a.hooks.run("complete",s),r&&r.call(s.element)}if(a.hooks.run("before-sanity-check",s),(o=s.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!s.code)return a.hooks.run("complete",s),void(r&&r.call(s.element));if(a.hooks.run("before-highlight",s),s.grammar)if(t&&e.Worker){var c=new Worker(a.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:s.language,code:s.code,immediateClose:!0}))}else u(a.highlight(s.code,s.grammar,s.language));else u(a.util.encode(s.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(a.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=a.tokenize(r.code,r.grammar),a.hooks.run("after-tokenize",r),i.stringify(a.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new s;return u(a,a.head,e),o(e,a,n,a.head,0),function(e){for(var n=[],t=e.head.next;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=a.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=a.hooks.all[e];if(t&&t.length)for(var r,i=0;r=t[i++];)r(n)}},Token:i};function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var h=t[f];h=Array.isArray(h)?h:[h];for(var d=0;d<h.length;++d){if(g&&g.cause==f+","+d)return;var v=h[d],p=v.inside,m=!!v.lookbehind,y=!!v.greedy,k=v.alias;if(y&&!v.pattern.global){var x=v.pattern.toString().match(/[imsuy]*$/)[0];v.pattern=RegExp(v.pattern.source,x+"g")}for(var b=v.pattern||v,w=r.next,A=s;w!==n.tail&&!(g&&A>=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&&(j<O||"string"==typeof C.value);C=C.next)L++,j+=C.value.length;L--,E=e.slice(A,j),P.index-=A}else if(!(P=l(b,0,E,m)))continue;S=P.index;var N=P[0],_=E.slice(0,S),M=E.slice(S+N.length),W=A+E.length;g&&W>g.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<t&&r!==e.tail;a++)r=r.next;n.next=r,r.prev=n,e.length-=a}if(e.Prism=a,i.stringify=function e(n,t){if("string"==typeof n)return n;if(Array.isArray(n)){var r="";return n.forEach((function(n){r+=e(n,t)})),r}var i={type:n.type,content:e(n.content,t),tag:"span",classes:["token",n.type],attributes:{},language:t},l=n.alias;l&&(Array.isArray(l)?Array.prototype.push.apply(i.classes,l):i.classes.push(l)),a.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=" "+s+'="'+(i.attributes[s]||"").replace(/"/g,"&quot;")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+o+">"+i.content+"</"+i.tag+">"},!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:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\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:/<!\[CDATA\[[\s\S]*?\]\]>/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(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\[CDATA\[|\]\]>$/i;var t={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:<!\\[CDATA\\[(?:[^\\]]|\\](?!\\]>))*\\]\\]>|(?!<!\\[CDATA\\[)[^])*?(?=</__>)".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<s.length;i++)o[s[i]]=e.languages.bash[s[i]];e.languages.sh=e.languages.bash,e.languages.shell=e.languages.bash}(Prism);
+Prism.languages.c=Prism.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\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<n;s++)e=e.replace(/<<self>>/g,(function(){return"(?:"+e+")"}));return e.replace(/<<self>>/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("<(?:[^<>;=+\\-*/%&|^]|<<self>>)*>",2),b=a("\\((?:[^()]|<<self>>)*\\)",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>>|\\(<<self>>*\\)",[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>>|\\(<<self>>*\\)",[R]),2),P=n("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}",[F,A]),U=a(n("[^\"'/()]|/(?!\\*)|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>|\\(<<self>>*\\)",[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(?!<keyword>)\\w+(?:\\s*\\.\\s*\\w+)*\\b".replace(/<keyword>/g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp("(\\b(?:class|concept|enum|struct|typename)\\s+)(?!<keyword>)\\w+".replace(/<keyword>/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]*>|'+"<mod-name>(?:\\s*:\\s*<mod-name>)?|:\\s*<mod-name>".replace(/<mod-name>/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])(?:<SP_BS>)?|<SP_BS>)".replace(/<SP_BS>/g,(function(){return"\\\\[\r\n](?:\\s|\\\\[\r\n]|#.*(?!.))*(?![\\s#]|\\\\[\r\n])"})),r="\"(?:[^\"\\\\\r\n]|\\\\(?:\r\n|[^]))*\"|'(?:[^'\\\\\r\n]|\\\\(?:\r\n|[^]))*'",t="--[\\w-]+=(?:<STR>|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)".replace(/<STR>/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(/<OPT>/g,(function(){return t})).replace(/<SP>/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<SP>)?\\w+<SP>)<OPT>(?:<SP><OPT>)*","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<SP>)?HEALTHCHECK<SP>(?:<OPT><SP>)*)(?:CMD|NONE)\\b","i"),lookbehind:!0,greedy:!0},{pattern:a("(^(?:ONBUILD<SP>)?FROM<SP>(?:<OPT><SP>)*(?!--)[^ \t\\\\]+<SP>)AS","i"),lookbehind:!0,greedy:!0},{pattern:a("(^ONBUILD<SP>)\\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+)(?!<keyword>)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?".replace(/<keyword>/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<i.length&&!(r>=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="(?:\\{<S>*\\.{3}(?:[^{}]|<BRACES>)*\\})";function a(t,n){return t=t.replace(/<S>/g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(/<BRACES>/g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(/<SPREAD>/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>+(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+|<BRACES>))?|<SPREAD>))*<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("<SPREAD>"),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("=<BRACES>"),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;a<n.length;a++){var o=n[a],i=!1;if("string"!=typeof o&&("tag"===o.type&&o.content[0]&&"tag"===o.content[0].type?"</"===o.content[0].content[0].content?e.length>0&&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);a<n.length-1&&("string"==typeof n[a+1]||"plain-text"===n[a+1].type)&&(r+=s(n[a+1]),n.splice(a+1,1)),a>0&&("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$]|(?=</))(?:"+t.pattern.source+")",t.pattern.flags),t.lookbehind=!0}(Prism);
+!function(e){e.languages.ruby=e.languages.extend("clike",{comment:{pattern:/#.*|^=begin\s[\s\S]*?^=end/m,greedy:!0},"class-name":{pattern:/(\b(?:class|module)\s+|\bcatch\s+\()[\w.\\]+|\b[A-Z_]\w*(?=\s*\.\s*new\b)/,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/,operator:/\.{2,3}|&\.|===|<?=>|[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,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="/\\*(?:[^*/]|\\*(?!/)|/(?!\\*)|<self>)*\\*/",t=0;t<2;t++)a=a.replace(/<self>/g,(function(){return a}));a=a.replace(/<self>/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<s;f++)e(n,f);break;default:i(t=n.inside||(n.inside={}))}}function i(a){if(a&&!a.tab){for(var n in r)r.hasOwnProperty(n)&&(a[n]=r[n]);for(var n in a)a.hasOwnProperty(n)&&!r[n]&&("rest"===n?i(a.rest):e(a,n))}}}();
+!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e=[],t={},n=function(){};Prism.plugins.toolbar={};var a=Prism.plugins.toolbar.registerButton=function(n,a){var r;r="function"==typeof a?a:function(e){var t;return"function"==typeof a.onClick?((t=document.createElement("button")).type="button",t.addEventListener("click",(function(){a.onClick.call(this,e)}))):"string"==typeof a.url?(t=document.createElement("a")).href=a.url:t=document.createElement("span"),a.className&&t.classList.add(a.className),t.textContent=a.text,t},n in t?console.warn('There is a button with the key "'+n+'" registered already.'):e.push(t[n]=r)},r=Prism.plugins.toolbar.hook=function(a){var r=a.element.parentNode;if(r&&/pre/i.test(r.nodeName)&&!r.parentNode.classList.contains("code-toolbar")){var o=document.createElement("div");o.classList.add("code-toolbar"),r.parentNode.insertBefore(o,r),o.appendChild(r);var i=document.createElement("div");i.classList.add("toolbar");var l=e,d=function(e){for(;e;){var t=e.getAttribute("data-toolbar-order");if(null!=t)return(t=t.trim()).length?t.split(/\s*,\s*/g):[];e=e.parentElement}}(a.element);d&&(l=d.map((function(e){return t[e]||n}))),l.forEach((function(e){var t=e(a);if(t){var n=document.createElement("div");n.classList.add("toolbar-item"),n.appendChild(t),i.appendChild(n)}})),o.appendChild(i)}};a("label",(function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-label")){var n,a,r=t.getAttribute("data-label");try{a=document.querySelector("template#"+r)}catch(e){}return a?n=a.content:(t.hasAttribute("data-url")?(n=document.createElement("a")).href=t.getAttribute("data-url"):n=document.createElement("span"),n.textContent=r),n}})),Prism.hooks.add("complete",r)}}();
+!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document)if(Prism.plugins.toolbar){var e={none:"Plain text",plain:"Plain text",plaintext:"Plain text",text:"Plain text",txt:"Plain text",html:"HTML",xml:"XML",svg:"SVG",mathml:"MathML",ssml:"SSML",rss:"RSS",css:"CSS",clike:"C-like",js:"JavaScript",abap:"ABAP",abnf:"ABNF",al:"AL",antlr4:"ANTLR4",g4:"ANTLR4",apacheconf:"Apache Configuration",apl:"APL",aql:"AQL",ino:"Arduino",arff:"ARFF",armasm:"ARM Assembly","arm-asm":"ARM Assembly",art:"Arturo",asciidoc:"AsciiDoc",adoc:"AsciiDoc",aspnet:"ASP.NET (C#)",asm6502:"6502 Assembly",asmatmel:"Atmel AVR Assembly",autohotkey:"AutoHotkey",autoit:"AutoIt",avisynth:"AviSynth",avs:"AviSynth","avro-idl":"Avro IDL",avdl:"Avro IDL",awk:"AWK",gawk:"GAWK",sh:"Shell",basic:"BASIC",bbcode:"BBcode",bbj:"BBj",bnf:"BNF",rbnf:"RBNF",bqn:"BQN",bsl:"BSL (1C:Enterprise)",oscript:"OneScript",csharp:"C#",cs:"C#",dotnet:"C#",cpp:"C++",cfscript:"CFScript",cfc:"CFScript",cil:"CIL",cilkc:"Cilk/C","cilk-c":"Cilk/C",cilkcpp:"Cilk/C++","cilk-cpp":"Cilk/C++",cilk:"Cilk/C++",cmake:"CMake",cobol:"COBOL",coffee:"CoffeeScript",conc:"Concurnas",csp:"Content-Security-Policy","css-extras":"CSS Extras",csv:"CSV",cue:"CUE",dataweave:"DataWeave",dax:"DAX",django:"Django/Jinja2",jinja2:"Django/Jinja2","dns-zone-file":"DNS zone file","dns-zone":"DNS zone file",dockerfile:"Docker",dot:"DOT (Graphviz)",gv:"DOT (Graphviz)",ebnf:"EBNF",editorconfig:"EditorConfig",ejs:"EJS",etlua:"Embedded Lua templating",erb:"ERB","excel-formula":"Excel Formula",xlsx:"Excel Formula",xls:"Excel Formula",fsharp:"F#","firestore-security-rules":"Firestore security rules",ftl:"FreeMarker Template Language",gml:"GameMaker Language",gamemakerlanguage:"GameMaker Language",gap:"GAP (CAS)",gcode:"G-code",gdscript:"GDScript",gedcom:"GEDCOM",gettext:"gettext",po:"gettext",glsl:"GLSL",gn:"GN",gni:"GN","linker-script":"GNU Linker Script",ld:"GNU Linker Script","go-module":"Go module","go-mod":"Go module",graphql:"GraphQL",hbs:"Handlebars",hs:"Haskell",hcl:"HCL",hlsl:"HLSL",http:"HTTP",hpkp:"HTTP Public-Key-Pins",hsts:"HTTP Strict-Transport-Security",ichigojam:"IchigoJam","icu-message-format":"ICU Message Format",idr:"Idris",ignore:".ignore",gitignore:".gitignore",hgignore:".hgignore",npmignore:".npmignore",inform7:"Inform 7",javadoc:"JavaDoc",javadoclike:"JavaDoc-like",javastacktrace:"Java stack trace",jq:"JQ",jsdoc:"JSDoc","js-extras":"JS Extras",json:"JSON",webmanifest:"Web App Manifest",json5:"JSON5",jsonp:"JSONP",jsstacktrace:"JS stack trace","js-templates":"JS Templates",keepalived:"Keepalived Configure",kts:"Kotlin Script",kt:"Kotlin",kumir:"KuMir (КуМир)",kum:"KuMir (КуМир)",latex:"LaTeX",tex:"TeX",context:"ConTeXt",lilypond:"LilyPond",ly:"LilyPond",emacs:"Lisp",elisp:"Lisp","emacs-lisp":"Lisp",llvm:"LLVM IR",log:"Log file",lolcode:"LOLCODE",magma:"Magma (CAS)",md:"Markdown","markup-templating":"Markup templating",matlab:"MATLAB",maxscript:"MAXScript",mel:"MEL",metafont:"METAFONT",mongodb:"MongoDB",moon:"MoonScript",n1ql:"N1QL",n4js:"N4JS",n4jsd:"N4JS","nand2tetris-hdl":"Nand To Tetris HDL",naniscript:"Naninovel Script",nani:"Naninovel Script",nasm:"NASM",neon:"NEON",nginx:"nginx",nsis:"NSIS",objectivec:"Objective-C",objc:"Objective-C",ocaml:"OCaml",opencl:"OpenCL",openqasm:"OpenQasm",qasm:"OpenQasm",parigp:"PARI/GP",objectpascal:"Object Pascal",psl:"PATROL Scripting Language",pcaxis:"PC-Axis",px:"PC-Axis",peoplecode:"PeopleCode",pcode:"PeopleCode",php:"PHP",phpdoc:"PHPDoc","php-extras":"PHP Extras","plant-uml":"PlantUML",plantuml:"PlantUML",plsql:"PL/SQL",powerquery:"PowerQuery",pq:"PowerQuery",mscript:"PowerQuery",powershell:"PowerShell",promql:"PromQL",properties:".properties",protobuf:"Protocol Buffers",purebasic:"PureBasic",pbfasm:"PureBasic",purs:"PureScript",py:"Python",qsharp:"Q#",qs:"Q#",q:"Q (kdb+ database)",qml:"QML",rkt:"Racket",cshtml:"Razor C#",razor:"Razor C#",jsx:"React JSX",tsx:"React TSX",renpy:"Ren'py",rpy:"Ren'py",res:"ReScript",rest:"reST (reStructuredText)",robotframework:"Robot Framework",robot:"Robot Framework",rb:"Ruby",sas:"SAS",sass:"Sass (Sass)",scss:"Sass (SCSS)","shell-session":"Shell session","sh-session":"Shell session",shellsession:"Shell session",sml:"SML",smlnj:"SML/NJ",solidity:"Solidity (Ethereum)",sol:"Solidity (Ethereum)","solution-file":"Solution file",sln:"Solution file",soy:"Soy (Closure Template)",sparql:"SPARQL",rq:"SPARQL","splunk-spl":"Splunk SPL",sqf:"SQF: Status Quo Function (Arma 3)",sql:"SQL",stata:"Stata Ado",iecst:"Structured Text (IEC 61131-3)",supercollider:"SuperCollider",sclang:"SuperCollider",systemd:"Systemd configuration file","t4-templating":"T4 templating","t4-cs":"T4 Text Templates (C#)",t4:"T4 Text Templates (C#)","t4-vb":"T4 Text Templates (VB)",tap:"TAP",tt2:"Template Toolkit 2",toml:"TOML",trickle:"trickle",troy:"troy",trig:"TriG",ts:"TypeScript",tsconfig:"TSConfig",uscript:"UnrealScript",uc:"UnrealScript",uorazor:"UO Razor Script",uri:"URI",url:"URL",vbnet:"VB.Net",vhdl:"VHDL",vim:"vim","visual-basic":"Visual Basic",vba:"VBA",vb:"Visual Basic",wasm:"WebAssembly","web-idl":"Web IDL",webidl:"Web IDL",wgsl:"WGSL",wiki:"Wiki markup",wolfram:"Wolfram language",nb:"Mathematica Notebook",wl:"Wolfram language",xeoracube:"XeoraCube","xml-doc":"XML doc (.net)",xojo:"Xojo (REALbasic)",xquery:"XQuery",yaml:"YAML",yml:"YAML",yang:"YANG"};Prism.plugins.toolbar.registerButton("show-language",(function(a){var t=a.element.parentNode;if(t&&/pre/i.test(t.nodeName)){var o,i=t.getAttribute("data-language")||e[a.language]||((o=a.language)?(o.substring(0,1).toUpperCase()+o.substring(1)).replace(/s(?=cript)/,"S"):o);if(i){var s=document.createElement("span");return s.textContent=i,s}}}))}else console.warn("Show Languages plugin loaded before Toolbar plugin.")}();
+!function(){function t(t){var e=document.createElement("textarea");e.value=t.getText(),e.style.top="0",e.style.left="0",e.style.position="fixed",document.body.appendChild(e),e.focus(),e.select();try{var o=document.execCommand("copy");setTimeout((function(){o?t.success():t.error()}),1)}catch(e){setTimeout((function(){t.error(e)}),1)}document.body.removeChild(e)}"undefined"!=typeof Prism&&"undefined"!=typeof document&&(Prism.plugins.toolbar?Prism.plugins.toolbar.registerButton("copy-to-clipboard",(function(e){var o=e.element,n=function(t){var e={copy:"Copy","copy-error":"Press Ctrl+C to copy","copy-success":"Copied!","copy-timeout":5e3};for(var o in e){for(var n="data-prismjs-"+o,c=t;c&&!c.hasAttribute(n);)c=c.parentElement;c&&(e[o]=c.getAttribute(n))}return e}(o),c=document.createElement("button");c.className="copy-to-clipboard-button",c.setAttribute("type","button");var r=document.createElement("span");return c.appendChild(r),u("copy"),function(e,o){e.addEventListener("click",(function(){!function(e){navigator.clipboard?navigator.clipboard.writeText(e.getText()).then(e.success,(function(){t(e)})):t(e)}(o)}))}(c,{getText:function(){return o.textContent},success:function(){u("copy-success"),i()},error:function(){u("copy-error"),setTimeout((function(){!function(t){window.getSelection().selectAllChildren(t)}(o)}),1),i()}}),c;function i(){setTimeout((function(){u("copy")}),n["copy-timeout"])}function u(t){r.textContent=n[t],c.setAttribute("data-copy-state",t)}})):console.warn("Copy to Clipboard plugin loaded before Toolbar plugin."))}();
diff --git a/web/source/settings/admin/reports/detail.jsx b/web/source/settings/admin/reports/detail.jsx
index d686b92bd..70d576080 100644
--- a/web/source/settings/admin/reports/detail.jsx
+++ b/web/source/settings/admin/reports/detail.jsx
@@ -140,18 +140,23 @@ function ReportedToot({ toot }) {
const account = toot.account;
return (
- <article className="toot expanded">
- <section className="author">
- <a>
- <img className="avatar" src={account.avatar} alt="" />
- <span className="displayname">
- {account.display_name.trim().length > 0 ? account.display_name : account.username}
- <span className="sr-only">.</span>
- </span>
- <span className="username">@{account.username}</span>
- </a>
- </section>
- <section className="body">
+ <article className="status expanded">
+ <header className="status-header">
+ <address>
+ <a style={{margin: 0}}>
+ <img className="avatar" src={account.avatar} alt="" />
+ <dl className="author-strap">
+ <dt className="sr-only">Display name</dt>
+ <dd className="displayname text-cutoff">
+ {account.display_name.trim().length > 0 ? account.display_name : account.username}
+ </dd>
+ <dt className="sr-only">Username</dt>
+ <dd className="username text-cutoff">@{account.username}</dd>
+ </dl>
+ </a>
+ </address>
+ </header>
+ <section className="status-body">
<div className="text">
<div className="content">
{toot.spoiler_text?.length > 0
@@ -164,8 +169,17 @@ function ReportedToot({ toot }) {
<TootMedia media={toot.media_attachments} sensitive={toot.sensitive} />
}
</section>
- <aside className="info">
- <time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
+ <aside className="status-info">
+ <dl class="status-stats">
+ <div class="stats-grouping">
+ <div class="stats-item published-at text-cutoff">
+ <dt class="sr-only">Published</dt>
+ <dd>
+ <time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
+ </dd>
+ </div>
+ </div>
+ </dl>
</aside>
</article>
);
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 }) {
<div className="usernames">
<Username user={from} link={false} /> reported <Username user={target} link={false} />
</div>
- <h3 className="status">
+ <h3 className="report-status">
{report.action_taken ? "Resolved" : "Open"}
</h3>
</div>
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
<div className="profile">
- <div className="header">
- <div className="header-image">
+ <div className="profile-header">
+ <div className="header-image-wrapper">
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
</div>
<div className="basic-info" aria-hidden="true">
<a className="avatar" href={avatar}>
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
</a>
- <span className="displayname text-cutoff">
- {display_name.trim().length > 0 ? display_name : username}
- <span className="sr-only">.</span>
- </span>
- <span className="username text-cutoff">@{username}</span>
- {(role && role.name != "user") &&
- <div className={`role ${role.name}`}>
- <span className="sr-only">Role: </span>{role.name}
- </div>
- }
+ <dl className="namerole">
+ <dt className="sr-only">Display name</dt>
+ <dd className="displayname text-cutoff">{display_name.trim().length > 0 ? display_name : username}</dd>
+ <dt className="sr-only">Username</dt>
+ <dd className="username text-cutoff">@{username}</dd>
+ <dt className="sr-only">Role</dt>
+ {
+ (role && role.name != "user") ?
+ <>
+ <dd className="sr-only">Role</dd>
+ <dt className={`role ${role.name}`}>{role.name}</dt>
+ </>
+ : null
+ }
+ </dl>
</div>
</div>
</div>
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 (
- <article className="toot expanded">
- <section className="author">
- <a>
- <img className="avatar" src={account.avatar} alt="" />
- <span className="displayname">
- {account.display_name.trim().length > 0 ? account.display_name : account.username}
- <span className="sr-only">.</span>
- </span>
- <span className="username">@{account.username}</span>
- </a>
- </section>
- <section className="body">
+ <article className="status expanded">
+ <header className="status-header">
+ <address>
+ <a style={{margin: 0}}>
+ <img className="avatar" src={account.avatar} alt="" />
+ <dl className="author-strap">
+ <dt className="sr-only">Display name</dt>
+ <dd className="displayname text-cutoff">
+ {account.display_name.trim().length > 0 ? account.display_name : account.username}
+ </dd>
+ <dt className="sr-only">Username</dt>
+ <dd className="username text-cutoff">@{account.username}</dd>
+ </dl>
+ </a>
+ </address>
+ </header>
+ <section className="status-body">
<div className="text">
- {children}
+ <div className="content">
+ {children}
+ </div>
</div>
</section>
</article>
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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section>
- <h1>404: Page Not Found</h1>
- <p>
- 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.
- </p>
- <p>
- If you believe this 404 was an error, you can contact
- the instance admin. Provide them with the following request
- Request ID: <code>{{.requestID}}</code>.
- </p>
- </section>
+ <section>
+ <h1>404: Not Found</h1>
+ <p>
+ GoToSocial only serves Public statuses via the web.
+ </p>
+ <p>
+ 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.
+ </p>
+ <p>
+ If you believe this 404 was an error, you can contact
+ the instance admin. Provide them with the following
+ request ID: <code>{{- .requestID -}}</code>.
+ </p>
+ </section>
</main>
-
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
-<main>
- <section class="about">
- <h1>About</h1>
- <div>
- {{.instance.Description |noescape}}
- </div>
+{{- define "description" -}}
+{{- if .instance.Description }}
+{{ .instance.Description | noescape }}
+{{- else }}
+<p>No description has yet been set for this instance.<p>
+{{- end }}
+{{- end -}}
- <div>
- <h2 id="languages">Languages</h2>
- <p>
- {{ if .languages }}
- This instance prefers the following languages:
- <ol>
- {{range .languages}}
- <li>{{.}}</li>
- {{end}}
- </ol>
- {{ else }}
- This instance does not have any preferred languages.
- {{ end }}
- </p>
- </div>
+{{- define "registrationLimits" -}}
+{{- if .instance.Registrations -}}
+ Registration is enabled; new signups can be submitted to this instance.<br/>
+ {{- 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 -}}
- <div>
- <h2 id="contact">Admin Contact</h2>
- {{if .instance.ContactAccount}}
- <a href="{{.instance.ContactAccount.URL}}" class="account-card">
- <img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
- <h3>
- {{if .instance.ContactAccount.DisplayName}}{{emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName)}}{{else}}{{.instance.ContactAccount.Username}}{{end}}
- </h3>
- <span>@{{.instance.ContactAccount.Username}}</span>
- </a><br />
- {{end}}
- {{if .instance.Email}}
- Email: <a href="mailto:{{.instance.Email}}">{{.instance.Email}}</a>
- {{end}}
- </div>
+{{- define "customCSSLimits" -}}
+{{- if .instance.Configuration.Accounts.AllowCustomCSS -}}
+Users are allowed to set <a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> for their profiles.
+{{- else -}}
+<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> is not enabled for user profiles.
+{{- end -}}
+{{- end -}}
- <div>
- <h2 id="rules">Rules</h2>
- <ol>
- {{range .instance.Rules}}
- <li>{{.Text}}</li>
- {{end}}
- </ol>
- </div>
+{{- define "statusLimits" -}}
+Statuses can contain up to&nbsp;
+{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and&nbsp;
+{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
+{{- end -}}
- <div>
- <h2 id="features">Features</h2>
- <ul>
- <li>
- Registration is
- {{if .instance.Registrations}}
- enabled{{if .instance.ApprovalRequired}}, but requires admin approval{{end}}.
- {{else}}
- disabled.
- {{end}}
- </li>
- {{if .instance.Configuration.Accounts.AllowCustomCSS}}
- <li>
- Users are allowed to set <a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/"
- target="_blank" rel="noopener noreferrer">Custom CSS</a> for their profiles.
- </li>
- {{end}}
- <li>
- Toots can contain up to {{.instance.Configuration.Statuses.MaxCharacters}} characters and
- {{.instance.Configuration.Statuses.MaxMediaAttachments}} media attachments.
- </li>
- <li>
- Polls can have up to {{.instance.Configuration.Polls.MaxOptions}} options, with
- {{.instance.Configuration.Polls.MaxCharactersPerOption}} characters each.
- </li>
- </ul>
- </div>
+{{- define "pollLimits" -}}
+Polls can have up to&nbsp;
+{{- .instance.Configuration.Polls.MaxOptions }} options, with&nbsp;
+{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
+{{- end -}}
- <div>
- <h2 id="moderated-servers">Moderated servers</h2>
- <p>
- 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.</br>
- {{if .blocklistExposed}}
- <a href="/about/suspended">View the list of suspended domains</a>
- {{else}}
- This instance does not publically share this list.
- {{end}}
- </p>
- </div>
-
- <div>
- <h2 id="stats">Instance Statistics</h2>
- <ul>
- <li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
- <li>Posts: <span class="count">{{.instance.Stats.status_count}}</span></li>
- <li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
- </ul>
- </div>
- </section>
+{{- with . }}
+<main class="about">
+ <section class="about-section" role="region" aria-labelledby="about">
+ <h3 id="about">About {{ .instance.Title -}}</h3>
+ {{- with . }}
+ {{- include "description" . | indent 2 }}
+ {{- end }}
+ </section>
+ <section class="about-section" role="region" aria-labelledby="contact">
+ <h3 id="contact">Admin Contact</h3>
+ {{- if .instance.ContactAccount }}
+ <a href="{{- .instance.ContactAccount.URL -}}" class="account-card">
+ <img class="avatar" src="{{- .instance.ContactAccount.Avatar -}}" alt=""/>
+ <h3>
+ {{- if .instance.ContactAccount.DisplayName -}}
+ {{- emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName) -}}
+ {{- else -}}
+ {{- .instance.ContactAccount.Username -}}
+ {{- end -}}
+ </h3>
+ <span>@{{- .instance.ContactAccount.Username -}}</span>
+ </a>
+ {{- else }}
+ <p>This instance has not yet set a contact account.</p>
+ {{- end }}
+ {{- if .instance.Email }}
+ <p>Email: <a href="mailto:{{- .instance.Email -}}">{{- .instance.Email -}}</a></p>
+ {{- else }}
+ <p>This instance has not yet set a contact email address.</p>
+ {{- end }}
+ </section>
+ <section class="about-section" role="region" aria-labelledby="languages">
+ <h3 id="languages">Languages</h3>
+ {{- if .languages }}
+ <p>This instance prefers the following languages:</p>
+ <ol>
+ {{- range .languages }}
+ <li>{{- . -}}</li>
+ {{- end }}
+ </ol>
+ {{- else }}
+ <p>This instance does not have any preferred languages.</p>
+ {{- end }}
+ </section>
+ <section class="about-section" role="region" aria-labelledby="rules">
+ <h3 id="rules">Instance Rules</h3>
+ <p>This instance has the following rules:</p>
+ {{- if .instance.Rules }}
+ <ol>
+ {{- range .instance.Rules }}
+ <li>{{- .Text -}}</li>
+ {{- end }}
+ </ol>
+ {{- else }}
+ <p>This instance has not yet set any rules.</p>
+ {{- end }}
+ </section>
+ <section class="about-section" role="region" aria-labelledby="features">
+ <h3 id="features">Instance Features</h3>
+ <ul>
+ <li>{{- template "registrationLimits" . -}}</li>
+ <li>{{- template "customCSSLimits" . -}}</li>
+ <li>{{- template "statusLimits" . -}}</li>
+ <li>{{- template "pollLimits" . -}}</li>
+ </ul>
+ </section>
+ <section class="about-section" role="region" aria-labelledby="moderated-servers">
+ <h3 id="moderated-servers">Moderated servers</h3>
+ <p>
+ 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:
+ </p>
+ <ul>
+ <li>Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.</li>
+ <li>Interaction between the two instances is cut off in both directions; neither instance can interact with the other.</li>
+ <li>No new data from the blocked instance will be created on the instance that blocks it.</li>
+ </ul>
+ <p>
+ {{- if .blocklistExposed }}
+ <a href="/about/suspended">View the list of domains blocked by this instance</a>
+ {{- else }}
+ This instance does not publically share their list of blocked domains.
+ {{- end }}
+ </p>
+ </section>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
- <main>
- <form action="/oauth/authorize" method="POST">
- <h1>Hi {{.user}}!</h1>
- <p>
- Application <b>{{.appname}}</b>
- {{if len .appwebsite | eq 0 | not}}
- ({{.appwebsite}})
- {{end}}
- would like to perform actions on your behalf, with scope <em>{{.scope}}</em>.
- </p>
- <p>The application will redirect to {{.redirect}} to continue.</p>
- <p>
- <button
- type="submit"
- style="width:200px;"
- >
- Allow
- </button>
- </p>
- </form>
- </main>
-{{ template "footer.tmpl" .}} \ No newline at end of file
+{{- with . }}
+<main>
+ <form action="/oauth/authorize" method="POST">
+ <h1>Hi {{ .user -}}!</h1>
+ <p>
+ Application
+ {{- if .appwebsite }}
+ <a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
+ {{- else }}
+ <b>{{- .appname -}}</b>
+ {{- end }}
+ would like to perform actions on your behalf, with scope
+ <em>{{- .scope -}}</em>.
+ </p>
+ <p>
+ To continue, the application will redirect to: <code>{{- .redirect -}}</code>
+ </p>
+ <button type="submit" style="width:200px;">Allow</button>
+ </form>
+</main>
+{{- 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section>
- <h1>Email Address Confirmed</h1>
- <p>Thanks {{.username}}! Your email address <b>{{.email}}</b> has been confirmed.<p>
- </section>
+ <section>
+ <h1>Email Address Confirmed</h1>
+ <p>Thanks {{ .username -}}! Your email address <b>{{- .email -}}</b> has been confirmed.<p>
+ </section>
</main>
-
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section>
- <h1>Suspended Instances</h1>
- <p>
- The following list of domains have been suspended by the administrator(s) of this server.
- </p>
- <p>
- 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.
- </p>
- <div class="list domain-blocklist">
- <div class="header entry">
- <div class="domain">Domain</div>
- <div class="public_comment">Public comment</div>
- </div>
- {{range .blocklist}}
- <div class="entry" id="{{.Domain}}">
- <div class="domain">
- <a class="text-cutoff" href="#{{.Domain}}" title="{{.Domain}}">{{.Domain}}</a>
- </div>
- <div class="public_comment">
- <p>
- {{.PublicComment}}
- </p>
- </div>
- </div>
- {{end}}
- </div>
- </section>
+ <section>
+ <h1>Suspended Instances</h1>
+ <p>
+ The following list of domains have been suspended
+ by the administrator(s) of this server.
+ </p>
+ <p>
+ 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.
+ </p>
+ <div class="list domain-blocklist">
+ <div class="header entry">
+ <div class="domain">Domain</div>
+ <div class="public_comment">Public comment</div>
+ </div>
+ {{- range .blocklist }}
+ <div class="entry" id="{{- .Domain -}}">
+ <div class="domain">
+ <a class="text-cutoff" href="#{{- .Domain -}}" title="{{- .Domain -}}">{{- .Domain -}}</a>
+ </div>
+ <div class="public_comment">
+ <p>{{- .PublicComment -}}</p>
+ </div>
+ </div>
+ {{- end }}
+ </div>
+ </section>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section class="error">
- <h1>An error occured:</h1>
- <pre>{{.error}}</pre>
- {{if .requestID}}
- <div>
- <span>Request ID:</span> <code>{{.requestID}}</code>
- </div>
- {{end}}
- </section>
+ <section class="error">
+ <h1>An error occured:</h1>
+ <pre>{{- .error -}}</pre>
+ {{- if .requestID }}
+ <div>
+ <span>Request ID:</span> <code>{{- .requestID -}}</code>
+ </div>
+ {{- end }}
+ </section>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
- <main>
- <form action="/oauth/finalize" method="POST">
- <h1>Hi {{.name}}!</h1>
- <p>
- You are about to sign-up to {{ .instance.Title }} (<code>{{ .instance.URI }}</code>)
- <br>
- To ensure the best experience for you, we need you to provide some additional details.
- </p>
- {{if .error}}
- <section class="error">
- <span>❌</span> <pre>{{.error}}</pre>
- </section>
- {{end}}
- <div class="callout">
- <p class="callout-title">Important</p>
- <p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
- </div>
- <div class="labelinput">
- <label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
- <input type="text"
- class="form-control"
- name="username"
- required
- placeholder="Please enter your desired username" value="{{ .preferredUsername }}">
- </div>
- <input type="hidden" name="name" value="{{ .name }}">
- <button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
- </form>
- </main>
-{{ template "footer.tmpl" .}}
+{{- with . }}
+<main>
+ <form action="/oauth/finalize" method="POST">
+ <h1>Hi {{ .name -}}!</h1>
+ <p>
+ You are about to sign-up to {{ .instance.Title -}}.
+ To ensure the best experience for you, we need you to provide some additional details.
+ </p>
+ <div class="callout">
+ <p class="callout-title">Important</p>
+ <p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
+ </div>
+ <div class="labelinput">
+ <label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
+ <input
+ type="text"
+ class="form-control"
+ name="username"
+ required
+ placeholder="Please enter your desired username"
+ value="{{- .preferredUsername -}}"
+ >
+ </div>
+ <input type="hidden" name="name" value="{{- .name -}}">
+ <button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
+ </form>
+</main>
+{{- 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main class="lightgray">
- <div id="root">
- </div>
+ <div id="root"></div>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
-*/ -}}
-
-
-{{- /*
- 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
-*/ -}}
-<!DOCTYPE html>
-<!-- header.tmpl -->
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
-
- {{- /*
- 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
- */ -}}
- <meta name="robots" content="{{ if .robotsMeta }}{{ .robotsMeta }}{{ else }}noindex, nofollow{{ end }}">
-
- {{- /*
- 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 }}<meta name="og:locale" content="{{ .ogMeta.Locale }}">{{ end }}
- <meta property="og:type" content="{{ .ogMeta.Type }}">
- <meta property="og:title" content="{{ .ogMeta.Title }}">
- <meta property="og:url" content="{{ .ogMeta.URL }}">
- <meta property="og:site_name" content="{{ .ogMeta.SiteName }}">
- <meta property="og:description" {{ .ogMeta.Description | noescapeAttr }}>
- {{ if .ogMeta.ArticlePublisher }}
- <meta property="og:article:publisher" content="{{ .ogMeta.ArticlePublisher }}">
- <meta property="og:article:author" content="{{ .ogMeta.ArticleAuthor }}">
- <meta property="og:article:modified_time" content="{{ .ogMeta.ArticleModifiedTime }}">
- <meta property="og:article:published_time" content="{{ .ogMeta.ArticlePublishedTime }}">
- {{ end }}
- {{ if .ogMeta.ProfileUsername }}<meta property="og:profile:username" content="{{ .ogMeta.ProfileUsername }}">{{ end }}
- <meta property="og:image" content="{{ .ogMeta.Image }}">
- {{ if .ogMeta.ImageAlt }}<meta property="og:image:alt" content="{{ .ogMeta.ImageAlt }}">{{ end }}
- {{ if .ogMeta.ImageWidth }}
- <meta property="og:image:width" content="{{ .ogMeta.ImageWidth }}">
- <meta property="og:image:height" content="{{ .ogMeta.ImageHeight }}">
- {{ 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
- */ -}}
- <link rel="icon" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
- <link rel="apple-touch-icon" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
- <link rel="apple-touch-startup-image" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
-
- {{- /*
- 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 -}}
- <link rel="alternate" type="application/rss+xml" href="{{ .rssFeed }}" title="{{ template "instanceTitle" . }}">
- {{- 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.
- */ -}}
- <link rel="preload" href="/assets/dist/_colors.css" as="style">
- <link rel="preload" href="/assets/dist/base.css" as="style">
- {{ range .stylesheets }}<link rel="preload" href="{{ . }}" as="style">{{ end }}
- <link rel="stylesheet" href="/assets/dist/_colors.css">
- <link rel="stylesheet" href="/assets/dist/base.css">
- {{ range .stylesheets }}<link rel="stylesheet" href="{{ . }}">{{ end }}
- <title>{{ template "instanceTitle" . }}</title>
-</head>
-
-<body>
- <div class="page">
- <header>
- <a aria-label="{{ .instance.Title }}. Go to instance homepage" href="/" class="nounderline header">
- <img src="{{ .instance.Thumbnail }}"
- alt="{{ if .instance.ThumbnailDescription }}{{ .instance.ThumbnailDescription }}{{ else }}Instance Logo{{ end }}" />
- <h1>
- {{ .instance.Title }}
- </h1>
- </a>
- </header>
- <div class="content"> \ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
-<section class="excerpt-top">
- home to <span class="count">{{.instance.Stats.user_count}}</span> users
- who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
- federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.
-</section>
-<main class="lightgray">
- <section>
- <div className="short-description">
- {{.instance.ShortDescription |noescape}}
- </div>
- </section>
- <section class="apps">
- <p>
- GoToSocial does not provide its own webclient, but implements the Mastodon client API.
- You can use this server through a variety of other clients:
- </p>
- <div class="applist">
- <div class="entry">
- <svg role="img" aria-labelledby="semaphoreTitle semaphoreDesc" class="logo redraw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 146 120">
- <title id="semaphoreTitle">The Semaphore logo</title>
- <desc id="semaphoreDesc">A waving flag</desc>
- <path d="M68.13 0C53.94 0 42.81 20 13.9 27.1l-2.23-5.29a6.5 6.5 0 0 0-5.17-10.4 6.5 6.5 0 0 0-.81 12.95L46.2 120l5.99-2.5-14.42-33.33c22.8-6.86 32.51-22.16 49.83-20.58 9.9.9 4.87 19.56 8.11 17.93 16.22-8.15 32.44-11.41 50.29-11.41-7.96-9.78-17.38-20.55-22.71-31.74L120.8 32c-2.32-7.33-2.56-14.75.87-22.22-9.74-3.26-21.1 0-32.45 4.9C82.2 9.77 79.5 0 68.13 0zM15.26 30.42c8.95 6.63 13.63 13.86 16.07 20.94l1.62 6.32c1.24 6.58 1.07 12.8 1.27 18.03z"></path>
- </svg>
- <div>
- <h2>Semaphore</h2>
- <p>Semaphore is a web client designed for speed and simplicity.</p>
- <a href="https://semaphore.social/" target="_blank" rel="noopener">Use Semaphore</a>
- </div>
- </div>
- <div class="entry">
- <img class="logo" src="/assets/tusky.svg" alt="The Tusky mascot, a cartoon elephant tooting happily"/>
- <div>
- <h2>Tusky</h2>
- <p>Tusky is a lightweight mobile client for Android.</p>
- <a href="https://tusky.app" target="_blank" rel="noopener">Get Tusky</a>
- </div>
- </div>
- <div class="entry">
- <img class="logo" src="/assets/feditext.svg" alt="The Feditext logo, the characters ft at a slight angle">
- <div>
- <h2>Feditext</h2>
- <p>Feditext (beta) is a beautiful client for iOS, iPadOS and macOS.</p>
- <a href="https://fedi.software/@Feditext" target="_blank" rel="noopener">Get Feditext</a>
- </div>
- </div>
- <div class="entry">
- <img class="logo" src="/assets/mastodon.svg" alt="The Mastodon logo, the character M in a speech bubble">
- <div>
- <h2>More clients</h2>
- <p>Or try one of the clients listed on the official Mastodon page.</p>
- <a href="https://joinmastodon.org/apps" target="_blank" rel="noopener">Get Mastodon apps</a>
- </div>
- </div>
- </div>
- </section>
+{{- define "shortDescription" -}}
+{{- if .instance.ShortDescription }}
+{{ .instance.ShortDescription | noescape }}
+{{- else }}
+<p>No short description has yet been set for this instance.<p>
+{{- end }}
+{{- end -}}
+
+{{- with . }}
+<main class="about">
+ <section class="about-section" role="region" aria-labelledby="about">
+ <h3 id="about">About this instance</h3>
+ {{- include "shortDescription" . | indent 2 }}
+ <a href="/about">See more details</a>
+ </section>
+ {{- include "index_apps.tmpl" . | indent 1 }}
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with . }}
+<section role="region" class="about-section apps" aria-labelledby="apps">
+ <h3 id="apps">Client applications</h3>
+ <p>
+ GoToSocial does not provide its own webclient, but implements the Mastodon client API.
+ You can use this server through a variety of other clients:
+ </p>
+ <ul class="applist nodot" role="group">
+ <li class="applist-entry">
+ <div class="applist-text">
+ <p><strong>Semaphore</strong> is a web client designed for speed and simplicity.</p>
+ <a
+ href="https://semaphore.social/"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ Use Semaphore
+ </a>
+ </div>
+ <svg
+ role="img"
+ aria-labelledby="semaphore-title semaphore-desc"
+ class="applist-logo redraw"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 146 120"
+ width="100"
+ height="100"
+ >
+ <title id="semaphore-title">The Semaphore logo</title>
+ <desc id="semaphore-desc">A waving flag</desc>
+ <path d="M68.13 0C53.94 0 42.81 20 13.9 27.1l-2.23-5.29a6.5 6.5 0 0 0-5.17-10.4 6.5 6.5 0 0 0-.81 12.95L46.2 120l5.99-2.5-14.42-33.33c22.8-6.86 32.51-22.16 49.83-20.58 9.9.9 4.87 19.56 8.11 17.93 16.22-8.15 32.44-11.41 50.29-11.41-7.96-9.78-17.38-20.55-22.71-31.74L120.8 32c-2.32-7.33-2.56-14.75.87-22.22-9.74-3.26-21.1 0-32.45 4.9C82.2 9.77 79.5 0 68.13 0zM15.26 30.42c8.95 6.63 13.63 13.86 16.07 20.94l1.62 6.32c1.24 6.58 1.07 12.8 1.27 18.03z"></path>
+ </svg>
+ </li>
+ <li class="applist-entry">
+ <div class="applist-text">
+ <p><strong>Tusky</strong> is a lightweight mobile client for Android.</p>
+ <a
+ href="https://tusky.app"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ Get Tusky
+ </a>
+ </div>
+ <img
+ class="applist-logo"
+ src="/assets/tusky.svg"
+ alt="The Tusky mascot, a cartoon elephant tooting happily"
+ title="The Tusky mascot, a cartoon elephant tooting happily"
+ width="100"
+ height="100"
+ />
+ </li>
+ <li class="applist-entry">
+ <div class="applist-text">
+ <p><strong>Feditext</strong> (beta) is a beautiful client for iOS, iPadOS and macOS.</p>
+ <a
+ href="https://fedi.software/@Feditext"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ Get Feditext
+ </a>
+ </div>
+ <img
+ class="applist-logo"
+ src="/assets/feditext.svg"
+ alt="The Feditext logo, the characters 'ft' at a slight angle"
+ title="The Feditext logo, the characters 'ft' at a slight angle"
+ width="100"
+ height="100"
+ />
+ </li>
+ <li class="applist-entry">
+ <div class="applist-text">
+ <p>Or try one of the <strong>Mastodon clients</strong> listed on the official Mastodon page.</p>
+ <a
+ href="https://joinmastodon.org/apps"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ Get Mastodon apps
+ </a>
+ </div>
+ <img
+ class="applist-logo"
+ src="/assets/mastodon.svg"
+ alt="The Mastodon logo, the character 'M' in a speech bubble"
+ title="The Mastodon logo, the character 'M' in a speech bubble"
+ width="100"
+ height="100"
+ />
+ </li>
+ </ul>
+</section>
+{{- 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section class="oob-token">
- <h1>Hi {{ .user }}!</h1>
- <p>Here's your out-of-band token with scope "<em>{{.scope}}</em>", use it wisely:</p>
- <code>{{ .oobToken }}</code>
- </section>
+ <section class="oob-token">
+ <h1>Hi {{ .user -}}!</h1>
+ <p>Here's your out-of-band token with scope "<em>{{- .scope -}}</em>", use it wisely:</p>
+ <code>{{- .oobToken -}}</code>
+ </section>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- /*
+ 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 -}}
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="robots" content="{{- if .robotsMeta -}}{{- .robotsMeta -}}{{- else -}}noindex, nofollow{{- end -}}">
+ {{- if .ogMeta }}
+ {{- include "page_ogmeta.tmpl" . | indent 2 }}
+ {{- else }}
+ {{- end }}
+ {{- if .rssFeed }}
+ <link rel="alternate" type="application/rss+xml" href="{{- .rssFeed -}}" title="{{- template "instanceTitle" . -}}">
+ {{- else }}
+ {{- end }}
+ {{- if .account }}
+ <link rel="alternate" type="application/activity+json" href="/users/{{- .account.Username -}}">
+ {{- else if .status }}
+ <link rel="alternate" type="application/activity+json" href="/users/{{- .status.Account.Username -}}/statuses/{{- .status.ID -}}">
+ {{- else }}
+ {{- end }}
+ <link rel="icon" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
+ <link rel="apple-touch-icon" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
+ <link rel="apple-touch-startup-image" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
+ {{- include "page_stylesheets.tmpl" . | indent 2 }}
+ {{- range .javascript }}
+ <script type="text/javascript" src="{{- . -}}" async="" defer=""></script>
+ {{- end }}
+ <title>{{- template "instanceTitle" . -}}</title>
+ </head>
+ <body class="page">
+ <header class="page-header">
+ {{- include "page_header.tmpl" . | indent 3 }}
+ </header>
+ <div class="page-content">
+ {{- include .pageContent . | indent 3 | outdentPre }}
+ </div>
+ <footer class="page-footer">
+ {{- include "page_footer.tmpl" . | indent 3 }}
+ </footer>
+ </body>
+</html> \ 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with . }}
+<nav>
+ <ul class="nodot">
+ <li id="about">
+ <a
+ href="/about"
+ class="nounderline"
+ >
+ About {{ .instance.Title }}
+ </a>
+ </li>
+ <li id="version">
+ <a
+ href="https://github.com/superseriousbusiness/gotosocial"
+ class="nounderline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ <span aria-hidden="true">🦥</span>
+ Source - GoToSocial {{ .instance.Version }}
+ <span aria-hidden="true">🦥</span>
+ </a>
+ </li>
+ {{- if .instance.ContactAccount }}
+ <li id="contact">
+ <a
+ href="/@{{- .instance.ContactAccount.Username -}}"
+ class="nounderline"
+ >
+ Contact account - {{ .instance.ContactAccount.Username }}
+ </a>
+ </li>
+ {{- end }}
+ {{- if .instance.Email }}
+ <li id="email">
+ <a
+ href="mailto:{{- .instance.Email -}}"
+ class="nounderline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ Email - {{ .instance.Email }}
+ </a>
+ </li>
+ {{- end }}
+ </ul>
+</nav>
+{{- 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- define "thumbnailDescription" -}}
+{{- if .instance.ThumbnailDescription -}}
+{{- .instance.ThumbnailDescription -}}
+{{- else -}}
+Instance Logo
+{{- end -}}
+{{- end -}}
+
+{{- define "strapUsers" -}}
+{{- with .instance.Stats.user_count -}}
+ {{- if eq . 1 -}}
+ <span class="count">{{- . -}}</span> user
+ {{- else -}}
+ <span class="count">{{- . -}}</span> users
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "strapPosts" -}}
+{{- with .instance.Stats.status_count -}}
+ {{- if eq . 1 -}}
+ <span class="count">{{- . -}}</span> post
+ {{- else -}}
+ <span class="count">{{- . -}}</span> posts
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "strapInstances" -}}
+{{- with .instance.Stats.domain_count -}}
+ {{- if eq . 1 -}}
+ <span class="count">{{- . -}}</span> other instance
+ {{- else -}}
+ <span class="count">{{- . -}}</span> other instances
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- with . }}
+<a aria-label="{{- .instance.Title -}}. Go to instance homepage" href="/" class="nounderline">
+ <img
+ src="{{- .instance.Thumbnail -}}"
+ alt="{{- template "thumbnailDescription" . -}}"
+ title="{{- template "thumbnailDescription" . -}}"
+ width="100"
+ height="100"
+ />
+ <h1>{{- .instance.Title -}}</h1>
+</a>
+{{- if .showStrap }}
+<aside>home to {{ template "strapUsers" . }} who wrote {{ template "strapPosts" . }}, federating with {{ template "strapInstances" . }}</aside>
+{{- 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- /*
+ 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 }}
+<meta name="og:locale" content="{{- .Locale -}}">
+{{- else }}
+{{- end }}
+<meta property="og:type" content="{{- .Type -}}">
+<meta property="og:title" content="{{- demojify .Title | noescape -}}">
+<meta property="og:url" content="{{- .URL -}}">
+<meta property="og:site_name" content="{{- .SiteName -}}">
+<meta property="og:description" {{ demojify .Description | noescapeAttr -}}>
+{{- if .ArticlePublisher }}
+<meta property="og:article:publisher" content="{{ .ArticlePublisher }}">
+<meta property="og:article:author" content="{{ .ArticleAuthor }}">
+<meta property="og:article:modified_time" content="{{ .ArticleModifiedTime }}">
+<meta property="og:article:published_time" content="{{ .ArticlePublishedTime }}">
+{{- else }}
+{{- end }}
+{{- if .ProfileUsername }}
+<meta property="og:profile:username" content="{{- .ProfileUsername -}}">
+{{- else }}
+{{- end }}
+<meta property="og:image" content="{{- .Image -}}">
+{{- if .ImageAlt }}
+<meta property="og:image:alt" content="{{- .ImageAlt -}}">
+{{- else }}
+{{- end }}
+{{- if .ImageWidth }}
+<meta property="og:image:width" content="{{ .ImageWidth }}">
+<meta property="og:image:height" content="{{ .ImageHeight }}">
+{{- 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- /*
+ 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 . }}
+<link rel="preload" href="/assets/dist/_colors.css" as="style">
+<link rel="preload" href="/assets/dist/base.css" as="style">
+<link rel="preload" href="/assets/dist/page.css" as="style">
+{{- range .stylesheets }}
+<link rel="preload" href="{{- . -}}" as="style">
+{{- end }}
+<link rel="stylesheet" href="/assets/dist/_colors.css">
+<link rel="stylesheet" href="/assets/dist/base.css">
+<link rel="stylesheet" href="/assets/dist/page.css">
+{{- range .stylesheets }}
+<link rel="stylesheet" href="{{- . -}}">
+{{- 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
-
+{{- with . }}
<main class="profile">
- <div class="header">
- <div class="header-image">
- {{ if .account.Header }}
- <img src="{{.account.Header}}" alt="" />
- {{ end }}
- </div>
- <div class="basic-info" aria-hidden="true">
- <a class="avatar" href="{{.account.Avatar}}">
- <img src="{{.account.Avatar}}" alt="">
- </a>
- <span class="displayname text-cutoff">
- {{if .account.DisplayName}}
- {{emojify .account.Emojis (escape .account.DisplayName)}}
- {{else}}
- {{.account.Username}}
- {{end}}
- </span>
- <span class="username text-cutoff">@{{.account.Username}}@{{.instance.AccountDomain}}</span>
- {{- /* Only render account role if 1. it's present and 2. it's not equal to the standard 'user' role */ -}}
- {{ if and (.account.Role) (ne .account.Role.Name "user") }}
- <div class="role {{ .account.Role.Name }}">
- {{ .account.Role.Name }}
- </div>
- {{ end }}
- </div>
- <div class="sr-only">
- 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 }}
- </div>
- </div>
-
- <div class="column-split">
-
- <section class="about-user">
- <div class="col-header">
- <h1>About</h1>
- </div>
-
- <div class="fields">
- {{ range .account.Fields }}
- <div class="field">
- <b>{{emojify $.account.Emojis (noescape .Name)}}</b>
- <span>{{emojify $.account.Emojis (noescape .Value)}}</span>
- </div>
- {{ end }}
- </div>
-
- <div class="bio">
- {{ if .account.Note }}
- {{emojify .account.Emojis (noescape .account.Note)}}
- {{else}}
- This GoToSocial user hasn't written a bio yet!
- {{end}}
- </div>
-
- <div class="sr-only" role="group">
- <span>Joined on {{.account.CreatedAt | timestampVague}}.</span>
- <span>{{.account.StatusesCount}} post{{if .account.StatusesCount | eq 1 | not}}s{{end}}.</span>
- <span>Followed by {{.account.FollowersCount}}.</span>
- <span>Following {{.account.FollowingCount}}.</span>
- </div>
-
- <div class="accountstats" aria-hidden="true">
- <b>Joined</b><time datetime="{{.account.CreatedAt}}">{{.account.CreatedAt | timestampVague}}</time>
- <b>Posts</b><span>{{.account.StatusesCount}}</span>
- <b>Followed by</b><span>{{.account.FollowersCount}}</span>
- <b>Following</b><span>{{.account.FollowingCount}}</span>
- </div>
- </section>
-
- <section class="toots">
- {{ if .pinned_statuses }}
- <div class="col-header">
- <h2>Pinned posts</h2>
- <a href="#recent">jump to recent</a>
- </div>
- <section class="thread">
- {{ range .pinned_statuses }}
- <article class="toot expanded" id="{{.ID}}">
- {{ template "status.tmpl" .}}
- </article>
- {{ end }}
- </section>
- {{ end }}
-
- <div class="col-header">
- <h2 id="recent" tabindex="-1">Recent posts</h2>
- {{ if .rssFeed }}
- <a href="{{ .rssFeed }}" class="rss-icon" aria-label="RSS feed">
- <i class="fa fa-rss-square" aria-hidden="true"></i>
- </a>
- {{ end }}
- </div>
-
- <section class="thread">
- {{ if not .statuses }}
- <div data-nosnippet class="nothinghere">Nothing here!</div>
- {{ else }}
- {{ range .statuses }}
- <article class="toot expanded" id="{{.ID}}">
- {{ template "status.tmpl" .}}
- </article>
- {{ end }}
- {{ end }}
- </section>
-
- <div class="backnextlinks">
- {{ if .show_back_to_top }}
- <a href="/@{{ .account.Username }}">Back to top</a>
- {{ end }}
- {{ if .statuses_next }}
- <a href="{{ .statuses_next }}" class="next">Show older</a>
- {{ end }}
- </div>
- </section>
- </div>
+ <h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
+ <section class="profile-header" role="region" aria-label="Basic info">
+ <div class="header-image-wrapper">
+ <img
+ src="{{- .account.Header -}}"
+ alt="Header for {{ .account.Username -}}"
+ title="Header for {{ .account.Username -}}"
+ />
+ </div>
+ <div class="basic-info">
+ <a class="avatar" href="{{- .account.Avatar -}}">
+ <img
+ src="{{- .account.Avatar -}}"
+ alt="Avatar for {{ .account.Username -}}"
+ title="Avatar for {{ .account.Username -}}"
+ />
+ </a>
+ <dl class="namerole">
+ <dt class="sr-only">Display name</dt>
+ <dd class="displayname text-cutoff">
+ {{- if .account.DisplayName -}}
+ {{- emojify .account.Emojis (escape .account.DisplayName) -}}
+ {{- else -}}
+ {{- .account.Username -}}
+ {{- end -}}
+ </dd>
+ <dt class="sr-only">Username</dt>
+ <dd class="username text-cutoff">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
+ {{- if and (.account.Role) (ne .account.Role.Name "user") }}
+ <dt class="sr-only">Role</dt>
+ <dd class="role {{ .account.Role.Name -}}">{{- .account.Role.Name -}}</dd>
+ {{- end }}
+ </dl>
+ </div>
+ </section>
+ <div class="column-split">
+ <section class="about-user" role="region" aria-labelledby="about-header">
+ <div class="col-header">
+ <h3 id="about-header">About<span class="sr-only">&nbsp;{{- .account.Username -}}</span></h3>
+ </div>
+ {{- if .account.Fields }}
+ {{- include "profile_fields.tmpl" . | indent 3 }}
+ {{- end }}
+ <h4 class="sr-only">Bio</h4>
+ <div class="bio">
+ {{- if .account.Note }}
+ {{ emojify .account.Emojis (noescape .account.Note) }}
+ {{- else }}
+ <p>This GoToSocial user hasn't written a bio yet!</p>
+ {{- end }}
+ </div>
+ <h4 class="sr-only">Stats</h4>
+ <dl class="accountstats">
+ <dt>Joined</dt>
+ <dd><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
+ <dt>Posts</dt>
+ <dd>{{- .account.StatusesCount -}}</dd>
+ <dt>Followed by</dt>
+ <dd>{{- .account.FollowersCount -}}</dd>
+ <dt>Following</dt>
+ <dd>{{- .account.FollowingCount -}}</dd>
+ </dl>
+ </section>
+ <div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
+ {{- if .pinned_statuses }}
+ <section class="pinned statuses" aria-labelledby="pinned">
+ <div class="col-header">
+ <h3 id="pinned">Pinned posts</h3>
+ <a href="#recent">jump to recent</a>
+ </div>
+ <div class="thread">
+ {{- range .pinned_statuses }}
+ <article
+ class="status expanded"
+ {{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
+ >
+ {{- include "status.tmpl" . | indent 6 }}
+ </article>
+ {{- end }}
+ </div>
+ </section>
+ {{- end }}
+ <section class="recent statuses" aria-labelledby="recent">
+ <div class="col-header">
+ <h3 id="recent" tabindex="-1">Recent posts</h3>
+ {{- if .rssFeed }}
+ <a href="{{- .rssFeed -}}" class="rss-icon" aria-label="RSS feed">
+ <i class="fa fa-rss-square" aria-hidden="true"></i>
+ </a>
+ {{- end }}
+ </div>
+ <div class="thread">
+ {{- if not .statuses }}
+ <div data-nosnippet class="nothinghere">Nothing here!</div>
+ {{- else }}
+ {{- range .statuses }}
+ <article
+ class="status expanded"
+ {{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
+ >
+ {{- include "status.tmpl" . | indent 6 }}
+ </article>
+ {{- end }}
+ {{- end }}
+ </div>
+ <nav class="backnextlinks">
+ {{- if .show_back_to_top }}
+ <a href="/@{{- .account.Username -}}">Back to top</a>
+ {{- end }}
+ {{- if .statuses_next }}
+ <a href="{{- .statuses_next -}}" class="next">Show older</a>
+ {{- end }}
+ </nav>
+ </section>
+ </div>
+ </div>
</main>
-
-{{ 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
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 <http://www.gnu.org/licenses/>.
*/ -}}
- <!-- footer.tmpl -->
- </div>
- <footer>
- <div id="version">
- <a name="Source code" href="https://github.com/superseriousbusiness/gotosocial">
- GoToSocial <span class="accent">{{.instance.Version}}</span>
- </a>
- </div>
- {{ if .instance.ContactAccount }}
- <div id="contact">
- Contact: <a href="{{.instance.ContactAccount.URL}}" class="nounderline">{{.instance.ContactAccount.Username}}</a><br>
- </div>
- {{ end }}
- {{ if .instance.Email }}
- <div id="email">
- Email: <a href="mailto:{{.instance.Email}}" class="nounderline">{{.instance.Email}}</a><br>
- </div>
- {{ end }}
- </footer>
- </div>
- {{ if .javascript }}
- {{ range .javascript }}
- <script src="{{.}}"></script>
- {{ end }}
- {{ end }}
-</body>
-</html> \ No newline at end of file
+{{- with . }}
+<div class="fields">
+ <h4 class="sr-only">Fields</h4>
+ <dl>
+ {{- range .account.Fields }}
+ <div class="field">
+ <dt>{{- emojify $.account.Emojis (noescape .Name) -}}</dt>
+ <dd>{{- emojify $.account.Emojis (noescape .Value) -}}</dd>
+ </div>
+ {{- end }}
+ </dl>
+</div>
+{{- 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 <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
+{{- with . }}
<main>
- <section class="login">
- <h1>Login</h1>
+ <section class="sign-in" aria-labelledby="sign-in">
+ <h2 id="sign-in">Sign in</h2>
<form action="/auth/sign_in" method="POST">
<div class="labelinput">
<label for="email">Email</label>
@@ -30,8 +30,8 @@
<label for="password">Password</label>
<input type="password" class="form-control" name="password" required placeholder="Please enter your password">
</div>
- <button type="submit" class="btn btn-success">Login</button>
+ <button type="submit" class="btn btn-success">Sign in</button>
</form>
</section>
</main>
-{{ 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 <http://www.gnu.org/licenses/>.
*/ -}}
-<a data-nosnippet href="{{- .URL -}}" class="toot-link">Open thread</a>
-<section class="author">
- <a href="{{- .Account.URL -}}">
- <img class="avatar" src="{{- .Account.Avatar -}}" alt="">
- <span aria-hidden="true" class="displayname">
- {{- if .Account.DisplayName -}}
- {{- emojify .Account.Emojis (escape .Account.DisplayName) -}}
- {{- else -}}
- {{- .Account.Username -}}
- {{- end -}}
- </span>
- <span aria-hidden="true" class="username">@{{- .Account.Username -}}</span>
- <span class="sr-only">
- {{- if .Account.DisplayName -}}
- {{- emojify .Account.Emojis (escape .Account.DisplayName) -}}. Username: @{{ .Account.Acct -}}.
- {{- else -}}
- @{{- .Account.Acct -}}.
- {{- end -}}
- </span>
- </a>
-</section>
-<section class="body">
- {{- if .SpoilerText }}
- <details class="text-spoiler">
- <summary>
- <span class="spoiler-text" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (escape .SpoilerText) -}}</span>
- <span class="button" role="button" tabindex="0">Toggle visibility</span>
- </summary>
- <div class="text">
- <div class="content" lang="{{- .LanguageTag.TagStr -}}">
- {{ emojify .Emojis (noescape .Content) }}
- </div>
- {{- if .Poll }}
- {{ template "status_poll.tmpl" . }}
- {{- end }}
- </div>
- </details>
- {{- else }}
- <div class="text">
- <div class="content" lang="{{- .LanguageTag.TagStr -}}">
- {{ emojify .Emojis (noescape .Content) }}
- </div>
- {{- if .Poll }}
- {{ template "status_poll.tmpl" . }}
- {{- end }}
- </div>
- {{- end }}
- {{- if .MediaAttachments }}
- {{ template "status_attachments.tmpl" . }}
- {{- end }}
-</section>
-<aside class="info">
- <dl class="sr-only">
- <dt>Published<dt>
- <dd>{{- .CreatedAt | timestampPrecise -}}</dd>
- {{- if .LanguageTag.DisplayStr }}
- <dt>Language</dt>
- <dd>{{ .LanguageTag.DisplayStr }}</dd>
- {{- end }}
- </dl>
- <time aria-hidden="true" datetime="{{- .CreatedAt -}}">{{- .CreatedAt | timestampPrecise -}}</time>
- <div class="stats" role="group">
- <div class="stats-item">
- <span aria-hidden="true"><i class="fa fa-reply-all"></i> {{ .RepliesCount -}}</span>
- <span class="sr-only">{{- .RepliesCount }} {{ if .RepliesCount | eq 1 }}reply{{ else }}replies{{ end -}}</span>
- </div>
- <div class="stats-item">
- <span aria-hidden="true"><i class="fa fa-star"></i> {{ .FavouritesCount -}}</span>
- <span class="sr-only">{{- .FavouritesCount }} {{ if .FavouritesCount | eq 1 }}favourite{{ else }}favourites{{ end -}}</span>
- </div>
- <div class="stats-item">
- <span aria-hidden="true"><i class="fa fa-retweet"></i> {{ .ReblogsCount -}}</span>
- <span class="sr-only">{{- .ReblogsCount }} {{ if .ReblogsCount | eq 1 }}boost{{ else }}boosts{{ end -}}</span>
- </div>
- {{- if .Pinned }}
- <div class="stats-item">
- <i class="fa fa-thumb-tack" aria-hidden="true"></i>
- <span class="sr-only">pinned</span>
- </div>
- {{- end }}
- {{- if .LanguageTag.DisplayStr }}
- <div aria-hidden="true" class="stats-item language" title="Language: {{ .LanguageTag.DisplayStr }}">{{ .LanguageTag.TagStr }}</div>
- {{- end }}
- </div>
+{{- define "statusContent" -}}
+{{- with .Content }}
+<div class="content" lang="{{- $.LanguageTag.TagStr -}}">
+ {{ noescape . | emojify $.Emojis }}
+</div>
+{{- end }}
+{{- end -}}
+
+{{- /*
+ When including this template, always wrap
+ it in an appropriate <article></article>!
+*/ -}}
+
+{{- with . }}
+<header class="status-header">
+ {{- include "status_header.tmpl" . | indent 1 }}
+</header>
+<div class="status-body">
+ {{- if .SpoilerText }}
+ <details class="text-spoiler">
+ <summary>
+ <span class="spoiler-text" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (escape .SpoilerText) -}}</span>
+ <span class="button" role="button" tabindex="0">Toggle visibility</span>
+ </summary>
+ <div class="text">
+ {{- with . }}
+ {{- include "statusContent" . | indent 3 }}
+ {{- end }}
+ {{- if .Poll }}
+ {{- include "status_poll.tmpl" . | indent 3 }}
+ {{- end }}
+ </div>
+ </details>
+ {{- else }}
+ <div class="text">
+ {{- with . }}
+ {{- include "statusContent" . | indent 2 }}
+ {{- end }}
+ {{- if .Poll }}
+ {{- include "status_poll.tmpl" . | indent 2 }}
+ {{- end }}
+ </div>
+ {{- end }}
+ {{- if .MediaAttachments }}
+ {{- include "status_attachments.tmpl" . | indent 1 }}
+ {{- end }}
+</div>
+<aside class="status-info" aria-hidden="true">
+ {{- include "status_info.tmpl" . | indent 1 }}
</aside>
+{{- if .Local }}
+<a
+ href="{{- .URL -}}"
+ class="status-link"
+ data-nosnippet
+ title="Open thread at this post"
+>
+ Open thread at this post
+</a>
+{{- else }}
+<a
+ href="{{- .URL -}}"
+ class="status-link"
+ data-nosnippet
+ rel="nofollow noreferrer noopener" target="_blank"
+ title="Open remote post (opens in a new window)"
+>
+ Open remote post (opens in a new window)
+</a>
+{{- end }}
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_attachments.tmpl b/web/template/status_attachments.tmpl
index bd26c82a6..b257f2211 100644
--- a/web/template/status_attachments.tmpl
+++ b/web/template/status_attachments.tmpl
@@ -18,77 +18,119 @@
*/ -}}
{{- /*
- Template for rendering a gallery of status media attachments.
- To use this template, pass a web view status into it.
+ Template for rendering a gallery of status media attachments.
+ To use this template, pass a web view status into it.
*/ -}}
-{{ with .MediaAttachments }}
- <div class="media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{- end -}}">
- {{- range $index, $media := . }}
- <div class="media-wrapper">
- <details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{ end -}}>
- <summary>
- <div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
- <span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
- <i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
- <i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
- </span>
- {{- if eq .Type "video" }}
- <video {{- if .Description }} title="{{- $media.Description -}}" {{- end -}}>
- <source type="video/mp4" src="{{- $media.URL -}}"/>
- </video>
- {{- else if eq .Type "image" }}
- <img src="{{- $media.PreviewURL -}}" {{- if .Description }} title="{{- $media.Description -}}" {{- end }}/>
- {{- end }}
- </summary>
- {{- if eq .Type "video" }}
- <video
- class="plyr-video photoswipe-slide"
- controls
- data-pswp-index="{{- $index -}}"
- data-pswp-width="{{- $media.Meta.Original.Width -}}px"
- data-pswp-height="{{- $media.Meta.Original.Height -}}px"
- {{- if .Description }}
- alt="{{- $media.Description -}}"
- title="{{- $media.Description -}}"
- {{- end }}
- >
- <source type="video/mp4" src="{{- $media.URL -}}"/>
- </video>
- {{- else if eq .Type "image" }}
- <a
- class="photoswipe-slide"
- href="{{- $media.URL -}}"
- target="_blank"
- data-pswp-width="{{- $media.Meta.Original.Width -}}px"
- data-pswp-height="{{- $media.Meta.Original.Height -}}px"
- data-cropped="true"
- {{- if .Description }}
- title="{{- $media.Description -}}"
- {{- end }}
- >
- <img src="{{$media.PreviewURL}}" {{if .Description}}alt="{{$media.Description}}" {{end}} />
- </a>
- {{- else }}
- <a
- class="unknown-attachment"
- href="{{- $media.RemoteURL -}}"
- target="_blank"
- {{- if .Description }}
- title="Link to external media: {{ $media.Description -}}&#10;&#13;{{- $media.RemoteURL -}}"
- {{- else }}
- title="Link to external media.&#10;&#13;{{- $media.RemoteURL -}}"
- {{- end }}
- >
- <div class="placeholder" aria-hidden="true">
- <i class="placeholder-external-link fa fa-external-link"></i>
- <i class="placeholder-icon fa fa-file-text"></i>
- <div class="placeholder-link-to">External media</div>
- </div>
- </a>
- {{- end }}
- </details>
- </div>
- {{- end }}
- </div>
+{{- define "imagePreview" }}
+<img
+ src="{{- .PreviewURL -}}"
+ loading="lazy"
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="{{- .Meta.Original.Width -}}"
+ height="{{- .Meta.Original.Height -}}"
+/>
{{- end }}
+
+{{- define "videoPreview" }}
+<video
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="{{- .Meta.Original.Width -}}"
+ height="{{- .Meta.Original.Height -}}"
+>
+ <source type="video/mp4" src="{{- .URL -}}"/>
+</video>
+{{- end }}
+
+{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
+{{- define "attachmentsLength" -}}
+{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
+{{- end -}}
+
+{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
+{{- define "galleryClass" -}}
+media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
+{{- end -}}
+
+{{- with .MediaAttachments }}
+<div
+ class="{{- template "galleryClass" . -}}"
+ role="group"
+ aria-label="{{- template "attachmentsLength" . -}}"
+>
+ {{- range $index, $media := . }}
+ <div class="media-wrapper">
+ <details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{- end -}}>
+ <summary>
+ <div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
+ <span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
+ <i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
+ <i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
+ </span>
+ {{- if eq .Type "video" }}
+ {{- include "videoPreview" $media | indent 4 }}
+ {{- else if eq .Type "image" }}
+ {{- include "imagePreview" $media | indent 4 }}
+ {{- end }}
+ </summary>
+ {{- if eq .Type "video" }}
+ <video
+ class="plyr-video photoswipe-slide"
+ controls
+ data-pswp-index="{{- $index -}}"
+ data-pswp-width="{{- $media.Meta.Original.Width -}}px"
+ data-pswp-height="{{- $media.Meta.Original.Height -}}px"
+ {{- if .Description }}
+ alt="{{- $media.Description -}}"
+ title="{{- $media.Description -}}"
+ {{- end }}
+ >
+ <source type="video/mp4" src="{{- $media.URL -}}"/>
+ </video>
+ {{- else if eq .Type "image" }}
+ <a
+ class="photoswipe-slide"
+ href="{{- $media.URL -}}"
+ target="_blank"
+ data-pswp-width="{{- $media.Meta.Original.Width -}}px"
+ data-pswp-height="{{- $media.Meta.Original.Height -}}px"
+ data-cropped="true"
+ {{- if .Description }}
+ alt="{{- $media.Description -}}"
+ title="{{- $media.Description -}}"
+ {{- end }}
+ >
+ {{- with $media }}
+ {{- include "imagePreview" . | indent 4 }}
+ {{- end }}
+ </a>
+ {{- else }}
+ <a
+ class="unknown-attachment"
+ href="{{- $media.RemoteURL -}}"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ {{- if .Description }}
+ title="Open external media: {{ $media.Description -}}&#10;&#13;{{- $media.RemoteURL -}}"
+ {{- else }}
+ title="Open external media.&#10;&#13;{{- $media.RemoteURL -}}"
+ {{- end }}
+ >
+ <div class="placeholder" aria-hidden="true">
+ <i class="placeholder-external-link fa fa-external-link"></i>
+ <i class="placeholder-icon fa fa-file-text"></i>
+ <div class="placeholder-link-to">External media</div>
+ </div>
+ </a>
+ {{- end }}
+ </details>
+ </div>
+ {{- end }}
+</div>
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_attributes.tmpl b/web/template/status_attributes.tmpl
new file mode 100644
index 000000000..92659b6b2
--- /dev/null
+++ b/web/template/status_attributes.tmpl
@@ -0,0 +1,55 @@
+{{- /*
+// 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- define "ariaLabel" -}}
+@{{ .Account.Acct -}}, {{ timestamp .CreatedAt -}}
+{{- if .LanguageTag -}}
+ , language {{ .LanguageTag.DisplayStr -}}
+{{- end -}}
+{{- if .MediaAttachments -}}
+ , has media
+{{- end -}}
+{{- if .RepliesCount -}}
+ {{- if eq .RepliesCount 1 -}}
+ , 1 reply
+ {{- else -}}
+ , {{ .RepliesCount }} replies
+ {{- end -}}
+{{- end -}}
+{{- if .FavouritesCount -}}
+ {{- if eq .FavouritesCount 1 -}}
+ , 1 favourite
+ {{- else -}}
+ , {{ .FavouritesCount }} favourites
+ {{- end -}}
+{{- end -}}
+{{- if .ReblogsCount -}}
+ {{- if eq .ReblogsCount 1 -}}
+ , 1 boost
+ {{- else -}}
+ , {{ .ReblogsCount }} boosts
+ {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{- with . }}
+id="{{- .ID -}}{{- if .Pinned -}}-pinned{{- end -}}"
+role="region"
+aria-label="{{- template "ariaLabel" . -}}"
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_header.tmpl b/web/template/status_header.tmpl
new file mode 100644
index 000000000..8946a1030
--- /dev/null
+++ b/web/template/status_header.tmpl
@@ -0,0 +1,56 @@
+{{- /*
+// 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with .Account }}
+<address>
+ {{- if $.Local }}
+ <a
+ href="{{- .URL -}}"
+ rel="author"
+ title="Open profile"
+ >
+ {{- else }}
+ <a
+ href="{{- .URL -}}"
+ rel="author nofollow noreferrer noopener" target="_blank"
+ title="Open remote profile (opens in a new window)"
+ >
+ {{- end }}
+ <img
+ class="avatar"
+ aria-hidden="true"
+ src="{{- .Avatar -}}"
+ alt="Avatar for {{ .Username -}}"
+ title="Avatar for {{ .Username -}}"
+ >
+ <div class="author-strap">
+ <span class="displayname text-cutoff">
+ {{- if .DisplayName -}}
+ {{- emojify .Emojis (escape .DisplayName) -}}
+ {{- else -}}
+ {{- .Username -}}
+ {{- end -}}
+ </span>
+ <span class="sr-only">,</span>
+ <span class="username text-cutoff">@{{- .Acct -}}</span>
+ </div>
+ <span class="sr-only">(open profile)</span>
+ </a>
+</address>
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_info.tmpl b/web/template/status_info.tmpl
new file mode 100644
index 000000000..c5ba3ef35
--- /dev/null
+++ b/web/template/status_info.tmpl
@@ -0,0 +1,74 @@
+{{- /*
+// 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 <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with . }}
+<dl class="status-stats">
+ <div class="stats-grouping">
+ <div class="stats-item published-at text-cutoff">
+ <dt class="sr-only">Published</dt>
+ <dd>
+ <time datetime="{{- .CreatedAt -}}">{{- .CreatedAt | timestampPrecise -}}</time>
+ </dd>
+ </div>
+ <div class="stats-grouping">
+ <div class="stats-item" title="Replies">
+ <dt>
+ <span class="sr-only">Replies</span>
+ <i class="fa fa-reply-all" aria-hidden="true"></i>
+ </dt>
+ <dd>{{- .RepliesCount -}}</dd>
+ </div>
+ <div class="stats-item" title="Faves">
+ <dt>
+ <span class="sr-only">Favourites</span>
+ <i class="fa fa-star" aria-hidden="true"></i>
+ </dt>
+ <dd>{{- .FavouritesCount -}}</dd>
+ </div>
+ <div class="stats-item" title="Boosts">
+ <dt>
+ <span class="sr-only">Reblogs</span>
+ <i class="fa fa-retweet" aria-hidden="true"></i>
+ </dt>
+ <dd>{{- .ReblogsCount -}}</dd>
+ </div>
+ {{- if .Pinned }}
+ <div class="stats-item" title="Pinned">
+ <dt>
+ <span class="sr-only">Pinned</span>
+ <i class="fa fa-thumb-tack" aria-hidden="true"></i>
+ </dt>
+ <dd class="sr-only">{{- .Pinned -}}</dd>
+ </div>
+ {{- else }}
+ {{- end }}
+ </div>
+ </div>
+ {{- if .LanguageTag.DisplayStr }}
+ <div class="stats-item language" title="{{ .LanguageTag.DisplayStr }}">
+ <dt class="sr-only">Language</dt>
+ <dd>
+ <span class="sr-only">{{ .LanguageTag.DisplayStr }}</span>
+ <span aria-hidden="true">{{- .LanguageTag.TagStr -}}</span>
+ </dd>
+ </div>
+ {{- else }}
+ {{- end }}
+</dl>
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_poll.tmpl b/web/template/status_poll.tmpl
index a900f5e74..8cb5dde8f 100644
--- a/web/template/status_poll.tmpl
+++ b/web/template/status_poll.tmpl
@@ -18,51 +18,64 @@
*/ -}}
{{- /*
- Template for rendering a web view of a poll.
- To use this template, pass a web view status into it.
+ Template for rendering a web view of a poll.
+ To use this template, pass a web view status into it.
*/ -}}
- <figure class="poll">
- <figcaption class="poll-info">
- <span class="poll-expiry">
- {{- if .Poll.Multiple -}}
- Multiple-choice poll&nbsp;
- {{- else -}}
- Poll&nbsp;
- {{- end -}}
- {{- if .Poll.Expired -}}
- closed {{ .Poll.ExpiresAt | timestampPrecise -}}
- {{- else if .Poll.ExpiresAt -}}
- open until {{ .Poll.ExpiresAt | timestampPrecise -}}
- {{- else -}}
- open forever
- {{- end -}}
- </span>
- <span class="total-votes">Total votes: {{ .Poll.VotesCount }}</span>
- </figcaption>
- <ul class="poll-options">
- {{- range $index, $pollOption := .WebPollOptions }}
- <li class="poll-option">
- <label aria-hidden="true" for="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (noescape $pollOption.Title) -}}</label>
- <meter aria-hidden="true" id="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" min="0" max="100" value="{{- $pollOption.VoteShare -}}">{{- $pollOption.VoteShare -}}&#37;</meter>
- <div class="sr-only">Option {{ increment $index }}:&nbsp;<span lang="{{ .LanguageTag.TagStr }}">{{ emojify .Emojis (noescape $pollOption.Title) -}}</span></div>
- <div class="poll-vote-summary">
- {{- if isNil $pollOption.VotesCount }}
- Results not yet published.
- {{- else -}}
- {{- with deref $pollOption.VotesCount }}
- <span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}&#37;</span>
- <span class="poll-vote-count">
- {{- if eq . 1 -}}
- {{- . }} vote
- {{- else -}}
- {{- . }} votes
- {{- end -}}
- </span>
- {{- end -}}
- {{- end }}
- </div>
- </li>
- {{- end }}
- </ul>
- </figure>
+{{- define "votes" -}}
+ {{- if eq . 1 -}}
+ {{- . -}}&nbsp;vote
+ {{- else -}}
+ {{- . }}&nbsp;votes
+ {{- end -}}
+{{- end -}}
+
+{{- with . }}
+<figure class="poll">
+ <figcaption class="poll-info">
+ <span class="poll-expiry">
+ {{- if .Poll.Multiple -}}
+ Multiple-choice poll&nbsp;
+ {{- else -}}
+ Poll&nbsp;
+ {{- end -}}
+ {{- if .Poll.Expired -}}
+ closed <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
+ {{- else if .Poll.ExpiresAt -}}
+ open until <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
+ {{- else -}}
+ open forever
+ {{- end -}}
+ </span>
+ <span class="sr-only">,</span>
+ <span class="total-votes">
+ {{- template "votes" .Poll.VotesCount -}}&nbsp;
+ {{- if .Poll.Expired -}}
+ total
+ {{- else -}}
+ so far
+ {{- end -}}
+ </span>
+ </figcaption>
+ <ul class="poll-options nodot">
+ {{- range $index, $pollOption := .WebPollOptions }}
+ <li class="poll-option">
+ <span class="sr-only">Option {{ increment $index }},</span>
+ <span lang="{{- .LanguageTag.TagStr -}}">{{ emojify .Emojis (noescape $pollOption.Title) }}</span>
+ <meter aria-hidden="true" min="0" max="100" value="{{- $pollOption.VoteShare -}}"></meter>
+ <div class="poll-vote-summary">
+ {{- if isNil $pollOption.VotesCount }}
+ Results not yet published.
+ {{- else }}
+ {{- with deref $pollOption.VotesCount }}
+ <span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}&#37;</span>
+ <span class="sr-only">,</span>
+ <span class="poll-vote-count">{{- template "votes" . -}}</span>
+ {{- end }}
+ {{- end }}
+ </div>
+ </li>
+ {{- end }}
+ </ul>
+</figure>
+{{- end }} \ No newline at end of file
diff --git a/web/template/tag.tmpl b/web/template/tag.tmpl
index c84d7a1a4..c1bba0720 100644
--- a/web/template/tag.tmpl
+++ b/web/template/tag.tmpl
@@ -17,11 +17,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
-
-<main class="thread">
- <h2 id="tag-name" tabindex="-1">#{{.tagName}}</h2>
- <p>There's nothing here yet!</p>
+{{- with . }}
+<main>
+ <h2 id="tag-name" tabindex="-1">#{{- .tagName -}}</h2>
+ <p>There's nothing here!</p>
+ <p>
+ For privacy reasons, GoToSocial doesn't (yet) implement public web views of tag timelines.
+ To soften the blow, here's a tongue twister: "I squeeze the soft sloth often in the mothy loft" 🦥
+ </p>
</main>
-
-{{ template "footer.tmpl" .}} \ No newline at end of file
+{{- end }} \ No newline at end of file
diff --git a/web/template/thread.tmpl b/web/template/thread.tmpl
index f2b61019d..2231a5ab8 100644
--- a/web/template/thread.tmpl
+++ b/web/template/thread.tmpl
@@ -17,22 +17,45 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
-{{ template "header.tmpl" .}}
-<main>
- <section data-nosnippet class="thread">
- {{range .context.Ancestors}}
- <article class="toot" id="{{.ID}}">
- {{ template "status.tmpl" .}}
- </article>
- {{end}}
- <article class="toot expanded" id="{{.status.ID}}">
- {{ template "status.tmpl" .status}}
- </article>
- {{range .context.Descendants}}
- <article class="toot" id="{{.ID}}">
- {{ template "status.tmpl" .}}
- </article>
- {{end}}
- </section>
+{{- define "threadLength" -}}
+ {{- with $length := add (len $.context.Ancestors) (len $.context.Descendants) | increment -}}
+ {{- if eq $length 1 -}}
+ {{- $length }} post
+ {{- else -}}
+ {{- $length }} posts
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{- with . }}
+<main data-nosnippet class="thread" aria-labelledby="thread-summary">
+ <div class="col-header">
+ <h2 id="thread-summary">Thread with {{ template "threadLength" . -}}</h2>
+ <a href="#{{- .status.ID -}}">jump to expanded post</a>
+ </div>
+ {{- range .context.Ancestors }}
+ <article
+ class="status"
+ {{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
+ >
+ {{- include "status.tmpl" . | indent 2 }}
+ </article>
+ {{- end }}
+ {{- with .status }}
+ <article
+ class="status expanded"
+ {{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
+ >
+ {{- include "status.tmpl" . | indent 2 }}
+ </article>
+ {{- end }}
+ {{- range .context.Descendants }}
+ <article
+ class="status"
+ {{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
+ >
+ {{- include "status.tmpl" . | indent 2 }}
+ </article>
+ {{- end }}
</main>
-{{ template "footer.tmpl" .}} \ No newline at end of file
+{{- end }} \ No newline at end of file