From 8b511b29e64dea3c5a705d3e8121c36c36516480 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sun, 31 Mar 2024 09:30:46 -0600 Subject: [PATCH 01/14] Adds description for new ordering of pages feature. --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6073bf0..da97a2c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,12 @@ If a page contains a `` element, Andrew picks it up and uses that as the If the page does not contain a `<title>` element, then Andrew will use the file name of that file as the link name. ## ordering of pages -In this release, Andrew serves you page links ascii-betically. +If a page contains the meta element `<meta name=andrew-created-on value="2024-03-12>` in its `<head>`, Andrew orders on these tags. +If the page does not contain the meta element, it uses the mtime of the file to try and determine ordering. This means that if you edit a page +that does not contain the `andrew-created-on` element, then you will push it back to the top of the list. + +If your page contains an `andrew-created-on` meta element, the time must be formatted in accordance with <SOME STANDARD HERE>. If your `andrew-created-on` +contains a date but not a time, Andrew assumes the page was created at midnight. ## sitemap.xml When the endpoint `baseUrl/sitemap.xml` is visited, Andrew will automatically generate a sitemap containing paths to all html pages. From 22d7ff2c7fe5b7f1eea755e5dcd9d90accdd736b Mon Sep 17 00:00:00 2001 From: Gwyn <gwyn@playtechnique.io> Date: Sun, 31 Mar 2024 09:43:16 -0600 Subject: [PATCH 02/14] Starting to work out what getting the pages sorted means. This is beginning by adding a new test to establish the interface. --- andrew_server.go | 5 +-- andrew_server_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++ page_parser.go | 3 ++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/andrew_server.go b/andrew_server.go index 18c6b2e..6e0ada7 100644 --- a/andrew_server.go +++ b/andrew_server.go @@ -99,7 +99,7 @@ func (a AndrewServer) Serve(w http.ResponseWriter, r *http.Request) { // If the page does not contain this element, it is written to the http.ResponseWriter as it is. // If the page does contain an AndrewIndexBody element, serveIndexPage calls out to buildIndexBody to create // the correct body of the page and then renders it into the AndrewIndexBody. -func (a AndrewServer) serveIndexPage(w http.ResponseWriter, r *http.Request, pagePath string) { +func (a AndrewServer) serveIndexPage(w http.ResponseWriter, _ *http.Request, pagePath string) { // /index.html becomes index.html // /articles/page.html becomes articles/page.html @@ -135,7 +135,8 @@ func (a AndrewServer) serveIndexPage(w http.ResponseWriter, r *http.Request, pag } } -// buildAndrewIndexBody receives the path to a file. It traverses the file system starting at the directory containing +// buildAndrewIndexBody receives the path to a file, currently normally an index file. +// It traverses the file system starting at the directory containing // that file, finds all html files that are _not_ index.html files and returns them // as a list of html links to those pages. func (a AndrewServer) buildAndrewIndexBody(indexPagePath string) ([]string, error) { diff --git a/andrew_server_test.go b/andrew_server_test.go index d46540a..e528b50 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -14,6 +14,7 @@ import ( "strings" "testing" "testing/fstest" + "time" "github.com/google/go-cmp/cmp" "github.com/playtechnique/andrew" @@ -344,6 +345,81 @@ func TestMainCalledWithHelpDisplaysHelp(t *testing.T) { } } +func TestMainCalledWithNoArgsUsesDefaults(t *testing.T) { + t.Parallel() + + contentRoot, address, baseUrl := andrew.ParseArgs([]string{}) + + if contentRoot != andrew.DefaultContentRoot { + t.Errorf("contentroot should be %s, received %q", andrew.DefaultContentRoot, contentRoot) + } + + if address != andrew.DefaultAddress { + t.Errorf("address should be %s, received %q", andrew.DefaultAddress, address) + } + + if baseUrl != andrew.DefaultBaseUrl { + t.Errorf("baseUrl should be %s, received %q", andrew.DefaultBaseUrl, baseUrl) + } + +} + +func TestMainCalledWithArgsOverridesDefaults(t *testing.T) { + t.Parallel() + + contentRoot, address, baseUrl := andrew.ParseArgs([]string{"1", "2", "3"}) + + if contentRoot != "1" { + t.Errorf("contentroot should be %s, received %q", "1", contentRoot) + } + + if address != "2" { + t.Errorf("address should be %s, received %q", "2", address) + } + + if baseUrl != "3" { + t.Errorf("baseUrl should be %s, received %q", "3", baseUrl) + } + +} + +func TestMainCalledWithInvalidAddressPanics(t *testing.T) { + t.Parallel() + args := []string{".", "notanipaddress"} + nullLogger := new(bytes.Buffer) + + // No need to check whether `recover()` is nil. Just turn off the panic. + defer func() { + err := recover() + if err == nil { + t.Fatalf("Expected panic with invalid address, received %v", err) + } + }() + + andrew.Main(args, nullLogger) + +} + +func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { + + expected := ` +<buncha hrefs> +` + contentRoot := fstest.MapFS{ + "b.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 6, 0, 0, 0, time.UTC)}, + "a.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 5, 0, 0, 0, time.UTC)}, + "index.html": &fstest.MapFile{Data: []byte("{{ .AndrewIndexBody }}")}, + } + + page := andrew.AndrewPage{Path: path} + received := page.GenerateAndrewIndexBody("index.html") + + if expected != received { + t.Errorf(cmp.Diff(expected, received)) + } + +} + // startAndrewServer starts an andrew and returns the localhost url that you can run http gets against // to retrieve data from that server func startAndrewServer(t *testing.T, contentRoot fs.FS) string { diff --git a/page_parser.go b/page_parser.go index d47773f..ee706cb 100644 --- a/page_parser.go +++ b/page_parser.go @@ -8,6 +8,9 @@ import ( "golang.org/x/net/html" ) +type AndrewPage struct { +} + // titleFromHTMLTitleElement returns the content of the "title" tag or an empty string. // The error value "no title element found" is returned if title is not discovered // or is set to an empty string. From 3ec4af7247d898863ee1a2381a024d2a606b81c5 Mon Sep 17 00:00:00 2001 From: Gwyn <gwyn@playtechnique.io> Date: Fri, 19 Apr 2024 09:00:14 -0600 Subject: [PATCH 03/14] The test that shows whether we are successfully sorting on pages. --- andrew_server_test.go | 52 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/andrew_server_test.go b/andrew_server_test.go index 456c717..b574fd9 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -174,10 +174,10 @@ func TestGettingADirectoryDefaultsToIndexHtml(t *testing.T) { </body> `) + // fstest.MapFS does not create directory-like objects, so we need a real file system in this test. contentRoot := t.TempDir() os.MkdirAll(contentRoot+"/pages", 0o755) - // fstest.MapFS does not create directory-like objects, so we need a real file system in this test. err := os.WriteFile(contentRoot+"/pages/index.html", expected, 0o755) if err != nil { t.Fatal(err) @@ -292,7 +292,6 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { } testUrl := startAndrewServer(t, contentRoot) - resp, err := http.Get(testUrl + "/parentDir/index.html") if err != nil { @@ -305,6 +304,9 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { t.Fatal(err) } + // The test is displaying parentDir/childDir/1-2-3.html as its link; this is because generateAndrewIndexBody now returns AndrewPages, + // and the link that these maintain internally is their URL. Instead of the URL, we need a link path. + // GetSiblingsAndChildren maintains a localContentRoot variable that contains the directory we are residing within expectedIndex := ` <!doctype HTML> <head> </head> @@ -439,21 +441,53 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) { } +// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that +// when the list of links andrew generates for the {{.AndrewIndexBody}} are +// sorted by mtime, not using the ascii sorting order. func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { expected := ` <buncha hrefs> ` - contentRoot := fstest.MapFS{ - "b.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 6, 0, 0, 0, time.UTC)}, - "a.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 5, 0, 0, 0, time.UTC)}, - "index.html": &fstest.MapFile{Data: []byte("{{ .AndrewIndexBody }}")}, + + contentRoot := t.TempDir() + + // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. + // above might be wrong + err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } + + // This test requires having two files which are in one order when sorted + // ascii-betically and in another order by date time, so that we can tell + // what file attribute andrew is actually sorting on. + now := time.Now() + older := now.Add(-10 * time.Minute) + + os.Chtimes(contentRoot+"/a.html", now, now) + os.Chtimes(contentRoot+"/b.html", older, older) + + server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot)} + page, err := andrew.NewPage(server, "index.html") + + if err != nil { + t.Fatal(err) } - page := andrew.NewPage(contentRoot, "index.html") - received := page.GenerateAndrewIndexBody() + received := page.Content - if expected != received { + if expected != string(received) { t.Errorf(cmp.Diff(expected, received)) } From 96432b75072d26c5467bb036426415c24ad464bf Mon Sep 17 00:00:00 2001 From: Gwyn <gwyn@playtechnique.io> Date: Tue, 23 Apr 2024 22:03:04 -0600 Subject: [PATCH 04/14] Starting to prod at the meta tag --- README.md | 11 +- andrew_server.go | 13 +- andrew_server_test.go | 2 +- page.go | 77 ++-- page.html | 984 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1051 insertions(+), 36 deletions(-) create mode 100644 page.html diff --git a/README.md b/README.md index e79138f..81c97c7 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,17 @@ if fanfics/index.html contains `{{ .AndrewIndexBody }}`, that'll be replaced wit If a page contains a `<title>` element, Andrew picks it up and uses that as the name of a link. If the page does not contain a `<title>` element, then Andrew will use the file name of that file as the link name. +## meta elements +Andrew parses meta tags and makes them accessible on its AndrewPage object. + +For a meta element to be picked up, it must be formatted with andrew- prepending the meta element's name, like this `<meta name="andrew-<rest of the name>" value="your-value">` + +### valid meta elements +<meta name="andrew-created-on" value="2024-03-12"> +<meta name="andrew-tag" value="diary entry"> + ## ordering of pages -If a page contains the meta element `<meta name=andrew-created-on value="2024-03-12>` in its `<head>`, Andrew orders on these tags. +If a page contains the meta element `<meta name=andrew-created-on value="2024-03-12">` in its `<head>`, Andrew orders on these tags. If the page does not contain the meta element, it uses the mtime of the file to try and determine ordering. This means that if you edit a page that does not contain the `andrew-created-on` element, then you will push it back to the top of the list. diff --git a/andrew_server.go b/andrew_server.go index 759335f..46ea799 100644 --- a/andrew_server.go +++ b/andrew_server.go @@ -19,23 +19,24 @@ type AndrewServer struct { SiteFiles fs.FS //The files being served BaseUrl string //The URL used in any links generated for this website that should contain the hostname. Address string //IpAddress:Port combo to be served on. - andrewindexbodytemplate string //The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server. + Andrewindexbodytemplate string //The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server. } const ( - DefaultContentRoot = "." - DefaultAddress = ":8080" - DefaultBaseUrl = "http://localhost:8080" + AndrewIndexBodyTemplate = "AndrewIndexBody" + DefaultContentRoot = "." + DefaultAddress = ":8080" + DefaultBaseUrl = "http://localhost:8080" ) func NewAndrewServer(contentRoot fs.FS, address string, baseUrl string) (AndrewServer, error) { - return AndrewServer{SiteFiles: contentRoot, andrewindexbodytemplate: "AndrewIndexBody", Address: address, BaseUrl: baseUrl}, nil + return AndrewServer{SiteFiles: contentRoot, Andrewindexbodytemplate: "AndrewIndexBody", Address: address, BaseUrl: baseUrl}, nil } func Main(args []string, printDest io.Writer) int { help := `Usage: andrew [contentRoot] [address] [baseUrl] - contentRoot: The root directory of your content. Defaults to '.' if not specified. - - address: The address to bind to. Defaults to 'localhost:8080' if not specified. If in doubt, you probably want 0.0.0.0:<something> + - address: The address to bind to. Defaults to 'localhost:8080' if not specified. If in doubt, you probably want '0.0.0.0:<some free port>' - base URL: The protocol://hostname for your server. Defaults to 'http://localhost:8080' if not specified. Used to generate sitemap/rss feed accurately. -h, --help: Display this help message. diff --git a/andrew_server_test.go b/andrew_server_test.go index b574fd9..ed424b1 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -478,7 +478,7 @@ func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { os.Chtimes(contentRoot+"/a.html", now, now) os.Chtimes(contentRoot+"/b.html", older, older) - server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot)} + server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} page, err := andrew.NewPage(server, "index.html") if err != nil { diff --git a/page.go b/page.go index db96184..ca2f286 100644 --- a/page.go +++ b/page.go @@ -16,8 +16,8 @@ type AndrewPage struct { Title string // According to https://datatracker.ietf.org/doc/html/rfc1738#section-3.1, the subsection of a // URL after the procol://hostname is the UrlPath. - UrlPath string - // + UrlPath string + Meta []string Content string PublishTime time.Time } @@ -57,6 +57,8 @@ func NewPage(server AndrewServer, pageUrl string) (AndrewPage, error) { } } + // pageMeta := getMeta(pagePath, pageContent) + return AndrewPage{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}, nil } @@ -102,7 +104,7 @@ func buildAndrewIndexBody(server AndrewServer, startingPageUrl string, pageConte panic(err) } - err = t.Execute(&templateBuffer, map[string]string{server.andrewindexbodytemplate: links.String()}) + err = t.Execute(&templateBuffer, map[string]string{server.Andrewindexbodytemplate: links.String()}) if err != nil { //TODO: swap this for proper error handling @@ -121,39 +123,41 @@ func buildAndrewIndexLink(page AndrewPage, cssIdNumber int) []byte { return b } -// titleFromHTMLTitleElement returns the content of the "title" tag or an empty string. -// The error value "no title element found" is returned if title is not discovered -// or is set to an empty string. -func titleFromHTMLTitleElement(fileContent []byte) (string, error) { - - doc, err := html.Parse(bytes.NewReader(fileContent)) - if err != nil { - return "", err +// getAttribute recursively descends an html node tree, searching for +// the attribute provided. Once the attribute is discovered, it returns. +func getAttributes(attribute string, n *html.Node) []string { + var attributes []string + + //n.Type no longer matches html.ElementNode; n is now a document, not a node + if n.Type == html.ElementNode { + for _, a := range n.Attr { + if a.Key == attribute { + attributes = append(attributes, a.Val) + } + } } - title := getAttribute("title", doc) - if title == "" { - return "", fmt.Errorf("no title element found") + for c := n.FirstChild; c != nil; c = c.NextSibling { + attributes = append(attributes, getAttributes(attribute, c)...) } - return title, nil + + return attributes } -// getAttribute recursively descends an html node tree, searching for -// the attribute provided. Once the attribute is discovered, it returns. -func getAttribute(attribute string, n *html.Node) string { - if n.Type == html.ElementNode && n.Data == attribute { - if n.FirstChild != nil { - return n.FirstChild.Data - } +func getMeta(htmlContent []byte) ([]string, error) { + element := "meta" + + doc, err := html.Parse(bytes.NewReader(htmlContent)) + if err != nil { + return []string{}, err } - for c := n.FirstChild; c != nil; c = c.NextSibling { - result := getAttribute(attribute, c) - if result != "" { - return result - } + meta := getAttributes(element, doc) + + if len(meta) == 0 { + return meta, fmt.Errorf("no %s element found", element) } - return "" + return meta, nil } func getTitle(htmlFilePath string, htmlContent []byte) (string, error) { @@ -168,3 +172,20 @@ func getTitle(htmlFilePath string, htmlContent []byte) (string, error) { } return title, nil } + +// titleFromHTMLTitleElement returns the content of the "title" tag or an empty string. +// The error value "no title element found" is returned if title is not discovered +// or is set to an empty string. +func titleFromHTMLTitleElement(fileContent []byte) (string, error) { + + doc, err := html.Parse(bytes.NewReader(fileContent)) + if err != nil { + return "", err + } + + title := getAttributes("title", doc) + if len(title) == 0 { + return "", fmt.Errorf("no title element found") + } + return title[0], nil +} diff --git a/page.html b/page.html new file mode 100644 index 0000000..c9f0c8f --- /dev/null +++ b/page.html @@ -0,0 +1,984 @@ +<head> + + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" id="wixDesktopViewport"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="generator" content="Wix.com Website Builder"> + + <link rel="icon" sizes="192x192" href="https://static.wixstatic.com/media/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif/v1/fill/w_192%2Ch_192%2Clg_1%2Cusm_0.66_1.00_0.01/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif" type="image/png"> + <link rel="shortcut icon" href="https://static.wixstatic.com/media/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif/v1/fill/w_32%2Ch_32%2Clg_1%2Cusm_0.66_1.00_0.01/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif" type="image/png"> + <link rel="apple-touch-icon" href="https://static.wixstatic.com/media/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif/v1/fill/w_180%2Ch_180%2Clg_1%2Cusm_0.66_1.00_0.01/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif" type="image/png"> + + <!-- Safari Pinned Tab Icon --> + <!-- <link rel="mask-icon" href="https://static.wixstatic.com/media/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif/v1/fill/w_32%2Ch_32%2Clg_1%2Cusm_0.66_1.00_0.01/0cad88_e47565df768144e29a7a45eb260cb03f%7Emv2.gif"> --> + + <!-- Original trials --> + + <script type="text/javascript" async="" src="https://assets.pinterest.com/js/pinit_main.js?0.20832230090654258"></script><script src="https://browser.sentry-cdn.com/6.18.2/bundle.min.js"></script><script type="text/javascript" async="" src="https://www.google-analytics.com/plugins/ua/ec.js"></script><script type="text/javascript" async="" src="https://www.google-analytics.com/analytics.js"></script><script type="text/javascript" async="" src="https://www.googletagmanager.com/gtag/js?id=G-816TP4563Y&l=dataLayer&cx=c"></script><script async="" src="https://static.parastorage.com/services/wix-thunderbolt/dist/originTrials.41d7301a.bundle.min.js"></script> + + + <!-- Legacy Polyfills --> + <script nomodule="" src="https://static.parastorage.com/unpkg/core-js-bundle@3.2.1/minified.js"></script> + <script nomodule="" src="https://static.parastorage.com/unpkg/focus-within-polyfill@5.0.9/dist/focus-within-polyfill.js"></script> + + + <script nomodule="" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=fetch"></script> + + + <!-- Performance API Polyfills --> + <script> + (function () { + var noop = function noop() {}; + if ("performance" in window === false) { + window.performance = {}; + } + window.performance.mark = performance.mark || noop; + window.performance.measure = performance.measure || noop; + if ("now" in window.performance === false) { + var nowOffset = Date.now(); + if (performance.timing && performance.timing.navigationStart) { + nowOffset = performance.timing.navigationStart; + } + window.performance.now = function now() { + return Date.now() - nowOffset; + }; + } + })(); + </script> + + <!-- Globals Definitions --> + <script> + (function () { + var now = Date.now() + window.initialTimestamps = { + initialTimestamp: now, + initialRequestTimestamp: Math.round(performance.timeOrigin ? performance.timeOrigin : now - performance.now()) + } + + window.thunderboltTag = "libs-releases-GA-local" + window.thunderboltVersion = "1.13916.0" + })(); + </script> + + + + + <!-- sendFedopsLoadStarted.inline --> + <script type="application/json" id="wix-fedops">{"data":{"site":{"metaSiteId":"ab4d26d1-8d2c-49ee-a64a-c9ff2ad0df20","userId":"13af7afe-d37f-4ccf-b177-d1b132dc4735","siteId":"d236c163-a543-4c54-9b39-38b58fcfe2ff","externalBaseUrl":"https:\/\/www.nocoastcrossfit.com","siteRevision":839,"siteType":"UGC","dc":"42","isResponsive":false,"editorName":"Unknown","sessionId":"22a895f1-013d-40ca-a943-9acfca428499","isSEO":false,"appNameForBiEvents":"thunderbolt"},"rollout":{"siteAssetsVersionsRollout":false,"isDACRollout":0,"isTBRollout":false},"fleetConfig":{"fleetName":"wix-thunderbolt","type":"GA","code":0},"requestUrl":"https:\/\/www.nocoastcrossfit.com\/","isInSEO":false,"platformOnSite":true}}</script> + <script>window.fedops = JSON.parse(document.getElementById('wix-fedops').textContent)</script> + <script id="sendFedopsLoadStarted"> + !function(){function r(r,e){var t,n=Object.keys(r);return Object.getOwnPropertySymbols&&(t=Object.getOwnPropertySymbols(r),e&&(t=t.filter(function(e){return Object.getOwnPropertyDescriptor(r,e).enumerable})),n.push.apply(n,t)),n}function e(n){for(var e=1;e<arguments.length;e++){var i=null!=arguments[e]?arguments[e]:{};e%2?r(Object(i),!0).forEach(function(e){var r,t;r=n,e=i[t=e],(t=function(e){e=function(e,r){if("object"!=typeof e||null===e)return e;var t=e[Symbol.toPrimitive];if(void 0===t)return("string"===r?String:Number)(e);r=t.call(e,r||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}(e,"string");return"symbol"==typeof e?e:String(e)}(t))in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e}):Object.getOwnPropertyDescriptors?Object.defineProperties(n,Object.getOwnPropertyDescriptors(i)):r(Object(i)).forEach(function(e){Object.defineProperty(n,e,Object.getOwnPropertyDescriptor(i,e))})}return n}var n=function(e){var r,t,n=!1;if(null===(r=window.viewerModel)||void 0===r||!/\(iP(hone|ad|od);/i.test(null===(r=window)||void 0===r||null===(t=r.navigator)||void 0===t?void 0:t.userAgent))try{n=navigator.sendBeacon(e)}catch(e){}n||((new Image).src=e)};var t,i,o,a,s=window.fedops.data,c=s.site,d=s.rollout,l=s.fleetConfig,u=s.requestUrl,p=s.isInSEO,f=s.frogOnUserDomain,m=function(e){e=e.navigator.userAgent;return!/instagram.+google\/google/i.test(e)&&/bot|google(?!play)|phantom|crawl|spider|headless|slurp|facebookexternal|Lighthouse|PTST|^mozilla\/4\.0$|^\s*$/i.test(e)?"ua":""}(window)||function(){try{if(window.self===window.top)return""}catch(e){}return"iframe"}()||function(){if(!Function.prototype.bind)return"bind";var e=window,r=e.document,t=e.navigator;if(!r||!t)return"document";var n=t.webdriver,i=t.userAgent,e=t.plugins,t=t.languages;if(n)return"webdriver";if(!e||Array.isArray(e))return"plugins";if(null!==(e=Object.getOwnPropertyDescriptor(e,"0"))&&void 0!==e&&e.writable)return"plugins-extra";if(!i)return"userAgent";if(0<i.indexOf("Snapchat")&&r.hidden)return"Snapchat";if(!t||0===t.length||!Object.isFrozen(t))return"languages";try{throw Error()}catch(e){if(e instanceof Error){t=e.stack;if(t&&/ (\(internal\/)|(\(?file:\/)/.test(t))return"stack"}}return""}()||(p?"seo":""),w=!!m,p=(t=document.cookie,i=function(){return performance.getEntriesByType("navigation")[0].serverTiming||[]},a="none",!(t=t.match(/ssr-caching="?cache[,#]\s*desc=([\w-]+)(?:[,#]\s*varnish=(\w+))?(?:[,#]\s*dc[,#]\s*desc=([\w-]+))?(?:"|;|$)/))&&window.PerformanceServerTiming&&(o=(i=function(e){var r,t;try{r=e()}catch(e){r=[]}var n=[];return r.forEach(function(e){switch(e.name){case"cache":n[1]=e.description;break;case"varnish":n[2]=e.description;break;case"dc":t=e.description}}),{microPop:t,matches:n}}(i)).microPop,t=i.matches),t&&t.length&&(a=`${t[1]},${t[2]||"none"}`,o=o||t[3]),"none"!==a||(t="undefined"!=typeof performance?performance.timing:null)&&t.responseStart-t.requestStart==0&&(a="browser"),e({caching:a,isCached:a.includes("hit")},o?{microPop:o}:{})),v=p.isCached,g=p.caching,h=p.microPop,b={WixSite:1,UGC:2,Template:3}[c.siteType]||0,x=c.isResponsive?"thunderbolt-responsive":"thunderbolt",p=d.isDACRollout,d=d.siteAssetsVersionsRollout,y=p?1:0,O=d?1:0,S=0===l.code||1===l.code?l.code:null,P=Date.now()-window.initialTimestamps.initialTimestamp,T=Math.round(performance.now()),j=document.visibilityState,d=window,_=d.fedops,l=d.addEventListener,A=d.thunderboltVersion;_.apps=_.apps||{},_.apps[x]={startLoadTime:T},_.sessionId=c.sessionId,_.vsi="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var r=16*Math.random()|0;return("x"===e?r:3&r|8).toString(16)}),_.is_cached=v,_.phaseStarted=I(28),_.phaseEnded=I(22),performance.mark("[cache] "+g+(h?" ["+h+"]":"")),_.reportError=function(e){var r=(null==e?void 0:e.reason)||(null==e?void 0:e.message);r?($(26,`&errorInfo=${r}&errorType=load`),k({error:{name:"load",message:r,stack:e?.stack}})):e.preventDefault()},l("error",_.reportError),l("unhandledrejection",_.reportError);var E=!1;function $(e,r){var t=1<arguments.length&&void 0!==r?r:"";u.includes("suppressbi=true")||(t=(f?c.externalBaseUrl.replace(/^https?:\/\//,"")+"/_frog":"//frog.wix.com")+"/bolt-performance?src=72&evid="+e+"&appName="+x+"&is_rollout="+S+"&is_sav_rollout="+O+"&is_dac_rollout="+y+"&dc="+c.dc+(h?"µPop="+h:"")+"&is_cached="+v+"&msid="+c.metaSiteId+"&session_id="+window.fedops.sessionId+"&ish="+w+"&isb="+w+(w?"&isbr="+m:"")+"&vsi="+window.fedops.vsi+"&caching="+g+(E?",browser_cache":"")+"&pv="+j+"&pn=1&v="+A+"&url="+encodeURIComponent(u)+"&st="+b+`&ts=${P}&tsn=${T}`+t,n(t))}function k({transaction:e,error:r}){e=[{fullArtifactId:"com.wixpress.html-client.wix-thunderbolt",componentId:`thunderbolt${window.fedops.data.site.isResponsive?"-responsive":""}`,platform:"viewer",msid:window.fedops.data.site.metaSiteId,sessionId:window.fedops.sessionId,sessionTime:Date.now()-window.initialTimestamps.initialTimestamp,logLevel:r?"ERROR":"INFO",message:r?.message??(e?.name&&`${e.name} START`),errorName:r?.name,errorStack:r?.stack,transactionName:e?.name,transactionAction:e&&"START",isSsr:!1,dataCenter:c.dc,isCached:!!v,isRollout:!!S,isHeadless:!!w,isDacRollout:!!y,isSavRollout:!!O}];try{var t=JSON.stringify({messages:e});return navigator.sendBeacon("https://panorama.wixapps.net/api/v1/bulklog",t)}catch(e){console.error(e)}}function I(n){return function(e,r){var t=`&name=${e}&duration=${Date.now()-P}`,e=r&&r.paramsOverrides?Object.keys(r.paramsOverrides).map(function(e){return e+"="+r.paramsOverrides[e]}).join("&"):"";$(n,e?`${t}&${e}`:t)}}l("pageshow",function(e){e.persisted&&(E||(E=!0,_.is_cached=!0))},!0),window.__browser_deprecation__||($(21,`&platformOnSite=${window.fedops.data.platformOnSite}`),k({transaction:{name:"PANORAMA_COMPONENT_LOAD"}}))}(); + </script> + + <!-- Polyfills check --> + <script> + if ( + typeof Promise === 'undefined' || + typeof Set === 'undefined' || + typeof Object.assign === 'undefined' || + typeof Array.from === 'undefined' || + typeof Symbol === 'undefined' + ) { + // send bi in order to detect the browsers in which polyfills are not working + window.fedops.phaseStarted('missing_polyfills') + } + </script> + + <!-- Essential Viewer Model --> + <script type="application/json" id="wix-essential-viewer-model">{"fleetConfig":{"fleetName":"wix-thunderbolt","type":"GA","code":0},"mode":{"qa":false,"enableTestApi":false,"debug":false,"ssrIndicator":false,"ssrOnly":false,"siteAssetsFallback":"enable"},"componentsLibrariesTopology":[{"artifactId":"editor-elements","namespace":"wixui","url":"https:\/\/static.parastorage.com\/services\/editor-elements\/1.12021.0"},{"artifactId":"editor-elements","namespace":"dsgnsys","url":"https:\/\/static.parastorage.com\/services\/editor-elements\/1.12021.0"}],"siteFeaturesConfigs":{"sessionManager":{"isRunningInDifferentSiteContext":false}},"language":{"userLanguage":"en"},"siteAssets":{"clientTopology":{"mediaRootUrl":"https:\/\/static.wixstatic.com","staticMediaUrl":"https:\/\/static.wixstatic.com\/media","moduleRepoUrl":"https:\/\/static.parastorage.com\/unpkg","fileRepoUrl":"https:\/\/static.parastorage.com\/services","siteAssetsUrl":"https:\/\/siteassets.parastorage.com","pageJsonServerUrls":["https:\/\/pages.parastorage.com","https:\/\/staticorigin.wixstatic.com","https:\/\/www.nocoastcrossfit.com","https:\/\/fallback.wix.com\/wix-html-editor-pages-webapp\/page"],"pathOfTBModulesInFileRepoForFallback":"wix-thunderbolt\/dist\/"}},"siteFeatures":["assetsLoader","businessLogger","captcha","clickHandlerRegistrar","commonConfig","componentsLoader","componentsRegistry","consentPolicy","cyclicTabbing","environmentWixCodeSdk","environment","lightbox","locationWixCodeSdk","navigationManager","navigationPhases","ooi","pages","panorama","renderer","reporter","routerFetch","router","scrollRestoration","seoWixCodeSdk","seo","sessionManager","siteMembersWixCodeSdk","siteMembers","siteScrollBlocker","siteWixCodeSdk","stores","structureApi","thunderboltInitializer","tpaCommons","translations","warmupData","windowMessageRegistrar","windowWixCodeSdk","wixCustomElementComponent","wixEmbedsApi","componentsReact","platform"],"site":{"externalBaseUrl":"https:\/\/www.nocoastcrossfit.com","isSEO":false},"media":{"staticMediaUrl":"https:\/\/static.wixstatic.com\/media","mediaRootUrl":"https:\/\/static.wixstatic.com\/","staticVideoUrl":"https:\/\/video.wixstatic.com\/"},"requestUrl":"https:\/\/www.nocoastcrossfit.com\/","rollout":{"siteAssetsVersionsRollout":false,"isDACRollout":0,"isTBRollout":false},"commonConfig":{"brand":"wix","host":"VIEWER","bsi":"","consentPolicy":{},"consentPolicyHeader":{}},"interactionSampleRatio":0.01,"dynamicModelUrl":"https:\/\/www.nocoastcrossfit.com\/_api\/v2\/dynamicmodel","accessTokensUrl":"https:\/\/www.nocoastcrossfit.com\/_api\/v1\/access-tokens","experiments":{"specs.thunderbolt.allowWEBPTransformation":true}}</script> + <script>window.viewerModel = JSON.parse(document.getElementById('wix-essential-viewer-model').textContent)</script> + + + <script data-url="https://static.parastorage.com/services/wix-thunderbolt/dist/handleAccessTokens.inline.1abdcf11.bundle.min.js">!function(){"use strict";const e="tbReady",n=window.viewerModel.experiments["specs.thunderbolt.replaceDynamicModel"]?window.viewerModel.accessTokensUrl:window.viewerModel.dynamicModelUrl;if(viewerModel.experiments["specs.thunderbolt.hardenFetchAndXHR"]){let i=fetch;function t(){try{window.tb.init({fetch:i})}catch(e){console.error("Failed to initialize Thunderbolt:",e)}finally{window.removeEventListener(e,t),i=fetch}}addEventListener(e,t)}else window.fetchDynamicModel=()=>window.viewerModel.siteFeaturesConfigs.sessionManager.isRunningInDifferentSiteContext?Promise.resolve({}):fetch(n,{credentials:"same-origin"}).then((function(e){if(!e.ok)throw new Error(`[${e.status}]${e.statusText}`);return e.json()})),window.dynamicModelPromise=window.fetchDynamicModel()}(); +//# sourceMappingURL=https://static.parastorage.com/services/wix-thunderbolt/dist/handleAccessTokens.inline.1abdcf11.bundle.min.js.map</script> + + + <script> + window.commonConfig = viewerModel.commonConfig + + </script> + + + <!-- initCustomElements --> + <meta name="wix-dynamic-custom-elements" content="DropDownMenu"> + + <!-- Initial CSS --> + <style data-url="https://static.parastorage.com/services/wix-thunderbolt/dist/main.c1956e3f.min.css">body.device-mobile-optimized{overflow-x:hidden;overflow-y:scroll}body.device-mobile-optimized:not(.responsive) #SITE_CONTAINER{margin-left:auto;margin-right:auto;overflow-x:visible;position:relative;width:320px}body.device-mobile-optimized:not(.responsive):not(.blockSiteScrolling) #SITE_CONTAINER{margin-top:0}body.device-mobile-optimized>*{max-width:100%!important}body.device-mobile-optimized #site-root{overflow-x:hidden;overflow-y:hidden}@supports(overflow:clip){body.device-mobile-optimized #site-root{overflow-x:clip;overflow-y:clip}}body.device-mobile-non-optimized #SITE_CONTAINER #site-root{overflow-x:hidden;overflow-y:auto}body.device-mobile-non-optimized.fullScreenMode{background-color:#5f6360}body.device-mobile-non-optimized.fullScreenMode #MOBILE_ACTIONS_MENU,body.device-mobile-non-optimized.fullScreenMode #SITE_BACKGROUND,body.device-mobile-non-optimized.fullScreenMode #site-root,body.fullScreenMode #WIX_ADS{visibility:hidden}body.fullScreenMode{overflow-x:hidden!important;overflow-y:hidden!important}body.fullScreenMode.device-mobile-optimized #TINY_MENU{opacity:0;pointer-events:none}body.fullScreenMode-scrollable.device-mobile-optimized{overflow-x:hidden!important;overflow-y:auto!important}body.fullScreenMode-scrollable.device-mobile-optimized #masterPage,body.fullScreenMode-scrollable.device-mobile-optimized #site-root{overflow-x:hidden!important;overflow-y:hidden!important}body.fullScreenMode-scrollable.device-mobile-optimized #SITE_BACKGROUND,body.fullScreenMode-scrollable.device-mobile-optimized #masterPage{height:auto!important}body.fullScreenMode-scrollable.device-mobile-optimized #masterPage.mesh-layout{height:0!important}body.blockSiteScrolling{position:fixed;width:100%}body.blockSiteScrolling #SITE_CONTAINER{margin-top:calc(var(--blocked-site-scroll-margin-top)*-1)}body.blockSiteScrolling:not(.responsive) #WIX_ADS{margin-top:var(--blocked-site-scroll-margin-top)}body.blockSiteScrollingWithOverflow{overflow-y:hidden}@keyframes slide-horizontal-new{0%{transform:translateX(100%)}}@keyframes slide-horizontal-old{80%{opacity:1}to{opacity:0;transform:translateX(-100%)}}@keyframes slide-vertical-new{0%{transform:translateY(-100%)}}@keyframes slide-vertical-old{80%{opacity:1}to{opacity:0;transform:translateY(100%)}}@keyframes out-in-new{0%{opacity:0}}@keyframes out-in-old{to{opacity:0}}html[data-page-transition=SlideHorizontal]::view-transition-old(page-group){animation:slide-horizontal-old .6s cubic-bezier(.83,0,.17,1) forwards;mix-blend-mode:normal}html[data-page-transition=SlideHorizontal]::view-transition-new(page-group){animation:slide-horizontal-new .6s cubic-bezier(.83,0,.17,1) backwards;mix-blend-mode:normal}html[data-page-transition=SlideVertical]::view-transition-old(page-group){animation:slide-vertical-old .6s cubic-bezier(.83,0,.17,1) forwards;mix-blend-mode:normal}html[data-page-transition=SlideVertical]::view-transition-new(page-group){animation:slide-vertical-new .6s cubic-bezier(.83,0,.17,1) backwards;mix-blend-mode:normal}html[data-page-transition=OutIn]::view-transition-old(page-group){animation:out-in-old .35s cubic-bezier(.64,0,.78,0) forwards}html[data-page-transition=OutIn]::view-transition-new(page-group){animation:out-in-new .35s cubic-bezier(.22,1,.36,1) .35s backwards}@media(prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-new(*),::view-transition-old(*){animation:none!important}}body,html{background:transparent;border:0;margin:0;outline:0;padding:0;vertical-align:baseline}body{--scrollbar-width:0px;font-family:Arial,Helvetica,sans-serif;font-size:10px}body,html{height:100%}body{overflow-x:auto;overflow-y:scroll}body:not(.responsive) #site-root{min-width:var(--site-width);width:100%}body:not([data-js-loaded]) [data-hide-prejs]{visibility:hidden}#SITE_CONTAINER{position:relative}:root{--one-unit:1vw}@supports(container-type:inline-size){:root{--one-unit:1cqw}}</style> +<style data-url="https://static.parastorage.com/services/wix-thunderbolt/dist/main.renderer.ab468218.min.css">a,abbr,acronym,address,applet,b,big,blockquote,button,caption,center,cite,code,dd,del,dfn,div,dl,dt,em,fieldset,font,footer,form,h1,h2,h3,h4,h5,h6,header,i,iframe,img,ins,kbd,label,legend,li,nav,object,ol,p,pre,q,s,samp,section,small,span,strike,strong,sub,sup,table,tbody,td,tfoot,th,thead,title,tr,tt,u,ul,var{background:transparent;border:0;margin:0;outline:0;padding:0;vertical-align:baseline}input,select,textarea{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif}ol,ul{list-style:none}blockquote,q{quotes:none}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}a{cursor:pointer;text-decoration:none}.testStyles{overflow-y:hidden}.reset-button{-webkit-appearance:none;background:none;border:0;color:inherit;font:inherit;line-height:normal;outline:0;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}:focus{outline:none}#site-root{margin:0 auto;min-height:100%;position:relative;top:var(--wix-ads-height)}#site-root img:not([src]){visibility:hidden}#site-root svg img:not([src]){visibility:visible}.auto-generated-link{color:inherit}#SCROLL_TO_BOTTOM,#SCROLL_TO_TOP{height:0}.has-click-trigger{cursor:pointer}.fullScreenOverlay{bottom:0;display:flex;justify-content:center;left:0;overflow-y:hidden;position:fixed;right:0;top:-60px;z-index:1005}.fullScreenOverlay>.fullScreenOverlayContent{bottom:0;left:0;margin:0 auto;overflow:hidden;position:absolute;right:0;top:60px;transform:translateZ(0)}[data-mesh-id$=centeredContent],[data-mesh-id$=form],[data-mesh-id$=inlineContent]{pointer-events:none;position:relative}[data-mesh-id$=-gridWrapper],[data-mesh-id$=-rotated-wrapper]{pointer-events:none}[data-mesh-id$=-gridContainer]>*,[data-mesh-id$=-rotated-wrapper]>*,[data-mesh-id$=inlineContent]>:not([data-mesh-id$=-gridContainer]){pointer-events:auto}.device-mobile-optimized #masterPage.mesh-layout #SOSP_CONTAINER_CUSTOM_ID{grid-area:2/1/3/2;-ms-grid-row:2;position:relative}#masterPage.mesh-layout{-ms-grid-rows:max-content max-content min-content max-content;-ms-grid-columns:100%;align-items:start;display:-ms-grid;display:grid;grid-template-columns:100%;grid-template-rows:max-content max-content min-content max-content;justify-content:stretch}#masterPage.mesh-layout #PAGES_CONTAINER,#masterPage.mesh-layout #SITE_FOOTER-placeholder,#masterPage.mesh-layout #SITE_FOOTER_WRAPPER,#masterPage.mesh-layout #SITE_HEADER-placeholder,#masterPage.mesh-layout #SITE_HEADER_WRAPPER,#masterPage.mesh-layout #SOSP_CONTAINER_CUSTOM_ID[data-state~=mobileView],#masterPage.mesh-layout #soapAfterPagesContainer,#masterPage.mesh-layout #soapBeforePagesContainer{-ms-grid-row-align:start;-ms-grid-column-align:start;-ms-grid-column:1}#masterPage.mesh-layout #SITE_HEADER-placeholder,#masterPage.mesh-layout #SITE_HEADER_WRAPPER{grid-area:1/1/2/2;-ms-grid-row:1}#masterPage.mesh-layout #PAGES_CONTAINER,#masterPage.mesh-layout #soapAfterPagesContainer,#masterPage.mesh-layout #soapBeforePagesContainer{grid-area:3/1/4/2;-ms-grid-row:3}#masterPage.mesh-layout #soapAfterPagesContainer,#masterPage.mesh-layout #soapBeforePagesContainer{width:100%}#masterPage.mesh-layout #PAGES_CONTAINER{align-self:stretch}#masterPage.mesh-layout main#PAGES_CONTAINER{display:block}#masterPage.mesh-layout #SITE_FOOTER-placeholder,#masterPage.mesh-layout #SITE_FOOTER_WRAPPER{grid-area:4/1/5/2;-ms-grid-row:4}#masterPage.mesh-layout #SITE_PAGES,#masterPage.mesh-layout [data-mesh-id=PAGES_CONTAINERcenteredContent],#masterPage.mesh-layout [data-mesh-id=PAGES_CONTAINERinlineContent]{height:100%}#masterPage.mesh-layout.desktop>*{width:100%}#masterPage.mesh-layout #PAGES_CONTAINER,#masterPage.mesh-layout #SITE_FOOTER_WRAPPER,#masterPage.mesh-layout #SITE_HEADER_WRAPPER,#masterPage.mesh-layout #SITE_PAGES,#masterPage.mesh-layout #masterPageinlineContent,#masterPage.mesh-layout:not(.one-doc) #SITE_FOOTER,#masterPage.mesh-layout:not(.one-doc) #SITE_HEADER{position:relative}#masterPage.mesh-layout.one-doc #SITE_HEADER{grid-area:1/1/2/2}#masterPage.mesh-layout.one-doc #SITE_FOOTER{grid-area:4/1/5/2}[data-z-counter]{z-index:0}[data-z-counter="0"]{z-index:auto}.wixSiteProperties{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:root{--wst-button-color-fill-primary:rgb(var(--color_48));--wst-button-color-border-primary:rgb(var(--color_49));--wst-button-color-text-primary:rgb(var(--color_50));--wst-button-color-fill-primary-hover:rgb(var(--color_51));--wst-button-color-border-primary-hover:rgb(var(--color_52));--wst-button-color-text-primary-hover:rgb(var(--color_53));--wst-button-color-fill-primary-disabled:rgb(var(--color_54));--wst-button-color-border-primary-disabled:rgb(var(--color_55));--wst-button-color-text-primary-disabled:rgb(var(--color_56));--wst-button-color-fill-secondary:rgb(var(--color_57));--wst-button-color-border-secondary:rgb(var(--color_58));--wst-button-color-text-secondary:rgb(var(--color_59));--wst-button-color-fill-secondary-hover:rgb(var(--color_60));--wst-button-color-border-secondary-hover:rgb(var(--color_61));--wst-button-color-text-secondary-hover:rgb(var(--color_62));--wst-button-color-fill-secondary-disabled:rgb(var(--color_63));--wst-button-color-border-secondary-disabled:rgb(var(--color_64));--wst-button-color-text-secondary-disabled:rgb(var(--color_65));--wst-color-fill-base-1:rgb(var(--color_36));--wst-color-fill-base-2:rgb(var(--color_37));--wst-color-fill-base-shade-1:rgb(var(--color_38));--wst-color-fill-base-shade-2:rgb(var(--color_39));--wst-color-fill-base-shade-3:rgb(var(--color_40));--wst-color-fill-accent-1:rgb(var(--color_41));--wst-color-fill-accent-2:rgb(var(--color_42));--wst-color-fill-accent-3:rgb(var(--color_43));--wst-color-fill-accent-4:rgb(var(--color_44));--wst-color-fill-background-primary:rgb(var(--color_11));--wst-color-fill-background-secondary:rgb(var(--color_12));--wst-color-text-primary:rgb(var(--color_15));--wst-color-text-secondary:rgb(var(--color_14));--wst-color-action:rgb(var(--color_18));--wst-color-disabled:rgb(var(--color_39));--wst-color-title:rgb(var(--color_45));--wst-color-subtitle:rgb(var(--color_46));--wst-color-line:rgb(var(--color_47));--wst-font-style-h2:var(--font_2);--wst-font-style-h3:var(--font_3);--wst-font-style-h4:var(--font_4);--wst-font-style-h5:var(--font_5);--wst-font-style-h6:var(--font_6);--wst-font-style-body-large:var(--font_7);--wst-font-style-body-medium:var(--font_8);--wst-font-style-body-small:var(--font_9);--wst-font-style-body-x-small:var(--font_10)}</style> + + <meta name="format-detection" content="telephone=no"> + <meta name="skype_toolbar" content="skype_toolbar_parser_compatible"> + + + + <!-- Overriding Globals JS Head --> + + + + + + <!--pageHtmlEmbeds.head start--> + <script type="wix/htmlEmbeds" id="pageHtmlEmbeds.head start"></script> + + <script type="wix/htmlEmbeds" id="pageHtmlEmbeds.head end"></script> + <!--pageHtmlEmbeds.head end--> + + + <!-- head performance data start --> + + <!-- head performance data end --> + + +<meta http-equiv="X-Wix-Meta-Site-Id" content="ab4d26d1-8d2c-49ee-a64a-c9ff2ad0df20"> +<meta http-equiv="X-Wix-Application-Instance-Id" content="d236c163-a543-4c54-9b39-38b58fcfe2ff"> + + <meta http-equiv="X-Wix-Published-Version" content="839"> + + + + <meta http-equiv="etag" content="bug"> + +<!-- render-head end --> + +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt_bootstrap.ff052e73.min.css">.sNF2R0{opacity:0}.hLoBV3{transition:opacity var(--transition-duration) cubic-bezier(.37,0,.63,1)}.Rdf41z,.hLoBV3{opacity:1}.ftlZWo{transition:opacity var(--transition-duration) cubic-bezier(.37,0,.63,1)}.ATGlOr,.ftlZWo{opacity:0}.KQSXD0{transition:opacity var(--transition-duration) cubic-bezier(.64,0,.78,0)}.KQSXD0,.pagQKE{opacity:1}._6zG5H{opacity:0;transition:opacity var(--transition-duration) cubic-bezier(.22,1,.36,1)}.BB49uC{transform:translateX(100%)}.j9xE1V{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.ICs7Rs,.j9xE1V{transform:translateX(0)}.DxijZJ{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.B5kjYq,.DxijZJ{transform:translateX(-100%)}.cJijIV{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.cJijIV,.hOxaWM{transform:translateX(0)}.T9p3fN{transform:translateX(100%);transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.qDxYJm{transform:translateY(100%)}.aA9V0P{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.YPXPAS,.aA9V0P{transform:translateY(0)}.Xf2zsA{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.Xf2zsA,.y7Kt7s{transform:translateY(-100%)}.EeUgMu{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.EeUgMu,.fdHrtm{transform:translateY(0)}.WIFaG4{transform:translateY(100%);transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}body:not(.responsive) .JsJXaX{overflow-x:clip}[data-view-transition=page-transition] .JsJXaX{view-transition-name:page-group}.AnQkDU{display:grid;grid-template-columns:1fr;grid-template-rows:1fr;height:100%}.AnQkDU>div{align-self:stretch!important;justify-self:stretch!important}.StylableButton2545352419__root{-archetype:box;border:none;box-sizing:border-box;cursor:pointer;display:block;height:100%;min-height:10px;min-width:10px;padding:0;touch-action:manipulation;width:100%}.StylableButton2545352419__root[disabled]{pointer-events:none}.StylableButton2545352419__root.StylableButton2545352419--hasBackgroundColor{background-color:var(--corvid-background-color)!important}.StylableButton2545352419__root.StylableButton2545352419--hasBorderColor{border-color:var(--corvid-border-color)!important}.StylableButton2545352419__root.StylableButton2545352419--hasBorderRadius{border-radius:var(--corvid-border-radius)!important}.StylableButton2545352419__root.StylableButton2545352419--hasBorderWidth{border-width:var(--corvid-border-width)!important}.StylableButton2545352419__root.StylableButton2545352419--hasColor,.StylableButton2545352419__root.StylableButton2545352419--hasColor .StylableButton2545352419__label{color:var(--corvid-color)!important}.StylableButton2545352419__link{-archetype:box;box-sizing:border-box;color:#000;text-decoration:none}.StylableButton2545352419__container{align-items:center;display:flex;flex-basis:auto;flex-direction:row;flex-grow:1;height:100%;justify-content:center;overflow:hidden;transition:all .2s ease,visibility 0s;width:100%}.StylableButton2545352419__label{-archetype:text;-controller-part-type:LayoutChildDisplayDropdown,LayoutFlexChildSpacing(first);max-width:100%;min-width:1.8em;overflow:hidden;text-align:center;text-overflow:ellipsis;transition:inherit;white-space:nowrap}.StylableButton2545352419__root.StylableButton2545352419--isMaxContent .StylableButton2545352419__label{text-overflow:unset}.StylableButton2545352419__root.StylableButton2545352419--isWrapText .StylableButton2545352419__label{min-width:10px;overflow-wrap:break-word;white-space:break-spaces;word-break:break-word}.StylableButton2545352419__icon{-archetype:icon;-controller-part-type:LayoutChildDisplayDropdown,LayoutFlexChildSpacing(last);flex-shrink:0;height:50px;min-width:1px;transition:inherit}.StylableButton2545352419__icon.StylableButton2545352419--override{display:block!important}.StylableButton2545352419__icon svg,.StylableButton2545352419__icon>div{display:flex;height:inherit;width:inherit}.HcOXKn{-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}ol.font_100,ul.font_100{color:#080808;font-family:"Arial, Helvetica, sans-serif",serif;font-size:10px;font-style:normal;font-variant:normal;font-weight:400;letter-spacing:normal;line-height:normal;margin:0;text-decoration:none}ol.font_100 li,ul.font_100 li{margin-bottom:12px}ol.wix-list-text-align,ul.wix-list-text-align{list-style-position:inside}ol.wix-list-text-align h1,ol.wix-list-text-align h2,ol.wix-list-text-align h3,ol.wix-list-text-align h4,ol.wix-list-text-align h5,ol.wix-list-text-align h6,ol.wix-list-text-align p,ul.wix-list-text-align h1,ul.wix-list-text-align h2,ul.wix-list-text-align h3,ul.wix-list-text-align h4,ul.wix-list-text-align h5,ul.wix-list-text-align h6,ul.wix-list-text-align p{display:inline}.ONIxfn{cursor:pointer}.WUKwEB{clip:rect(0 0 0 0);border:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.QxJLC3 [data-attr-richtext-marker=true]{display:block}.QxJLC3 [data-attr-richtext-marker=true] table{border-collapse:collapse;margin:15px 0;width:100%}.QxJLC3 [data-attr-richtext-marker=true] table td{padding:12px;position:relative}.QxJLC3 [data-attr-richtext-marker=true] table td:after{border-bottom:1px solid currentColor;border-left:1px solid currentColor;bottom:0;content:"";left:0;opacity:.2;position:absolute;right:0;top:0}.QxJLC3 [data-attr-richtext-marker=true] table tr td:last-child:after{border-right:1px solid currentColor}.QxJLC3 [data-attr-richtext-marker=true] table tr:first-child td:after{border-top:1px solid currentColor}.uGVkMG{direction:var(--text-direction);min-height:var(--min-height);min-width:var(--min-width)}.uGVkMG .edKzOf{word-wrap:break-word;height:100%;overflow-wrap:break-word;position:relative;width:100%}.uGVkMG .edKzOf ul{list-style:disc inside}.uGVkMG .edKzOf li{margin-bottom:12px}.SxM0TO blockquote,.SxM0TO h1,.SxM0TO h2,.SxM0TO h3,.SxM0TO h4,.SxM0TO h5,.SxM0TO h6,.SxM0TO p{letter-spacing:normal;line-height:normal}.nJYhU3{min-height:var(--min-height);min-width:var(--min-width)}.nJYhU3 .edKzOf{word-wrap:break-word;height:100%;overflow-wrap:break-word;position:relative;width:100%}.nJYhU3 .edKzOf ol,.nJYhU3 .edKzOf ul{letter-spacing:normal;line-height:normal;margin-inline-start:.5em;padding-inline-start:1.3em}.nJYhU3 .edKzOf ul{list-style-type:disc}.nJYhU3 .edKzOf ol{list-style-type:decimal}.nJYhU3 .edKzOf ol ul,.nJYhU3 .edKzOf ul ul{line-height:normal;list-style-type:circle}.nJYhU3 .edKzOf ol ol ul,.nJYhU3 .edKzOf ol ul ul,.nJYhU3 .edKzOf ul ol ul,.nJYhU3 .edKzOf ul ul ul{line-height:normal;list-style-type:square}.nJYhU3 .edKzOf li{font-style:inherit;font-weight:inherit;letter-spacing:normal;line-height:inherit}.nJYhU3 .edKzOf h1,.nJYhU3 .edKzOf h2,.nJYhU3 .edKzOf h3,.nJYhU3 .edKzOf h4,.nJYhU3 .edKzOf h5,.nJYhU3 .edKzOf h6,.nJYhU3 .edKzOf p{letter-spacing:normal;line-height:normal;margin-block:0;margin:0}.nJYhU3 .edKzOf a{color:inherit}.SxM0TO,.c9GqVL{word-wrap:break-word;direction:var(--text-direction);min-height:var(--min-height);min-width:var(--min-width);overflow-wrap:break-word;pointer-events:none;text-align:start}.SxM0TO>*,.c9GqVL>*{pointer-events:auto}.SxM0TO li,.c9GqVL li{font-style:inherit;font-weight:inherit;letter-spacing:normal;line-height:inherit}.SxM0TO ol,.SxM0TO ul,.c9GqVL ol,.c9GqVL ul{letter-spacing:normal;line-height:normal;margin-inline-end:0;margin-inline-start:.5em;padding-inline-end:0;padding-inline-start:1.3em}.SxM0TO ul,.c9GqVL ul{list-style-type:disc}.SxM0TO ol,.c9GqVL ol{list-style-type:decimal}.SxM0TO ol ul,.SxM0TO ul ul,.c9GqVL ol ul,.c9GqVL ul ul{list-style-type:circle}.SxM0TO ol ol ul,.SxM0TO ol ul ul,.SxM0TO ul ol ul,.SxM0TO ul ul ul,.c9GqVL ol ol ul,.c9GqVL ol ul ul,.c9GqVL ul ol ul,.c9GqVL ul ul ul{list-style-type:square}.SxM0TO blockquote,.SxM0TO h1,.SxM0TO h2,.SxM0TO h3,.SxM0TO h4,.SxM0TO h5,.SxM0TO h6,.SxM0TO p,.c9GqVL blockquote,.c9GqVL h1,.c9GqVL h2,.c9GqVL h3,.c9GqVL h4,.c9GqVL h5,.c9GqVL h6,.c9GqVL p{margin-block:0;margin:0}.SxM0TO a,.c9GqVL a{color:inherit}.Vd6aQZ{overflow:hidden;padding:0;pointer-events:none;white-space:nowrap}.mHZSwn{display:none}.nDEeB0{cursor:pointer}.lvxhkV{bottom:0;left:0;position:absolute;right:0;top:0;width:100%}.QJjwEo{transform:translateY(-100%);transition:.2s ease-in}.kdBXfh{transition:.2s}.MP52zt{opacity:0;transition:.2s ease-in}.MP52zt.Bhu9m5{z-index:-1!important}.LVP8Wf{opacity:1;transition:.2s}.VrZrC0{height:auto}.VrZrC0,.cKxVkc{position:relative;width:100%}:host(:not(.device-mobile-optimized)) .vlM3HR,body:not(.device-mobile-optimized) .vlM3HR{margin-left:calc((100% - var(--site-width))/2);width:var(--site-width)}.AT7o0U[data-focuscycled=active]{outline:1px solid transparent}.AT7o0U[data-focuscycled=active]:not(:focus-within){outline:2px solid transparent;transition:outline .01s ease}.AT7o0U .vlM3HR{bottom:0;left:0;position:absolute;right:0;top:0}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}.WzbAF8 .mpGTIt .O6KwRn{display:var(--item-display);height:var(--item-size);margin:var(--item-margin);width:var(--item-size)}.WzbAF8 .mpGTIt .O6KwRn:last-child{margin:0}.WzbAF8 .mpGTIt .O6KwRn .oRtuWN{display:block}.WzbAF8 .mpGTIt .O6KwRn .oRtuWN .YaS0jR{height:var(--item-size);width:var(--item-size)}.WzbAF8 .mpGTIt{height:100%;position:absolute;white-space:nowrap;width:100%}:host(.device-mobile-optimized) .WzbAF8 .mpGTIt,body.device-mobile-optimized .WzbAF8 .mpGTIt{white-space:normal}.u9k3ts{bottom:0;left:0;position:absolute;right:0;top:0}.WQ4fSJ{cursor:pointer}.aizuI7{-webkit-tap-highlight-color:rgba(0,0,0,0);fill:var(--fill);fill-opacity:var(--fill-opacity);stroke:var(--stroke);stroke-opacity:var(--stroke-opacity);stroke-width:var(--stroke-width);filter:var(--drop-shadow,none);opacity:var(--opacity);transform:var(--flip)}.aizuI7,.aizuI7 svg{bottom:0;left:0;position:absolute;right:0;top:0}.aizuI7 svg{height:var(--svg-calculated-height,100%);margin:auto;padding:var(--svg-calculated-padding,0);width:var(--svg-calculated-width,100%)}.TcoJIb *{vector-effect:non-scaling-stroke}.R8rhQA svg,.elfY4_ svg{overflow:visible!important}@supports(-webkit-hyphens:none){.aizuI7.elfY4_{will-change:filter}}.big2ZD{display:grid;grid-template-columns:1fr;grid-template-rows:1fr;height:calc(100% - var(--wix-ads-height));left:0;margin-top:var(--wix-ads-height);position:fixed;top:0;width:100%}.SHHiV9,.big2ZD{pointer-events:none;z-index:var(--pinned-layer-in-container,var(--above-all-in-container))}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt_bootstrap-classic.f7f39044.min.css">.PlZyDq{touch-action:manipulation}.uDW_Qe{align-items:center;box-sizing:border-box;display:flex;justify-content:var(--label-align);min-width:100%;text-align:initial;width:-moz-max-content;width:max-content}.uDW_Qe:before{max-width:var(--margin-start,0)}.uDW_Qe:after,.uDW_Qe:before{align-self:stretch;content:"";flex-grow:1}.uDW_Qe:after{max-width:var(--margin-end,0)}.FubTgk{height:100%}.FubTgk .uDW_Qe{border-radius:var(--corvid-border-radius,var(--rd,0));bottom:0;box-shadow:var(--shd,0 1px 4px rgba(0,0,0,.6));left:0;position:absolute;right:0;top:0;transition:var(--trans1,border-color .4s ease 0s,background-color .4s ease 0s)}.FubTgk .uDW_Qe:link,.FubTgk .uDW_Qe:visited{border-color:transparent}.FubTgk .l7_2fn{color:var(--corvid-color,rgb(var(--txt,var(--color_15,color_15))));font:var(--fnt,var(--font_5));margin:0;position:relative;transition:var(--trans2,color .4s ease 0s);white-space:nowrap}.FubTgk[aria-disabled=false] .uDW_Qe{background-color:var(--corvid-background-color,rgba(var(--bg,var(--color_17,color_17)),var(--alpha-bg,1)));border:solid var(--corvid-border-color,rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1))) var(--corvid-border-width,var(--brw,0));cursor:pointer!important}:host(.device-mobile-optimized) .FubTgk[aria-disabled=false]:active .uDW_Qe,body.device-mobile-optimized .FubTgk[aria-disabled=false]:active .uDW_Qe{background-color:rgba(var(--bgh,var(--color_18,color_18)),var(--alpha-bgh,1));border-color:rgba(var(--brdh,var(--color_15,color_15)),var(--alpha-brdh,1))}:host(.device-mobile-optimized) .FubTgk[aria-disabled=false]:active .l7_2fn,body.device-mobile-optimized .FubTgk[aria-disabled=false]:active .l7_2fn{color:rgb(var(--txth,var(--color_15,color_15)))}:host(:not(.device-mobile-optimized)) .FubTgk[aria-disabled=false]:hover .uDW_Qe,body:not(.device-mobile-optimized) .FubTgk[aria-disabled=false]:hover .uDW_Qe{background-color:rgba(var(--bgh,var(--color_18,color_18)),var(--alpha-bgh,1));border-color:rgba(var(--brdh,var(--color_15,color_15)),var(--alpha-brdh,1))}:host(:not(.device-mobile-optimized)) .FubTgk[aria-disabled=false]:hover .l7_2fn,body:not(.device-mobile-optimized) .FubTgk[aria-disabled=false]:hover .l7_2fn{color:rgb(var(--txth,var(--color_15,color_15)))}.FubTgk[aria-disabled=true] .uDW_Qe{background-color:rgba(var(--bgd,204,204,204),var(--alpha-bgd,1));border-color:rgba(var(--brdd,204,204,204),var(--alpha-brdd,1));border-style:solid;border-width:var(--corvid-border-width,var(--brw,0))}.FubTgk[aria-disabled=true] .l7_2fn{color:rgb(var(--txtd,255,255,255))}.uUxqWY{align-items:center;box-sizing:border-box;display:flex;justify-content:var(--label-align);min-width:100%;text-align:initial;width:-moz-max-content;width:max-content}.uUxqWY:before{max-width:var(--margin-start,0)}.uUxqWY:after,.uUxqWY:before{align-self:stretch;content:"";flex-grow:1}.uUxqWY:after{max-width:var(--margin-end,0)}.Vq4wYb[aria-disabled=false] .uUxqWY{cursor:pointer}:host(.device-mobile-optimized) .Vq4wYb[aria-disabled=false]:active .wJVzSK,body.device-mobile-optimized .Vq4wYb[aria-disabled=false]:active .wJVzSK{color:rgb(var(--txth,var(--color_15,color_15)));transition:var(--trans,color .4s ease 0s)}:host(:not(.device-mobile-optimized)) .Vq4wYb[aria-disabled=false]:hover .wJVzSK,body:not(.device-mobile-optimized) .Vq4wYb[aria-disabled=false]:hover .wJVzSK{color:rgb(var(--txth,var(--color_15,color_15)));transition:var(--trans,color .4s ease 0s)}.Vq4wYb .uUxqWY{bottom:0;left:0;position:absolute;right:0;top:0}.Vq4wYb .wJVzSK{color:var(--corvid-color,rgb(var(--txt,var(--color_15,color_15))));font:var(--fnt,var(--font_5));transition:var(--trans,color .4s ease 0s);white-space:nowrap}.Vq4wYb[aria-disabled=true] .wJVzSK{color:rgb(var(--txtd,255,255,255))}:host(:not(.device-mobile-optimized)) .CohWsy,body:not(.device-mobile-optimized) .CohWsy{display:flex}:host(:not(.device-mobile-optimized)) .V5AUxf,body:not(.device-mobile-optimized) .V5AUxf{-moz-column-gap:var(--margin);column-gap:var(--margin);display:flex;flex-direction:var(--items-direction);margin:0 auto;position:relative;width:calc(100% - var(--padding)*2)}:host(:not(.device-mobile-optimized)) .V5AUxf>*,body:not(.device-mobile-optimized) .V5AUxf>*{flex:var(--column-flex) 1 0%;left:0;margin-bottom:var(--padding);margin-top:var(--padding);min-width:0;position:relative;top:0}:host(.device-mobile-optimized) .V5AUxf,body.device-mobile-optimized .V5AUxf{display:block;padding:var(--padding) 0;position:relative}:host(.device-mobile-optimized) .V5AUxf>*,body.device-mobile-optimized .V5AUxf>*{margin-bottom:var(--margin);position:relative}:host(.device-mobile-optimized) .V5AUxf>:first-child,body.device-mobile-optimized .V5AUxf>:first-child{margin-top:var(--firstChildMarginTop,0)}:host(.device-mobile-optimized) .V5AUxf>:last-child,body.device-mobile-optimized .V5AUxf>:last-child{margin-bottom:var(--lastChildMarginBottom)}.LIhNy3{backface-visibility:hidden}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}.if7Vw2{height:100%;left:0;-webkit-mask-image:var(--mask-image,none);mask-image:var(--mask-image,none);-webkit-mask-position:var(--mask-position,0);mask-position:var(--mask-position,0);-webkit-mask-repeat:var(--mask-repeat,no-repeat);mask-repeat:var(--mask-repeat,no-repeat);-webkit-mask-size:var(--mask-size,100%);mask-size:var(--mask-size,100%);overflow:hidden;pointer-events:var(--fill-layer-background-media-pointer-events);position:absolute;top:0;width:100%}.if7Vw2.f0uTJH{clip:rect(0,auto,auto,0)}.if7Vw2 .i1tH8h{height:100%;position:absolute;top:0;width:100%}.if7Vw2 .DXi4PB{height:var(--fill-layer-image-height,100%);opacity:var(--fill-layer-image-opacity)}.if7Vw2 .DXi4PB img{height:100%;width:100%}@supports(-webkit-hyphens:none){.if7Vw2.f0uTJH{clip:auto;-webkit-clip-path:inset(0)}}.wG8dni{height:100%}.tcElKx{background-color:var(--bg-overlay-color);background-image:var(--bg-gradient);transition:var(--inherit-transition)}.ImALHf,.Ybjs9b{opacity:var(--fill-layer-video-opacity)}.UWmm3w{bottom:var(--media-padding-bottom);height:var(--media-padding-height);position:absolute;top:var(--media-padding-top);width:100%}.Yjj1af{transform:scale(var(--scale,1));transition:var(--transform-duration,transform 0s)}.ImALHf{height:100%;position:relative;width:100%}._uqPqy{-webkit-clip-path:var(--fill-layer-clip);clip-path:var(--fill-layer-clip)}._uqPqy,.eKyYhK{position:absolute;top:0}._uqPqy,.eKyYhK,.x0mqQS img{height:100%;width:100%}.pnCr6P{opacity:0}.blf7sp,.pnCr6P{position:absolute;top:0}.blf7sp{height:0;left:0;overflow:hidden;width:0}.rWP3Gv{left:0;pointer-events:var(--fill-layer-background-media-pointer-events);position:var(--fill-layer-background-media-position)}.Tr4n3d,.rWP3Gv,.wRqk6s{height:100%;top:0;width:100%}.wRqk6s{position:absolute}.Tr4n3d{background-color:var(--fill-layer-background-overlay-color);opacity:var(--fill-layer-background-overlay-blend-opacity-fallback,1);position:var(--fill-layer-background-overlay-position);transform:var(--fill-layer-background-overlay-transform)}@supports(mix-blend-mode:overlay){.Tr4n3d{mix-blend-mode:var(--fill-layer-background-overlay-blend-mode);opacity:var(--fill-layer-background-overlay-blend-opacity,1)}}.VXAmO2{--divider-pin-height__:min(1,calc(var(--divider-layers-pin-factor__) + 1));--divider-pin-layer-height__:var(--divider-layers-pin-factor__);--divider-pin-border__:min(1,calc(var(--divider-layers-pin-factor__) / -1 + 1));height:calc(var(--divider-height__) + var(--divider-pin-height__)*var(--divider-layers-size__)*var(--divider-layers-y__))}.VXAmO2,.VXAmO2 .dy3w_9{left:0;position:absolute;width:100%}.VXAmO2 .dy3w_9{--divider-layer-i__:var(--divider-layer-i,0);background-position:left calc(50% + var(--divider-offset-x__) + var(--divider-layers-x__)*var(--divider-layer-i__)) bottom;background-repeat:repeat-x;border-bottom-style:solid;border-bottom-width:calc(var(--divider-pin-border__)*var(--divider-layer-i__)*var(--divider-layers-y__));height:calc(var(--divider-height__) + var(--divider-pin-layer-height__)*var(--divider-layer-i__)*var(--divider-layers-y__));opacity:calc(1 - var(--divider-layer-i__)/(var(--divider-layer-i__) + 1))}.UORcXs{--divider-height__:var(--divider-top-height,auto);--divider-offset-x__:var(--divider-top-offset-x,0px);--divider-layers-size__:var(--divider-top-layers-size,0);--divider-layers-y__:var(--divider-top-layers-y,0px);--divider-layers-x__:var(--divider-top-layers-x,0px);--divider-layers-pin-factor__:var(--divider-top-layers-pin-factor,0);border-top:var(--divider-top-padding,0) solid var(--divider-top-color,currentColor);opacity:var(--divider-top-opacity,1);top:0;transform:var(--divider-top-flip,scaleY(-1))}.UORcXs .dy3w_9{background-image:var(--divider-top-image,none);background-size:var(--divider-top-size,contain);border-color:var(--divider-top-color,currentColor);bottom:0;filter:var(--divider-top-filter,none)}.UORcXs .dy3w_9[data-divider-layer="1"]{display:var(--divider-top-layer-1-display,block)}.UORcXs .dy3w_9[data-divider-layer="2"]{display:var(--divider-top-layer-2-display,block)}.UORcXs .dy3w_9[data-divider-layer="3"]{display:var(--divider-top-layer-3-display,block)}.Io4VUz{--divider-height__:var(--divider-bottom-height,auto);--divider-offset-x__:var(--divider-bottom-offset-x,0px);--divider-layers-size__:var(--divider-bottom-layers-size,0);--divider-layers-y__:var(--divider-bottom-layers-y,0px);--divider-layers-x__:var(--divider-bottom-layers-x,0px);--divider-layers-pin-factor__:var(--divider-bottom-layers-pin-factor,0);border-bottom:var(--divider-bottom-padding,0) solid var(--divider-bottom-color,currentColor);bottom:0;opacity:var(--divider-bottom-opacity,1);transform:var(--divider-bottom-flip,none)}.Io4VUz .dy3w_9{background-image:var(--divider-bottom-image,none);background-size:var(--divider-bottom-size,contain);border-color:var(--divider-bottom-color,currentColor);bottom:0;filter:var(--divider-bottom-filter,none)}.Io4VUz .dy3w_9[data-divider-layer="1"]{display:var(--divider-bottom-layer-1-display,block)}.Io4VUz .dy3w_9[data-divider-layer="2"]{display:var(--divider-bottom-layer-2-display,block)}.Io4VUz .dy3w_9[data-divider-layer="3"]{display:var(--divider-bottom-layer-3-display,block)}.YzqVVZ{overflow:visible;position:relative}.mwF7X1{backface-visibility:hidden}.YGilLk{cursor:pointer}.MW5IWV{height:100%;left:0;-webkit-mask-image:var(--mask-image,none);mask-image:var(--mask-image,none);-webkit-mask-position:var(--mask-position,0);mask-position:var(--mask-position,0);-webkit-mask-repeat:var(--mask-repeat,no-repeat);mask-repeat:var(--mask-repeat,no-repeat);-webkit-mask-size:var(--mask-size,100%);mask-size:var(--mask-size,100%);overflow:hidden;pointer-events:var(--fill-layer-background-media-pointer-events);position:absolute;top:0;width:100%}.MW5IWV.N3eg0s{clip:rect(0,auto,auto,0)}.MW5IWV .Kv1aVt{height:100%;position:absolute;top:0;width:100%}.MW5IWV .dLPlxY{height:var(--fill-layer-image-height,100%);opacity:var(--fill-layer-image-opacity)}.MW5IWV .dLPlxY img{height:100%;width:100%}@supports(-webkit-hyphens:none){.MW5IWV.N3eg0s{clip:auto;-webkit-clip-path:inset(0)}}.VgO9Yg{height:100%}.LWbAav{background-color:var(--bg-overlay-color);background-image:var(--bg-gradient);transition:var(--inherit-transition)}.K_YxMd,.yK6aSC{opacity:var(--fill-layer-video-opacity)}.NGjcJN{bottom:var(--media-padding-bottom);height:var(--media-padding-height);position:absolute;top:var(--media-padding-top);width:100%}.mNGsUM{transform:scale(var(--scale,1));transition:var(--transform-duration,transform 0s)}.K_YxMd{height:100%;position:relative;width:100%}.bX9O_S{-webkit-clip-path:var(--fill-layer-clip);clip-path:var(--fill-layer-clip)}.Z_wCwr,.bX9O_S{position:absolute;top:0}.Jxk_UL img,.Z_wCwr,.bX9O_S{height:100%;width:100%}.K8MSra{opacity:0}.K8MSra,.YTb3b4{position:absolute;top:0}.YTb3b4{height:0;left:0;overflow:hidden;width:0}.SUz0WK{left:0;pointer-events:var(--fill-layer-background-media-pointer-events);position:var(--fill-layer-background-media-position)}.FNxOn5,.SUz0WK,.m4khSP{height:100%;top:0;width:100%}.FNxOn5{position:absolute}.m4khSP{background-color:var(--fill-layer-background-overlay-color);opacity:var(--fill-layer-background-overlay-blend-opacity-fallback,1);position:var(--fill-layer-background-overlay-position);transform:var(--fill-layer-background-overlay-transform)}@supports(mix-blend-mode:overlay){.m4khSP{mix-blend-mode:var(--fill-layer-background-overlay-blend-mode);opacity:var(--fill-layer-background-overlay-blend-opacity,1)}}._C0cVf{bottom:0;left:0;position:absolute;right:0;top:0;width:100%}.hFwGTD{transform:translateY(-100%);transition:.2s ease-in}.IQgXoP{transition:.2s}.Nr3Nid{opacity:0;transition:.2s ease-in}.Nr3Nid.l4oO6c{z-index:-1!important}.iQuoC4{opacity:1;transition:.2s}.CJF7A2{height:auto}.CJF7A2,.U4Bvut{position:relative;width:100%}:host(:not(.device-mobile-optimized)) .G5K6X8,body:not(.device-mobile-optimized) .G5K6X8{margin-left:calc((100% - var(--site-width))/2);width:var(--site-width)}.xU8fqS[data-focuscycled=active]{outline:1px solid transparent}.xU8fqS[data-focuscycled=active]:not(:focus-within){outline:2px solid transparent;transition:outline .01s ease}.xU8fqS ._4XcTfy{background-color:var(--screenwidth-corvid-background-color,rgba(var(--bg,var(--color_11,color_11)),var(--alpha-bg,1)));border-bottom:var(--brwb,0) solid var(--screenwidth-corvid-border-color,rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1)));border-top:var(--brwt,0) solid var(--screenwidth-corvid-border-color,rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1)));bottom:0;box-shadow:var(--shd,0 0 5px rgba(0,0,0,.7));left:0;position:absolute;right:0;top:0}.xU8fqS .gUbusX{background-color:rgba(var(--bgctr,var(--color_11,color_11)),var(--alpha-bgctr,1));border-radius:var(--rd,0);bottom:var(--brwb,0);top:var(--brwt,0)}.xU8fqS .G5K6X8,.xU8fqS .gUbusX{left:0;position:absolute;right:0}.xU8fqS .G5K6X8{bottom:0;top:0}:host(.device-mobile-optimized) .xU8fqS .G5K6X8,body.device-mobile-optimized .xU8fqS .G5K6X8{left:10px;right:10px}.SPY_vo{pointer-events:none}.BmZ5pC{min-height:calc(100vh/var(--zoom-factor, 1) - var(--wix-ads-height));min-width:var(--site-width);position:var(--bg-position);top:var(--wix-ads-height)}.BmZ5pC,.nTOEE9{height:100%;width:100%}.nTOEE9{overflow:hidden}.nTOEE9.sqUyGm:hover{cursor:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3FpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODM3MEUzMUU4OTAyMTFFMzk3Q0FCMkFEODdDNzUzMjQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODM3MEUzMUQ4OTAyMTFFMzk3Q0FCMkFEODdDNzUzMjQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjk0ZTkyMTRlLThiNDQtNjc0My04MWZiLTZlYjIzYTA2ZjcwNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4bqsJgAAACF0lEQVR42mJgQAd8fHz/gdRvRigfxGEACCA4YvwPBMgCbgABBGOAJP6LiooiZBUUFMCC7969Awk6AQQQA1bAxMTUOnXq1P8/f/78j2zdf5BDQDgoKAgiyMgItv0/1AkozlgJlHwPpDWB+AhAACFL1EJVwvBPIGZHd8P/OXPmgI0F2YdmxXQUhX///sVQqK2tDVL4DFkhF8zK2NjY/4aGhshOOMJAJAB5ZjdAADGQCpiB4Cear3uwKQR74vv372BPLFq0CKZ4GnLcdMGiFtnXmzZtQo0Bdnb2r/b29nBFMIwUjkxghby8vHfFxMQwTMQWp0YggZcvX/5HBpqamhgKQdafAQnq6en9j4+P/4/me150nzsCPfYOKrkWKvYCymcjJozPgqIYIMAYcUjKAnEcELsDbVECOpkNiO8B+buAeCEQ3yUqFllYWNYh+4Obm/u/ubn5f0tLy//QPIqM90ATHVagDHTJH5BCfn7+/xcvXvyPC9y7d+8/KHqghv4FYj0M04BxeAOkQEhI6P+vX79QDECOeBj49+/ffzk5OZih91FyP4gAGiIDooH5hIGVlRUsAXQpGMMAMh+Y1xksLCzg5QxGrAFzwAxY2GzYsIGgC48cOYIclsuwBiIbG9sCmCJFRcX/+/fvxwi/EydOwIoDGH6JLQEiA26ga1egxSY2vAUpkcKKEV5iCwVOIObBU8w8RzLYgYHaAAACg5CxaxSLgwAAAABJRU5ErkJggg==),auto}.nTOEE9.C_JY0G:hover{cursor:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3FpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0I4QkNGQTI4OTAyMTFFMzg0RDlBRkM5NDA5QjczRTEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0I4QkNGQTE4OTAyMTFFMzg0RDlBRkM5NDA5QjczRTEiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjk0ZTkyMTRlLThiNDQtNjc0My04MWZiLTZlYjIzYTA2ZjcwNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7hiSPZAAACGklEQVR42mJgQAd8fHz/gdRvRigfxGEACCA4YvwPBMgCbgABBGOAJP6LiooiZBUUFMCC7969Awk6AQQQA1bAxMTUOnXq1P8/f/78j2zdf5BDQDgoKAgiyMgItv0/1AkozlgJlHwPpDWB+AhAACFL1EJVwvBPIGZHd8P/OXPm/EcHUA3TURT+/fsXQ6G2tjZI4TNkhVwwK2NjY/8bGhoiO+EIA5EA5JndAAHEQCpgBoKfaL7uwaYQHLrfv38He2LRokUwxdOQ46YLFrXIYNOmTagxwM7O/tXe3h4sCYs3EEYKRyawQl5e3rtiYmL/sQH0ODUCCbx8+RJFkaamJoZCkPVnQIJ6enr/4+Pj/6P5nhfd545Aj72DSq6Fir2A8tmICeOzoCgGCDBGHJKyQBwHxO5AW5SATmYD4ntA/i4gXgjEd4mKRRYWlnXI/uDm5v5vbm7+39LS8j80jyLjPdBEhxUoA13yB6SQn5///8WLF//jAvfu3fsPih6ooX+BWA/DNGAc3gApEBIS+v/r16//hMC/f//+y8nJwQy9j2wWC4gAGiIDooH5hIGVlRUsAXQpVq98/PgRVBAwWFhYMDx69AhczkBj7RdyFpgBC5sNGzYQdOGRI0eQw3IZVpvZ2NgWwBQpKir+379/P4ZBJ06cgBUHMPwSWwJEBtxA165Ai01seAtSIoUVI7zEFgqcQMyDp5h5jmSwAwO1AQBU5q033XYWQwAAAABJRU5ErkJggg==),auto}.rYiAuL{cursor:pointer}.gSXewE{height:0;left:0;overflow:hidden;position:absolute;top:0;width:0}.j7pOnl{box-sizing:border-box;height:100%;width:100%}.BI8PVQ{min-height:var(--image-min-height);min-width:var(--image-min-width)}.BI8PVQ img{filter:var(--filter-effect-svg-url);-webkit-mask-image:var(--mask-image,none);mask-image:var(--mask-image,none);-webkit-mask-position:var(--mask-position,0);mask-position:var(--mask-position,0);-webkit-mask-repeat:var(--mask-repeat,no-repeat);mask-repeat:var(--mask-repeat,no-repeat);-webkit-mask-size:var(--mask-size,100% 100%);mask-size:var(--mask-size,100% 100%);-o-object-position:var(--object-position);object-position:var(--object-position)}.MazNVa{left:var(--left,auto);position:var(--position-fixed,static);top:var(--top,auto);z-index:var(--z-index,auto)}.MazNVa .BI8PVQ img{box-shadow:0 0 0 #000;position:static;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.MazNVa .j7pOnl{display:block;overflow:hidden}.MazNVa .BI8PVQ{overflow:hidden}.c7cMWz{bottom:0;left:0;position:absolute;right:0;top:0}.FVGvCX{height:auto;position:relative;width:100%}body:not(.responsive) .zK7MhX{align-self:start;grid-area:1/1/1/1;height:100%;justify-self:stretch;left:0;position:relative}:host(:not(.device-mobile-optimized)) .c7cMWz,body:not(.device-mobile-optimized) .c7cMWz{margin-left:calc((100% - var(--site-width))/2);width:var(--site-width)}.fEm0Bo .c7cMWz{background-color:rgba(var(--bg,var(--color_11,color_11)),var(--alpha-bg,1));overflow:hidden}:host(.device-mobile-optimized) .c7cMWz,body.device-mobile-optimized .c7cMWz{left:10px;right:10px}.PFkO7r{bottom:0;left:0;position:absolute;right:0;top:0}.HT5ybB{height:auto;position:relative;width:100%}body:not(.responsive) .dBAkHi{align-self:start;grid-area:1/1/1/1;height:100%;justify-self:stretch;left:0;position:relative}:host(:not(.device-mobile-optimized)) .PFkO7r,body:not(.device-mobile-optimized) .PFkO7r{margin-left:calc((100% - var(--site-width))/2);width:var(--site-width)}:host(.device-mobile-optimized) .PFkO7r,body.device-mobile-optimized .PFkO7r{left:10px;right:10px}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[DropDownMenu_ShinyMenuIButtonSkin].43bd46d1.min.css">.BtJrhC,.tpImQl{box-sizing:border-box;height:100%;overflow:visible;position:relative;width:auto}.BtJrhC[data-state~=header] a,.BtJrhC[data-state~=header] div,[data-state~=header].tpImQl a,[data-state~=header].tpImQl div{cursor:default!important}.BtJrhC .VFOEQr,.tpImQl .VFOEQr{display:inline-block;height:100%;width:100%}.tpImQl{--display:inline-block;background:rgba(var(--bg,var(--color_11,color_11)),var(--alpha-bg,1));border-left:1px solid rgba(var(--sep,var(--color_15,color_15)),var(--alpha-sep,1));cursor:pointer;display:var(--display);transition:var(--trans,background-color .4s ease 0s)}.tpImQl .CT4EmF{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAL4CAYAAAC6F51xAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1MjgyM0FDRjc4NTVFMTExQURCRUEwRDE2OEU2QjI4MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4NDgxMDhDRTU1OTIxMUUxQjc4QUIzQ0M0NkYxMTE2NSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4NDgxMDhDRDU1OTIxMUUxQjc4QUIzQ0M0NkYxMTE2NSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjU0ODIzQUNGNzg1NUUxMTFBREJFQTBEMTY4RTZCMjgzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjUyODIzQUNGNzg1NUUxMTFBREJFQTBEMTY4RTZCMjgzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YZ6HtgAAAFVJREFUeNpi+P//fyQTAxCMEqPEKDFKjBKjxAgh/sKJf1hYOGX/YZH9h1cxVIyRkfEz3PL/hFlU5OJkMTDjCiFiALD1MJqQRolRYpQYJUaJwUMABBgAmxMz34m4jL0AAAAASUVORK5CYII=) repeat-x 50%;bottom:0;left:0;position:absolute;right:0;top:0}.tpImQl .t6GAPL{color:rgb(var(--txt,var(--color_15,color_15)));display:inline-block;font:var(--fnt,var(--font_1));padding:0 calc(var(--pad, 5px) + 10px);transition:var(--trans2,color .4s ease 0s)}.tpImQl[data-listposition=lonely],.tpImQl[data-listposition=lonely] .CT4EmF{border:0;border-radius:var(--rd,10px)}.tpImQl:first-child[data-direction=ltr],.tpImQl:last-child[data-direction=rtl]{border:0}.tpImQl[data-listposition=left],.tpImQl[data-listposition=left] .CT4EmF{border-radius:var(--rd,10px);border-bottom-right-radius:0;border-top-right-radius:0}.tpImQl[data-listposition=right],.tpImQl[data-listposition=right] .CT4EmF{border-radius:var(--rd,10px);border-bottom-left-radius:0;border-top-left-radius:0}.tpImQl[data-state~=drop]{background-color:transparent;border:0;display:block;transition:var(--trans,background-color .4s ease 0s);width:100%}.tpImQl[data-state~=drop] .CT4EmF{opacity:0;transition:var(--trans1,opacity .4s ease 0s)}.tpImQl[data-listposition=dropLonely],.tpImQl[data-listposition=dropLonely] .CT4EmF{border-radius:var(--rdDrop,10px)}.tpImQl[data-listposition=top],.tpImQl[data-listposition=top] .CT4EmF{border-radius:var(--rdDrop,10px);border-bottom-left-radius:0;border-bottom-right-radius:0}.tpImQl[data-listposition=bottom],.tpImQl[data-listposition=bottom] .CT4EmF{border-radius:var(--rdDrop,10px);border-top-left-radius:0;border-top-right-radius:0}.tpImQl[data-state~=drop]:hover .CT4EmF,.tpImQl[data-state~=drop][data-state~=over] .CT4EmF,.tpImQl[data-state~=drop][data-state~=selected] .CT4EmF{opacity:1;transition:var(--trans1,opacity .4s ease 0s)}.tpImQl[data-state~=link]:hover,.tpImQl[data-state~=over]{background:rgba(var(--bgh,var(--color_17,color_17)),var(--alpha-bgh,1));transition:var(--trans,background-color .4s ease 0s)}.tpImQl[data-state~=link]:hover .t6GAPL,.tpImQl[data-state~=over] .t6GAPL{color:rgb(var(--txth,var(--color_15,color_15)));display:inline-block;transition:var(--trans2,color .4s ease 0s)}.tpImQl[data-state~=selected]{background:rgba(var(--bgs,var(--color_18,color_18)),var(--alpha-bgs,1));transition:var(--trans,background-color .4s ease 0s)}.tpImQl[data-state~=selected] .t6GAPL{color:rgb(var(--txts,var(--color_15,color_15)));display:inline-block;transition:var(--trans2,color .4s ease 0s)}.xHxPc_{overflow-x:hidden}.xHxPc_ .EIZYxb{display:flex;flex-direction:column;height:100%;width:100%}.xHxPc_ .EIZYxb .UAXjiA{flex:1}.xHxPc_ .EIZYxb .YBArby{height:calc(100% - (var(--menuTotalBordersY, 0px)));overflow:visible;white-space:nowrap;width:calc(100% - (var(--menuTotalBordersX, 0px)))}.xHxPc_ .EIZYxb .YBArby .prNH8t{display:inline-block}.xHxPc_ .EIZYxb .YBArby .Or5gMw{display:block;width:100%}.xHxPc_ .DZGkiP{display:block;opacity:1;z-index:99999}.xHxPc_ .DZGkiP .hpiQIN{display:inherit;overflow:visible;visibility:inherit;white-space:nowrap;width:auto}.xHxPc_ .DZGkiP.m6DwE4{transition:visibility;transition-delay:.2s;visibility:visible}.xHxPc_ .DZGkiP .qbOWwq{display:inline-block}.xHxPc_ .B6ASxu{display:none}.vmPGjX>nav{bottom:0;left:0;position:absolute;right:0;top:0}.vmPGjX .YBArby{background:rgba(var(--bg,var(--color_11,color_11)),var(--alpha-bg,1)) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAL4CAYAAAC6F51xAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1MjgyM0FDRjc4NTVFMTExQURCRUEwRDE2OEU2QjI4MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4NDgxMDhDRTU1OTIxMUUxQjc4QUIzQ0M0NkYxMTE2NSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4NDgxMDhDRDU1OTIxMUUxQjc4QUIzQ0M0NkYxMTE2NSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjU0ODIzQUNGNzg1NUUxMTFBREJFQTBEMTY4RTZCMjgzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjUyODIzQUNGNzg1NUUxMTFBREJFQTBEMTY4RTZCMjgzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YZ6HtgAAAFVJREFUeNpi+P//fyQTAxCMEqPEKDFKjBKjxAgh/sKJf1hYOGX/YZH9h1cxVIyRkfEz3PL/hFlU5OJkMTDjCiFiALD1MJqQRolRYpQYJUaJwUMABBgAmxMz34m4jL0AAAAASUVORK5CYII=) repeat-x 50%;border-radius:var(--rd,10px);box-shadow:var(--shd,0 1px 4px rgba(0,0,0,.6));position:absolute}.vmPGjX .DZGkiP{margin-top:7px;position:absolute;visibility:hidden}.vmPGjX [data-dropmode=dropUp] .DZGkiP{margin-bottom:7px;margin-top:0}.vmPGjX .hpiQIN{background-color:rgba(var(--bgDrop,var(--color_11,color_11)),var(--alpha-bgDrop,1));border-radius:var(--rdDrop,10px);box-shadow:var(--shd,0 1px 4px rgba(0,0,0,.6))}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt_bootstrap-responsive.22c38c29.min.css">.r4OX7l,.xTjc1A{box-sizing:border-box;height:100%;overflow:visible;position:relative;width:auto}.r4OX7l[data-state~=header] a,.r4OX7l[data-state~=header] div,[data-state~=header].xTjc1A a,[data-state~=header].xTjc1A div{cursor:default!important}.r4OX7l .UiHgGh,.xTjc1A .UiHgGh{display:inline-block;height:100%;width:100%}.xTjc1A{--display:inline-block;cursor:pointer;display:var(--display);font:var(--fnt,var(--font_1))}.xTjc1A .yRj2ms{padding:0 var(--pad,5px)}.xTjc1A .JS76Uv{color:rgb(var(--txt,var(--color_15,color_15)));display:inline-block;padding:0 10px;transition:var(--trans,color .4s ease 0s)}.xTjc1A[data-state~=drop]{display:block;width:100%}.xTjc1A[data-state~=drop] .JS76Uv{padding:0 .5em}.xTjc1A[data-state~=link]:hover .JS76Uv,.xTjc1A[data-state~=over] .JS76Uv{color:rgb(var(--txth,var(--color_14,color_14)));transition:var(--trans,color .4s ease 0s)}.xTjc1A[data-state~=selected] .JS76Uv{color:rgb(var(--txts,var(--color_14,color_14)));transition:var(--trans,color .4s ease 0s)}.NHM1d1{overflow-x:hidden}.NHM1d1 .R_TAzU{display:flex;flex-direction:column;height:100%;width:100%}.NHM1d1 .R_TAzU .aOF1ks{flex:1}.NHM1d1 .R_TAzU .y7qwii{height:calc(100% - (var(--menuTotalBordersY, 0px)));overflow:visible;white-space:nowrap;width:calc(100% - (var(--menuTotalBordersX, 0px)))}.NHM1d1 .R_TAzU .y7qwii .Tg1gOB{display:inline-block}.NHM1d1 .R_TAzU .y7qwii .mvZ3NH{display:block;width:100%}.NHM1d1 .h3jCPd{display:block;opacity:1;z-index:99999}.NHM1d1 .h3jCPd .wkJ2fp{display:inherit;overflow:visible;visibility:inherit;white-space:nowrap;width:auto}.NHM1d1 .h3jCPd.DlGBN0{transition:visibility;transition-delay:.2s;visibility:visible}.NHM1d1 .h3jCPd .p90CkU{display:inline-block}.NHM1d1 .vh74Xw{display:none}.XwCBRN>nav{bottom:0;left:0;right:0;top:0}.XwCBRN .h3jCPd,.XwCBRN .y7qwii,.XwCBRN>nav{position:absolute}.XwCBRN .h3jCPd{margin-top:7px;visibility:hidden}.XwCBRN .h3jCPd[data-dropMode=dropUp]{margin-bottom:7px;margin-top:0}.XwCBRN .wkJ2fp{background-color:rgba(var(--bgDrop,var(--color_11,color_11)),var(--alpha-bgDrop,1));border-radius:var(--rd,0);box-shadow:var(--shd,0 1px 4px rgba(0,0,0,.6))}.P0dCOY .PJ4KCX{background-color:rgba(var(--bg,var(--color_11,color_11)),var(--alpha-bg,1));bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.d7EBK2{border-radius:var(--overflow-wrapper-border-radius)}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[MatrixGallery_MatrixGalleryTextOnCenterSkin].b2f2a24b.min.css">.WQdwxo{z-index:0}.WQdwxo .CFO7KK{--px-spacing:var(--spacing) + 1 /var(--number-of-columns) * var(--spacing);--pct-width:1/var(--number-of-columns) * 100%;display:flex;flex-wrap:wrap;margin-bottom:calc(var(--spacing)*-1);position:relative}.WQdwxo .CFO7KK .zmGziE{height:var(--row-height);margin-bottom:var(--spacing);margin-right:var(--spacing);position:relative;width:calc(var(--pct-width) - var(--px-spacing))}.WQdwxo .CFO7KK.GqSdVk .zmGziE:nth-child(10n),.WQdwxo .CFO7KK.K4v1Iq .zmGziE:nth-child(5n),.WQdwxo .CFO7KK.LdiWtM .zmGziE:nth-child(3n),.WQdwxo .CFO7KK.OGb9FN .zmGziE:nth-child(7n),.WQdwxo .CFO7KK.RNmlXJ .zmGziE:nth-child(1n),.WQdwxo .CFO7KK.VZFNG_ .zmGziE:nth-child(2n),.WQdwxo .CFO7KK.edqPBv .zmGziE:nth-child(9n),.WQdwxo .CFO7KK.sDpUrP .zmGziE:nth-child(4n),.WQdwxo .CFO7KK.yYU09P .zmGziE:nth-child(8n),.WQdwxo .CFO7KK.zwGMkZ .zmGziE:nth-child(6n){margin-right:0}.WQdwxo .pWuLEt{height:var(--show-more-container-height);pointer-events:none;position:relative}.WQdwxo .pWuLEt .EsrK3O{cursor:pointer;pointer-events:auto}.WQdwxo .pWuLEt{text-align:center}.WQdwxo .EsrK3O{color:rgb(var(--showMoreColor,var(--color_11,color_11)));font:var(--fntds,var(--font_9));line-height:2.3em;margin-top:2px;padding:0 10px;text-decoration:underline}.J4qk5E:not(.FaRnDh) .BCdhCr{bottom:0;left:0;position:absolute;right:0;top:0}.J4qk5E .lIeqhS{cursor:pointer}.J4qk5E .nWd_an{white-space:nowrap}.J4qk5E .VE0Sji{white-space:pre-line}.J4qk5E .VE0Sji,.J4qk5E .nWd_an{overflow:hidden;text-align:var(--text-align);text-overflow:ellipsis}.J4qk5E .DjiaC0,.J4qk5E .n8R8fO{bottom:0;left:0;position:absolute;right:0;top:0}.J4qk5E .BCdhCr{border-radius:var(--rd,0);box-shadow:var(--shd,0 1px 4px rgba(0,0,0,.6))}.J4qk5E .TVK6lq{background:rgba(var(--borderColor,var(--color_15,color_15)),var(--alpha-borderColor,1));border:solid var(--brw,0) rgba(var(--borderColor,var(--color_15,color_15)),var(--alpha-borderColor,1));border-radius:var(--rd,0);bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.J4qk5E .U062Z9{border-radius:var(--rd,0);overflow:hidden}.J4qk5E .Oa8_fR{background:rgba(var(--bgHover,var(--color_15,color_15)),var(--alpha-bgHover,1));border-radius:var(--rd,0);bottom:0;display:flex;flex-direction:column;left:0;opacity:0;padding:30px;position:absolute;right:0;top:0;transition:var(--trans,opacity .4s ease 0s)}.J4qk5E .nWd_an{color:rgb(var(--titleColor,var(--color_18,color_18)));font:var(--fntt,var(--font_7));white-space:pre-line}.J4qk5E .VE0Sji{color:rgb(var(--descColor,var(--color_11,color_11)));font:var(--fntds,var(--font_9))}.J4qk5E .VE0Sji,.J4qk5E .nWd_an{flex-shrink:0;line-height:1.3em;max-height:3.9em}.J4qk5E .VE0Sji:first-child,.J4qk5E .nWd_an:first-child{margin-top:auto}.J4qk5E .VE0Sji:last-child,.J4qk5E .nWd_an:last-child{margin-bottom:auto}.J4qk5E .nWd_an+.VE0Sji{margin-top:5px}.J4qk5E:hover .Oa8_fR{opacity:1}.J4qk5E .DjiaC0:focus-within .Oa8_fR{opacity:1}.YxfMAA{align-items:center;display:grid;justify-items:center;visibility:hidden}.YxfMAA>div:first-child{position:absolute}.YxfMAA img[src=""]{visibility:hidden}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[SkipToContentButton].39deac6a.min.css">.LHrbPP{background:#fff;border-radius:24px;color:#116dff;cursor:pointer;font-family:Helvetica,Arial,メイリオ,meiryo,ヒラギノ角ゴ pro w3,hiragino kaku gothic pro,sans-serif;font-size:14px;height:0;left:50%;margin-left:-94px;opacity:0;padding:0 24px 0 24px;pointer-events:none;position:absolute;top:60px;width:0;z-index:9999}.LHrbPP:focus{border:2px solid;height:40px;opacity:1;pointer-events:auto;width:auto}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[FiveGridLine_SolidLine].23b2f23d.min.css">.aVng1S{border-top:var(--lnw,2px) solid rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1));box-sizing:border-box;height:0}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[WPhoto_RoundPhoto].f22a9ddf.min.css">.nTOEE9{height:100%;overflow:hidden;width:100%}.nTOEE9.sqUyGm:hover{cursor:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3FpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODM3MEUzMUU4OTAyMTFFMzk3Q0FCMkFEODdDNzUzMjQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODM3MEUzMUQ4OTAyMTFFMzk3Q0FCMkFEODdDNzUzMjQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjk0ZTkyMTRlLThiNDQtNjc0My04MWZiLTZlYjIzYTA2ZjcwNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4bqsJgAAACF0lEQVR42mJgQAd8fHz/gdRvRigfxGEACCA4YvwPBMgCbgABBGOAJP6LiooiZBUUFMCC7969Awk6AQQQA1bAxMTUOnXq1P8/f/78j2zdf5BDQDgoKAgiyMgItv0/1AkozlgJlHwPpDWB+AhAACFL1EJVwvBPIGZHd8P/OXPmgI0F2YdmxXQUhX///sVQqK2tDVL4DFkhF8zK2NjY/4aGhshOOMJAJAB5ZjdAADGQCpiB4Cear3uwKQR74vv372BPLFq0CKZ4GnLcdMGiFtnXmzZtQo0Bdnb2r/b29nBFMIwUjkxghby8vHfFxMQwTMQWp0YggZcvX/5HBpqamhgKQdafAQnq6en9j4+P/4/me150nzsCPfYOKrkWKvYCymcjJozPgqIYIMAYcUjKAnEcELsDbVECOpkNiO8B+buAeCEQ3yUqFllYWNYh+4Obm/u/ubn5f0tLy//QPIqM90ATHVagDHTJH5BCfn7+/xcvXvyPC9y7d+8/KHqghv4FYj0M04BxeAOkQEhI6P+vX79QDECOeBj49+/ffzk5OZih91FyP4gAGiIDooH5hIGVlRUsAXQpGMMAMh+Y1xksLCzg5QxGrAFzwAxY2GzYsIGgC48cOYIclsuwBiIbG9sCmCJFRcX/+/fvxwi/EydOwIoDGH6JLQEiA26ga1egxSY2vAUpkcKKEV5iCwVOIObBU8w8RzLYgYHaAAACg5CxaxSLgwAAAABJRU5ErkJggg==),auto}.nTOEE9.C_JY0G:hover{cursor:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3FpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0I4QkNGQTI4OTAyMTFFMzg0RDlBRkM5NDA5QjczRTEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0I4QkNGQTE4OTAyMTFFMzg0RDlBRkM5NDA5QjczRTEiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjk0ZTkyMTRlLThiNDQtNjc0My04MWZiLTZlYjIzYTA2ZjcwNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpmMWUzNTlkMS1hYjZhLTNkNDctYmM0ZC03MWMyZDYyMWNmNDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7hiSPZAAACGklEQVR42mJgQAd8fHz/gdRvRigfxGEACCA4YvwPBMgCbgABBGOAJP6LiooiZBUUFMCC7969Awk6AQQQA1bAxMTUOnXq1P8/f/78j2zdf5BDQDgoKAgiyMgItv0/1AkozlgJlHwPpDWB+AhAACFL1EJVwvBPIGZHd8P/OXPm/EcHUA3TURT+/fsXQ6G2tjZI4TNkhVwwK2NjY/8bGhoiO+EIA5EA5JndAAHEQCpgBoKfaL7uwaYQHLrfv38He2LRokUwxdOQ46YLFrXIYNOmTagxwM7O/tXe3h4sCYs3EEYKRyawQl5e3rtiYmL/sQH0ODUCCbx8+RJFkaamJoZCkPVnQIJ6enr/4+Pj/6P5nhfd545Aj72DSq6Fir2A8tmICeOzoCgGCDBGHJKyQBwHxO5AW5SATmYD4ntA/i4gXgjEd4mKRRYWlnXI/uDm5v5vbm7+39LS8j80jyLjPdBEhxUoA13yB6SQn5///8WLF//jAvfu3fsPih6ooX+BWA/DNGAc3gApEBIS+v/r16//hMC/f//+y8nJwQy9j2wWC4gAGiIDooH5hIGVlRUsAXQpVq98/PgRVBAwWFhYMDx69AhczkBj7RdyFpgBC5sNGzYQdOGRI0eQw3IZVpvZ2NgWwBQpKir+379/P4ZBJ06cgBUHMPwSWwJEBtxA165Ai01seAtSIoUVI7zEFgqcQMyDp5h5jmSwAwO1AQBU5q033XYWQwAAAABJRU5ErkJggg==),auto}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}.rYiAuL{cursor:pointer}.gSXewE{height:0;left:0;overflow:hidden;position:absolute;top:0;width:0}.MVY5Lo{box-sizing:border-box;height:100%;width:100%}.NM_OGj{min-height:var(--image-min-height);min-width:var(--image-min-width)}.NM_OGj img{filter:var(--filter-effect-svg-url);-webkit-mask-image:var(--mask-image,none);mask-image:var(--mask-image,none);-webkit-mask-position:var(--mask-position,0);mask-position:var(--mask-position,0);-webkit-mask-repeat:var(--mask-repeat,no-repeat);mask-repeat:var(--mask-repeat,no-repeat);-webkit-mask-size:var(--mask-size,100% 100%);mask-size:var(--mask-size,100% 100%);-o-object-position:var(--object-position);object-position:var(--object-position)}.v__F4U .MVY5Lo{background-color:rgba(var(--brd,var(--color_11,color_11)),var(--alpha-brd,1));border-color:rgba(var(--brd,var(--color_11,color_11)),var(--alpha-brd,1));border-radius:var(--rd,5px);border-style:solid;border-width:var(--brw,2px);box-shadow:var(--shd,0 1px 3px rgba(0,0,0,.5));display:block;height:100%;overflow:hidden}.v__F4U .NM_OGj{border-radius:calc(var(--sizeRd, var(--rd, 5px)) - var(--sizeBrw, var(--brw, 2px)));overflow:hidden}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[SlideShowGallery_SlideShowCleanAndSimple].9f67a73f.min.css">.YxfMAA{align-items:center;display:grid;justify-items:center;visibility:hidden}.YxfMAA>div:first-child{position:absolute}.YxfMAA img[src=""]{visibility:hidden}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}.sNF2R0{opacity:0}.hLoBV3{transition:opacity var(--transition-duration) cubic-bezier(.37,0,.63,1)}.Rdf41z,.hLoBV3{opacity:1}.ftlZWo{transition:opacity var(--transition-duration) cubic-bezier(.37,0,.63,1)}.ATGlOr,.ftlZWo{opacity:0}.KQSXD0{transition:opacity var(--transition-duration) cubic-bezier(.64,0,.78,0)}.KQSXD0,.pagQKE{opacity:1}._6zG5H{opacity:0;transition:opacity var(--transition-duration) cubic-bezier(.22,1,.36,1)}.BB49uC{transform:translateX(100%)}.j9xE1V{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.ICs7Rs,.j9xE1V{transform:translateX(0)}.DxijZJ{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.B5kjYq,.DxijZJ{transform:translateX(-100%)}.cJijIV{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.cJijIV,.hOxaWM{transform:translateX(0)}.T9p3fN{transform:translateX(100%);transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.qDxYJm{transform:translateY(100%)}.aA9V0P{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.YPXPAS,.aA9V0P{transform:translateY(0)}.Xf2zsA{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.Xf2zsA,.y7Kt7s{transform:translateY(-100%)}.EeUgMu{transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.EeUgMu,.fdHrtm{transform:translateY(0)}.WIFaG4{transform:translateY(100%);transition:transform var(--transition-duration) cubic-bezier(.87,0,.13,1)}.kTA_a4{--force-state-metadata:forceShowButtons}.rvQAnj{z-index:0}.rvQAnj:not([data-image-mode=flexibleHeight]) .WUGTsY,.rvQAnj:not([data-image-mode=flexibleHeight]) .dP6TWI{bottom:0;left:0;position:absolute;right:0;top:0}.rvQAnj[data-image-mode=flexibleHeight] .dP6TWI{display:grid;grid-template-rows:min-content;position:relative}.rvQAnj[data-image-mode=flexibleHeight] .WUGTsY{grid-area:1/1/2/2;position:relative}.rvQAnj .n_KYTv{pointer-events:none}.rvQAnj .n_KYTv .Nod2eN{cursor:pointer;pointer-events:auto}.rvQAnj .WUGTsY{bottom:0;left:0;position:absolute;right:0;top:0}.rvQAnj .tYbtzz{cursor:pointer}.rvQAnj .tYbtzz .LfxvnL{display:inline-block}.rvQAnj .UakxP8{visibility:hidden}.rvQAnj .UakxP8,.rvQAnj .n_KYTv,.rvQAnj .sOdSKC{bottom:0;left:0;position:absolute;right:0;top:0}.rvQAnj .n_KYTv{display:grid;z-index:1}.rvQAnj .n_KYTv .Nod2eN{align-self:center}.rvQAnj .n_KYTv .iyMcQI{justify-self:start}.rvQAnj .n_KYTv .PgPQMu{justify-self:end}.rvQAnj{background:rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1));box-shadow:var(--shd,0 1px 3px rgba(0,0,0,.5));overflow:hidden}.rvQAnj,.rvQAnj .YKmufi{border-radius:var(--rd,0)}.rvQAnj .YKmufi{background:none;border:var(--brw,0) solid rgba(var(--brd,var(--color_15,color_15)),var(--alpha-brd,1));bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0}.rvQAnj .dP6TWI{border-radius:var(--rd,0);overflow:hidden}.rvQAnj .n_KYTv{transition:var(--trns,opacity .5s ease 0s)}.rvQAnj .n_KYTv .Nod2eN{background:rgba(var(--abg,var(--color_14,color_14)),var(--alpha-abg,1));bottom:0;height:134px;margin:auto;position:absolute;top:0;transition:var(--trns,opacity .5s ease 0s);width:32px}.rvQAnj .n_KYTv .Nod2eN .SLAFK5{background:url(https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/media/tiny_arrows.4355fe50.png) no-repeat 50% 0;bottom:0;height:16px;margin:auto;position:absolute;top:0;width:8px}.rvQAnj .n_KYTv .iyMcQI{left:0}.rvQAnj .n_KYTv .iyMcQI .SLAFK5{background-position:0 0;left:50%;margin-left:-4px}.rvQAnj .n_KYTv .PgPQMu{right:0}.rvQAnj .n_KYTv .PgPQMu .SLAFK5{background-position:100% 0;margin-right:-4px;right:50%}.rvQAnj.kTA_a4 .n_KYTv{opacity:1}.rvQAnj .nW9Xz5{opacity:0;padding:9px 10px 10px 0;position:absolute;right:var(--brw,0);text-align:right;transition:var(--trns,opacity .5s ease 0s);width:60px;z-index:1}.rvQAnj .nW9Xz5 .loRmar{color:rgba(var(--txt,var(--color_11,color_11)),var(--alpha-txt,1));display:inline-block;font-family:arial,helvetica,sans-serif;font-size:12px;padding-left:5px}.rvQAnj .nW9Xz5 .tYbtzz{min-height:10px;padding-left:10px;text-align:center;width:30px}.rvQAnj .nW9Xz5 .tYbtzz,.rvQAnj .nW9Xz5 .tYbtzz .LfxvnL{display:inline-block;position:relative}.rvQAnj .nW9Xz5 .tYbtzz.kOeWeF .LfxvnL{border:5px solid transparent;border-left:5px solid rgba(var(--txt,var(--color_11,color_11)),var(--alpha-txt,1));height:0;width:0}.rvQAnj .nW9Xz5 .tYbtzz.BEUaC2 .LfxvnL{border-left:2px solid rgba(var(--txt,var(--color_11,color_11)),var(--alpha-txt,1));border-right:2px solid rgba(var(--txt,var(--color_11,color_11)),var(--alpha-txt,1));height:10px;margin-right:5px;width:1px}.rvQAnj:focus-within .OfEa1e,.rvQAnj:focus-within .nW9Xz5,.rvQAnj:hover .OfEa1e,.rvQAnj:hover .nW9Xz5{opacity:1}:host(.device-mobile-optimized) .rvQAnj:focus-within .Nod2eN,body.device-mobile-optimized .rvQAnj:focus-within .Nod2eN{background:rgba(var(--abgh,var(--color_15,color_15)),var(--alpha-abgh,1));transition:var(--trns,opacity .5s ease 0s)}.rvQAnj .Nod2eN:hover{background:rgba(var(--abgh,var(--color_15,color_15)),var(--alpha-abgh,1));transition:var(--trns,opacity .5s ease 0s)}.rvQAnj.EFXFOU .nW9Xz5{left:0;right:auto;text-align:left}.cLiG_l .FfVTJS,.cLiG_l .VcuCbf,.cLiG_l .dAzcPH,.cLiG_l .qD5RnK,.cLiG_l:not([data-image-mode=flexibleWidthFixed]) .oMaBOp{bottom:0;left:0;position:absolute;right:0;top:0}.cLiG_l[data-image-mode=flexibleHeight] .KR5jBs,.cLiG_l[data-image-mode=flexibleHeight] .dAzcPH{position:relative}.cLiG_l[data-image-mode=flexibleHeight] .KR5jBs{display:block}.cLiG_l[data-image-mode=flexibleWidthFixed] .oMaBOp{position:absolute}.cLiG_l .LVfQdj{display:block}.cLiG_l .SKiqtb{cursor:pointer}.cLiG_l.zL1_j0 .R0UYn1{text-align:right}.cLiG_l{border-radius:var(--rd,0);margin:var(--brw,0);overflow:hidden}.cLiG_l .KR5jBs{margin:calc(var(--brw, 0)*-1)}.cLiG_l .qD5RnK{border-radius:var(--rd,0);overflow:hidden}.cLiG_l .R0UYn1{background-color:rgba(var(--bg,var(--color_15,color_15)),var(--alpha-bg,1));border-radius:var(--rd,0);border-top-left-radius:0;border-top-right-radius:0;bottom:calc(var(--s, 0px) - 1px);left:var(--brw,0);margin:calc(var(--brw, 0)*-1);min-height:18px;opacity:0;padding:10px 100px 10px 10px;position:absolute;right:var(--brw,0);transition:var(--trns,opacity .5s ease 0s)}.cLiG_l .R0UYn1 .fK2Wn6{color:rgb(var(--txt,var(--color_11,color_11)));font:var(--fntt,var(--font_6))}.cLiG_l .R0UYn1 .IQyQnK{color:rgb(var(--txt,var(--color_11,color_11)));font:var(--fntds,var(--font_8));white-space:pre-line}.cLiG_l.zL1_j0 .R0UYn1{padding:10px 10px 10px 100px}</style> +<style data-href="https://static.parastorage.com/services/editor-elements-library/dist/thunderbolt/rb_wixui.thunderbolt[ClassicSection].78038a64.min.css">.Oqnisf{overflow:visible}.cM88eO{backface-visibility:hidden}.YtfWHd{left:0;position:absolute;top:0}.HlRz5e{display:block;height:100%;width:100%}.HlRz5e img{max-width:var(--wix-img-max-width,100%)}.HlRz5e[data-animate-blur] img{filter:blur(9px);transition:filter .8s ease-in}.HlRz5e[data-animate-blur] img[data-load-done]{filter:none}.MW5IWV{height:100%;left:0;-webkit-mask-image:var(--mask-image,none);mask-image:var(--mask-image,none);-webkit-mask-position:var(--mask-position,0);mask-position:var(--mask-position,0);-webkit-mask-repeat:var(--mask-repeat,no-repeat);mask-repeat:var(--mask-repeat,no-repeat);-webkit-mask-size:var(--mask-size,100%);mask-size:var(--mask-size,100%);overflow:hidden;pointer-events:var(--fill-layer-background-media-pointer-events);position:absolute;top:0;width:100%}.MW5IWV.N3eg0s{clip:rect(0,auto,auto,0)}.MW5IWV .Kv1aVt{height:100%;position:absolute;top:0;width:100%}.MW5IWV .dLPlxY{height:var(--fill-layer-image-height,100%);opacity:var(--fill-layer-image-opacity)}.MW5IWV .dLPlxY img{height:100%;width:100%}@supports(-webkit-hyphens:none){.MW5IWV.N3eg0s{clip:auto;-webkit-clip-path:inset(0)}}.VgO9Yg{height:100%}.LWbAav{background-color:var(--bg-overlay-color);background-image:var(--bg-gradient);transition:var(--inherit-transition)}.K_YxMd,.yK6aSC{opacity:var(--fill-layer-video-opacity)}.NGjcJN{bottom:var(--media-padding-bottom);height:var(--media-padding-height);position:absolute;top:var(--media-padding-top);width:100%}.mNGsUM{transform:scale(var(--scale,1));transition:var(--transform-duration,transform 0s)}.K_YxMd{height:100%;position:relative;width:100%}.bX9O_S{-webkit-clip-path:var(--fill-layer-clip);clip-path:var(--fill-layer-clip)}.Z_wCwr,.bX9O_S{position:absolute;top:0}.Jxk_UL img,.Z_wCwr,.bX9O_S{height:100%;width:100%}.K8MSra{opacity:0}.K8MSra,.YTb3b4{position:absolute;top:0}.YTb3b4{height:0;left:0;overflow:hidden;width:0}.SUz0WK{left:0;pointer-events:var(--fill-layer-background-media-pointer-events);position:var(--fill-layer-background-media-position)}.FNxOn5,.SUz0WK,.m4khSP{height:100%;top:0;width:100%}.FNxOn5{position:absolute}.m4khSP{background-color:var(--fill-layer-background-overlay-color);opacity:var(--fill-layer-background-overlay-blend-opacity-fallback,1);position:var(--fill-layer-background-overlay-position);transform:var(--fill-layer-background-overlay-transform)}@supports(mix-blend-mode:overlay){.m4khSP{mix-blend-mode:var(--fill-layer-background-overlay-blend-mode);opacity:var(--fill-layer-background-overlay-blend-opacity,1)}}.dkukWC{--divider-pin-height__:min(1,calc(var(--divider-layers-pin-factor__) + 1));--divider-pin-layer-height__:var(--divider-layers-pin-factor__);--divider-pin-border__:min(1,calc(var(--divider-layers-pin-factor__) / -1 + 1));height:calc(var(--divider-height__) + var(--divider-pin-height__)*var(--divider-layers-size__)*var(--divider-layers-y__))}.dkukWC,.dkukWC .FRCqDF{left:0;position:absolute;width:100%}.dkukWC .FRCqDF{--divider-layer-i__:var(--divider-layer-i,0);background-position:left calc(50% + var(--divider-offset-x__) + var(--divider-layers-x__)*var(--divider-layer-i__)) bottom;background-repeat:repeat-x;border-bottom-style:solid;border-bottom-width:calc(var(--divider-pin-border__)*var(--divider-layer-i__)*var(--divider-layers-y__));height:calc(var(--divider-height__) + var(--divider-pin-layer-height__)*var(--divider-layer-i__)*var(--divider-layers-y__));opacity:calc(1 - var(--divider-layer-i__)/(var(--divider-layer-i__) + 1))}.xnZvZH{--divider-height__:var(--divider-top-height,auto);--divider-offset-x__:var(--divider-top-offset-x,0px);--divider-layers-size__:var(--divider-top-layers-size,0);--divider-layers-y__:var(--divider-top-layers-y,0px);--divider-layers-x__:var(--divider-top-layers-x,0px);--divider-layers-pin-factor__:var(--divider-top-layers-pin-factor,0);border-top:var(--divider-top-padding,0) solid var(--divider-top-color,currentColor);opacity:var(--divider-top-opacity,1);top:0;transform:var(--divider-top-flip,scaleY(-1))}.xnZvZH .FRCqDF{background-image:var(--divider-top-image,none);background-size:var(--divider-top-size,contain);border-color:var(--divider-top-color,currentColor);bottom:0;filter:var(--divider-top-filter,none)}.xnZvZH .FRCqDF[data-divider-layer="1"]{display:var(--divider-top-layer-1-display,block)}.xnZvZH .FRCqDF[data-divider-layer="2"]{display:var(--divider-top-layer-2-display,block)}.xnZvZH .FRCqDF[data-divider-layer="3"]{display:var(--divider-top-layer-3-display,block)}.MBOSCN{--divider-height__:var(--divider-bottom-height,auto);--divider-offset-x__:var(--divider-bottom-offset-x,0px);--divider-layers-size__:var(--divider-bottom-layers-size,0);--divider-layers-y__:var(--divider-bottom-layers-y,0px);--divider-layers-x__:var(--divider-bottom-layers-x,0px);--divider-layers-pin-factor__:var(--divider-bottom-layers-pin-factor,0);border-bottom:var(--divider-bottom-padding,0) solid var(--divider-bottom-color,currentColor);bottom:0;opacity:var(--divider-bottom-opacity,1);transform:var(--divider-bottom-flip,none)}.MBOSCN .FRCqDF{background-image:var(--divider-bottom-image,none);background-size:var(--divider-bottom-size,contain);border-color:var(--divider-bottom-color,currentColor);bottom:0;filter:var(--divider-bottom-filter,none)}.MBOSCN .FRCqDF[data-divider-layer="1"]{display:var(--divider-bottom-layer-1-display,block)}.MBOSCN .FRCqDF[data-divider-layer="2"]{display:var(--divider-bottom-layer-2-display,block)}.MBOSCN .FRCqDF[data-divider-layer="3"]{display:var(--divider-bottom-layer-3-display,block)}</style> +<style data-href="https://static.parastorage.com/services/wix-thunderbolt/dist/TPABaseComponent.27e1e284.chunk.min.css">.OQ8Tzd,.nKphmK{overflow:hidden}.nKphmK{height:100%;position:relative;width:100%}.nKphmK:-ms-fullscreen{min-height:auto!important}.nKphmK:fullscreen{min-height:auto!important}.pvlz2w{visibility:hidden}</style> +<style data-href="https://static.parastorage.com/services/wix-thunderbolt/dist/group_7.bae0ce0c.chunk.min.css">.TWFxr5{height:auto!important}.May50y{overflow:hidden!important}</style> +<title>April 22nd - 27th Programming + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 1e62cc359992a1a553644a8d28eb8ef1ab7ecc41 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sun, 12 May 2024 16:55:02 -0600 Subject: [PATCH 05/14] Starting to parse meta --- page.go | 24 ++++++++++++------------ page_test.go | 4 ++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/page.go b/page.go index 93e6167..d878b7b 100644 --- a/page.go +++ b/page.go @@ -138,21 +138,21 @@ func getAttributes(attribute string, n *html.Node) []string { return attributes } -// func getMeta(htmlContent []byte) ([]string, error) { -// element := "meta" +func getMeta(htmlContent []byte) ([]string, error) { + element := "meta" -// doc, err := html.Parse(bytes.NewReader(htmlContent)) -// if err != nil { -// return []string{}, err -// } + doc, err := html.Parse(bytes.NewReader(htmlContent)) + if err != nil { + return []string{}, err + } -// meta := getAttributes(element, doc) + meta := getAttributes(element, doc) -// if len(meta) == 0 { -// return meta, fmt.Errorf("no %s element found", element) -// } -// return meta, nil -// } + if len(meta) == 0 { + return meta, fmt.Errorf("no %s element found", element) + } + return meta, nil +} func getTitle(htmlFilePath string, htmlContent []byte) (string, error) { title, err := titleFromHTMLTitleElement(htmlContent) diff --git a/page_test.go b/page_test.go index 131f139..4969830 100644 --- a/page_test.go +++ b/page_test.go @@ -38,3 +38,7 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) { t.Fatal(cmp.Diff("", received)) } } + +func TestMetaIsPopulatedWithExpectedElements(t *testing.T) { + +} From e5f73af70a0ea7b649dcd0836d1ffd4bbcc7e286 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sat, 1 Jun 2024 21:01:10 -0600 Subject: [PATCH 06/14] Adds an architecture document for new devs to guide themselves with --- ARCHITECTURE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..906a7e1 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,15 @@ +Andrew has two primary concepts: the Server and the Page. + +The Server owns: +* answering questions about the layout of files in the directory structure +* understanding the kinds of file that are being served +* serving those files +If you are answering a question about files, the Server's got the answers. The server creates Pages +and serves those Pages. + + +The Page owns the content and metadata. + +Page tracks the content of a specific file and various pieces of metadata about it. + +For example, the page parses the contents of a file and parses out the andrew metadata headers, so that when the Server wants to present those elements to an end-user they're already built. \ No newline at end of file From 3d7348bd46bf9e94d91ce441497a44f218401bea Mon Sep 17 00:00:00 2001 From: Gwyn Date: Tue, 4 Jun 2024 21:18:41 -0600 Subject: [PATCH 07/14] Refactors getAttributes to be more accurately named as getTags. Also fixes the implementation so it's pulling back tags now. --- page.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/page.go b/page.go index d878b7b..17b9040 100644 --- a/page.go +++ b/page.go @@ -117,28 +117,35 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte { return b } -// getAttribute recursively descends an html node tree, searching for +// getTags recursively descends an html node tree, searching for // the attribute provided. Once the attribute is discovered, it returns. -func getAttributes(attribute string, n *html.Node) []string { - var attributes []string - - //n.Type no longer matches html.ElementNode; n is now a document, not a node - if n.Type == html.ElementNode { - for _, a := range n.Attr { - if a.Key == attribute { - attributes = append(attributes, a.Val) +func getTags(attribute string, n *html.Node) []string { + var tags []string + + // getTag recursively descends an html node tree, searching for + // the attribute provided. Once the attribute is discovered, it appends to attributes. + var getTag func(n *html.Node) + + getTag = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == attribute { + if n.FirstChild != nil { + tags = append(tags, n.FirstChild.Data) + return } } - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - attributes = append(attributes, getAttributes(attribute, c)...) + for c := n.FirstChild; c != nil; c = c.NextSibling { + getTag(c) + } } - return attributes + // Start the recursion from the root node + getTag(n) + + return tags } -func getMeta(htmlContent []byte) ([]string, error) { +func GetMetaElements(htmlContent []byte) ([]string, error) { element := "meta" doc, err := html.Parse(bytes.NewReader(htmlContent)) @@ -146,7 +153,7 @@ func getMeta(htmlContent []byte) ([]string, error) { return []string{}, err } - meta := getAttributes(element, doc) + meta := getTags(element, doc) if len(meta) == 0 { return meta, fmt.Errorf("no %s element found", element) @@ -176,7 +183,7 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) { return "", err } - title := getAttributes("title", doc) + title := getTags("title", doc) if len(title) == 0 { return "", fmt.Errorf("no title element found") } From 5dcf2ac6ac83fd72f5c7c4d43bb227c5db86f90c Mon Sep 17 00:00:00 2001 From: Gwyn Date: Mon, 10 Jun 2024 20:00:29 -0600 Subject: [PATCH 08/14] Progress: the test now fails with a recognisable data structure. --- andrew_server_test.go | 72 +++++++++++++++++++++---------------------- page.go | 26 +++++++++++----- page_test.go | 25 ++++++++++++++- 3 files changed, 78 insertions(+), 45 deletions(-) diff --git a/andrew_server_test.go b/andrew_server_test.go index a5d02a3..cc569f1 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -426,54 +426,54 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) { // TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that // when the list of links andrew generates for the {{.AndrewIndexBody}} are // sorted by mtime, not using the ascii sorting order. -func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { +// func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { - expected := ` - -` +// expected := ` +// +// ` - contentRoot := t.TempDir() +// contentRoot := t.TempDir() - // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. - // above might be wrong - err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) - if err != nil { - t.Fatal(err) - } +// // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. +// // above might be wrong +// err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) +// if err != nil { +// t.Fatal(err) +// } - err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700) - if err != nil { - t.Fatal(err) - } +// err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700) +// if err != nil { +// t.Fatal(err) +// } - err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700) - if err != nil { - t.Fatal(err) - } +// err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700) +// if err != nil { +// t.Fatal(err) +// } - // This test requires having two files which are in one order when sorted - // ascii-betically and in another order by date time, so that we can tell - // what file attribute andrew is actually sorting on. - now := time.Now() - older := now.Add(-10 * time.Minute) +// // This test requires having two files which are in one order when sorted +// // ascii-betically and in another order by date time, so that we can tell +// // what file attribute andrew is actually sorting on. +// now := time.Now() +// older := now.Add(-10 * time.Minute) - os.Chtimes(contentRoot+"/a.html", now, now) - os.Chtimes(contentRoot+"/b.html", older, older) +// os.Chtimes(contentRoot+"/a.html", now, now) +// os.Chtimes(contentRoot+"/b.html", older, older) - server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} - page, err := andrew.NewPage(server, "index.html") +// server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} +// page, err := andrew.NewPage(server, "index.html") - if err != nil { - t.Fatal(err) - } +// if err != nil { +// t.Fatal(err) +// } - received := page.Content +// received := page.Content - if expected != string(received) { - t.Errorf(cmp.Diff(expected, received)) - } +// if expected != string(received) { +// t.Errorf(cmp.Diff(expected, received)) +// } -} +// } // newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against // to retrieve data from that server diff --git a/page.go b/page.go index 17b9040..9c6be51 100644 --- a/page.go +++ b/page.go @@ -117,21 +117,31 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte { return b } -// getTags recursively descends an html node tree, searching for -// the attribute provided. Once the attribute is discovered, it returns. -func getTags(attribute string, n *html.Node) []string { - var tags []string +// getTags recursively descends an html node tree for the requested tag, +// searching both data and attributes to find information about the node that's requested. +func getTags(tag string, n *html.Node) []string { + var tagContent []string // getTag recursively descends an html node tree, searching for // the attribute provided. Once the attribute is discovered, it appends to attributes. var getTag func(n *html.Node) getTag = func(n *html.Node) { - if n.Type == html.ElementNode && n.Data == attribute { + if n.Type == html.ElementNode && n.Data == tag { + + if n.Attr != nil { + for _, attribute := range n.Attr { + tagContent = append(tagContent, attribute.Key) + tagContent = append(tagContent, attribute.Val) + } + } + if n.FirstChild != nil { - tags = append(tags, n.FirstChild.Data) - return + //Tag attributes and tag Data are being conflated here. + tagContent = append(tagContent, n.FirstChild.Data) } + + return } for c := n.FirstChild; c != nil; c = c.NextSibling { @@ -142,7 +152,7 @@ func getTags(attribute string, n *html.Node) []string { // Start the recursion from the root node getTag(n) - return tags + return tagContent } func GetMetaElements(htmlContent []byte) ([]string, error) { diff --git a/page_test.go b/page_test.go index 4969830..f3dd3e9 100644 --- a/page_test.go +++ b/page_test.go @@ -1,6 +1,7 @@ package andrew import ( + "slices" "testing" "github.com/google/go-cmp/cmp" @@ -39,6 +40,28 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) { } } -func TestMetaIsPopulatedWithExpectedElements(t *testing.T) { +func TestMetaPopulatesATag(t *testing.T) { + expected := []string{"andrew-created-at 2025-03-01"} + received, err := GetMetaElements([]byte("")) + if err != nil { + t.Fatal(err) + } + + if !slices.Equal(expected, received) { + t.Fatal(cmp.Diff(expected, received)) + } } + +// func TestMetaIsPopulatedWithExpectedElements(t *testing.T) { +// expected := map[string]string{"andrew-created-at": "2025-03-01"} +// received, err := GetMetaElements([]byte("")) + +// if err != nil { +// t.Fatal(err) +// } + +// if received != expected { +// t.Fatal(cmp.Diff(expected, received)) +// } +// } From d08d0f7aea59a5aafc54b5104a9e9cfc9afc0f2d Mon Sep 17 00:00:00 2001 From: Gwyn Date: Wed, 12 Jun 2024 06:37:19 -0600 Subject: [PATCH 09/14] Correctly parsing the meta tags as a map now. --- page.go | 58 +++++++++++++++++++++++++++++++++++----------------- page_test.go | 28 ++++++++++++------------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/page.go b/page.go index 9c6be51..e27f681 100644 --- a/page.go +++ b/page.go @@ -33,6 +33,11 @@ type Page struct { PublishTime time.Time } +type TagInfo struct { + Data string + Attributes map[string]string +} + // NewPage creates a Page from a URL by reading the corresponding file from the // AndrewServer's SiteFiles. func NewPage(server Server, pageUrl string) (Page, error) { @@ -117,28 +122,42 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte { return b } -// getTags recursively descends an html node tree for the requested tag, +// getTagInfo recursively descends an html node tree for the requested tag, +// searching both data and attributes to find information about the node that's requested. +// getTagInfo recursively descends an html node tree for the requested tag, // searching both data and attributes to find information about the node that's requested. -func getTags(tag string, n *html.Node) []string { - var tagContent []string +func getTagInfo(tag string, n *html.Node) TagInfo { + var tagDataAndAttributes TagInfo = TagInfo{ + Data: "", + Attributes: make(map[string]string), + } // getTag recursively descends an html node tree, searching for - // the attribute provided. Once the attribute is discovered, it appends to attributes. + // the attribute provided. Once the attribute is discovered, it first checks + // for any Attributes available on the html node. If there are no Attributes, + // the key won't exist in the tagDataAndAttributes map. + // If there is data, it will append to attributes. var getTag func(n *html.Node) getTag = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == tag { + a := "" + b := "" if n.Attr != nil { - for _, attribute := range n.Attr { - tagContent = append(tagContent, attribute.Key) - tagContent = append(tagContent, attribute.Val) + for _, attr := range n.Attr { + switch attr.Key { + case "content": + b = attr.Val + case "name": + a = attr.Val + } + tagDataAndAttributes.Attributes[a] = b } } if n.FirstChild != nil { - //Tag attributes and tag Data are being conflated here. - tagContent = append(tagContent, n.FirstChild.Data) + tagDataAndAttributes.Data = n.FirstChild.Data } return @@ -152,23 +171,24 @@ func getTags(tag string, n *html.Node) []string { // Start the recursion from the root node getTag(n) - return tagContent + return tagDataAndAttributes } -func GetMetaElements(htmlContent []byte) ([]string, error) { +func GetMetaElements(htmlContent []byte) (map[string]string, error) { element := "meta" doc, err := html.Parse(bytes.NewReader(htmlContent)) if err != nil { - return []string{}, err + return map[string]string{}, err } - meta := getTags(element, doc) + tagInfo := getTagInfo(element, doc) - if len(meta) == 0 { - return meta, fmt.Errorf("no %s element found", element) + if len(tagInfo.Attributes) == 0 { + return tagInfo.Attributes, fmt.Errorf("no %s element found", element) } - return meta, nil + + return tagInfo.Attributes, nil } func getTitle(htmlFilePath string, htmlContent []byte) (string, error) { @@ -193,9 +213,9 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) { return "", err } - title := getTags("title", doc) - if len(title) == 0 { + tagInfo := getTagInfo("title", doc) + if len(tagInfo.Data) == 0 { return "", fmt.Errorf("no title element found") } - return title[0], nil + return tagInfo.Data, nil } diff --git a/page_test.go b/page_test.go index f3dd3e9..786089b 100644 --- a/page_test.go +++ b/page_test.go @@ -1,7 +1,7 @@ package andrew import ( - "slices" + "maps" "testing" "github.com/google/go-cmp/cmp" @@ -40,28 +40,28 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) { } } -func TestMetaPopulatesATag(t *testing.T) { - expected := []string{"andrew-created-at 2025-03-01"} +func TestOneMetaTagPopulatesATag(t *testing.T) { + expected := map[string]string{"andrew-created-at": "2025-03-01"} received, err := GetMetaElements([]byte("")) if err != nil { t.Fatal(err) } - if !slices.Equal(expected, received) { + if !maps.Equal(expected, received) { t.Fatal(cmp.Diff(expected, received)) } } -// func TestMetaIsPopulatedWithExpectedElements(t *testing.T) { -// expected := map[string]string{"andrew-created-at": "2025-03-01"} -// received, err := GetMetaElements([]byte("")) +func TestMultipleMetaTagsPopulatedWithExpectedElements(t *testing.T) { + expected := map[string]string{"andrew-created-at": "2025-03-01", "andrew-roflcopter": "hippolol"} + received, err := GetMetaElements([]byte(" ")) -// if err != nil { -// t.Fatal(err) -// } + if err != nil { + t.Fatal(err) + } -// if received != expected { -// t.Fatal(cmp.Diff(expected, received)) -// } -// } + if !maps.Equal(expected, received) { + t.Fatal(cmp.Diff(expected, received)) + } +} From 33920f5f535ce9af80ddeb7b940f7e3ae74789bd Mon Sep 17 00:00:00 2001 From: Gwyn Date: Tue, 25 Jun 2024 08:07:04 -0600 Subject: [PATCH 10/14] Adds a page PublishTime Extracts a little server functionality into a new linksbuilder file, just to keep it cleaner, no other reason. Uncomments the test for link ordering. It fails this moment, but one step at a time. --- andrew_server.go | 34 +- andrew_server_test.go | 77 ++-- linksbuilder.go | 49 +++ page.go | 105 ++--- page.html | 984 ------------------------------------------ 5 files changed, 143 insertions(+), 1106 deletions(-) create mode 100644 linksbuilder.go delete mode 100644 page.html diff --git a/andrew_server.go b/andrew_server.go index aa8ee09..48e0666 100644 --- a/andrew_server.go +++ b/andrew_server.go @@ -146,11 +146,8 @@ func CheckPageErrors(err error) (string, int) { // GetSiblingsAndChildren accepts a path to a file and a filter function. // It infers the directory that the file resides within, and then recurses the Server's fs.FS // to return all of the files both in the same directory and further down in the directory structure. -// To filter these down to only files that you care about, pass in a filter function. -// The filter is called in the context of fs.WalkDir. It is handed fs.WalkDir's path and directory entry, -// in that order, and is expected to return a boolean false. -// If that error is nil then the current file being evaluated is skipped for consideration. -func (a Server) GetSiblingsAndChildren(pagePath string, filter func(string, fs.DirEntry) bool) ([]Page, error) { +func (a Server) GetSiblingsAndChildren(pagePath string) ([]Page, error) { + pages := []Page{} localContentRoot := path.Dir(pagePath) @@ -159,19 +156,28 @@ func (a Server) GetSiblingsAndChildren(pagePath string, filter func(string, fs.D return err } - if filter(path, d) { - // links require a URL relative to the page we're discovering siblings from, not from - // the root of the file system - page, err := NewPage(a, path) - page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/")) + // We don't list index files in our collection of siblings and children, because I don't + // want a link back to a page that contains only links. + if strings.Contains(path, "index.html") { + return nil + } + + // If the file we're considering isn't an html file, let's move on with our day. + if !strings.Contains(path, "html") { + return nil + } - if err != nil { - return err - } + // links require a URL relative to the page we're discovering siblings from, not from + // the root of the file system + page, err := NewPage(a, path) + page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/")) - pages = append(pages, page) + if err != nil { + return err } + pages = append(pages, page) + return nil }) diff --git a/andrew_server_test.go b/andrew_server_test.go index cc569f1..ebbc183 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -291,9 +291,6 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { t.Fatal(err) } - // The test is displaying parentDir/childDir/1-2-3.html as its link; this is because generateAndrewIndexBody now returns AndrewPages, - // and the link that these maintain internally is their URL. Instead of the URL, we need a link path. - // GetSiblingsAndChildren maintains a localContentRoot variable that contains the directory we are residing within expectedIndex := ` @@ -426,54 +423,54 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) { // TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that // when the list of links andrew generates for the {{.AndrewIndexBody}} are // sorted by mtime, not using the ascii sorting order. -// func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { +// This test requires having two files which are in one order when sorted +// ascii-betically and in another order by date time, so that we can tell +// what file attribute andrew is actually sorting on. +func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { + expected := `b_newer.html +a_older.html +` -// expected := ` -// -// ` + contentRoot := t.TempDir() -// contentRoot := t.TempDir() + // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. + // above might be wrong + err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) + if err != nil { + t.Fatal(err) + } -// // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. -// // above might be wrong -// err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) -// if err != nil { -// t.Fatal(err) -// } + err = os.WriteFile(contentRoot+"/a_older.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } -// err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700) -// if err != nil { -// t.Fatal(err) -// } + err = os.WriteFile(contentRoot+"/b_newer.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } -// err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700) -// if err != nil { -// t.Fatal(err) -// } + now := time.Now() + older := now.Add(-10 * time.Minute) -// // This test requires having two files which are in one order when sorted -// // ascii-betically and in another order by date time, so that we can tell -// // what file attribute andrew is actually sorting on. -// now := time.Now() -// older := now.Add(-10 * time.Minute) + os.Chtimes(contentRoot+"/b_newer.html", now, now) + os.Chtimes(contentRoot+"/a_older.html", older, older) -// os.Chtimes(contentRoot+"/a.html", now, now) -// os.Chtimes(contentRoot+"/b.html", older, older) + server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} -// server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} -// page, err := andrew.NewPage(server, "index.html") + page, err := andrew.NewPage(server, "index.html") -// if err != nil { -// t.Fatal(err) -// } + if err != nil { + t.Fatal(err) + } -// received := page.Content + received := page.Content -// if expected != string(received) { -// t.Errorf(cmp.Diff(expected, received)) -// } + if expected != string(received) { + t.Errorf(cmp.Diff(expected, received)) + } -// } +} // newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against // to retrieve data from that server @@ -516,7 +513,5 @@ func newTestAndrewServer(t *testing.T, contentRoot fs.FS) *andrew.Server { // Wait for server to be confirmed ready <-ready - t.Logf("Running server on %s\n", addr) - return server } diff --git a/linksbuilder.go b/linksbuilder.go new file mode 100644 index 0000000..080b2eb --- /dev/null +++ b/linksbuilder.go @@ -0,0 +1,49 @@ +package andrew + +import ( + "bytes" + "fmt" + "text/template" +) + +// Building the template body requires information from both the Server and the Page. +// The Server supplies information from the file system, such as the siblings of a page +// in its directory. +// The Page supplies information from with the page itself, like the created-at date or +// the title of the page. + +// BuildPageBodyWithLinks receives the path to a file, currently normally an index file. +// It traverses the file system starting at the directory containing +// that file, finds all html files that are _not_ index.html files and returns them +// as a list of html links to those pages. +func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPage Page) ([]byte, error) { + + var links bytes.Buffer + + for i, sibling := range siblings { + links.Write(buildAndrewIndexLink(sibling.UrlPath, sibling.Title, i)) + } + + templateBuffer := bytes.Buffer{} + // execute template here, write it to something and then return it as the pageContent + t, err := template.New(startingPage.UrlPath).Parse(startingPage.Content) + + if err != nil { + // TODO: swap this for proper error handling + panic(err) + } + + err = t.Execute(&templateBuffer, map[string]string{"AndrewIndexBody": links.String()}) + if err != nil { + return templateBuffer.Bytes(), err + } + + return templateBuffer.Bytes(), nil +} + +// buildAndrewIndexLink encapsulates the format of the link +func buildAndrewIndexLink(urlPath string, title string, cssIdNumber int) []byte { + link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title) + b := []byte(link) + return b +} diff --git a/page.go b/page.go index e27f681..b0814b9 100644 --- a/page.go +++ b/page.go @@ -6,18 +6,11 @@ import ( "io/fs" "path" "strings" - "text/template" "time" "golang.org/x/net/html" ) -const ( - // The index.html has overhead associated with processing its internals, so it gets - // processed separately from other pages. - indexIdentifier = "index.html" -) - // Page tracks the content of a specific file and various pieces of metadata about it. // The Page makes creating links and serving content convenient, as it lets me offload // the parsing of any elements into a constructor, so that when I need to present those @@ -28,7 +21,6 @@ type Page struct { // According to https://datatracker.ietf.org/doc/html/rfc1738#section-3.1, the subsection of a // URL after the procol://hostname is the UrlPath. UrlPath string - Meta []string Content string PublishTime time.Time } @@ -40,86 +32,69 @@ type TagInfo struct { // NewPage creates a Page from a URL by reading the corresponding file from the // AndrewServer's SiteFiles. +// NewPage does this by reading the page content from disk, then parsing out various +// metadata that are convenient to have quick access to, such as the page title or the +// publish time. func NewPage(server Server, pageUrl string) (Page, error) { pageContent, err := fs.ReadFile(server.SiteFiles, pageUrl) if err != nil { return Page{}, err } + pageInfo, err := fs.Stat(server.SiteFiles, pageUrl) + if err != nil { + return Page{}, err + } + // The fs.FS documentation notes that paths should not start with a leading slash. pagePath := strings.TrimPrefix(pageUrl, "/") + pageTitle, err := getTitle(pagePath, pageContent) if err != nil { return Page{}, err } - if strings.Contains(pageUrl, indexIdentifier) { - pageContent, err = buildAndrewIndexBody(server, pageUrl, pageContent) - if err != nil { - return Page{}, err - } - } - - // pageMeta := getMeta(pagePath, pageContent) - return Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}, nil -} + page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle} -// SetUrlPath updates the UrlPath on a pre-existing Page. -func (a Page) SetUrlPath(urlPath string) Page { - return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath} -} + if strings.Contains(pageUrl, "index.html") { + siblings, err := server.GetSiblingsAndChildren(page.UrlPath) -// buildAndrewIndexBody receives the path to a file, currently normally an index file. -// It traverses the file system starting at the directory containing -// that file, finds all html files that are _not_ index.html files and returns them -// as a list of html links to those pages. -func buildAndrewIndexBody(server Server, startingPageUrl string, pageContent []byte) ([]byte, error) { - filterIndexFiles := func(path string, d fs.DirEntry) bool { - if strings.Contains(path, "index.html") { - return false + if err != nil { + return page, err } - if !strings.Contains(path, "html") { - return false + pageContent, err = BuildPageBodyWithLinks(siblings, pageUrl, page) + if err != nil { + return Page{}, err } - return true + page.Content = string(pageContent) } - siblings, err := server.GetSiblingsAndChildren(startingPageUrl, filterIndexFiles) + meta, err := GetMetaElements(pageContent) if err != nil { - return pageContent, err + return Page{}, err } - var links bytes.Buffer - cssIdNumber := 0 + publishTime, ok := meta["andrew-created-at"] - for _, sibling := range siblings { - links.Write(buildAndrewIndexLink(sibling, cssIdNumber)) - cssIdNumber = cssIdNumber + 1 - } + if ok { + page.PublishTime, err = time.Parse(time.DateOnly, publishTime) - templateBuffer := bytes.Buffer{} - // execute template here, write it to something and then return it as the pageContent - t, err := template.New(startingPageUrl).Parse(string(pageContent)) - if err != nil { - // TODO: swap this for proper error handling - panic(err) + if err != nil { + // log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time") + page.PublishTime = pageInfo.ModTime() + } + } else { + page.PublishTime = pageInfo.ModTime() } - err = t.Execute(&templateBuffer, map[string]string{server.Andrewindexbodytemplate: links.String()}) - if err != nil { - // TODO: swap this for proper error handling - panic(err) - } - return templateBuffer.Bytes(), nil + return page, nil } -// buildAndrewIndexLink encapsulates the format of the link -func buildAndrewIndexLink(page Page, cssIdNumber int) []byte { - link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), page.UrlPath, page.Title) - b := []byte(link) - return b +// SetUrlPath updates the UrlPath on a pre-existing Page. +func (a Page) SetUrlPath(urlPath string) Page { + return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath} } // getTagInfo recursively descends an html node tree for the requested tag, @@ -141,18 +116,18 @@ func getTagInfo(tag string, n *html.Node) TagInfo { getTag = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == tag { - a := "" - b := "" + attrName := "" + attrVal := "" if n.Attr != nil { for _, attr := range n.Attr { switch attr.Key { case "content": - b = attr.Val + attrVal = attr.Val case "name": - a = attr.Val + attrName = attr.Val } - tagDataAndAttributes.Attributes[a] = b + tagDataAndAttributes.Attributes[attrName] = attrVal } } @@ -184,10 +159,6 @@ func GetMetaElements(htmlContent []byte) (map[string]string, error) { tagInfo := getTagInfo(element, doc) - if len(tagInfo.Attributes) == 0 { - return tagInfo.Attributes, fmt.Errorf("no %s element found", element) - } - return tagInfo.Attributes, nil } diff --git a/page.html b/page.html deleted file mode 100644 index c9f0c8f..0000000 --- a/page.html +++ /dev/null @@ -1,984 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -April 22nd - 27th Programming - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From b872fe7633d7670521ba2ef997d1d7daef317d49 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Wed, 26 Jun 2024 06:58:55 -0600 Subject: [PATCH 11/14] Renames AndrewIndexBody to AndrewTableOfContents This more naturally reflects what this tag is doing: it's a TOC from here on down. --- README.md | 16 ++++++++-------- andrew_server.go | 28 ++++++++++++++-------------- andrew_server_test.go | 26 +++++++++++++------------- linksbuilder.go | 10 +++++----- todo.md | 2 +- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e6e0bf6..5f878ee 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ baseUrl is the hostname you're serving from. This is a part of sitemaps and rss e.g. `https://playtechnique.io` -## rendering the .AndrewIndexBody +## rendering the .AndrewTableOfContents Given this file system structure: ```text index.html @@ -44,19 +44,19 @@ fanfics/ what-if-elves-rode-mice-pt2.html ``` -if articles/index.html contains `{{ .AndrewIndexBody }}` anywhere, that will be replaced with: +if articles/index.html contains `{{ .AndrewTableOfContents }}` anywhere, that will be replaced with: ```html - article 1 - article 2 + article 1 + article 2 ``` -if fanfics/index.html contains `{{ .AndrewIndexBody }}`, that'll be replaced with: +if fanfics/index.html contains `{{ .AndrewTableOfContents }}`, that'll be replaced with: ```html - Potter and Draco - what-if-elves-rode-mice-pt1.html - what-if-elves-rode-mice-pt2.html + Potter and Draco + what-if-elves-rode-mice-pt1.html + what-if-elves-rode-mice-pt2.html ``` ## page titles diff --git a/andrew_server.go b/andrew_server.go index 48e0666..a57d340 100644 --- a/andrew_server.go +++ b/andrew_server.go @@ -16,28 +16,28 @@ import ( // When a URL is requested, Server creates an Page for the file referenced // in that URL and then serves the Page. type Server struct { - SiteFiles fs.FS // The files being served - BaseUrl string // The URL used in any links generated for this website that should contain the hostname. - Address string // IpAddress:Port combo to be served on. - Andrewindexbodytemplate string // The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server. - HTTPServer *http.Server + SiteFiles fs.FS // The files being served + BaseUrl string // The URL used in any links generated for this website that should contain the hostname. + Address string // IpAddress:Port combo to be served on. + Andrewtableofcontentstemplate string // The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server. + HTTPServer *http.Server } const ( - AndrewIndexBodyTemplate = "AndrewIndexBody" - DefaultContentRoot = "." - DefaultAddress = ":8080" - DefaultBaseUrl = "http://localhost:8080" + AndrewTableOfContentsTemplate = "AndrewTableOfContents" + DefaultContentRoot = "." + DefaultAddress = ":8080" + DefaultBaseUrl = "http://localhost:8080" ) -// NewServer is a constructor. Its primary role is setting the default andrewindexbodytemplate. +// NewServer is a constructor. Its primary role is setting the default andrewtableofcontentstemplate. // Returns an [Server]. func NewServer(contentRoot fs.FS, address, baseUrl string) *Server { s := &Server{ - SiteFiles: contentRoot, - Andrewindexbodytemplate: "AndrewIndexBody", - Address: address, - BaseUrl: baseUrl, + SiteFiles: contentRoot, + Andrewtableofcontentstemplate: "AndrewTableOfContents", + Address: address, + BaseUrl: baseUrl, } mux := http.NewServeMux() mux.HandleFunc("/", s.Serve) diff --git a/andrew_server_test.go b/andrew_server_test.go index ebbc183..637503d 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -216,7 +216,7 @@ func TestServerServesIndexPageByDefault(t *testing.T) { } } -func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) { +func TestAndrewTableOfContentsIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) { t.Parallel() contentRoot := fstest.MapFS{ @@ -224,7 +224,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) -{{ .AndrewIndexBody }} +{{ .AndrewTableOfContents }} `)}, "pages/1-2-3.html": &fstest.MapFile{Data: []byte(` @@ -251,7 +251,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) -1-2-3 Page +1-2-3 Page ` @@ -260,7 +260,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) } } -func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { +func TestAndrewTableOfContentsIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { t.Parallel() contentRoot := fstest.MapFS{ @@ -268,7 +268,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { -{{ .AndrewIndexBody }} +{{ .AndrewTableOfContents }} `)}, "parentDir/childDir/1-2-3.html": &fstest.MapFile{Data: []byte(` @@ -295,7 +295,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) { -1-2-3 Page +1-2-3 Page ` @@ -420,22 +420,22 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) { andrew.Main(args, nullLogger) } -// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that -// when the list of links andrew generates for the {{.AndrewIndexBody}} are +// TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime is verifying that +// when the list of links andrew generates for the {{.AndrewTableOfContents}} are // sorted by mtime, not using the ascii sorting order. // This test requires having two files which are in one order when sorted // ascii-betically and in another order by date time, so that we can tell // what file attribute andrew is actually sorting on. -func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { - expected := `b_newer.html -a_older.html +func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T) { + expected := `b_newer.html +a_older.html ` contentRoot := t.TempDir() // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. // above might be wrong - err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700) + err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewTableOfContents}}"), 0o700) if err != nil { t.Fatal(err) } @@ -456,7 +456,7 @@ func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) { os.Chtimes(contentRoot+"/b_newer.html", now, now) os.Chtimes(contentRoot+"/a_older.html", older, older) - server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate} + server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewtableofcontentstemplate: andrew.AndrewTableOfContentsTemplate} page, err := andrew.NewPage(server, "index.html") diff --git a/linksbuilder.go b/linksbuilder.go index 080b2eb..24aceee 100644 --- a/linksbuilder.go +++ b/linksbuilder.go @@ -21,7 +21,7 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag var links bytes.Buffer for i, sibling := range siblings { - links.Write(buildAndrewIndexLink(sibling.UrlPath, sibling.Title, i)) + links.Write(buildAndrewTableOfContentsLink(sibling.UrlPath, sibling.Title, i)) } templateBuffer := bytes.Buffer{} @@ -33,7 +33,7 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag panic(err) } - err = t.Execute(&templateBuffer, map[string]string{"AndrewIndexBody": links.String()}) + err = t.Execute(&templateBuffer, map[string]string{"AndrewTableOfContents": links.String()}) if err != nil { return templateBuffer.Bytes(), err } @@ -41,9 +41,9 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag return templateBuffer.Bytes(), nil } -// buildAndrewIndexLink encapsulates the format of the link -func buildAndrewIndexLink(urlPath string, title string, cssIdNumber int) []byte { - link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title) +// buildAndrewTableOfContentsLink encapsulates the format of the link +func buildAndrewTableOfContentsLink(urlPath string, title string, cssIdNumber int) []byte { + link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title) b := []byte(link) return b } diff --git a/todo.md b/todo.md index 5709558..3859bf2 100644 --- a/todo.md +++ b/todo.md @@ -11,5 +11,5 @@ * github workflow for homebrew * html escape the paths you're serving * pull out any article summaries into parent card -* extract the function that builds the index body out of serveIndexPage and pass it in instead. You can also pass in AndrewIndexBody; that +* extract the function that builds the index body out of serveIndexPage and pass it in instead. You can also pass in AndrewTableOfContents; that would give you the flexibility to render different kinds of functions based upon the presence of different template strings. \ No newline at end of file From 782ba99af1a8b18cb3e74010f1844457fa7ec1f9 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sat, 29 Jun 2024 08:32:45 -0600 Subject: [PATCH 12/14] Add a sort function that can handle dates, so the links are ordered. Also simplifies some of the PublishTime handling using RAII --- andrew_server.go | 6 ++--- andrew_server_test.go | 5 ++-- linksbuilder.go | 10 ++------ page.go | 54 ++++++++++++++++++++++++++----------------- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/andrew_server.go b/andrew_server.go index a57d340..d3a2717 100644 --- a/andrew_server.go +++ b/andrew_server.go @@ -169,14 +169,14 @@ func (a Server) GetSiblingsAndChildren(pagePath string) ([]Page, error) { // links require a URL relative to the page we're discovering siblings from, not from // the root of the file system - page, err := NewPage(a, path) - page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/")) + s_page, err := NewPage(a, path) + s_page = s_page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/")) if err != nil { return err } - pages = append(pages, page) + pages = append(pages, s_page) return nil }) diff --git a/andrew_server_test.go b/andrew_server_test.go index 637503d..4b1d4c4 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -427,9 +427,8 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) { // ascii-betically and in another order by date time, so that we can tell // what file attribute andrew is actually sorting on. func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T) { - expected := `b_newer.html -a_older.html -` + expected := `b_newer.html` + + `a_older.html` contentRoot := t.TempDir() diff --git a/linksbuilder.go b/linksbuilder.go index 24aceee..d2ce29f 100644 --- a/linksbuilder.go +++ b/linksbuilder.go @@ -6,17 +6,11 @@ import ( "text/template" ) -// Building the template body requires information from both the Server and the Page. -// The Server supplies information from the file system, such as the siblings of a page -// in its directory. -// The Page supplies information from with the page itself, like the created-at date or -// the title of the page. - -// BuildPageBodyWithLinks receives the path to a file, currently normally an index file. +// BuildAndrewTOCLinks receives the path to a file, currently normally an index file. // It traverses the file system starting at the directory containing // that file, finds all html files that are _not_ index.html files and returns them // as a list of html links to those pages. -func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPage Page) ([]byte, error) { +func BuildAndrewTOCLinks(siblings []Page, startingPage Page) ([]byte, error) { var links bytes.Buffer diff --git a/page.go b/page.go index b0814b9..78dcc0f 100644 --- a/page.go +++ b/page.go @@ -5,6 +5,7 @@ import ( "fmt" "io/fs" "path" + "sort" "strings" "time" @@ -54,22 +55,7 @@ func NewPage(server Server, pageUrl string) (Page, error) { return Page{}, err } - page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle} - - if strings.Contains(pageUrl, "index.html") { - siblings, err := server.GetSiblingsAndChildren(page.UrlPath) - - if err != nil { - return page, err - } - - pageContent, err = BuildPageBodyWithLinks(siblings, pageUrl, page) - if err != nil { - return Page{}, err - } - - page.Content = string(pageContent) - } + page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle, PublishTime: pageInfo.ModTime()} meta, err := GetMetaElements(pageContent) if err != nil { @@ -79,14 +65,31 @@ func NewPage(server Server, pageUrl string) (Page, error) { publishTime, ok := meta["andrew-created-at"] if ok { - page.PublishTime, err = time.Parse(time.DateOnly, publishTime) + andrewCreatedAt, err := time.Parse(time.DateOnly, publishTime) if err != nil { + fmt.Println("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time") // log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time") - page.PublishTime = pageInfo.ModTime() + } else { + page.PublishTime = andrewCreatedAt } - } else { - page.PublishTime = pageInfo.ModTime() + } + + if strings.Contains(pageUrl, "index.html") { + siblings, err := server.GetSiblingsAndChildren(page.UrlPath) + + if err != nil { + return page, err + } + + orderedSiblings := sortPages(siblings) + + pageContent, err = BuildAndrewTOCLinks(orderedSiblings, page) + if err != nil { + return Page{}, err + } + + page.Content = string(pageContent) } return page, nil @@ -94,7 +97,7 @@ func NewPage(server Server, pageUrl string) (Page, error) { // SetUrlPath updates the UrlPath on a pre-existing Page. func (a Page) SetUrlPath(urlPath string) Page { - return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath} + return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath, PublishTime: a.PublishTime} } // getTagInfo recursively descends an html node tree for the requested tag, @@ -190,3 +193,12 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) { } return tagInfo.Data, nil } + +func sortPages(pages []Page) []Page { + + sort.Slice(pages, func(i, j int) bool { + return pages[i].PublishTime.After(pages[j].PublishTime) + }) + + return pages +} From 5939aee8bbe2e8dcfd40aece1b941521cf27d781 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sat, 29 Jun 2024 10:46:08 -0600 Subject: [PATCH 13/14] Adds a test to prove andrew-published-at is parsed. Also updates the meta tag I look for to andrew-published-at. --- andrew_server_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++ page.go | 6 ++-- page_test.go | 8 +++--- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/andrew_server_test.go b/andrew_server_test.go index 4b1d4c4..f56e37f 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -3,6 +3,7 @@ package andrew_test import ( "bytes" "errors" + "fmt" "io" "io/fs" "net" @@ -471,6 +472,71 @@ func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T) } +// TestArticlesOrderInAndrewTableOfContentsIsOverridable is verifying that +// when a page contains an andrew-publish-time meta element then the list of links andrew +// generates for the {{.AndrewTableOfContents}} are +// sorted by the meta element, then the mtime, not using the ascii sorting order. +// This test requires having several files which are in one order when sorted +// by modtime and in another order by andrew-publish-time time, so that we can tell +// what file attribute andrew is actually sorting on. +func TestArticlesOrderInAndrewTableOfContentsIsOverridable(t *testing.T) { + expected := `b_newest.html` + + `c_newer.html` + + `a_older.html` + + contentRoot := t.TempDir() + + // fstest.MapFS does not enforce file permissions, so we need a real file system in this test. + // above might be wrong + err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewTableOfContents}}"), 0o700) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(contentRoot+"/a_older.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(contentRoot+"/c_newer.html", []byte{}, 0o700) + if err != nil { + t.Fatal(err) + } + + now := time.Now() + + newest := now.Add(24 * time.Hour) + formattedDate := newest.Format("2006-01-02") + + content := fmt.Sprintf(``, formattedDate) + + err = os.WriteFile(contentRoot+"/b_newest.html", []byte(content), 0o700) + if err != nil { + t.Fatal(err) + } + + older := now.Add(-10 * time.Minute) + + os.Chtimes(contentRoot+"/c_newer.html", now, now) + os.Chtimes(contentRoot+"/a_older.html", older, older) + os.Chtimes(contentRoot+"/b_newest.html", older, older) + + server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewtableofcontentstemplate: andrew.AndrewTableOfContentsTemplate} + + page, err := andrew.NewPage(server, "index.html") + + if err != nil { + t.Fatal(err) + } + + received := page.Content + + if expected != string(received) { + t.Errorf(cmp.Diff(expected, received)) + } + +} + // newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against // to retrieve data from that server func newTestAndrewServer(t *testing.T, contentRoot fs.FS) *andrew.Server { diff --git a/page.go b/page.go index 78dcc0f..b6e4851 100644 --- a/page.go +++ b/page.go @@ -62,14 +62,14 @@ func NewPage(server Server, pageUrl string) (Page, error) { return Page{}, err } - publishTime, ok := meta["andrew-created-at"] + publishTime, ok := meta["andrew-publish-time"] if ok { andrewCreatedAt, err := time.Parse(time.DateOnly, publishTime) if err != nil { - fmt.Println("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time") - // log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time") + return Page{}, err + // log.Logger("could not parse meta tag andrew-publish-time using time.Parse. Defaulting to mod time") } else { page.PublishTime = andrewCreatedAt } diff --git a/page_test.go b/page_test.go index 786089b..ee35016 100644 --- a/page_test.go +++ b/page_test.go @@ -41,8 +41,8 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) { } func TestOneMetaTagPopulatesATag(t *testing.T) { - expected := map[string]string{"andrew-created-at": "2025-03-01"} - received, err := GetMetaElements([]byte("")) + expected := map[string]string{"andrew-publish-time": "2025-03-01"} + received, err := GetMetaElements([]byte("")) if err != nil { t.Fatal(err) @@ -54,8 +54,8 @@ func TestOneMetaTagPopulatesATag(t *testing.T) { } func TestMultipleMetaTagsPopulatedWithExpectedElements(t *testing.T) { - expected := map[string]string{"andrew-created-at": "2025-03-01", "andrew-roflcopter": "hippolol"} - received, err := GetMetaElements([]byte(" ")) + expected := map[string]string{"andrew-publish-time": "2025-03-01", "andrew-roflcopter": "hippolol"} + received, err := GetMetaElements([]byte(" ")) if err != nil { t.Fatal(err) From 6e1af7e54f84287b1140a9b5b88c4bfbd38e2dd2 Mon Sep 17 00:00:00 2001 From: Gwyn Date: Sat, 29 Jun 2024 10:54:10 -0600 Subject: [PATCH 14/14] Update test to use newer andrew-publish-time meta tag. --- andrew_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/andrew_server_test.go b/andrew_server_test.go index f56e37f..3bb4449 100644 --- a/andrew_server_test.go +++ b/andrew_server_test.go @@ -508,7 +508,7 @@ func TestArticlesOrderInAndrewTableOfContentsIsOverridable(t *testing.T) { newest := now.Add(24 * time.Hour) formattedDate := newest.Format("2006-01-02") - content := fmt.Sprintf(``, formattedDate) + content := fmt.Sprintf(``, formattedDate) err = os.WriteFile(contentRoot+"/b_newest.html", []byte(content), 0o700) if err != nil {