\n"
-
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- // Create Request and Response
- req, err := http.NewRequest("GET", "/", nil)
- assertions.AssertNoError(t, err, "Error creating request")
- rr := httptest.NewRecorder()
- router := main.SingleUserRouter(&repo)
- router.ServeHTTP(rr, req)
-
- // Check if title is in the response body
- assertions.AssertNotContains(t, rr.Body.String(), "Articles September 2019")
-}
-
-func TestSingleUserUserPostListHandler(t *testing.T) {
- repo, user := getSingleUserTestRepo()
- user.CreateNewPost(owl.PostMeta{
- Title: "post-1",
- Type: "article",
- }, "hi")
- user.CreateNewPost(owl.PostMeta{
- Title: "post-2",
- Type: "note",
- }, "hi")
- list := owl.PostList{
- Title: "list-1",
- Id: "list-1",
- Include: []string{"article"},
- }
- user.AddPostList(list)
-
- // Create Request and Response
- req, err := http.NewRequest("GET", user.ListUrl(list), nil)
- assertions.AssertNoError(t, err, "Error creating request")
- rr := httptest.NewRecorder()
- router := main.SingleUserRouter(&repo)
- router.ServeHTTP(rr, req)
-
- assertions.AssertStatus(t, rr, http.StatusOK)
-
- // Check the response body contains names of users
- assertions.AssertContains(t, rr.Body.String(), "post-1")
- assertions.AssertNotContains(t, rr.Body.String(), "post-2")
-}
diff --git a/cmd/owl/web/webmention_test.go b/cmd/owl/web/webmention_test.go
deleted file mode 100644
index 84faf3e..0000000
--- a/cmd/owl/web/webmention_test.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package web_test
-
-import (
- "h4kor/owl-blogs"
- main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/test/assertions"
- "net/http"
- "net/http/httptest"
- "net/url"
- "os"
- "strconv"
- "strings"
- "testing"
-)
-
-func setupWebmentionTest(repo owl.Repository, user owl.User, target string, source string) (*httptest.ResponseRecorder, error) {
-
- data := url.Values{}
- data.Set("target", target)
- data.Set("source", source)
-
- // Create Request and Response
- req, err := http.NewRequest("POST", user.UrlPath()+"webmention/", strings.NewReader(data.Encode()))
- req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
-
- if err != nil {
- return nil, err
- }
-
- rr := httptest.NewRecorder()
- router := main.Router(&repo)
- router.ServeHTTP(rr, req)
-
- return rr, nil
-}
-
-func TestWebmentionHandleAccepts(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := post.FullUrl()
- source := "https://example.com"
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusAccepted)
-
-}
-
-func TestWebmentionWrittenToPost(t *testing.T) {
-
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := post.FullUrl()
- source := "https://example.com"
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusAccepted)
- assertions.AssertLen(t, post.IncomingWebmentions(), 1)
-}
-
-//
-// https://www.w3.org/TR/webmention/#h-request-verification
-//
-
-// The receiver MUST check that source and target are valid URLs [URL]
-// and are of schemes that are supported by the receiver.
-// (Most commonly this means checking that the source and target schemes are http or https).
-func TestWebmentionSourceValidation(t *testing.T) {
-
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := post.FullUrl()
- source := "ftp://example.com"
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusBadRequest)
-}
-
-func TestWebmentionTargetValidation(t *testing.T) {
-
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := "ftp://example.com"
- source := post.FullUrl()
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusBadRequest)
-}
-
-// The receiver MUST reject the request if the source URL is the same as the target URL.
-
-func TestWebmentionSameTargetAndSource(t *testing.T) {
-
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := post.FullUrl()
- source := post.FullUrl()
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusBadRequest)
-}
-
-// The receiver SHOULD check that target is a valid resource for which it can accept Webmentions.
-// This check SHOULD happen synchronously to reject invalid Webmentions before more in-depth verification begins.
-// What a "valid resource" means is up to the receiver.
-func TestValidationOfTarget(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- target := post.FullUrl()
- target = target[:len(target)-1] + "invalid"
- source := post.FullUrl()
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusBadRequest)
-}
-
-func TestAcceptWebmentionForAlias(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("test-1")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "post-1"}, "")
-
- content := "---\n"
- content += "title: Test\n"
- content += "aliases: \n"
- content += " - /foo/bar\n"
- content += " - /foo/baz\n"
- content += "---\n"
- content += "This is a test"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- target := "https://example.com/foo/bar"
- source := "https://example.com"
-
- rr, err := setupWebmentionTest(repo, user, target, source)
- assertions.AssertNoError(t, err, "Error setting up webmention test")
-
- assertions.AssertStatus(t, rr, http.StatusAccepted)
-}
diff --git a/cmd/owl/webmention.go b/cmd/owl/webmention.go
deleted file mode 100644
index 5c2dba1..0000000
--- a/cmd/owl/webmention.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package main
-
-import (
- "h4kor/owl-blogs"
- "sync"
-
- "github.com/spf13/cobra"
-)
-
-var postId string
-
-func init() {
- rootCmd.AddCommand(webmentionCmd)
- webmentionCmd.Flags().StringVar(
- &postId, "post", "",
- "specify the post to send webmentions for. Otherwise, all posts will be checked.",
- )
-}
-
-var webmentionCmd = &cobra.Command{
- Use: "webmention",
- Short: "Send webmentions for posts, optionally for a specific user",
- Long: `Send webmentions for posts, optionally for a specific user`,
- Run: func(cmd *cobra.Command, args []string) {
- repo, err := owl.OpenRepository(repoPath)
- if err != nil {
- println("Error opening repository: ", err.Error())
- return
- }
-
- var users []owl.User
- if user == "" {
- // send webmentions for all users
- users, err = repo.Users()
- if err != nil {
- println("Error getting users: ", err.Error())
- return
- }
- } else {
- // send webmentions for a specific user
- user, err := repo.GetUser(user)
- users = append(users, user)
- if err != nil {
- println("Error getting user: ", err.Error())
- return
- }
- }
-
- processPost := func(user owl.User, post owl.Post) error {
- println("Webmentions for post: ", post.Title())
-
- err := post.ScanForLinks()
- if err != nil {
- println("Error scanning post for links: ", err.Error())
- return err
- }
-
- webmentions := post.OutgoingWebmentions()
- println("Found ", len(webmentions), " links")
- wg := sync.WaitGroup{}
- wg.Add(len(webmentions))
- for _, webmention := range webmentions {
- go func(webmention owl.WebmentionOut) {
- defer wg.Done()
- sendErr := post.SendWebmention(webmention)
- if sendErr != nil {
- println("Error sending webmentions: ", sendErr.Error())
- } else {
- println("Webmention sent to ", webmention.Target)
- }
- }(webmention)
- }
- wg.Wait()
- return nil
- }
-
- for _, user := range users {
- if postId != "" {
- // send webmentions for a specific post
- post, err := user.GetPost(postId)
- if err != nil {
- println("Error getting post: ", err.Error())
- return
- }
- processPost(user, post)
- return
- }
-
- posts, err := user.PublishedPosts()
- if err != nil {
- println("Error getting posts: ", err.Error())
- }
-
- for _, post := range posts {
- processPost(user, post)
- }
- }
- },
-}
diff --git a/directories.go b/directories.go
deleted file mode 100644
index 0e377aa..0000000
--- a/directories.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package owl
-
-import (
- "os"
- "strings"
-)
-
-func dirExists(path string) bool {
- _, err := os.Stat(path)
- return err == nil
-}
-
-// lists all files/dirs in a directory, not recursive
-func listDir(path string) []string {
- dir, _ := os.Open(path)
- defer dir.Close()
- files, _ := dir.Readdirnames(-1)
- return files
-}
-
-func fileExists(path string) bool {
- _, err := os.Stat(path)
- return err == nil
-}
-
-func toDirectoryName(name string) string {
- name = strings.ToLower(strings.ReplaceAll(name, " ", "-"))
- // remove all non-alphanumeric characters
- name = strings.Map(func(r rune) rune {
- if r >= 'a' && r <= 'z' {
- return r
- }
- if r >= 'A' && r <= 'Z' {
- return r
- }
- if r >= '0' && r <= '9' {
- return r
- }
- if r == '-' {
- return r
- }
- return -1
- }, name)
- return name
-}
diff --git a/embed.go b/embed.go
deleted file mode 100644
index b01283e..0000000
--- a/embed.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package owl
-
-import "embed"
-
-//go:embed embed/*
-var embed_files embed.FS
diff --git a/embed/article/detail.html b/embed/article/detail.html
deleted file mode 100644
index 63e8349..0000000
--- a/embed/article/detail.html
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.Bookmark.Url }}
-
- Bookmark:
- {{ if .Post.Meta.Bookmark.Text }}
- {{.Post.Meta.Bookmark.Text}}
- {{ else }}
- {{.Post.Meta.Bookmark.Url}}
- {{ end }}
-
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/auth.html b/embed/auth.html
deleted file mode 100644
index 1e4eec1..0000000
--- a/embed/auth.html
+++ /dev/null
@@ -1,24 +0,0 @@
-Authorization for {{.ClientId}}
-
-Requesting scope:
-
- {{range $index, $element := .Scopes}}
- - {{$element}}
- {{end}}
-
-
-
-
-
\ No newline at end of file
diff --git a/embed/bookmark/detail.html b/embed/bookmark/detail.html
deleted file mode 100644
index ece34bc..0000000
--- a/embed/bookmark/detail.html
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.Reply.Url }}
-
- In reply to:
- {{ if .Post.Meta.Reply.Text }}
- {{.Post.Meta.Reply.Text}}
- {{ else }}
- {{.Post.Meta.Reply.Url}}
- {{ end }}
-
-
- {{ end }}
-
- {{ if .Post.Meta.Bookmark.Url }}
-
- Bookmark:
- {{ if .Post.Meta.Bookmark.Text }}
- {{.Post.Meta.Bookmark.Text}}
- {{ else }}
- {{.Post.Meta.Bookmark.Url}}
- {{ end }}
-
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/editor/editor.html b/embed/editor/editor.html
deleted file mode 100644
index 7e23604..0000000
--- a/embed/editor/editor.html
+++ /dev/null
@@ -1,127 +0,0 @@
-
- Write Article/Page
-
-
-
-
- Upload Photo
-
-
-
-
- Write Recipe
-
-
-
-
- Write Note
-
-
-
-
- Write Reply
-
-
-
-
- Bookmark
-
-
\ No newline at end of file
diff --git a/embed/editor/login.html b/embed/editor/login.html
deleted file mode 100644
index f3e7dc4..0000000
--- a/embed/editor/login.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{{ if eq .Error "wrong_password" }}
-
- Wrong Password
-
-{{ end }}
-
-
-
\ No newline at end of file
diff --git a/embed/error.html b/embed/error.html
deleted file mode 100644
index e3000ab..0000000
--- a/embed/error.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
- {{ .Error }}
- {{ .Message }}
-
\ No newline at end of file
diff --git a/embed/initial/base.html b/embed/initial/base.html
deleted file mode 100644
index d9993ad..0000000
--- a/embed/initial/base.html
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
-
-
-
-
- {{ .Title }} - {{ .User.Config.Title }}
-
- {{ if .User.FaviconUrl }}
-
- {{ else }}
-
- {{ end }}
-
-
- {{ if .Description }}
-
-
- {{ end }}
- {{ if .Type }}
-
- {{ end }}
- {{ if .SelfUrl }}
-
- {{ end }}
-
-
-
- {{ if .User.AuthUrl }}
-
-
-
-
- {{ end }}
-
-
-
-
-
- {{ .Content }}
-
-
-
-
-
diff --git a/embed/initial/header.html b/embed/initial/header.html
deleted file mode 100644
index dc71910..0000000
--- a/embed/initial/header.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
- {{ range .UserLinks }}
- - {{.Text}}
- {{ end }}
-
\ No newline at end of file
diff --git a/embed/initial/repo/base.html b/embed/initial/repo/base.html
deleted file mode 100644
index 4afaa2b..0000000
--- a/embed/initial/repo/base.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
- {{ .Title }}
-
-
-
-
- {{ .Content }}
-
-
-
\ No newline at end of file
diff --git a/embed/initial/static/pico.min.css b/embed/initial/static/pico.min.css
deleted file mode 100644
index a4fbbd8..0000000
--- a/embed/initial/static/pico.min.css
+++ /dev/null
@@ -1,5 +0,0 @@
-@charset "UTF-8";/*!
- * Pico.css v1.5.3 (https://picocss.com)
- * Copyright 2019-2022 - Licensed under MIT
- */:root{--font-family:system-ui,-apple-system,"Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--line-height:1.5;--font-weight:400;--font-size:16px;--border-radius:0.25rem;--border-width:1px;--outline-width:3px;--spacing:1rem;--typography-spacing-vertical:1.5rem;--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing);--grid-spacing-vertical:0;--grid-spacing-horizontal:var(--spacing);--form-element-spacing-vertical:0.75rem;--form-element-spacing-horizontal:1rem;--nav-element-spacing-vertical:1rem;--nav-element-spacing-horizontal:0.5rem;--nav-link-spacing-vertical:0.5rem;--nav-link-spacing-horizontal:0.5rem;--form-label-font-weight:var(--font-weight);--transition:0.2s ease-in-out}@media (min-width:576px){:root{--font-size:17px}}@media (min-width:768px){:root{--font-size:18px}}@media (min-width:992px){:root{--font-size:19px}}@media (min-width:1200px){:root{--font-size:20px}}@media (min-width:576px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 2.5)}}@media (min-width:768px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3)}}@media (min-width:992px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3.5)}}@media (min-width:1200px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 4)}}@media (min-width:576px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}@media (min-width:992px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.75)}}@media (min-width:1200px){article{--block-spacing-horizontal:calc(var(--spacing) * 2)}}dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing)}@media (min-width:576px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2.5);--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 3);--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}a{--text-decoration:none}a.contrast,a.secondary{--text-decoration:underline}small{--font-size:0.875em}h1,h2,h3,h4,h5,h6{--font-weight:700}h1{--font-size:2rem;--typography-spacing-vertical:3rem}h2{--font-size:1.75rem;--typography-spacing-vertical:2.625rem}h3{--font-size:1.5rem;--typography-spacing-vertical:2.25rem}h4{--font-size:1.25rem;--typography-spacing-vertical:1.874rem}h5{--font-size:1.125rem;--typography-spacing-vertical:1.6875rem}[type=checkbox],[type=radio]{--border-width:2px}[type=checkbox][role=switch]{--border-width:3px}tfoot td,tfoot th,thead td,thead th{--border-width:3px}:not(thead):not(tfoot)>*>td{--font-size:0.875em}code,kbd,pre,samp{--font-family:"Menlo","Consolas","Roboto Mono","Ubuntu Monospace","Noto Mono","Oxygen Mono","Liberation Mono",monospace,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}kbd{--font-weight:bolder}:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--background-color:#fff;--color:hsl(205deg, 20%, 32%);--h1-color:hsl(205deg, 30%, 15%);--h2-color:#24333e;--h3-color:hsl(205deg, 25%, 23%);--h4-color:#374956;--h5-color:hsl(205deg, 20%, 32%);--h6-color:#4d606d;--muted-color:hsl(205deg, 10%, 50%);--muted-border-color:hsl(205deg, 20%, 94%);--primary:hsl(195deg, 85%, 41%);--primary-hover:hsl(195deg, 90%, 32%);--primary-focus:rgba(16, 149, 193, 0.125);--primary-inverse:#fff;--secondary:hsl(205deg, 15%, 41%);--secondary-hover:hsl(205deg, 20%, 32%);--secondary-focus:rgba(89, 107, 120, 0.125);--secondary-inverse:#fff;--contrast:hsl(205deg, 30%, 15%);--contrast-hover:#000;--contrast-focus:rgba(89, 107, 120, 0.125);--contrast-inverse:#fff;--mark-background-color:#fff2ca;--mark-color:#543a26;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:transparent;--form-element-border-color:hsl(205deg, 14%, 68%);--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:transparent;--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205deg, 18%, 86%);--form-element-disabled-border-color:hsl(205deg, 14%, 68%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#c62828;--form-element-invalid-active-border-color:#d32f2f;--form-element-invalid-focus-color:rgba(211, 47, 47, 0.125);--form-element-valid-border-color:#388e3c;--form-element-valid-active-border-color:#43a047;--form-element-valid-focus-color:rgba(67, 160, 71, 0.125);--switch-background-color:hsl(205deg, 16%, 77%);--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:hsl(205deg, 18%, 86%);--range-active-border-color:hsl(205deg, 16%, 77%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:#f6f8f9;--code-background-color:hsl(205deg, 20%, 94%);--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330deg, 40%, 50%);--code-property-color:hsl(185deg, 40%, 40%);--code-value-color:hsl(40deg, 20%, 50%);--code-comment-color:hsl(205deg, 14%, 68%);--accordion-border-color:var(--muted-border-color);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:var(--background-color);--card-border-color:var(--muted-border-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698),0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024),0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03),0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036),0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302),0.5rem 1rem 6rem rgba(27, 40, 50, 0.06),0 0 0 0.0625rem rgba(27, 40, 50, 0.015);--card-sectionning-background-color:#fbfbfc;--dropdown-background-color:#fbfbfc;--dropdown-border-color:#e1e6eb;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:hsl(205deg, 20%, 94%);--modal-overlay-background-color:rgba(213, 220, 226, 0.8);--progress-background-color:hsl(205deg, 18%, 86%);--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(65, 84, 98, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(255, 255, 255, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(255, 255, 255, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(115, 130, 140, 0.999)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(65, 84, 98, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(198, 40, 40, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(65, 84, 98, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(65, 84, 98, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(56, 142, 60, 0.999)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E")}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme=light]){color-scheme:dark;--background-color:#11191f;--color:hsl(205deg, 16%, 77%);--h1-color:hsl(205deg, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205deg, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205deg, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205deg, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195deg, 85%, 41%);--primary-hover:hsl(195deg, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205deg, 15%, 41%);--secondary-hover:hsl(205deg, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205deg, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205deg, 25%, 23%);--form-element-disabled-border-color:hsl(205deg, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205deg, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330deg, 30%, 50%);--code-property-color:hsl(185deg, 30%, 50%);--code-value-color:hsl(40deg, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205deg, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.9);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(255, 255, 255, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(0, 0, 0, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(115, 130, 140, 0.999)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(183, 28, 28, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(46, 125, 50, 0.999)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E")}}[data-theme=dark]{color-scheme:dark;--background-color:#11191f;--color:hsl(205deg, 16%, 77%);--h1-color:hsl(205deg, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205deg, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205deg, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205deg, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195deg, 85%, 41%);--primary-hover:hsl(195deg, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205deg, 15%, 41%);--secondary-hover:hsl(205deg, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205deg, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205deg, 25%, 23%);--form-element-disabled-border-color:hsl(205deg, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205deg, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330deg, 30%, 50%);--code-property-color:hsl(185deg, 30%, 50%);--code-value-color:hsl(40deg, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205deg, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.9);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(255, 255, 255, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(0, 0, 0, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(115, 130, 140, 0.999)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(183, 28, 28, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(162, 175, 185, 0.999)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgba(46, 125, 50, 0.999)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E")}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;text-rendering:optimizeLegibility;background-color:var(--background-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);line-height:var(--line-height);font-family:var(--font-family);overflow-wrap:break-word;cursor:default;-moz-tab-size:4;-o-tab-size:4;tab-size:4}main{display:block}body{width:100%;margin:0}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--block-spacing-vertical) 0}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--spacing);padding-left:var(--spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:992px){.container{max-width:920px}}@media (min-width:1200px){.container{max-width:1130px}}section{margin-bottom:var(--block-spacing-vertical)}.grid{grid-column-gap:var(--grid-spacing-horizontal);grid-row-gap:var(--grid-spacing-vertical);display:grid;grid-template-columns:1fr;margin:0}@media (min-width:992px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}figure{display:block;margin:0;padding:0;overflow-x:auto}figure figcaption{padding:calc(var(--spacing) * .5) 0;color:var(--muted-color)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,figure,form,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-style:normal;font-weight:var(--font-weight);font-size:var(--font-size)}[role=link],a{--color:var(--primary);--background-color:transparent;outline:0;background-color:var(--background-color);color:var(--color);-webkit-text-decoration:var(--text-decoration);text-decoration:var(--text-decoration);transition:background-color var(--transition),color var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition)}[role=link]:is([aria-current],:hover,:active,:focus),a:is([aria-current],:hover,:active,:focus){--color:var(--primary-hover);--text-decoration:underline}[role=link]:focus,a:focus{--background-color:var(--primary-focus)}[role=link].secondary,a.secondary{--color:var(--secondary)}[role=link].secondary:is([aria-current],:hover,:active,:focus),a.secondary:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}[role=link].secondary:focus,a.secondary:focus{--background-color:var(--secondary-focus)}[role=link].contrast,a.contrast{--color:var(--contrast)}[role=link].contrast:is([aria-current],:hover,:active,:focus),a.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}[role=link].contrast:focus,a.contrast:focus{--background-color:var(--contrast-focus)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);font-family:var(--font-family)}h1{--color:var(--h1-color)}h2{--color:var(--h2-color)}h3{--color:var(--h3-color)}h4{--color:var(--h4-color)}h5{--color:var(--h5-color)}h6{--color:var(--h6-color)}:where(address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--typography-spacing-vertical)}.headings,hgroup{margin-bottom:var(--typography-spacing-vertical)}.headings>*,hgroup>*{margin-bottom:0}.headings>:last-child,hgroup>:last-child{--color:var(--muted-color);--font-weight:unset;font-size:1rem;font-family:unset}p{margin-bottom:var(--typography-spacing-vertical)}small{font-size:var(--font-size)}:where(dl,ol,ul){padding-right:0;padding-left:var(--spacing);-webkit-padding-start:var(--spacing);padding-inline-start:var(--spacing);-webkit-padding-end:0;padding-inline-end:0}:where(dl,ol,ul) li{margin-bottom:calc(var(--typography-spacing-vertical) * .25)}:where(dl,ol,ul) :is(dl,ol,ul){margin:0;margin-top:calc(var(--typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--mark-background-color);color:var(--mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--typography-spacing-vertical) 0;padding:var(--spacing);border-right:none;border-left:.25rem solid var(--blockquote-border-color);-webkit-border-start:0.25rem solid var(--blockquote-border-color);border-inline-start:0.25rem solid var(--blockquote-border-color);-webkit-border-end:none;border-inline-end:none}blockquote footer{margin-top:calc(var(--typography-spacing-vertical) * .5);color:var(--blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--ins-color);text-decoration:none}del{color:var(--del-color)}::-moz-selection{background-color:var(--primary-focus)}::selection{background-color:var(--primary-focus)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}button{display:block;width:100%;margin-bottom:var(--spacing)}[role=button]{display:inline-block;text-decoration:none}[role=button],button,input[type=button],input[type=reset],input[type=submit]{--background-color:var(--primary);--border-color:var(--primary);--color:var(--primary-inverse);--box-shadow:var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[role=button]:is([aria-current],:hover,:active,:focus),button:is([aria-current],:hover,:active,:focus),input[type=button]:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus),input[type=submit]:is([aria-current],:hover,:active,:focus){--background-color:var(--primary-hover);--border-color:var(--primary-hover);--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--color:var(--primary-inverse)}[role=button]:focus,button:focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--primary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).secondary,input[type=reset]{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);cursor:pointer}:is(button,input[type=submit],input[type=button],[role=button]).secondary:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover);--color:var(--secondary-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).secondary:focus,input[type=reset]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--secondary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).contrast{--background-color:var(--contrast);--border-color:var(--contrast);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:is([aria-current],:hover,:active,:focus){--background-color:var(--contrast-hover);--border-color:var(--contrast-hover);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--contrast-focus)}:is(button,input[type=submit],input[type=button],[role=button]).outline,input[type=reset].outline{--background-color:transparent;--color:var(--primary)}:is(button,input[type=submit],input[type=button],[role=button]).outline:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--background-color:transparent;--color:var(--primary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary,input[type=reset].outline{--color:var(--secondary)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast{--color:var(--contrast)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}:where(button,[type=submit],[type=button],[type=reset],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]),a[role=button]:not([href]){opacity:.5;pointer-events:none}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox]):not([type=radio]):not([type=range]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2)}fieldset{margin:0;margin-bottom:var(--spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--spacing) * .25);font-weight:var(--form-label-font-weight,var(--font-weight))}input:not([type=checkbox]):not([type=radio]),select,textarea{width:100%}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal)}input,select,textarea{--background-color:var(--form-element-background-color);--border-color:var(--form-element-border-color);--color:var(--form-element-color);--box-shadow:none;border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}:where(select,textarea):is(:active,:focus),input:not([type=submit]):not([type=button]):not([type=reset]):not([type=checkbox]):not([type=radio]):not([readonly]):is(:active,:focus){--background-color:var(--form-element-active-background-color)}:where(select,textarea):is(:active,:focus),input:not([type=submit]):not([type=button]):not([type=reset]):not([role=switch]):not([readonly]):is(:active,:focus){--border-color:var(--form-element-active-border-color)}input:not([type=submit]):not([type=button]):not([type=reset]):not([type=range]):not([type=file]):not([readonly]):focus,select:focus,textarea:focus{--box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit]):not([type=button]):not([type=reset]),select,textarea),input:not([type=submit]):not([type=button]):not([type=reset])[disabled],select[disabled],textarea[disabled]{--background-color:var(--form-element-disabled-background-color);--border-color:var(--form-element-disabled-border-color);opacity:var(--form-element-disabled-opacity);pointer-events:none}:where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid]{padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--form-element-spacing-horizontal);-webkit-padding-start:var(--form-element-spacing-horizontal)!important;padding-inline-start:var(--form-element-spacing-horizontal)!important;-webkit-padding-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid=false]{background-image:var(--icon-valid)}:where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid=true]{background-image:var(--icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--border-color:var(--form-element-valid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--border-color:var(--form-element-invalid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid=false],[dir=rtl] :where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid=true],[dir=rtl] :where(input,select,textarea):not([type=checkbox]):not([type=radio])[aria-invalid]{background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--form-element-placeholder-color);opacity:1}input:not([type=checkbox]):not([type=radio]),select,textarea{margin-bottom:var(--spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple]):not([size]){padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-left:var(--form-element-spacing-horizontal);-webkit-padding-start:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal);-webkit-padding-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);background-image:var(--icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}[dir=rtl] select:not([multiple]):not([size]){background-position:center left .75rem}:where(input,select,textarea)+small{display:block;width:100%;margin-top:calc(var(--spacing) * -.75);margin-bottom:var(--spacing);color:var(--muted-color)}label>:where(input,select,textarea){margin-top:calc(var(--spacing) * .25)}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-right:.375em;margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:.375em;margin-inline-end:.375em;border-width:var(--border-width);font-size:inherit;vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-right:.375em;margin-bottom:0;cursor:pointer}[type=checkbox]:indeterminate{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color);--color:var(--switch-color);width:2.25em;height:1.25em;border:var(--border-width) solid var(--border-color);border-radius:1.25em;background-color:var(--background-color);line-height:1.25em}[type=checkbox][role=switch]:focus{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color)}[type=checkbox][role=switch]:checked{--background-color:var(--switch-checked-background-color);--border-color:var(--switch-checked-background-color)}[type=checkbox][role=switch]:before{display:block;width:calc(1.25em - (var(--border-width) * 2));height:100%;border-radius:50%;background-color:var(--color);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:checked{background-image:none}[type=checkbox][role=switch]:checked::before{margin-left:calc(1.125em - var(--border-width));-webkit-margin-start:calc(1.125em - var(--border-width));margin-inline-start:calc(1.125em - var(--border-width))}[type=checkbox]:checked[aria-invalid=false],[type=checkbox][aria-invalid=false],[type=checkbox][role=switch]:checked[aria-invalid=false],[type=checkbox][role=switch][aria-invalid=false],[type=radio]:checked[aria-invalid=false],[type=radio][aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}[type=checkbox]:checked[aria-invalid=true],[type=checkbox][aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=checkbox][role=switch][aria-invalid=true],[type=radio]:checked[aria-invalid=true],[type=radio][aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=date],input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=datetime-local],input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=month],input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=time],input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=week]{--icon-position:0.75rem;--icon-width:1rem;padding-right:calc(var(--icon-width) + var(--icon-position));background-image:var(--icon-date);background-position:center right var(--icon-position);background-size:var(--icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=time]{background-image:var(--icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--icon-width);margin-right:calc(var(--icon-width) * -1);margin-left:var(--icon-position);opacity:0}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--color:var(--muted-color);padding:calc(var(--form-element-spacing-vertical) * .5) 0;border:0;border-radius:0;background:0 0}[type=file]::-webkit-file-upload-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing)/ 2);margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-webkit-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::file-selector-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing)/ 2);margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-webkit-file-upload-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::file-selector-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-webkit-file-upload-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing)/ 2);margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-webkit-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-webkit-file-upload-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-ms-browse{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-ms-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-ms-browse:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-webkit-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-moz-range-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-moz-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-ms-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-ms-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-moz-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-ms-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]:focus,[type=range]:hover{--range-border-color:var(--range-active-border-color);--range-thumb-color:var(--range-thumb-hover-color)}[type=range]:active{--range-thumb-color:var(--range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search]{-webkit-padding-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);border-radius:5rem;background-image:var(--icon-search);background-position:center left 1.125rem;background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search][aria-invalid]{-webkit-padding-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search][aria-invalid=false]{background-image:var(--icon-search),var(--icon-valid)}input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search][aria-invalid=true]{background-image:var(--icon-search),var(--icon-invalid)}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;display:none}[dir=rtl] :where(input):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--spacing)/ 2) var(--spacing);border-bottom:var(--border-width) solid var(--table-border-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--border-width) solid var(--table-border-color);border-bottom:0}table[role=grid] tbody tr:nth-child(odd){background-color:var(--table-row-stripped-background-color)}code,kbd,pre,samp{font-size:.875em;font-family:var(--font-family)}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--border-radius);background:var(--code-background-color);color:var(--code-color);font-weight:var(--font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem .5rem}pre{display:block;margin-bottom:var(--spacing);overflow-x:auto}pre>code{display:block;padding:var(--spacing);background:0 0;font-size:14px;line-height:var(--line-height)}code b{color:var(--code-tag-color);font-weight:var(--font-weight)}code i{color:var(--code-property-color);font-style:normal}code u{color:var(--code-value-color);text-decoration:none}code em{color:var(--code-comment-color);font-style:normal}kbd{background-color:var(--code-kbd-background-color);color:var(--code-kbd-color);vertical-align:baseline}hr{height:0;border:0;border-top:1px solid var(--muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}details{display:block;margin-bottom:var(--spacing);padding-bottom:var(--spacing);border-bottom:var(--border-width) solid var(--accordion-border-color)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--transition)}details summary:not([role]){color:var(--accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;-webkit-margin-start:calc(var(--spacing,1rem) * 0.5);margin-inline-start:calc(var(--spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--transition)}details summary:focus{outline:0}details summary:focus:not([role=button]){color:var(--accordion-active-summary-color)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--line-height,1.5));background-image:var(--icon-chevron-button)}details summary[role=button]:not(.outline).contrast::after{background-image:var(--icon-chevron-button-inverse)}details[open]>summary{margin-bottom:calc(var(--spacing))}details[open]>summary:not([role]):not(:focus){color:var(--accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin:var(--block-spacing-vertical) 0;padding:var(--block-spacing-vertical) var(--block-spacing-horizontal);border-radius:var(--border-radius);background:var(--card-background-color);box-shadow:var(--card-box-shadow)}article>footer,article>header{margin-right:calc(var(--block-spacing-horizontal) * -1);margin-left:calc(var(--block-spacing-horizontal) * -1);padding:calc(var(--block-spacing-vertical) * .66) var(--block-spacing-horizontal);background-color:var(--card-sectionning-background-color)}article>header{margin-top:calc(var(--block-spacing-vertical) * -1);margin-bottom:var(--block-spacing-vertical);border-bottom:var(--border-width) solid var(--card-border-color);border-top-right-radius:var(--border-radius);border-top-left-radius:var(--border-radius)}article>footer{margin-top:var(--block-spacing-vertical);margin-bottom:calc(var(--block-spacing-vertical) * -1);border-top:var(--border-width) solid var(--card-border-color);border-bottom-right-radius:var(--border-radius);border-bottom-left-radius:var(--border-radius)}:root{--scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:var(--spacing);border:0;background-color:var(--modal-overlay-background-color);color:var(--color)}dialog article{max-height:calc(100vh - var(--spacing) * 2);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>footer,dialog article>header{padding:calc(var(--block-spacing-vertical) * .5) var(--block-spacing-horizontal)}dialog article>header .close{margin:0;margin-left:var(--spacing);float:right}dialog article>footer{text-align:right}dialog article>footer [role=button]{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type){margin-left:calc(var(--spacing) * .5)}dialog article p:last-of-type{margin:0}dialog article .close{display:block;width:1rem;height:1rem;margin-top:calc(var(--block-spacing-vertical) * -.5);margin-bottom:var(--typography-spacing-vertical);margin-left:auto;background-image:var(--icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;opacity:.5;transition:opacity var(--transition)}dialog article .close:is([aria-current],:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--scrollbar-width,0);overflow:hidden;pointer-events:none}.modal-is-open dialog{pointer-events:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-name:fadeIn;animation-name:fadeIn}:where(.modal-is-opening,.modal-is-closing) dialog>article{-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-name:slideInDown;animation-name:slideInDown}.modal-is-closing dialog,.modal-is-closing dialog>article{-webkit-animation-delay:0s;animation-delay:0s;animation-direction:reverse}@-webkit-keyframes fadeIn{from{background-color:transparent}to{background-color:var(--modal-overlay-background-color)}}@keyframes fadeIn{from{background-color:transparent}to{background-color:var(--modal-overlay-background-color)}}@-webkit-keyframes slideInDown{from{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideInDown{from{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--nav-element-spacing-vertical) var(--nav-element-spacing-horizontal)}nav li>*{--spacing:0}nav :where(a,[role=link]){display:inline-block;margin:calc(var(--nav-link-spacing-vertical) * -1) calc(var(--nav-link-spacing-horizontal) * -1);padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);border-radius:var(--border-radius);text-decoration:none}nav :where(a,[role=link]):is([aria-current],:hover,:active,:focus){text-decoration:none}nav [role=button]{margin-right:inherit;margin-left:inherit;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--nav-element-spacing-vertical) * .5) var(--nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--spacing) * .5);overflow:hidden;border:0;border-radius:var(--border-radius);background-color:var(--progress-background-color);color:var(--progress-color)}progress::-webkit-progress-bar{border-radius:var(--border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--progress-color)}progress::-moz-progress-bar{background-color:var(--progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--progress-background-color) linear-gradient(to right,var(--progress-color) 30%,var(--progress-background-color) 30%) top left/150% 150% no-repeat;-webkit-animation:progressIndeterminate 1s linear infinite;animation:progressIndeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@-webkit-keyframes progressIndeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}@keyframes progressIndeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}details[role=list],li[role=list]{position:relative}details[role=list] summary+ul,li[role=list]>ul{display:flex;z-index:99;position:absolute;top:auto;right:0;left:0;flex-direction:column;margin:0;padding:0;border:var(--border-width) solid var(--dropdown-border-color);border-radius:var(--border-radius);border-top-right-radius:0;border-top-left-radius:0;background-color:var(--dropdown-background-color);box-shadow:var(--card-box-shadow);color:var(--dropdown-color);white-space:nowrap}details[role=list] summary+ul li,li[role=list]>ul li{width:100%;margin-bottom:0;padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);list-style:none}details[role=list] summary+ul li:first-of-type,li[role=list]>ul li:first-of-type{margin-top:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li:last-of-type,li[role=list]>ul li:last-of-type{margin-bottom:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li a,li[role=list]>ul li a{display:block;margin:calc(var(--form-element-spacing-vertical) * -.5) calc(var(--form-element-spacing-horizontal) * -1);padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);overflow:hidden;color:var(--dropdown-color);text-decoration:none;text-overflow:ellipsis}details[role=list] summary+ul li a:hover,li[role=list]>ul li a:hover{background-color:var(--dropdown-hover-background-color)}details[role=list] summary::after,li[role=list]>a::after{display:block;width:1rem;height:calc(1rem * var(--line-height,1.5));-webkit-margin-start:0.5rem;margin-inline-start:.5rem;float:right;transform:rotate(0);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}details[role=list]{padding:0;border-bottom:none}details[role=list] summary{margin-bottom:0}details[role=list] summary:not([role]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--form-element-border-color);border-radius:var(--border-radius);background-color:var(--form-element-background-color);color:var(--form-element-placeholder-color);line-height:inherit;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}details[role=list] summary:not([role]):active,details[role=list] summary:not([role]):focus{border-color:var(--form-element-active-border-color);background-color:var(--form-element-active-background-color)}details[role=list] summary:not([role]):focus{box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}details[role=list][open] summary{border-bottom-right-radius:0;border-bottom-left-radius:0}details[role=list][open] summary::before{display:block;z-index:1;position:fixed;top:0;right:0;bottom:0;left:0;background:0 0;content:"";cursor:default}nav details[role=list] summary,nav li[role=list] a{display:flex;direction:ltr}nav details[role=list] summary+ul,nav li[role=list]>ul{min-width:-webkit-fit-content;min-width:-moz-fit-content;min-width:fit-content;border-radius:var(--border-radius)}nav details[role=list] summary+ul li a,nav li[role=list]>ul li a{border-radius:0}nav details[role=list] summary,nav details[role=list] summary:not([role]){height:auto;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}nav details[role=list][open] summary{border-radius:var(--border-radius)}nav details[role=list] summary+ul{margin-top:var(--outline-width);-webkit-margin-start:0;margin-inline-start:0}nav details[role=list] summary[role=link]{margin-bottom:calc(var(--nav-link-spacing-vertical) * -1);line-height:var(--line-height)}nav details[role=list] summary[role=link]+ul{margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));-webkit-margin-start:calc(var(--nav-link-spacing-horizontal) * -1);margin-inline-start:calc(var(--nav-link-spacing-horizontal) * -1)}li[role=list] a:active~ul,li[role=list] a:focus~ul,li[role=list]:hover>ul{display:flex}li[role=list]>ul{display:none;margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));-webkit-margin-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal));margin-inline-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal))}li[role=list]>a::after{background-image:var(--icon-chevron)}[aria-busy=true]{cursor:progress}[aria-busy=true]:not(input):not(select):not(textarea)::before{display:inline-block;width:1em;height:1em;border:.1875em solid currentColor;border-radius:1em;border-right-color:transparent;content:"";vertical-align:text-bottom;vertical-align:-.125em;-webkit-animation:spinner .75s linear infinite;animation:spinner .75s linear infinite;opacity:var(--loading-spinner-opacity)}[aria-busy=true]:not(input):not(select):not(textarea):not(:empty)::before{margin-right:calc(var(--spacing) * .5);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing) * .5);margin-inline-end:calc(var(--spacing) * .5)}[aria-busy=true]:not(input):not(select):not(textarea):empty{text-align:center}a[aria-busy=true],button[aria-busy=true],input[type=button][aria-busy=true],input[type=reset][aria-busy=true],input[type=submit][aria-busy=true]{pointer-events:none}@-webkit-keyframes spinner{to{transform:rotate(360deg)}}@keyframes spinner{to{transform:rotate(360deg)}}[data-tooltip]{position:relative}[data-tooltip]:not(a):not(button):not(input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--border-radius);background:var(--tooltip-background-color);content:attr(data-tooltip);color:var(--tooltip-color);font-style:normal;font-weight:var(--font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--tooltip-background-color)}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-name:slide;animation-name:slide}[data-tooltip]:focus::after,[data-tooltip]:hover::after{-webkit-animation-name:slideCaret;animation-name:slideCaret}}@-webkit-keyframes slide{from{transform:translate(-50%,.75rem);opacity:0}to{transform:translate(-50%,-.25rem);opacity:1}}@keyframes slide{from{transform:translate(-50%,.75rem);opacity:0}to{transform:translate(-50%,-.25rem);opacity:1}}@-webkit-keyframes slideCaret{from{opacity:0}50%{transform:translate(-50%,-.25rem);opacity:0}to{transform:translate(-50%,0);opacity:1}}@keyframes slideCaret{from{opacity:0}50%{transform:translate(-50%,-.25rem);opacity:0}to{transform:translate(-50%,0);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-animation-delay:-1ms!important;animation-delay:-1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}}
-/*# sourceMappingURL=pico.min.css.map */
\ No newline at end of file
diff --git a/embed/note/detail.html b/embed/note/detail.html
deleted file mode 100644
index 0bf38cf..0000000
--- a/embed/note/detail.html
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/page/detail.html b/embed/page/detail.html
deleted file mode 100644
index c2f94b7..0000000
--- a/embed/page/detail.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
- {{.Title}}
-
- #
-
-
-
-
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/photo/detail.html b/embed/photo/detail.html
deleted file mode 100644
index b80e40a..0000000
--- a/embed/photo/detail.html
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.PhotoPath }}
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/post-list-photo.html b/embed/post-list-photo.html
deleted file mode 100644
index 61b8f2c..0000000
--- a/embed/post-list-photo.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
- {{range .}}
-
-
-
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/post-list.html b/embed/post-list.html
deleted file mode 100644
index 1d814be..0000000
--- a/embed/post-list.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
- {{range .}}
-
-
- {{ if eq .Meta.Type "note"}}
-
- {{.RenderedContent | noescape}}
- {{ else }}
-
- {{ end }}
-
- Published:
-
-
-
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/post.html b/embed/post.html
deleted file mode 100644
index b08301d..0000000
--- a/embed/post.html
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.Reply.Url }}
-
- In reply to:
- {{ if .Post.Meta.Reply.Text }}
- {{.Post.Meta.Reply.Text}}
- {{ else }}
- {{.Post.Meta.Reply.Url}}
- {{ end }}
-
-
- {{ end }}
-
- {{ if .Post.Meta.Bookmark.Url }}
-
- Bookmark:
- {{ if .Post.Meta.Bookmark.Text }}
- {{.Post.Meta.Bookmark.Text}}
- {{ else }}
- {{.Post.Meta.Bookmark.Url}}
- {{ end }}
-
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/recipe/detail.html b/embed/recipe/detail.html
deleted file mode 100644
index 2b73808..0000000
--- a/embed/recipe/detail.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
-
-
-
-
- {{ if .Post.Meta.Recipe.Yield }}
- Servings: {{ .Post.Meta.Recipe.Yield }}
- {{ if .Post.Meta.Recipe.Duration }}, {{end}}
-
- {{ end }}
-
- {{ if .Post.Meta.Recipe.Duration }}
- Prep Time:
- {{ end }}
-
-
-
Ingredients
-
-
- {{ range $ingredient := .Post.Meta.Recipe.Ingredients }}
- -
- {{ $ingredient }}
-
- {{ end }}
-
-
-
Instructions
-
-
- {{.Content}}
-
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/reply/detail.html b/embed/reply/detail.html
deleted file mode 100644
index c74f6bd..0000000
--- a/embed/reply/detail.html
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.Reply.Url }}
-
- In reply to:
- {{ if .Post.Meta.Reply.Text }}
- {{.Post.Meta.Reply.Text}}
- {{ else }}
- {{.Post.Meta.Reply.Url}}
- {{ end }}
-
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/untyped/detail.html b/embed/untyped/detail.html
deleted file mode 100644
index ece34bc..0000000
--- a/embed/untyped/detail.html
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
- {{.Title}}
-
- #
- Published:
-
- {{ if .Post.User.Config.AuthorName }}
- by
-
- {{ if .Post.User.AvatarUrl }}
-
- {{ end }}
- {{.Post.User.Config.AuthorName}}
-
- {{ end }}
-
-
-
-
-
- {{ if .Post.Meta.Reply.Url }}
-
- In reply to:
- {{ if .Post.Meta.Reply.Text }}
- {{.Post.Meta.Reply.Text}}
- {{ else }}
- {{.Post.Meta.Reply.Url}}
- {{ end }}
-
-
- {{ end }}
-
- {{ if .Post.Meta.Bookmark.Url }}
-
- Bookmark:
- {{ if .Post.Meta.Bookmark.Text }}
- {{.Post.Meta.Bookmark.Text}}
- {{ else }}
- {{.Post.Meta.Bookmark.Url}}
- {{ end }}
-
-
- {{ end }}
-
-
- {{.Content}}
-
-
-
- {{if .Post.ApprovedIncomingWebmentions}}
-
- Webmentions
-
-
- {{end}}
-
\ No newline at end of file
diff --git a/embed/user-list.html b/embed/user-list.html
deleted file mode 100644
index 13ec082..0000000
--- a/embed/user-list.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{{range .}}
-
-{{end}}
\ No newline at end of file
diff --git a/files.go b/files.go
deleted file mode 100644
index 9acc304..0000000
--- a/files.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package owl
-
-import (
- "os"
-
- "gopkg.in/yaml.v2"
-)
-
-func saveToYaml(path string, data interface{}) error {
- bytes, err := yaml.Marshal(data)
- if err != nil {
- return err
- }
- return os.WriteFile(path, bytes, 0644)
-}
-
-func loadFromYaml(path string, data interface{}) error {
- bytes, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- return yaml.Unmarshal(bytes, data)
-}
diff --git a/fixtures/image.png b/fixtures/image.png
deleted file mode 100644
index 538dcf9..0000000
Binary files a/fixtures/image.png and /dev/null differ
diff --git a/go.mod b/go.mod
deleted file mode 100644
index b6e1a3e..0000000
--- a/go.mod
+++ /dev/null
@@ -1,17 +0,0 @@
-module h4kor/owl-blogs
-
-go 1.18
-
-require (
- github.com/julienschmidt/httprouter v1.3.0
- github.com/spf13/cobra v1.5.0
- github.com/yuin/goldmark v1.4.13
- golang.org/x/net v0.1.0
- gopkg.in/yaml.v2 v2.4.0
-)
-
-require (
- github.com/inconshreveable/mousetrap v1.0.1 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
- golang.org/x/crypto v0.1.0 // indirect
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index 910f1b6..0000000
--- a/go.sum
+++ /dev/null
@@ -1,23 +0,0 @@
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
-github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
-golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
-golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/html.go b/html.go
deleted file mode 100644
index 7a86643..0000000
--- a/html.go
+++ /dev/null
@@ -1,269 +0,0 @@
-package owl
-
-import (
- "bytes"
- "errors"
- "io"
- "net/http"
- "net/url"
- "strings"
-
- "golang.org/x/net/html"
-)
-
-type HtmlParser interface {
- ParseHEntry(resp *http.Response) (ParsedHEntry, error)
- ParseLinks(resp *http.Response) ([]string, error)
- ParseLinksFromString(string) ([]string, error)
- GetWebmentionEndpoint(resp *http.Response) (string, error)
- GetRedirctUris(resp *http.Response) ([]string, error)
-}
-
-type OwlHtmlParser struct{}
-
-type ParsedHEntry struct {
- Title string
-}
-
-func collectText(n *html.Node, buf *bytes.Buffer) {
-
- if n.Type == html.TextNode {
- buf.WriteString(n.Data)
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- collectText(c, buf)
- }
-}
-
-func readResponseBody(resp *http.Response) (string, error) {
- defer resp.Body.Close()
- bodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", err
- }
- return string(bodyBytes), nil
-}
-
-func (OwlHtmlParser) ParseHEntry(resp *http.Response) (ParsedHEntry, error) {
- htmlStr, err := readResponseBody(resp)
- if err != nil {
- return ParsedHEntry{}, err
- }
- doc, err := html.Parse(strings.NewReader(htmlStr))
- if err != nil {
- return ParsedHEntry{}, err
- }
-
- var interpretHFeed func(*html.Node, *ParsedHEntry, bool) (ParsedHEntry, error)
- interpretHFeed = func(n *html.Node, curr *ParsedHEntry, parent bool) (ParsedHEntry, error) {
- attrs := n.Attr
- for _, attr := range attrs {
- if attr.Key == "class" && strings.Contains(attr.Val, "p-name") {
- buf := &bytes.Buffer{}
- collectText(n, buf)
- curr.Title = buf.String()
- return *curr, nil
- }
- }
-
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- interpretHFeed(c, curr, false)
- }
- return *curr, nil
- }
-
- var findHFeed func(*html.Node) (ParsedHEntry, error)
- findHFeed = func(n *html.Node) (ParsedHEntry, error) {
- attrs := n.Attr
- for _, attr := range attrs {
- if attr.Key == "class" && strings.Contains(attr.Val, "h-entry") {
- return interpretHFeed(n, &ParsedHEntry{}, true)
- }
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- entry, err := findHFeed(c)
- if err == nil {
- return entry, nil
- }
- }
- return ParsedHEntry{}, errors.New("no h-entry found")
- }
- return findHFeed(doc)
-}
-
-func (OwlHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
- htmlStr, err := readResponseBody(resp)
- if err != nil {
- return []string{}, err
- }
- return OwlHtmlParser{}.ParseLinksFromString(htmlStr)
-}
-
-func (OwlHtmlParser) ParseLinksFromString(htmlStr string) ([]string, error) {
- doc, err := html.Parse(strings.NewReader(htmlStr))
- if err != nil {
- return make([]string, 0), err
- }
-
- var findLinks func(*html.Node) ([]string, error)
- findLinks = func(n *html.Node) ([]string, error) {
- links := make([]string, 0)
- if n.Type == html.ElementNode && n.Data == "a" {
- for _, attr := range n.Attr {
- if attr.Key == "href" {
- links = append(links, attr.Val)
- }
- }
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- childLinks, _ := findLinks(c)
- links = append(links, childLinks...)
- }
- return links, nil
- }
- return findLinks(doc)
-}
-
-func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
- //request url
- requestUrl := resp.Request.URL
-
- // Check link headers
- for _, linkHeader := range resp.Header["Link"] {
- linkHeaderParts := strings.Split(linkHeader, ",")
- for _, linkHeaderPart := range linkHeaderParts {
- linkHeaderPart = strings.TrimSpace(linkHeaderPart)
- params := strings.Split(linkHeaderPart, ";")
- if len(params) != 2 {
- continue
- }
- for _, param := range params[1:] {
- param = strings.TrimSpace(param)
- if strings.Contains(param, "webmention") {
- link := strings.Split(params[0], ";")[0]
- link = strings.Trim(link, "<>")
- linkUrl, err := url.Parse(link)
- if err != nil {
- return "", err
- }
- return requestUrl.ResolveReference(linkUrl).String(), nil
- }
- }
- }
- }
-
- htmlStr, err := readResponseBody(resp)
- if err != nil {
- return "", err
- }
- doc, err := html.Parse(strings.NewReader(htmlStr))
- if err != nil {
- return "", err
- }
-
- var findEndpoint func(*html.Node) (string, error)
- findEndpoint = func(n *html.Node) (string, error) {
- if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "a") {
- for _, attr := range n.Attr {
- if attr.Key == "rel" {
- vals := strings.Split(attr.Val, " ")
- for _, val := range vals {
- if val == "webmention" {
- for _, attr := range n.Attr {
- if attr.Key == "href" {
- return attr.Val, nil
- }
- }
- }
- }
- }
- }
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- endpoint, err := findEndpoint(c)
- if err == nil {
- return endpoint, nil
- }
- }
- return "", errors.New("no webmention endpoint found")
- }
- linkUrlStr, err := findEndpoint(doc)
- if err != nil {
- return "", err
- }
- linkUrl, err := url.Parse(linkUrlStr)
- if err != nil {
- return "", err
- }
- return requestUrl.ResolveReference(linkUrl).String(), nil
-}
-
-func (OwlHtmlParser) GetRedirctUris(resp *http.Response) ([]string, error) {
- //request url
- requestUrl := resp.Request.URL
-
- htmlStr, err := readResponseBody(resp)
- if err != nil {
- return make([]string, 0), err
- }
- doc, err := html.Parse(strings.NewReader(htmlStr))
- if err != nil {
- return make([]string, 0), err
- }
-
- var findLinks func(*html.Node) ([]string, error)
- // Check link headers
- header_links := make([]string, 0)
- for _, linkHeader := range resp.Header["Link"] {
- linkHeaderParts := strings.Split(linkHeader, ",")
- for _, linkHeaderPart := range linkHeaderParts {
- linkHeaderPart = strings.TrimSpace(linkHeaderPart)
- params := strings.Split(linkHeaderPart, ";")
- if len(params) != 2 {
- continue
- }
- for _, param := range params[1:] {
- param = strings.TrimSpace(param)
- if strings.Contains(param, "redirect_uri") {
- link := strings.Split(params[0], ";")[0]
- link = strings.Trim(link, "<>")
- linkUrl, err := url.Parse(link)
- if err == nil {
- header_links = append(header_links, requestUrl.ResolveReference(linkUrl).String())
- }
- }
- }
- }
- }
-
- findLinks = func(n *html.Node) ([]string, error) {
- links := make([]string, 0)
- if n.Type == html.ElementNode && n.Data == "link" {
- // check for rel="redirect_uri"
- rel := ""
- href := ""
-
- for _, attr := range n.Attr {
- if attr.Key == "href" {
- href = attr.Val
- }
- if attr.Key == "rel" {
- rel = attr.Val
- }
- }
- if rel == "redirect_uri" {
- linkUrl, err := url.Parse(href)
- if err == nil {
- links = append(links, requestUrl.ResolveReference(linkUrl).String())
- }
- }
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- childLinks, _ := findLinks(c)
- links = append(links, childLinks...)
- }
- return links, nil
- }
- body_links, err := findLinks(doc)
- return append(body_links, header_links...), err
-}
diff --git a/http.go b/http.go
deleted file mode 100644
index 7a2f106..0000000
--- a/http.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package owl
-
-import (
- "io"
- "net/http"
- "net/url"
-)
-
-type HttpClient interface {
- Get(url string) (resp *http.Response, err error)
- Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
- PostForm(url string, data url.Values) (resp *http.Response, err error)
-}
-
-type OwlHttpClient = http.Client
diff --git a/owl_test.go b/owl_test.go
deleted file mode 100644
index 01c112a..0000000
--- a/owl_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package owl_test
-
-import (
- "h4kor/owl-blogs"
- "math/rand"
- "time"
-)
-
-func randomName() string {
- rand.Seed(time.Now().UnixNano())
- var letters = []rune("abcdefghijklmnopqrstuvwxyz")
- b := make([]rune, 8)
- for i := range b {
- b[i] = letters[rand.Intn(len(letters))]
- }
- return string(b)
-}
-
-func testRepoName() string {
- return "/tmp/" + randomName()
-}
-
-func randomUserName() string {
- return randomName()
-}
-
-func getTestUser() owl.User {
- repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{})
- user, _ := repo.CreateUser(randomUserName())
- return user
-}
-
-func getTestRepo(config owl.RepoConfig) owl.Repository {
- repo, _ := owl.CreateRepository(testRepoName(), config)
- return repo
-}
-
-func contains(s []string, e string) bool {
- for _, a := range s {
- if a == e {
- return true
- }
- }
- return false
-}
diff --git a/post.go b/post.go
deleted file mode 100644
index c759fd7..0000000
--- a/post.go
+++ /dev/null
@@ -1,478 +0,0 @@
-package owl
-
-import (
- "bytes"
- "errors"
- "net/url"
- "os"
- "path"
- "sort"
- "sync"
- "time"
-
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/extension"
- "github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer/html"
- "gopkg.in/yaml.v2"
-)
-
-type GenericPost struct {
- user *User
- id string
- metaLoaded bool
- meta PostMeta
- wmLock sync.Mutex
-}
-
-func (post *GenericPost) TemplateDir() string {
- return post.Meta().Type
-}
-
-type Post interface {
- TemplateDir() string
-
- // Actual Data
- User() *User
- Id() string
- Title() string
- Meta() PostMeta
- Content() []byte
- RenderedContent() string
- Aliases() []string
-
- // Filesystem
- Dir() string
- MediaDir() string
- ContentFile() string
-
- // Urls
- UrlPath() string
- FullUrl() string
- UrlMediaPath(filename string) string
-
- // Webmentions Support
- IncomingWebmentions() []WebmentionIn
- OutgoingWebmentions() []WebmentionOut
- PersistIncomingWebmention(webmention WebmentionIn) error
- PersistOutgoingWebmention(webmention *WebmentionOut) error
- AddIncomingWebmention(source string) error
- EnrichWebmention(webmention WebmentionIn) error
- ApprovedIncomingWebmentions() []WebmentionIn
- ScanForLinks() error
- SendWebmention(webmention WebmentionOut) error
-}
-
-type ReplyData struct {
- Url string `yaml:"url"`
- Text string `yaml:"text"`
-}
-type BookmarkData struct {
- Url string `yaml:"url"`
- Text string `yaml:"text"`
-}
-
-type RecipeData struct {
- Yield string `yaml:"yield"`
- Duration string `yaml:"duration"`
- Ingredients []string `yaml:"ingredients"`
-}
-
-type PostMeta struct {
- Type string `yaml:"type"`
- Title string `yaml:"title"`
- Description string `yaml:"description"`
- Aliases []string `yaml:"aliases"`
- Date time.Time `yaml:"date"`
- Draft bool `yaml:"draft"`
- Reply ReplyData `yaml:"reply"`
- Bookmark BookmarkData `yaml:"bookmark"`
- Recipe RecipeData `yaml:"recipe"`
- PhotoPath string `yaml:"photo"`
-}
-
-func (pm PostMeta) FormattedDate() string {
- return pm.Date.Format("02-01-2006 15:04:05")
-}
-
-func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
- type T struct {
- Type string `yaml:"type"`
- Title string `yaml:"title"`
- Description string `yaml:"description"`
- Aliases []string `yaml:"aliases"`
- Draft bool `yaml:"draft"`
- Reply ReplyData `yaml:"reply"`
- Bookmark BookmarkData `yaml:"bookmark"`
- Recipe RecipeData `yaml:"recipe"`
- PhotoPath string `yaml:"photo"`
- }
- type S struct {
- Date string `yaml:"date"`
- }
-
- var t T
- var s S
- if err := unmarshal(&t); err != nil {
- return err
- }
- if err := unmarshal(&s); err != nil {
- return err
- }
-
- pm.Type = t.Type
- if pm.Type == "" {
- pm.Type = "article"
- }
- pm.Title = t.Title
- pm.Description = t.Description
- pm.Aliases = t.Aliases
- pm.Draft = t.Draft
- pm.Reply = t.Reply
- pm.Bookmark = t.Bookmark
- pm.Recipe = t.Recipe
- pm.PhotoPath = t.PhotoPath
-
- possibleFormats := []string{
- "2006-01-02",
- time.Layout,
- time.ANSIC,
- time.UnixDate,
- time.RubyDate,
- time.RFC822,
- time.RFC822Z,
- time.RFC850,
- time.RFC1123,
- time.RFC1123Z,
- time.RFC3339,
- time.RFC3339Nano,
- time.Stamp,
- time.StampMilli,
- time.StampMicro,
- time.StampNano,
- }
-
- for _, format := range possibleFormats {
- if t, err := time.Parse(format, s.Date); err == nil {
- pm.Date = t
- break
- }
- }
-
- return nil
-}
-
-type PostWebmetions struct {
- Incoming []WebmentionIn `ymal:"incoming"`
- Outgoing []WebmentionOut `ymal:"outgoing"`
-}
-
-func (post *GenericPost) Id() string {
- return post.id
-}
-
-func (post *GenericPost) User() *User {
- return post.user
-}
-
-func (post *GenericPost) Dir() string {
- return path.Join(post.user.Dir(), "public", post.id)
-}
-
-func (post *GenericPost) IncomingWebmentionsFile() string {
- return path.Join(post.Dir(), "incoming_webmentions.yml")
-}
-
-func (post *GenericPost) OutgoingWebmentionsFile() string {
- return path.Join(post.Dir(), "outgoing_webmentions.yml")
-}
-
-func (post *GenericPost) MediaDir() string {
- return path.Join(post.Dir(), "media")
-}
-
-func (post *GenericPost) UrlPath() string {
- return post.user.UrlPath() + "posts/" + post.id + "/"
-}
-
-func (post *GenericPost) FullUrl() string {
- return post.user.FullUrl() + "posts/" + post.id + "/"
-}
-
-func (post *GenericPost) UrlMediaPath(filename string) string {
- return post.UrlPath() + "media/" + filename
-}
-
-func (post *GenericPost) Title() string {
- return post.Meta().Title
-}
-
-func (post *GenericPost) ContentFile() string {
- return path.Join(post.Dir(), "index.md")
-}
-
-func (post *GenericPost) Meta() PostMeta {
- if !post.metaLoaded {
- post.LoadMeta()
- }
- return post.meta
-}
-
-func (post *GenericPost) Content() []byte {
- // read file
- data, _ := os.ReadFile(post.ContentFile())
- return data
-}
-
-func (post *GenericPost) RenderedContent() string {
- data := post.Content()
-
- // trim yaml block
- // TODO this can be done nicer
- trimmedData := bytes.TrimSpace(data)
- // ensure that data ends with a newline
- trimmedData = append(trimmedData, []byte("\n")...)
- // check first line is ---
- if string(trimmedData[0:4]) == "---\n" {
- trimmedData = trimmedData[4:]
- // find --- end
- end := bytes.Index(trimmedData, []byte("\n---\n"))
- if end != -1 {
- data = trimmedData[end+5:]
- }
- }
-
- options := goldmark.WithRendererOptions()
- if config, _ := post.user.repo.Config(); config.AllowRawHtml {
- options = goldmark.WithRendererOptions(
- html.WithUnsafe(),
- )
- }
-
- markdown := goldmark.New(
- options,
- goldmark.WithExtensions(
- // meta.Meta,
- extension.GFM,
- ),
- )
- var buf bytes.Buffer
- context := parser.NewContext()
- if err := markdown.Convert(data, &buf, parser.WithContext(context)); err != nil {
- panic(err)
- }
-
- return buf.String()
-
-}
-
-func (post *GenericPost) Aliases() []string {
- return post.Meta().Aliases
-}
-
-func (post *GenericPost) LoadMeta() error {
- data := post.Content()
-
- // get yaml metadata block
- meta := PostMeta{}
- trimmedData := bytes.TrimSpace(data)
- // ensure that data ends with a newline
- trimmedData = append(trimmedData, []byte("\n")...)
- // check first line is ---
- if string(trimmedData[0:4]) == "---\n" {
- trimmedData = trimmedData[4:]
- // find --- end
- end := bytes.Index(trimmedData, []byte("---\n"))
- if end != -1 {
- metaData := trimmedData[:end]
- err := yaml.Unmarshal(metaData, &meta)
- if err != nil {
- return err
- }
- }
- }
-
- post.meta = meta
- return nil
-}
-
-func (post *GenericPost) IncomingWebmentions() []WebmentionIn {
- // return parsed webmentions
- fileName := post.IncomingWebmentionsFile()
- if !fileExists(fileName) {
- return []WebmentionIn{}
- }
-
- webmentions := []WebmentionIn{}
- loadFromYaml(fileName, &webmentions)
-
- return webmentions
-}
-
-func (post *GenericPost) OutgoingWebmentions() []WebmentionOut {
- // return parsed webmentions
- fileName := post.OutgoingWebmentionsFile()
- if !fileExists(fileName) {
- return []WebmentionOut{}
- }
-
- webmentions := []WebmentionOut{}
- loadFromYaml(fileName, &webmentions)
-
- return webmentions
-}
-
-// PersistWebmentionOutgoing persists incoming webmention
-func (post *GenericPost) PersistIncomingWebmention(webmention WebmentionIn) error {
- post.wmLock.Lock()
- defer post.wmLock.Unlock()
-
- wms := post.IncomingWebmentions()
-
- // if target is not in status, add it
- replaced := false
- for i, t := range wms {
- if t.Source == webmention.Source {
- wms[i].UpdateWith(webmention)
- replaced = true
- break
- }
- }
-
- if !replaced {
- wms = append(wms, webmention)
- }
-
- err := saveToYaml(post.IncomingWebmentionsFile(), wms)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-// PersistOutgoingWebmention persists a webmention to the webmention file.
-func (post *GenericPost) PersistOutgoingWebmention(webmention *WebmentionOut) error {
- post.wmLock.Lock()
- defer post.wmLock.Unlock()
-
- wms := post.OutgoingWebmentions()
-
- // if target is not in webmention, add it
- replaced := false
- for i, t := range wms {
- if t.Target == webmention.Target {
- wms[i].UpdateWith(*webmention)
- replaced = true
- break
- }
- }
-
- if !replaced {
- wms = append(wms, *webmention)
- }
-
- err := saveToYaml(post.OutgoingWebmentionsFile(), wms)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func (post *GenericPost) AddIncomingWebmention(source string) error {
- // Check if file already exists
- wm := WebmentionIn{
- Source: source,
- }
-
- defer func() {
- go post.EnrichWebmention(wm)
- }()
- return post.PersistIncomingWebmention(wm)
-}
-
-func (post *GenericPost) EnrichWebmention(webmention WebmentionIn) error {
- resp, err := post.user.repo.HttpClient.Get(webmention.Source)
- if err == nil {
- entry, err := post.user.repo.Parser.ParseHEntry(resp)
- if err == nil {
- webmention.Title = entry.Title
- return post.PersistIncomingWebmention(webmention)
- }
- }
- return err
-}
-
-func (post *GenericPost) ApprovedIncomingWebmentions() []WebmentionIn {
- webmentions := post.IncomingWebmentions()
- approved := []WebmentionIn{}
- for _, webmention := range webmentions {
- if webmention.ApprovalStatus == "approved" {
- approved = append(approved, webmention)
- }
- }
-
- // sort by retrieved date
- sort.Slice(approved, func(i, j int) bool {
- return approved[i].RetrievedAt.After(approved[j].RetrievedAt)
- })
- return approved
-}
-
-// ScanForLinks scans the post content for links and adds them to the
-// `status.yml` file for the post. The links are not scanned by this function.
-func (post *GenericPost) ScanForLinks() error {
- // this could be done in markdown parsing, but I don't want to
- // rely on goldmark for this (yet)
- postHtml := post.RenderedContent()
- links, _ := post.user.repo.Parser.ParseLinksFromString(postHtml)
- // add reply url if set
- if post.Meta().Reply.Url != "" {
- links = append(links, post.Meta().Reply.Url)
- }
- for _, link := range links {
- post.PersistOutgoingWebmention(&WebmentionOut{
- Target: link,
- })
- }
- return nil
-}
-
-func (post *GenericPost) SendWebmention(webmention WebmentionOut) error {
- defer post.PersistOutgoingWebmention(&webmention)
-
- // if last scan is less than 7 days ago, don't send webmention
- if webmention.ScannedAt.After(time.Now().Add(-7*24*time.Hour)) && !webmention.Supported {
- return errors.New("did not scan. Last scan was less than 7 days ago")
- }
-
- webmention.ScannedAt = time.Now()
-
- resp, err := post.user.repo.HttpClient.Get(webmention.Target)
- if err != nil {
- webmention.Supported = false
- return err
- }
-
- endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(resp)
- if err != nil {
- webmention.Supported = false
- return err
- }
- webmention.Supported = true
-
- // send webmention
- payload := url.Values{}
- payload.Set("source", post.FullUrl())
- payload.Set("target", webmention.Target)
- _, err = post.user.repo.HttpClient.PostForm(endpoint, payload)
-
- if err != nil {
- return err
- }
-
- // update webmention status
- webmention.LastSentAt = time.Now()
- return nil
-}
diff --git a/post_test.go b/post_test.go
deleted file mode 100644
index 4f5c727..0000000
--- a/post_test.go
+++ /dev/null
@@ -1,531 +0,0 @@
-package owl_test
-
-import (
- "h4kor/owl-blogs"
- "h4kor/owl-blogs/test/assertions"
- "h4kor/owl-blogs/test/mocks"
- "os"
- "path"
- "strconv"
- "sync"
- "testing"
- "time"
-)
-
-func TestCanGetPostTitle(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result := post.Title()
- assertions.AssertEqual(t, result, "testpost")
-}
-
-func TestMediaDir(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result := post.MediaDir()
- assertions.AssertEqual(t, result, path.Join(post.Dir(), "media"))
-}
-
-func TestPostUrlPath(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/"
- assertions.AssertEqual(t, post.UrlPath(), expected)
-}
-
-func TestPostFullUrl(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- expected := "http://localhost:8080/user/" + user.Name() + "/posts/" + post.Id() + "/"
- assertions.AssertEqual(t, post.FullUrl(), expected)
-}
-
-func TestPostUrlMediaPath(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/media/data.png"
- assertions.AssertEqual(t, post.UrlMediaPath("data.png"), expected)
-}
-
-func TestPostUrlMediaPathWithSubDir(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- expected := "/user/" + user.Name() + "/posts/" + post.Id() + "/media/foo/data.png"
- assertions.AssertEqual(t, post.UrlMediaPath("foo/data.png"), expected)
-}
-
-func TestDraftInMetaData(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- content := "---\n"
- content += "title: test\n"
- content += "draft: true\n"
- content += "---\n"
- content += "\n"
- content += "Write your post here.\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
- meta := post.Meta()
- assertions.AssertEqual(t, meta.Draft, true)
-}
-
-func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- content := "---\n"
- content += "title: test\n"
- content += "draft: true\n"
- content += "---\n"
- content += "\n"
- content += "\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
- html := post.RenderedContent()
- assertions.AssertNotContains(t, html, "\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
- html := post.RenderedContent()
- assertions.AssertContains(t, html, "\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- assertions.AssertEqual(t, post.Meta().Title, "test")
- assertions.AssertLen(t, post.Meta().Aliases, 1)
- assertions.AssertEqual(t, post.Meta().Draft, true)
- assertions.AssertEqual(t, post.Meta().Date.Format(time.RFC1123Z), "Wed, 17 Aug 2022 10:50:02 +0000")
- assertions.AssertEqual(t, post.Meta().Draft, true)
-}
-
-///
-/// Webmention
-///
-
-func TestPersistIncomingWebmention(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- webmention := owl.WebmentionIn{
- Source: "http://example.com/source",
- }
- err := post.PersistIncomingWebmention(webmention)
- assertions.AssertNoError(t, err, "Error persisting webmention")
- mentions := post.IncomingWebmentions()
- assertions.AssertLen(t, mentions, 1)
- assertions.AssertEqual(t, mentions[0].Source, webmention.Source)
-}
-
-func TestAddIncomingWebmentionCreatesFile(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- err := post.AddIncomingWebmention("https://example.com")
- assertions.AssertNoError(t, err, "Error adding webmention")
-
- mentions := post.IncomingWebmentions()
- assertions.AssertLen(t, mentions, 1)
-}
-
-func TestAddIncomingWebmentionNotOverwritingWebmention(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- post.PersistIncomingWebmention(owl.WebmentionIn{
- Source: "https://example.com",
- ApprovalStatus: "approved",
- })
-
- post.AddIncomingWebmention("https://example.com")
-
- mentions := post.IncomingWebmentions()
- assertions.AssertLen(t, mentions, 1)
-
- assertions.AssertEqual(t, mentions[0].ApprovalStatus, "approved")
-}
-
-func TestEnrichAddsTitle(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- post.AddIncomingWebmention("https://example.com")
- post.EnrichWebmention(owl.WebmentionIn{Source: "https://example.com"})
-
- mentions := post.IncomingWebmentions()
- assertions.AssertLen(t, mentions, 1)
- assertions.AssertEqual(t, mentions[0].Title, "Mock Title")
-}
-
-func TestApprovedIncomingWebmentions(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- webmention := owl.WebmentionIn{
- Source: "http://example.com/source",
- ApprovalStatus: "approved",
- RetrievedAt: time.Now(),
- }
- post.PersistIncomingWebmention(webmention)
- webmention = owl.WebmentionIn{
- Source: "http://example.com/source2",
- ApprovalStatus: "",
- RetrievedAt: time.Now().Add(time.Hour * -1),
- }
- post.PersistIncomingWebmention(webmention)
- webmention = owl.WebmentionIn{
- Source: "http://example.com/source3",
- ApprovalStatus: "approved",
- RetrievedAt: time.Now().Add(time.Hour * -2),
- }
- post.PersistIncomingWebmention(webmention)
- webmention = owl.WebmentionIn{
- Source: "http://example.com/source4",
- ApprovalStatus: "rejected",
- RetrievedAt: time.Now().Add(time.Hour * -3),
- }
- post.PersistIncomingWebmention(webmention)
-
- webmentions := post.ApprovedIncomingWebmentions()
- assertions.AssertLen(t, webmentions, 2)
-
- assertions.AssertEqual(t, webmentions[0].Source, "http://example.com/source")
- assertions.AssertEqual(t, webmentions[1].Source, "http://example.com/source3")
-
-}
-
-func TestScanningForLinks(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- content := "---\n"
- content += "title: test\n"
- content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
- content += "---\n"
- content += "\n"
- content += "[Hello](https://example.com/hello)\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- post.ScanForLinks()
- webmentions := post.OutgoingWebmentions()
- assertions.AssertLen(t, webmentions, 1)
- assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/hello")
-}
-
-func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- content := "---\n"
- content += "title: test\n"
- content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
- content += "---\n"
- content += "\n"
- content += "[Hello](https://example.com/hello)\n"
- content += "[Hello](https://example.com/hello)\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- post.ScanForLinks()
- post.ScanForLinks()
- post.ScanForLinks()
- webmentions := post.OutgoingWebmentions()
- assertions.AssertLen(t, webmentions, 1)
- assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/hello")
-}
-
-func TestScanningForLinksDoesAddReplyUrl(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- content := "---\n"
- content += "title: test\n"
- content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
- content += "reply:\n"
- content += " url: https://example.com/reply\n"
- content += "---\n"
- content += "\n"
- content += "Hi\n"
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
-
- post.ScanForLinks()
- webmentions := post.OutgoingWebmentions()
- assertions.AssertLen(t, webmentions, 1)
- assertions.AssertEqual(t, webmentions[0].Target, "https://example.com/reply")
-}
-
-func TestCanSendWebmention(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- webmention := owl.WebmentionOut{
- Target: "http://example.com",
- }
-
- err := post.SendWebmention(webmention)
- assertions.AssertNoError(t, err, "Error sending webmention")
-
- webmentions := post.OutgoingWebmentions()
-
- assertions.AssertLen(t, webmentions, 1)
- assertions.AssertEqual(t, webmentions[0].Target, "http://example.com")
- assertions.AssertEqual(t, webmentions[0].LastSentAt.IsZero(), false)
-}
-
-func TestSendWebmentionOnlyScansOncePerWeek(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- webmention := owl.WebmentionOut{
- Target: "http://example.com",
- ScannedAt: time.Now().Add(time.Hour * -24 * 6),
- }
-
- post.PersistOutgoingWebmention(&webmention)
- webmentions := post.OutgoingWebmentions()
- webmention = webmentions[0]
-
- err := post.SendWebmention(webmention)
- assertions.AssertError(t, err, "Expected error, got nil")
-
- webmentions = post.OutgoingWebmentions()
-
- assertions.AssertLen(t, webmentions, 1)
- assertions.AssertEqual(t, webmentions[0].ScannedAt, webmention.ScannedAt)
-}
-
-func TestSendingMultipleWebmentions(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- wg := sync.WaitGroup{}
- wg.Add(20)
-
- for i := 0; i < 20; i++ {
- go func(k int) {
- webmention := owl.WebmentionOut{
- Target: "http://example.com" + strconv.Itoa(k),
- }
- post.SendWebmention(webmention)
- wg.Done()
- }(i)
- }
-
- wg.Wait()
-
- webmentions := post.OutgoingWebmentions()
-
- assertions.AssertLen(t, webmentions, 20)
-}
-
-func TestReceivingMultipleWebmentions(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- wg := sync.WaitGroup{}
- wg.Add(20)
-
- for i := 0; i < 20; i++ {
- go func(k int) {
- post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
- wg.Done()
- }(i)
- }
-
- wg.Wait()
-
- webmentions := post.IncomingWebmentions()
-
- assertions.AssertLen(t, webmentions, 20)
-
-}
-
-func TestSendingAndReceivingMultipleWebmentions(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockHtmlParser{}
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- wg := sync.WaitGroup{}
- wg.Add(40)
-
- for i := 0; i < 20; i++ {
- go func(k int) {
- post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
- wg.Done()
- }(i)
- go func(k int) {
- webmention := owl.WebmentionOut{
- Target: "http://example.com" + strconv.Itoa(k),
- }
- post.SendWebmention(webmention)
- wg.Done()
- }(i)
- }
-
- wg.Wait()
-
- ins := post.IncomingWebmentions()
- outs := post.OutgoingWebmentions()
-
- assertions.AssertLen(t, ins, 20)
- assertions.AssertLen(t, outs, 20)
-}
-
-func TestComplexParallelWebmentions(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.HttpClient = &mocks.MockHttpClient{}
- repo.Parser = &mocks.MockParseLinksHtmlParser{
- Links: []string{
- "http://example.com/1",
- "http://example.com/2",
- "http://example.com/3",
- },
- }
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- wg := sync.WaitGroup{}
- wg.Add(60)
-
- for i := 0; i < 20; i++ {
- go func(k int) {
- post.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
- wg.Done()
- }(i)
- go func(k int) {
- webmention := owl.WebmentionOut{
- Target: "http://example.com/" + strconv.Itoa(k),
- }
- post.SendWebmention(webmention)
- wg.Done()
- }(i)
- go func() {
- post.ScanForLinks()
- wg.Done()
- }()
- }
-
- wg.Wait()
-
- ins := post.IncomingWebmentions()
- outs := post.OutgoingWebmentions()
-
- assertions.AssertLen(t, ins, 20)
- assertions.AssertLen(t, outs, 20)
-}
-
-func TestPostWithoutContent(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- user, _ := repo.CreateUser("testuser")
- post, _ := user.CreateNewPost(owl.PostMeta{}, "")
-
- result := post.RenderedContent()
- assertions.AssertEqual(t, result, "")
-}
-
-// func TestComplexParallelSimulatedProcessesWebmentions(t *testing.T) {
-// repoName := testRepoName()
-// repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{})
-// repo.HttpClient = &mocks.MockHttpClient{}
-// repo.Parser = &MockParseLinksHtmlParser{
-// Links: []string{
-// "http://example.com/1",
-// "http://example.com/2",
-// "http://example.com/3",
-// },
-// }
-// user, _ := repo.CreateUser("testuser")
-// post, _ := user.CreateNewPostFull(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
-// wg := sync.WaitGroup{}
-// wg.Add(40)
-
-// for i := 0; i < 20; i++ {
-// go func(k int) {
-// defer wg.Done()
-// fRepo, _ := owl.OpenRepository(repoName)
-// fUser, _ := fRepo.GetUser("testuser")
-// fPost, err := fUser.GetPost(post.Id())
-// if err != nil {
-// t.Error(err)
-// return
-// }
-// fPost.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
-// }(i)
-// go func(k int) {
-// defer wg.Done()
-// fRepo, _ := owl.OpenRepository(repoName)
-// fUser, _ := fRepo.GetUser("testuser")
-// fPost, err := fUser.GetPost(post.Id())
-// if err != nil {
-// t.Error(err)
-// return
-// }
-// webmention := owl.WebmentionOut{
-// Target: "http://example.com/" + strconv.Itoa(k),
-// }
-// fPost.SendWebmention(webmention)
-// }(i)
-// }
-
-// wg.Wait()
-
-// ins := post.IncomingWebmentions()
-
-// if len(ins) != 20 {
-// t.Errorf("Expected 20 webmentions, got %d", len(ins))
-// }
-
-// outs := post.OutgoingWebmentions()
-
-// if len(outs) != 20 {
-// t.Errorf("Expected 20 webmentions, got %d", len(outs))
-// }
-// }
diff --git a/release.sh b/release.sh
deleted file mode 100755
index 5ffa699..0000000
--- a/release.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-docker build . -t git.libove.org/h4kor/owl-blogs:$1
-docker push git.libove.org/h4kor/owl-blogs:$1
\ No newline at end of file
diff --git a/renderer.go b/renderer.go
deleted file mode 100644
index 3a5ece6..0000000
--- a/renderer.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package owl
-
-import (
- "bytes"
- _ "embed"
- "fmt"
- "html/template"
- "strings"
-)
-
-type PageContent struct {
- Title string
- Description string
- Content template.HTML
- Type string
- SelfUrl string
-}
-
-type PostRenderData struct {
- Title string
- Post Post
- Content template.HTML
-}
-
-type AuthRequestData struct {
- Me string
- ClientId string
- RedirectUri string
- State string
- Scope string
- Scopes []string // Split version of scope. filled by rendering function.
- ResponseType string
- CodeChallenge string
- CodeChallengeMethod string
- User User
- CsrfToken string
-}
-
-type EditorViewData struct {
- User User
- Error string
- CsrfToken string
-}
-
-type ErrorMessage struct {
- Error string
- Message string
-}
-
-func noescape(str string) template.HTML {
- return template.HTML(str)
-}
-
-func listUrl(user User, id string) string {
- return user.ListUrl(PostList{
- Id: id,
- })
-}
-
-func postUrl(user User, id string) string {
- post, _ := user.GetPost(id)
- return post.UrlPath()
-}
-
-func renderEmbedTemplate(templateFile string, data interface{}) (string, error) {
- templateStr, err := embed_files.ReadFile(templateFile)
- if err != nil {
- return "", err
- }
- return renderTemplateStr(templateStr, data)
-}
-
-func renderTemplateStr(templateStr []byte, data interface{}) (string, error) {
- t, err := template.New("_").Funcs(template.FuncMap{
- "noescape": noescape,
- "listUrl": listUrl,
- "postUrl": postUrl,
- }).Parse(string(templateStr))
- if err != nil {
- return "", err
- }
- var html bytes.Buffer
- err = t.Execute(&html, data)
- if err != nil {
- return "", err
- }
- return html.String(), nil
-}
-
-func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
- baseTemplate, _ := user.Template()
- t, err := template.New("index").Funcs(template.FuncMap{
- "noescape": noescape,
- "listUrl": listUrl,
- "postUrl": postUrl,
- }).Parse(baseTemplate)
- if err != nil {
- return "", err
- }
-
- full_data := struct {
- Title string
- Description string
- Content template.HTML
- Type string
- SelfUrl string
- User User
- }{
- Title: data.Title,
- Description: data.Description,
- Content: data.Content,
- Type: data.Type,
- SelfUrl: data.SelfUrl,
- User: user,
- }
-
- var html bytes.Buffer
- err = t.Execute(&html, full_data)
- return html.String(), err
-}
-
-func renderPostContent(post Post) (string, error) {
- buf := post.RenderedContent()
- postHtml, err := renderEmbedTemplate(
- fmt.Sprintf("embed/%s/detail.html", post.TemplateDir()),
- PostRenderData{
- Title: post.Title(),
- Post: post,
- Content: template.HTML(buf),
- },
- )
- return postHtml, err
-}
-
-func RenderPost(post Post) (string, error) {
- postHtml, err := renderPostContent(post)
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(*post.User(), PageContent{
- Title: post.Title(),
- Description: post.Meta().Description,
- Content: template.HTML(postHtml),
- Type: "article",
- SelfUrl: post.FullUrl(),
- })
-}
-
-func RenderIndexPage(user User) (string, error) {
- posts, _ := user.PrimaryFeedPosts()
-
- postHtml, err := renderEmbedTemplate("embed/post-list.html", posts)
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(user, PageContent{
- Title: "Index",
- Content: template.HTML(postHtml),
- })
-}
-
-func RenderPostList(user User, list *PostList) (string, error) {
- posts, _ := user.GetPostsOfList(*list)
- var postHtml string
- var err error
- if list.ListType == "photo" {
- postHtml, err = renderEmbedTemplate("embed/post-list-photo.html", posts)
- } else {
- postHtml, err = renderEmbedTemplate("embed/post-list.html", posts)
- }
-
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(user, PageContent{
- Title: "Index",
- Content: template.HTML(postHtml),
- })
-}
-
-func RenderUserAuthPage(reqData AuthRequestData) (string, error) {
- reqData.Scopes = strings.Split(reqData.Scope, " ")
- authHtml, err := renderEmbedTemplate("embed/auth.html", reqData)
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(reqData.User, PageContent{
- Title: "Auth",
- Content: template.HTML(authHtml),
- })
-}
-
-func RenderUserError(user User, error ErrorMessage) (string, error) {
- errHtml, err := renderEmbedTemplate("embed/error.html", error)
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(user, PageContent{
- Title: "Error",
- Content: template.HTML(errHtml),
- })
-}
-
-func RenderUserList(repo Repository) (string, error) {
- baseTemplate, _ := repo.Template()
- users, _ := repo.Users()
- userHtml, err := renderEmbedTemplate("embed/user-list.html", users)
- if err != nil {
- return "", err
- }
-
- data := PageContent{
- Title: "Index",
- Content: template.HTML(userHtml),
- }
-
- return renderTemplateStr([]byte(baseTemplate), data)
-}
-
-func RenderLoginPage(user User, error_type string, csrfToken string) (string, error) {
- loginHtml, err := renderEmbedTemplate("embed/editor/login.html", EditorViewData{
- User: user,
- Error: error_type,
- CsrfToken: csrfToken,
- })
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(user, PageContent{
- Title: "Login",
- Content: template.HTML(loginHtml),
- })
-}
-
-func RenderEditorPage(user User, csrfToken string) (string, error) {
- editorHtml, err := renderEmbedTemplate("embed/editor/editor.html", EditorViewData{
- User: user,
- CsrfToken: csrfToken,
- })
- if err != nil {
- return "", err
- }
-
- return renderIntoBaseTemplate(user, PageContent{
- Title: "Editor",
- Content: template.HTML(editorHtml),
- })
-}
diff --git a/renderer_test.go b/renderer_test.go
deleted file mode 100644
index 225cea7..0000000
--- a/renderer_test.go
+++ /dev/null
@@ -1,505 +0,0 @@
-package owl_test
-
-import (
- "h4kor/owl-blogs"
- "h4kor/owl-blogs/test/assertions"
- "os"
- "path"
- "testing"
- "time"
-)
-
-func TestCanRenderPost(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, err := owl.RenderPost(post)
-
- assertions.AssertNoError(t, err, "Error rendering post")
- assertions.AssertContains(t, result, "testpost")
-
-}
-
-func TestRenderOneMe(t *testing.T) {
- user := getTestUser()
- config := user.Config()
- config.Me = append(config.Me, owl.UserMe{
- Name: "Twitter",
- Url: "https://twitter.com/testhandle",
- })
-
- user.SetConfig(config)
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, err := owl.RenderPost(post)
-
- assertions.AssertNoError(t, err, "Error rendering post")
- assertions.AssertContains(t, result, "href=\"https://twitter.com/testhandle\" rel=\"me\"")
-
-}
-
-func TestRenderTwoMe(t *testing.T) {
- user := getTestUser()
- config := user.Config()
- config.Me = append(config.Me, owl.UserMe{
- Name: "Twitter",
- Url: "https://twitter.com/testhandle",
- })
- config.Me = append(config.Me, owl.UserMe{
- Name: "Github",
- Url: "https://github.com/testhandle",
- })
-
- user.SetConfig(config)
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, err := owl.RenderPost(post)
-
- assertions.AssertNoError(t, err, "Error rendering post")
- assertions.AssertContains(t, result, "href=\"https://twitter.com/testhandle\" rel=\"me\"")
- assertions.AssertContains(t, result, "href=\"https://github.com/testhandle\" rel=\"me\"")
-
-}
-
-func TestRenderPostHEntry(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "class=\"h-entry\"")
- assertions.AssertContains(t, result, "class=\"p-name\"")
- assertions.AssertContains(t, result, "class=\"e-content\"")
-
-}
-
-func TestRendererUsesBaseTemplate(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, err := owl.RenderPost(post)
-
- assertions.AssertNoError(t, err, "Error rendering post")
- assertions.AssertContains(t, result, "")
-}
-
-func TestIndexPageContainsHEntryAndUUrl(t *testing.T) {
- user := getTestUser()
- user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
-
- result, _ := owl.RenderIndexPage(user)
- assertions.AssertContains(t, result, "class=\"h-entry\"")
- assertions.AssertContains(t, result, "class=\"u-url\"")
-
-}
-
-func TestIndexPageDoesNotContainsArticle(t *testing.T) {
- user := getTestUser()
- user.CreateNewPost(owl.PostMeta{Type: "article"}, "hi")
-
- result, _ := owl.RenderIndexPage(user)
- assertions.AssertContains(t, result, "class=\"h-entry\"")
- assertions.AssertContains(t, result, "class=\"u-url\"")
-}
-
-func TestIndexPageDoesNotContainsReply(t *testing.T) {
- user := getTestUser()
- user.CreateNewPost(owl.PostMeta{Type: "reply", Reply: owl.ReplyData{Url: "https://example.com/post"}}, "hi")
-
- result, _ := owl.RenderIndexPage(user)
- assertions.AssertContains(t, result, "class=\"h-entry\"")
- assertions.AssertContains(t, result, "class=\"u-url\"")
-}
-
-func TestRenderIndexPageWithBrokenBaseTemplate(t *testing.T) {
- user := getTestUser()
- user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost1"}, "")
- user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost2"}, "")
-
- os.WriteFile(path.Join(user.Dir(), "meta/base.html"), []byte("{{content}}"), 0644)
-
- _, err := owl.RenderIndexPage(user)
- assertions.AssertError(t, err, "Expected error rendering index page")
-}
-
-func TestRenderUserList(t *testing.T) {
- repo := getTestRepo(owl.RepoConfig{})
- repo.CreateUser("user1")
- repo.CreateUser("user2")
-
- result, err := owl.RenderUserList(repo)
- assertions.AssertNoError(t, err, "Error rendering user list")
- assertions.AssertContains(t, result, "user1")
- assertions.AssertContains(t, result, "user2")
-}
-
-func TestRendersHeaderTitle(t *testing.T) {
- user := getTestUser()
- user.SetConfig(owl.UserConfig{
- Title: "Test Title",
- SubTitle: "Test SubTitle",
- HeaderColor: "#ff1337",
- })
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "Test Title")
- assertions.AssertContains(t, result, "Test SubTitle")
- assertions.AssertContains(t, result, "#ff1337")
-}
-
-func TestRenderPostIncludesRelToWebMention(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "rel=\"webmention\"")
-
- assertions.AssertContains(t, result, "href=\""+user.WebmentionUrl()+"\"")
-}
-
-func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- webmention := owl.WebmentionIn{
- Source: "http://example.com/source3",
- Title: "Test Title",
- ApprovalStatus: "approved",
- RetrievedAt: time.Now().Add(time.Hour * -2),
- }
- post.PersistIncomingWebmention(webmention)
- webmention = owl.WebmentionIn{
- Source: "http://example.com/source4",
- ApprovalStatus: "rejected",
- RetrievedAt: time.Now().Add(time.Hour * -3),
- }
- post.PersistIncomingWebmention(webmention)
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "http://example.com/source3")
- assertions.AssertContains(t, result, "Test Title")
- assertions.AssertNotContains(t, result, "http://example.com/source4")
-
-}
-
-func TestRenderPostNotMentioningWebmentionsIfNoAvail(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, _ := owl.RenderPost(post)
-
- assertions.AssertNotContains(t, result, "Webmention")
-
-}
-
-func TestRenderIncludesFullUrl(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
- result, _ := owl.RenderPost(post)
-
- assertions.AssertContains(t, result, "class=\"u-url\"")
- assertions.AssertContains(t, result, post.FullUrl())
-}
-
-func TestAddAvatarIfExist(t *testing.T) {
- user := getTestUser()
- os.WriteFile(path.Join(user.MediaDir(), "avatar.png"), []byte("test"), 0644)
-
- result, _ := owl.RenderIndexPage(user)
- assertions.AssertContains(t, result, "avatar.png")
-}
-
-func TestAuthorNameInPost(t *testing.T) {
- user := getTestUser()
- user.SetConfig(owl.UserConfig{
- Title: "Test Title",
- SubTitle: "Test SubTitle",
- HeaderColor: "#ff1337",
- AuthorName: "Test Author",
- })
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "Test Author")
-}
-
-func TestRenderReplyWithoutText(t *testing.T) {
-
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{
- Type: "reply",
- Reply: owl.ReplyData{
- Url: "https://example.com/post",
- },
- }, "Hi ")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "https://example.com/post")
-}
-
-func TestRenderReplyWithText(t *testing.T) {
-
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{
- Type: "reply",
- Reply: owl.ReplyData{
- Url: "https://example.com/post",
- Text: "This is a reply",
- },
- }, "Hi ")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "https://example.com/post")
-
- assertions.AssertContains(t, result, "This is a reply")
-}
-
-func TestRengerPostContainsBookmark(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "bookmark", Bookmark: owl.BookmarkData{Url: "https://example.com/post"}}, "hi")
-
- result, _ := owl.RenderPost(post)
- assertions.AssertContains(t, result, "https://example.com/post")
-}
-
-func TestOpenGraphTags(t *testing.T) {
- user := getTestUser()
- post, _ := user.CreateNewPost(owl.PostMeta{Type: "article", Title: "testpost"}, "")
-
- content := "---\n"
- content += "title: The Rock\n"
- content += "description: Dwayne Johnson\n"
- content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
- content += "---\n"
- content += "\n"
- content += "Hi \n"
-
- os.WriteFile(post.ContentFile(), []byte(content), 0644)
- post, _ = user.GetPost(post.Id())
- result, _ := owl.RenderPost(post)
-
- assertions.AssertContains(t, result, "")
- assertions.AssertContains(t, result, "")
- assertions.AssertContains(t, result, "")
- assertions.AssertContains(t, result, "")
-
-}
-
-func TestAddFaviconIfExist(t *testing.T) {
- user := getTestUser()
- os.WriteFile(path.Join(user.MediaDir(), "favicon.png"), []byte("test"), 0644)
-
- result, _ := owl.RenderIndexPage(user)
- assertions.AssertContains(t, result, "favicon.png")
-}
-
-func TestRenderUserAuth(t *testing.T) {
- user := getTestUser()
- user.ResetPassword("test")
- result, err := owl.RenderUserAuthPage(owl.AuthRequestData{
- User: user,
- })
- assertions.AssertNoError(t, err, "Error rendering user auth page")
- assertions.AssertContains(t, result, "