├── assets ├── css │ ├── custom.css │ ├── notice.css │ └── syntax.css ├── scss │ ├── custom.scss │ ├── water │ │ ├── builds │ │ │ ├── dark.css │ │ │ ├── light.css │ │ │ └── water.css │ │ ├── parts │ │ │ ├── _links.css │ │ │ ├── _core.css │ │ │ ├── _code.css │ │ │ ├── _print.css │ │ │ ├── _base.css │ │ │ ├── _typography.css │ │ │ ├── _range.css │ │ │ ├── _misc.css │ │ │ └── _forms.css │ │ ├── variables-light.css │ │ └── variables-dark.css │ ├── _share_buttons.scss │ └── main.scss ├── img │ └── default-404.jpeg └── js │ ├── main.js │ └── disqusloader.js ├── .gitignore ├── layouts ├── partials │ ├── custom_head.html │ ├── rfc3339.html │ ├── current_output_format.html │ ├── list_page_title.html │ ├── head │ │ ├── keywords.html │ │ ├── description.html │ │ ├── author.html │ │ ├── cover.html │ │ └── icon.html │ ├── footer.html │ ├── paginator.html │ ├── disqus.html │ ├── list_page_content.html │ ├── related.html │ ├── script.html │ ├── meta.html │ ├── head.html │ ├── schema_jsonld.html │ ├── header.html │ └── share_buttons.html ├── _default │ ├── single.md │ ├── _markup │ │ ├── render-heading.html │ │ ├── render-link.html │ │ ├── render-image.amp.html │ │ └── render-image.html │ ├── archives.html │ ├── list.html │ ├── index.md │ ├── baseof.html │ ├── single.html │ ├── terms.html │ ├── index.html │ └── baseof.amp.html ├── robots.txt ├── shortcodes │ ├── details.html │ ├── audio.html │ ├── video.html │ ├── admonition.html │ └── notice.html ├── 404.html └── sitemap.xml ├── .stylelintrc.json ├── static ├── favicon.ico ├── zoom.js │ ├── dist │ │ ├── index.js │ │ ├── index.d.ts │ │ ├── zoom.d.ts │ │ ├── common.d.ts │ │ ├── zoom.css │ │ ├── zoom-image.d.ts │ │ ├── common.js │ │ ├── zoom.js │ │ └── zoom-image.js │ ├── package.json │ ├── LICENSE │ ├── LICENSE.original │ └── README.md ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── sitemap.xsl ├── archetypes └── default.md ├── .prettierrc.json ├── package.json ├── theme.toml ├── icons ├── note.svg ├── info.svg ├── tip.svg └── warning.svg ├── README.md ├── LICENSE ├── i18n ├── zh-Hans.yaml └── en.yaml └── config.toml.example /assets/css/custom.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/scss/custom.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /layouts/partials/custom_head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /layouts/_default/single.md: -------------------------------------------------------------------------------- 1 | {{ .RawContent }} -------------------------------------------------------------------------------- /layouts/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Sitemap: {{ "sitemap.xml" | absURL }} 3 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss" 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/zoom.js/dist/index.js: -------------------------------------------------------------------------------- 1 | export { defaultConfig, dismissZoom, zoom } from "./zoom.js"; 2 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /assets/img/default-404.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/assets/img/default-404.jpeg -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/zoom.js/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Config, defaultConfig, dismissZoom, zoom } from "./zoom.js"; 2 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanji-dev/futu/HEAD/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/scss/water/builds/dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Forced dark theme version 3 | */ 4 | 5 | @import "../variables-dark"; 6 | @import "../parts/core"; 7 | -------------------------------------------------------------------------------- /assets/scss/water/builds/light.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Forced light theme version 3 | */ 4 | 5 | @import "../variables-light"; 6 | @import "../parts/core"; 7 | -------------------------------------------------------------------------------- /layouts/partials/rfc3339.html: -------------------------------------------------------------------------------- 1 | {{/* https://golang.org/src/time/format.go?s=15423:15465#L78 */}} 2 | {{ return (.Format "2006-01-02T15:04:05Z07:00") }} 3 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_links.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | color: var(--links); 4 | } 5 | 6 | a:hover { 7 | text-decoration: underline; 8 | } 9 | -------------------------------------------------------------------------------- /layouts/partials/current_output_format.html: -------------------------------------------------------------------------------- 1 | {{/* https://github.com/gohugoio/hugo/issues/9368 */}} 2 | {{ return (index (complement .AlternativeOutputFormats .OutputFormats) 0) }} 3 | -------------------------------------------------------------------------------- /layouts/partials/list_page_title.html: -------------------------------------------------------------------------------- 1 | {{ $title := .Title }} 2 | {{ if .Data.Plural }} 3 | {{ $title = printf "%s: %s" (i18n .Data.Plural) .Title }} 4 | {{ end }} 5 | {{ return $title }} 6 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_core.css: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | @import "typography"; 3 | @import "forms"; 4 | @import "range"; 5 | @import "links"; 6 | @import "code"; 7 | @import "misc"; 8 | @import "print"; 9 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-heading.html: -------------------------------------------------------------------------------- 1 | {{ .Text | safeHTML }} 2 | -------------------------------------------------------------------------------- /archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | tags: [] 6 | keywords: [] 7 | slug: "" 8 | --- 9 | 10 | **Insert Lead paragraph here.** 11 | 12 | ## New Cool Posts 13 | -------------------------------------------------------------------------------- /layouts/partials/head/keywords.html: -------------------------------------------------------------------------------- 1 | {{ $keywords := slice }} 2 | {{ if .Keywords }} 3 | {{ $keywords = .Keywords }} 4 | {{- else if .Site.Params.keywords -}} 5 | {{ $keywords = .Site.Params.keywords }} 6 | {{- end -}} 7 | {{ return $keywords }} 8 | -------------------------------------------------------------------------------- /layouts/shortcodes/details.html: -------------------------------------------------------------------------------- 1 | {{ .Scratch.Set "lastp" (sub (.Params | len) 1) }} 2 |
3 | 4 | {{ .Get (.Scratch.Get "lastp") }} 5 | 6 | {{ .Inner }} 7 |
8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "plugins": ["prettier-plugin-go-template"], 4 | "overrides": [ 5 | { 6 | "files": ["*.html"], 7 | "options": { 8 | "parser": "go-template" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "prettier": "^3.1.0", 4 | "prettier-plugin-go-template": "^0.0.15", 5 | "stylelint": "^15.11.0", 6 | "stylelint-config-standard": "^34.0.0", 7 | "stylelint-config-standard-scss": "^11.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-link.html: -------------------------------------------------------------------------------- 1 | {{ .Text | safeHTML }} 2 | {{- /* Strip trailing newline. */ -}} 3 | -------------------------------------------------------------------------------- /layouts/_default/archives.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{ $pages := where site.RegularPages "Type" "in" site.Params.mainSections }} 3 | {{ $pages = where $pages "Draft" "eq" false }} 4 | {{ partial "list_page_content.html" (dict "title" .Title "pages" $pages "total" (len $pages)) }} 5 | {{- end }} 6 | -------------------------------------------------------------------------------- /assets/scss/water/builds/water.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Automatic version: 3 | * Uses light theme by default but switches to dark theme 4 | * if a system-wide theme preference is set on the user's device. 5 | */ 6 | 7 | @import "../variables-light"; 8 | @import "../variables-dark"; 9 | @import "../parts/core"; 10 | -------------------------------------------------------------------------------- /layouts/shortcodes/audio.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | {{ with .Get "caption" }}
{{ . }}
{{ end }} 6 |
7 | -------------------------------------------------------------------------------- /layouts/shortcodes/video.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | {{ with .Get "caption" }}
{{ . }}
{{ end }} 6 |
7 | -------------------------------------------------------------------------------- /static/zoom.js/dist/zoom.d.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | padding: number; 3 | paddingNarrow: number; 4 | dismissScrollDelta: number; 5 | dismissTouchDelta: number; 6 | }; 7 | export declare const defaultConfig: Config; 8 | export declare function zoom(img: HTMLImageElement, cfg?: Config): void; 9 | export declare function dismissZoom(): void; 10 | -------------------------------------------------------------------------------- /layouts/partials/head/description.html: -------------------------------------------------------------------------------- 1 | {{ $description := "" }} 2 | {{ if .Description }} 3 | {{ $description = .Description | safeHTML }} 4 | {{- else if .IsPage -}} 5 | {{ $description = .Summary | plainify }} 6 | {{- else if .Site.Params.description -}} 7 | {{ $description = .Site.Params.description | safeHTML }} 8 | {{- end -}} 9 | {{ return $description }} 10 | -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, 6 | { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } 7 | ], 8 | "theme_color": "#ffffff", 9 | "background_color": "#ffffff", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /layouts/partials/footer.html: -------------------------------------------------------------------------------- 1 |

2 | {{ (printf (i18n "powered") (printf `Hugo %s` .Site.Hugo.Version)) | safeHTML }} 3 | | 4 | {{ i18n "theme" }} 5 | futu@v2 6 |
7 | Copyright © 8 | {{ .Site.Params.Since }}-{{ now.Format "2006" }} 9 | {{ .Site.Author.name }} All Rights Reserved. 10 |

11 | -------------------------------------------------------------------------------- /layouts/404.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 | {{ $image := resources.GetMatch "img/404.*" }} 3 | {{ if not $image }} 4 | {{ $image = resources.GetMatch "img/default-404.jpeg" }} 5 | {{ end }} 6 | {{ $image := $image.Resize (print $image.Width "x" $image.Height " webp") }} 7 | 404 Not Found 14 | {{ end }} 15 | -------------------------------------------------------------------------------- /layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | {{- define "title" }}{{ partial "list_page_title.html" . }} | {{ .Site.Title }}{{ end -}} 2 | {{- define "main" }} 3 | {{ $title := partial "list_page_title.html" . }} 4 | {{ $pages := where .Pages "Draft" "eq" false }} 5 | {{ $paginator := .Paginate $pages (.Site.Params.paginate | default 10) }} 6 | {{ partial "list_page_content.html" (dict "title" $title "pages" $paginator.Pages "total" (len $pages) ) }} 7 | {{ partial "paginator.html" $paginator }} 8 | {{- end }} 9 | -------------------------------------------------------------------------------- /static/zoom.js/dist/common.d.ts: -------------------------------------------------------------------------------- 1 | export declare function viewportWidth(docElem: HTMLElement): number; 2 | export declare function viewportHeight(docElem: HTMLElement): number; 3 | export declare function usableWidth(docElem: HTMLElement, offset: number): number; 4 | export declare function usableHeight(docElem: HTMLElement, offset: number): number; 5 | export declare function toOffset(padding: number): number; 6 | export declare function wrap(elem: Node, wrapper: Node): void; 7 | export declare function unwrap(elem: Node, wrapper: Node): void; 8 | -------------------------------------------------------------------------------- /layouts/partials/head/author.html: -------------------------------------------------------------------------------- 1 | {{ $author := newScratch }} 2 | {{ if .Params.author }} 3 | {{ $author.Set "name" (.Params.author | safeHTML) }} 4 | {{ with .Params.author_url }} 5 | {{ $author.Set "url" (. | safeHTML) }} 6 | {{ end }} 7 | {{ else }} 8 | {{ $author.Set "name" (.Site.Params.author.name | safeHTML) }} 9 | {{ with $.Site.Params.author.url }} 10 | {{ $author.Set "url" (. | safeHTML) }} 11 | {{ else }} 12 | {{ $author.Set "url" $.Site.BaseURL }} 13 | {{ end }} 14 | {{ end }} 15 | {{ return $author }} 16 | -------------------------------------------------------------------------------- /layouts/partials/paginator.html: -------------------------------------------------------------------------------- 1 | {{ if or .Prev .Next }} 2 | 3 | 17 | {{ end }} 18 | -------------------------------------------------------------------------------- /layouts/_default/index.md: -------------------------------------------------------------------------------- 1 | # {{ .Site.Title }} 2 | {{ $pages := where site.RegularPages "Type" "in" site.Params.mainSections }} 3 | {{- $pages := where $pages "Draft" "eq" false }} 4 | {{ i18n "archiveCounter" (len $pages) }} 5 | 6 | {{ range $pages.GroupByPublishDate "2006" }} 7 | {{- if ne .Key "0001" -}} 8 | ## {{ .Key }} 9 | {{- range .Pages }} 10 | {{- $postPath := replace .File.Path " " "%20" }} 11 | - {{ .PublishDate.Format "01-02" }} [{{ .Title }}](content/{{ $postPath }}) 12 | {{- end }} 13 | {{- end }} 14 | {{ end }} 15 | -------------------------------------------------------------------------------- /theme.toml: -------------------------------------------------------------------------------- 1 | # theme.toml template for a Hugo theme 2 | # See https://github.com/gohugoio/hugoThemesSiteBuilder#theme-configuration for an example 3 | 4 | name = "Futu" 5 | license = "MIT" 6 | licenselink = "https://github.com/yuanji-dev/futu/blob/master/LICENSE" 7 | description = "Yet another minimal hugo theme, built-in styles are modified upon water.css." 8 | homepage = "https://github.com/yuanji-dev/futu" 9 | tags = ["blog", "minimal"] 10 | features = [] 11 | min_version = "0.108.0" 12 | 13 | [author] 14 | name = "Yuanji" 15 | homepage = "https://blog.yuanji.dev" 16 | -------------------------------------------------------------------------------- /layouts/partials/disqus.html: -------------------------------------------------------------------------------- 1 | {{ $currentOutputFormat := (partial "current_output_format.html" .) }} 2 | {{ if and hugo.IsProduction (ne $currentOutputFormat.Name "amp") }} 3 | {{ with .Site.Config.Services.Disqus.Shortname }} 4 | {{ $disqusLoader := resources.Get "js/disqusloader.js" | minify }} 5 | 6 | 7 | 12 | {{ end }} 13 | {{ end }} 14 | -------------------------------------------------------------------------------- /layouts/partials/head/cover.html: -------------------------------------------------------------------------------- 1 | {{ $image := "" }} 2 | {{- with $.Params.images -}} 3 | {{ $image = index . 0 | absURL }} 4 | {{ else -}} 5 | {{- $images := $.Resources.ByType "image" -}} 6 | {{- $featured := $images.GetMatch "*feature*" -}} 7 | {{- if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end -}} 8 | {{- with $featured -}} 9 | {{ $image = $featured.Permalink }} 10 | {{- else -}} 11 | {{- with $.Site.Params.images -}} 12 | {{ $image = index . 0 | absURL }} 13 | {{- end -}} 14 | {{- end -}} 15 | {{- end }} 16 | {{ return $image }} 17 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_code.css: -------------------------------------------------------------------------------- 1 | code, 2 | samp, 3 | time { 4 | background: var(--background); 5 | color: var(--code); 6 | padding: 2.5px 5px; 7 | border-radius: 6px; 8 | font-size: 1em; 9 | } 10 | 11 | pre > code { 12 | padding: 10px; 13 | display: block; 14 | overflow-x: auto; 15 | } 16 | 17 | var { 18 | color: var(--variable); 19 | font-style: normal; 20 | font-family: monospace; 21 | } 22 | 23 | kbd { 24 | background: var(--background); 25 | border: 1px solid var(--border); 26 | border-radius: 2px; 27 | color: var(--text-main); 28 | padding: 2px 4px 2px 4px; 29 | } 30 | -------------------------------------------------------------------------------- /icons/note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/scss/water/variables-light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-body: #fff; 3 | --background: #efefef; 4 | --background-alt: #f7f7f7; 5 | --selection: #9e9e9e; 6 | --text-main: #363636; 7 | --text-bright: #000; 8 | --text-muted: #70777f; 9 | --links: #0076d1; 10 | --focus: #0096bfab; 11 | --border: #dbdbdb; 12 | --code: #000; 13 | --animation-duration: 0.1s; 14 | --button-base: #d0cfcf; 15 | --button-hover: #9b9b9b; 16 | --scrollbar-thumb: #aaa; 17 | --scrollbar-thumb-hover: var(--button-hover); 18 | --form-placeholder: #949494; 19 | --form-text: #1d1d1d; 20 | --variable: #39a33c; 21 | --highlight: #ff0; 22 | } 23 | -------------------------------------------------------------------------------- /icons/tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /layouts/_default/baseof.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ block "title" . }} 6 | {{ if .IsHome }}{{ else }}{{ if .Title }}{{ .Title }} | {{ end }}{{ end }}{{ site.Title }} 7 | {{ end }} 8 | 9 | {{- partial "head.html" . -}} 10 | {{- partial "custom_head.html" . -}} 11 | 12 | 13 |
{{- partial "header.html" . -}}
14 |
15 | {{- block "main" . }} 16 | {{- end }} 17 |
18 | 19 | {{- partial "script.html" . -}} 20 | 21 | 22 | -------------------------------------------------------------------------------- /layouts/partials/list_page_content.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ .title }}

3 | {{ i18n "archiveCounter" .total }} 4 |
5 | {{ range .pages.GroupByPublishDate "2006" }} 6 | {{- if ne .Key "0001" }} 7 |

{{ .Key }}

8 | {{ i18n "archiveCounter" (len .Pages) }} 9 | 17 | {{ end }} 18 | {{ end }} 19 | -------------------------------------------------------------------------------- /static/zoom.js/dist/zoom.css: -------------------------------------------------------------------------------- 1 | :not(.zoom-overlay-open) img.zoom-cursor { 2 | cursor: zoom-in; 3 | } 4 | img.zoom-img, 5 | .zoom-img-wrapper { 6 | position: relative; 7 | z-index: 666; 8 | transition: all 300ms; 9 | } 10 | .zoom-overlay-open img.zoom-img { 11 | cursor: zoom-out; 12 | } 13 | .zoom-overlay { 14 | z-index: 420; 15 | background: #fff; 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | pointer-events: none; 22 | filter: opacity(0); 23 | transition: filter 300ms ease-in-out; 24 | } 25 | .zoom-overlay-open .zoom-overlay { 26 | filter: opacity(1); 27 | } 28 | .zoom-overlay-open, 29 | .zoom-overlay-transitioning { 30 | cursor: default; 31 | } 32 | -------------------------------------------------------------------------------- /layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | {{ define "main" -}} 2 |
3 |
4 |

{{ .Title }}

5 | {{- partial "meta.html" . -}} 6 |
7 | {{ if (.Params.toc | default (.Site.Params.toc | default false )) }} 8 | {{ with .TableOfContents }} 9 | {{ if ne . "" }} 10 |
11 | {{ i18n "TOC" }} 12 | {{ . }} 13 |
14 | {{ end }} 15 | {{ end }} 16 | {{ end }} 17 | {{ .Content }} 18 |
19 | {{- partial "share_buttons.html" . -}} 20 | {{- partial "related.html" . -}} 21 | {{- partial "disqus.html" . -}} 22 |
23 | {{- end }} 24 | -------------------------------------------------------------------------------- /icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/zoom.js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nishanths/zoom.js", 3 | "version": "4.4.0", 4 | "description": "Medium.com-like image zoom plugin with no dependecies (fork of fat/zoom.js)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/nishanths/zoom.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/nishanths/zoom.js/issues" 11 | }, 12 | "author": "Nishanth Shanmugham", 13 | "homepage": "https://github.com/nishanths/zoom.js", 14 | "keywords": [ 15 | "zoom", 16 | "image" 17 | ], 18 | "license": "MIT", 19 | "private": false, 20 | "main": "", 21 | "module": "dist/index.js", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist", 25 | "LICENSE.original" 26 | ], 27 | "devDependencies": { 28 | "typescript": "^5.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /layouts/partials/related.html: -------------------------------------------------------------------------------- 1 | {{ $related := .Site.RegularPages.Related . }} 2 | {{ $related = where $related "Draft" "eq" false | first 5 }} 3 | {{ if (.Params.enableRelated | default (.Site.Params.enableRelated | default false)) }} 4 | {{ with $related }} 5 | 20 | {{ end }} 21 | {{ end }} 22 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | body, 3 | pre, 4 | code, 5 | summary, 6 | details, 7 | button, 8 | input, 9 | textarea { 10 | background-color: #fff; 11 | } 12 | 13 | button, 14 | input, 15 | textarea { 16 | border: 1px solid #000; 17 | } 18 | 19 | body, 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6, 26 | pre, 27 | code, 28 | button, 29 | input, 30 | textarea, 31 | footer, 32 | summary, 33 | strong { 34 | color: #000; 35 | } 36 | 37 | summary::marker { 38 | color: #000; 39 | } 40 | 41 | summary::-webkit-details-marker { 42 | color: #000; 43 | } 44 | 45 | tbody tr:nth-child(even) { 46 | background-color: #f2f2f2; 47 | } 48 | 49 | a { 50 | color: #00f; 51 | text-decoration: underline; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /layouts/partials/head/icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ with .Params.icon }} 5 | {{ $href := print "data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20100%20100%22%3E%3Ctext%20x=%2250%25%22%20y=%2250%25%22%20style=%22dominant-baseline:central;text-anchor:middle;font-size:90px;%22%3E" . "%3C/text%3E%3C/svg%3E" }} 6 | 7 | {{ end }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/zoom.js/dist/zoom-image.d.ts: -------------------------------------------------------------------------------- 1 | export declare class ZoomImage { 2 | readonly img: HTMLImageElement; 3 | private oldTransform; 4 | private wrapper; 5 | private overlay; 6 | private offset; 7 | private dismissCompleteNotified; 8 | private dismissCompleteCallbacks; 9 | private dismissModifiedDOM; 10 | constructor(img: HTMLImageElement, offset: number); 11 | private static makeOverlay; 12 | private static makeWrapper; 13 | private static elemOffset; 14 | private hackForceRepaint; 15 | private zoomModifyDOM; 16 | private dismissModifyDOM; 17 | private zoomAnimate; 18 | private dismissAnimate; 19 | zoom(): void; 20 | onDismissComplete(f: () => void): void; 21 | private notifyDismissComplete; 22 | dismiss(): void; 23 | dismissImmediate(): void; 24 | } 25 | -------------------------------------------------------------------------------- /layouts/shortcodes/admonition.html: -------------------------------------------------------------------------------- 1 | {{ if .IsNamedParams -}} 2 | {{ if eq (.Get "details") "true" -}} 3 |
4 | 5 | {{ .Get 1 }} 6 | 7 |
8 | {{ $.Page.RenderString .Inner }} 9 |
10 |
11 | {{- else -}} 12 |
13 |

{{ .Get 1 }}

14 |
15 | {{ $.Page.RenderString .Inner }} 16 |
17 |
18 | {{- end }} 19 | {{- else -}} 20 | {{ if eq (.Get 2) "true" -}} 21 |
22 | 23 | {{ .Get 1 }} 24 | 25 |
26 | {{ $.Page.RenderString .Inner }} 27 |
28 |
29 | {{- else -}} 30 |
31 |

{{ .Get 1 }}

32 |
33 | {{ $.Page.RenderString .Inner }} 34 |
35 |
36 | {{- end }} 37 | {{- end }} 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Futu 2 | 3 | Yet another minimal hugo theme, built-in styles are modified upon [water.css](https://github.com/kognise/water.css). 4 | 5 | ## Features 6 | 7 | - Minimal 8 | - Multilingual 9 | - [AMP](https://amp.dev/) 10 | - … 11 | 12 | ## Installation 13 | 14 | **Note: minimum supported Hugo version is [v0.108.0](https://github.com/gohugoio/hugo/releases/tag/v0.108.0)** 15 | 16 | You can copy [config.toml.example](config.toml.example) to your Hugo project, rename it to `config.toml` and change configurations as you want. 17 | 18 | To overwrite default favicons, you can generate your owns on [favicon.io](https://favicon.io/), put all extracted files directly under `static` directory of your own project. 19 | 20 | ## Used by 21 | 22 | This theme is used by: 23 | 24 | - [Yuanji's Blog](https://blog.yuanji.dev/) 25 | 26 | ## License 27 | 28 | [MIT](https://choosealicense.com/licenses/mit/) 29 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_base.css: -------------------------------------------------------------------------------- 1 | html { 2 | scrollbar-color: var(--scrollbar-thumb) var(--background-body); 3 | scrollbar-width: thin; 4 | } 5 | 6 | body { 7 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; 8 | line-height: 1.4; 9 | max-width: 800px; 10 | margin: 20px auto; 11 | padding: 0 10px; 12 | word-wrap: break-word; 13 | color: var(--text-main); 14 | background: var(--background-body); 15 | text-rendering: optimizeLegibility; 16 | } 17 | 18 | button, 19 | input, 20 | textarea { 21 | transition: 22 | background-color var(--animation-duration) linear, 23 | border-color var(--animation-duration) linear, 24 | color var(--animation-duration) linear, 25 | box-shadow var(--animation-duration) linear, 26 | transform var(--animation-duration) ease; 27 | } 28 | -------------------------------------------------------------------------------- /layouts/shortcodes/notice.html: -------------------------------------------------------------------------------- 1 | {{/* https://github.com/martignoni/hugo-notice */}} 2 | {{/* Available notice types: warning, info, note, tip */}} 3 | {{- $noticeType := .Get 0 | default "note" -}} 4 | 5 | {{/* Workaround markdownify inconsistency for single/multiple paragraphs */}} 6 | {{- $raw := (markdownify .Inner | chomp) -}} 7 | {{- $block := findRE "(?is)^<(?:address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h(?:1|2|3|4|5|6)|header|hgroup|hr|li|main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video)\\b" $raw 1 -}} 8 |
9 |

10 | 11 | {{ printf "icons/%s.svg" $noticeType | readFile | safeHTML }} 12 | 13 | {{- i18n $noticeType -}} 14 |

15 | {{- if or $block (not $raw) }} 16 | {{ $raw }} 17 | {{ else }} 18 |

{{ $raw }}

19 | {{ end -}} 20 |
21 | -------------------------------------------------------------------------------- /layouts/partials/script.html: -------------------------------------------------------------------------------- 1 | {{ $mainJS := resources.Get "js/main.js" | minify | fingerprint }} 2 | 3 | {{ template "_internal/google_analytics.html" . }} 4 | {{ if hugo.IsProduction }} 5 | {{ with $.Site.Params.cfBeaconToken -}} 6 | 7 | 12 | 13 | {{ end }} 14 | {{ end }} 15 | {{ if .Site.Params.enableZoomJS }} 16 | 31 | {{ end }} 32 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | const osDark = window.matchMedia("(prefers-color-scheme: dark)"); 2 | const modeSwitch = document.getElementById("mode-switch"); 3 | 4 | function darkModeOn() { 5 | document.documentElement.classList.add("dark-mode"); 6 | } 7 | function darkModeOff() { 8 | document.documentElement.classList.remove("dark-mode"); 9 | } 10 | 11 | const osDarkListener = (event) => { 12 | if (event.matches) { 13 | darkModeOn(); 14 | } else { 15 | darkModeOff(); 16 | } 17 | }; 18 | osDark.addEventListener("change", osDarkListener); 19 | osDarkListener(osDark); 20 | 21 | modeSwitch.addEventListener("click", () => { 22 | if (document.documentElement.classList.contains("dark-mode")) { 23 | darkModeOff(); 24 | localStorage.setItem("darkMode", "off"); 25 | } else { 26 | darkModeOn(); 27 | localStorage.setItem("darkMode", "on"); 28 | } 29 | }); 30 | 31 | if (localStorage.getItem("darkMode") === "on") { 32 | darkModeOn(); 33 | } else if (localStorage.getItem("darkMode") === "off") { 34 | darkModeOff(); 35 | } 36 | -------------------------------------------------------------------------------- /layouts/_default/terms.html: -------------------------------------------------------------------------------- 1 | {{- define "title" }}{{ i18n .Data.Plural }} | {{ .Site.Title }}{{ end -}} 2 | {{- define "main" -}} 3 | {{ $name := .Data.Plural -}} 4 | {{ $terms := .Data.Terms.ByCount -}} 5 | {{ $length := len $terms -}} 6 | {{ $taxonomy := i18n $name (dict "Count" $length) -}} 7 |
8 |

{{ i18n .Data.Plural }}

9 | 10 | {{ if eq $length 0 }} 11 | {{ i18n "zeroTaxonomyCounter" (dict "Taxonomy" $taxonomy) }} 12 | {{ else }} 13 | {{ i18n "taxonomyCounter" (dict "Count" $length "Taxonomy" $taxonomy) }} 14 | {{ end }} 15 | 16 |
17 | {{ range $key, $value := $terms }} 18 | {{ $termURL := print ($name | relLangURL) "/" ($value.Term | urlize) }} 19 | {{ if $.Site.Params.uglyURLs }} 20 | {{ $termURL = print $termURL ".html" }} 21 | {{ else }} 22 | {{ $termURL = print $termURL "/" }} 23 | {{ end }} 24 | #{{ $value.Term }} 25 | {{ $value.Count }} 26 | {{ end -}} 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /layouts/partials/meta.html: -------------------------------------------------------------------------------- 1 | 2 | {{ $createDate := .Date.Format (.Site.Params.dateFormatToUse | default "2006-01-02") }} 3 | {{ $updateDate := .Lastmod.Format (.Site.Params.dateFormatToUse | default "2006-01-02") }} 4 | 📅 5 | 6 | {{ if ne $createDate $updateDate }} 7 | ({{ with .GitInfo }} 8 | {{ $gitBranch := $.Site.Params.git.branch | default "main" }} 9 | {{ (i18n "updated") }} 10 | {{ else }} 11 | {{ (i18n "updated") }} 12 | {{ end }}) 13 | {{ end }} 14 | {{ with .Params.tags -}} 15 | | 🏷️ 16 | {{ range $i, $t := . -}} 17 | {{ if $i }},{{ end }} 18 | 19 | {{- end -}} 20 | {{- end }} 21 | 22 | -------------------------------------------------------------------------------- /assets/scss/_share_buttons.scss: -------------------------------------------------------------------------------- 1 | .resp-sharing-button { 2 | border-radius: 5px; 3 | transition: 25ms ease-out; 4 | padding: 0.5em 0.75em; 5 | 6 | &:hover { 7 | opacity: 0.8; 8 | } 9 | 10 | &__link { 11 | display: inline-block; 12 | text-decoration: none; 13 | color: #fff; 14 | margin-right: 0.5em; 15 | } 16 | 17 | &__icon { 18 | display: inline-block; 19 | fill: #fff; 20 | stroke: none; 21 | 22 | svg { 23 | width: 1em; 24 | height: 1em; 25 | margin: 0; 26 | vertical-align: middle; 27 | } 28 | } 29 | 30 | &--twitter { 31 | background-color: #000; 32 | } 33 | 34 | &--bluesky { 35 | background-color: #0285ff; 36 | } 37 | 38 | &--telegram { 39 | background-color: #26a5e4; 40 | } 41 | 42 | &--douban { 43 | background-color: #2d963d; 44 | } 45 | 46 | &--reddit { 47 | background-color: #ff4500; 48 | } 49 | 50 | &--facebook { 51 | background-color: #0866ff; 52 | } 53 | 54 | &--pocket { 55 | background-color: #ef3f56; 56 | } 57 | 58 | &--email { 59 | background-color: #777; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yuanji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /assets/scss/water/variables-dark.css: -------------------------------------------------------------------------------- 1 | :root.dark-mode { 2 | --background-body: #202b38; 3 | --background: #161f27; 4 | --background-alt: #1a242f; 5 | --selection: #1c76c5; 6 | --text-main: #dbdbdb; 7 | --text-bright: #fff; 8 | --text-muted: #a9b1ba; 9 | --links: #41adff; 10 | --focus: #0096bfab; 11 | --border: #526980; 12 | --code: #ffbe85; 13 | --animation-duration: 0.1s; 14 | --button-base: #0c151c; 15 | --button-hover: #040a0f; 16 | --scrollbar-thumb: var(--button-hover); 17 | --scrollbar-thumb-hover: #000; 18 | --form-placeholder: #a9a9a9; 19 | --form-text: #fff; 20 | --variable: #d941e2; 21 | --highlight: #efdb43; 22 | --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); 23 | } 24 | -------------------------------------------------------------------------------- /static/zoom.js/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Nishanth Shanmugham 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /static/zoom.js/LICENSE.original: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 @fat 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-image.amp.html: -------------------------------------------------------------------------------- 1 | {{ $image := .Page.Resources.GetMatch .Destination }} 2 | {{/* https://github.com/gohugoio/hugo/issues/5030 */}} 3 | {{ if ne $image.MediaType.SubType "gif" }} 4 | {{/* https://discourse.gohugo.io/t/image-conversion-without-resizing/32429 */}} 5 | {{ $image = $image.Resize (print $image.Width "x" $image.Height " webp") }} 6 | {{ end }} 7 | 8 | {{ if .IsBlock }} 9 |
10 | 18 |
{{ with .Title }}{{ . | safeHTML }}{{ else }}{{ .Text | safeHTML }}{{ end }}
19 |
20 | {{ else }} 21 | 29 | {{ with .Title }}{{ . }}{{ else }}{{ .Text }}{{ end }} 30 | {{ end }} 31 | -------------------------------------------------------------------------------- /i18n/zh-Hans.yaml: -------------------------------------------------------------------------------- 1 | prevPage: 2 | other: 上一页 3 | nextPage: 4 | other: 下一页 5 | wordCount: 6 | one: "约 {{ .Count }} 字" 7 | other: "约 {{ .Count }} 字" 8 | readingTime: 9 | one: "阅读 {{ .Count }} 分钟" 10 | other: "阅读 {{ .Count }} 分钟" 11 | readMore: 12 | other: "阅读更多" 13 | archive: 14 | other: "归档" 15 | tags: 16 | one: "标签" 17 | other: "标签" 18 | categories: 19 | one: "分类" 20 | other: "分类" 21 | archiveCounter: 22 | one: "共计 {{ .Count }} 篇" 23 | other: "共计 {{ .Count }} 篇" 24 | zeroTaxonomyCounter: 25 | other: "暂无{{ .Taxonomy }}" 26 | taxonomyCounter: 27 | one: "共计 {{ .Count }} 个{{ .Taxonomy }}" 28 | other: "共计 {{ .Count }} 个{{ .Taxonomy }}" 29 | powered: 30 | other: "由 %s 强力驱动" 31 | theme: 32 | other: "主题" 33 | caution: 34 | other: "请注意!" 35 | cautionMessage: 36 | other: "文中内容可能已过时,请谨慎使用。" 37 | TOC: 38 | other: "文章目录" 39 | search: 40 | other: "搜索" 41 | updated: 42 | other: "更新" 43 | viewAll: 44 | other: "查看全部" 45 | latest: 46 | other: "最新%s" 47 | seeAlso: 48 | other: "猜你喜欢" 49 | verified: 50 | other: "这个博客已通过认证" 51 | shareThisPost: 52 | other: "分享本文" 53 | warning: 54 | other: "警告" 55 | note: 56 | other: "注释" 57 | info: 58 | other: "信息" 59 | tip: 60 | other: "提示" 61 | -------------------------------------------------------------------------------- /layouts/_default/_markup/render-image.html: -------------------------------------------------------------------------------- 1 | {{ $image := .Page.Resources.GetMatch .Destination }} 2 | {{/* https://github.com/gohugoio/hugo/issues/5030 */}} 3 | {{ if ne $image.MediaType.SubType "gif" }} 4 | {{/* https://discourse.gohugo.io/t/image-conversion-without-resizing/32429 */}} 5 | {{ $image = $image.Resize (print $image.Width "x" $image.Height " webp") }} 6 | {{ end }} 7 | 8 | {{ if .IsBlock }} 9 |
10 | {{ .Text }} 19 |
{{ with .Title }}{{ . | safeHTML }}{{ else }}{{ .Text | safeHTML }}{{ end }}
20 |
21 | {{ else }} 22 | {{ .Text }} 31 | {{ with .Title }}{{ . | safeHTML }}{{ else }}{{ .Text | safeHTML }}{{ end }} 32 | {{ end }} 33 | -------------------------------------------------------------------------------- /layouts/_default/index.html: -------------------------------------------------------------------------------- 1 | {{ define "main" }} 2 | {{ $sectionSlice := slice }} 3 | {{ range site.Params.mainSections }} 4 | {{ $section := site.GetPage "section" . }} 5 | {{ $sectionSlice = $sectionSlice | append (dict "section" $section "date" (index $section.RegularPages.ByPublishDate.Reverse 0).PublishDate) }} 6 | {{ end }} 7 | {{ range sort $sectionSlice "date" "desc" }} 8 |
9 |
10 |

{{ printf (i18n "latest") .section.Title }}

11 |
12 | 24 |
25 | {{ end }} 26 | {{ end }} 27 | -------------------------------------------------------------------------------- /layouts/sitemap.xml: -------------------------------------------------------------------------------- 1 | {{ printf "" | safeHTML }} 2 | {{ printf "" ( "sitemap.xsl" | relURL ) | safeHTML }} 3 | 5 | {{ range .Data.Pages }} 6 | {{- if and (eq .Draft false) .Permalink -}} 7 | 8 | {{ .Permalink }}{{ if not .Lastmod.IsZero }} 9 | {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} 10 | {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} 11 | {{ .Sitemap.Priority }}{{ end }}{{ if .IsTranslated }}{{ range .Translations }} 12 | {{ end }} 17 | {{ end }} 22 | 23 | {{- end -}} 24 | {{ end }} 25 | 26 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_typography.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 2.2em; 3 | margin-top: 0; 4 | } 5 | 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6 { 12 | margin-bottom: 12px; 13 | margin-top: 24px; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6, 22 | strong { 23 | color: var(--text-bright); 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | b, 33 | strong, 34 | th { 35 | font-weight: 600; 36 | } 37 | 38 | q::before { 39 | content: none; 40 | } 41 | 42 | q::after { 43 | content: none; 44 | } 45 | 46 | blockquote, 47 | q { 48 | border-left: 4px solid var(--focus); 49 | margin: 1.5em 0; 50 | padding: 0.5em 1em; 51 | font-style: italic; 52 | } 53 | 54 | blockquote > footer { 55 | font-style: normal; 56 | border: 0; 57 | } 58 | 59 | blockquote cite { 60 | font-style: normal; 61 | } 62 | 63 | address { 64 | font-style: normal; 65 | } 66 | 67 | a[href^='mailto\:']::before { 68 | content: '📧 '; 69 | } 70 | 71 | a[href^='tel\:']::before { 72 | content: '📞 '; 73 | } 74 | 75 | a[href^='sms\:']::before { 76 | content: '💬 '; 77 | } 78 | 79 | mark { 80 | background-color: var(--highlight); 81 | border-radius: 2px; 82 | padding: 0 2px 0 2px; 83 | color: #000; 84 | } 85 | 86 | a > code, 87 | a > strong { 88 | color: inherit; 89 | } 90 | -------------------------------------------------------------------------------- /i18n/en.yaml: -------------------------------------------------------------------------------- 1 | prevPage: 2 | other: Prev 3 | nextPage: 4 | other: Next 5 | wordCount: 6 | one: "{{ .Count }} word" 7 | other: "{{ .Count }} words" 8 | readingTime: 9 | one: "{{ .Count }} min read" 10 | other: "{{ .Count }} mins read" 11 | readMore: 12 | other: "Read more..." 13 | archive: 14 | other: "Archive" 15 | tags: 16 | one: "Tag" 17 | other: "Tags" 18 | categories: 19 | one: "Category" 20 | other: "Categories" 21 | archiveCounter: 22 | one: "{{ .Count }} Item In Total" 23 | other: "{{ .Count }} Items In Total" 24 | zeroTaxonomyCounter: 25 | other: "No {{ .Taxonomy }}" 26 | taxonomyCounter: 27 | one: "{{ .Count }} {{ .Taxonomy }} In Total" 28 | other: "{{ .Count }} {{ .Taxonomy }} In Total" 29 | powered: 30 | other: "Powered by %s" 31 | theme: 32 | other: "Theme" 33 | caution: 34 | other: "Caution!" 35 | cautionMessage: 36 | other: "This article may contain outdated content." 37 | TOC: 38 | other: "What's on this Page" 39 | search: 40 | other: "Search" 41 | updated: 42 | other: "Updated" 43 | viewAll: 44 | other: "View all" 45 | latest: 46 | other: "Latest %s" 47 | seeAlso: 48 | other: "See also" 49 | verified: 50 | other: "This blog is verified" 51 | shareThisPost: 52 | other: "Share this post" 53 | warning: 54 | other: "Warning" 55 | note: 56 | other: "Note" 57 | info: 58 | other: "Info" 59 | tip: 60 | other: "Tip" 61 | -------------------------------------------------------------------------------- /static/zoom.js/dist/common.js: -------------------------------------------------------------------------------- 1 | // viewportWidth and viewportHeight return the width and height of the viewport. 2 | // The value does not include space occupied by any scrollbars. docElem must be 3 | // a document.documentElement value. 4 | export function viewportWidth(docElem) { return docElem.clientWidth; } 5 | export function viewportHeight(docElem) { return docElem.clientHeight; } 6 | // usableWidth and usableHeight return the maximum width and height of the 7 | // viewport that can be used to show the zoomed image. The value is defined 8 | // to be the viewport width (or height) minus the specified offset. 9 | // docElem must be a document.documentElement value. 10 | export function usableWidth(docElem, offset) { return viewportWidth(docElem) - offset; } 11 | export function usableHeight(docElem, offset) { return viewportHeight(docElem) - offset; } 12 | // toOffset converts a padding value to an offset value. 13 | // 14 | // Note that the internal API of this program is based largely on offset values, 15 | // but the public API is based on padding values, hence the necessity 16 | // for the conversion. 17 | export function toOffset(padding) { return padding * 2; } 18 | // wrap wraps elem inside wrapper. 19 | export function wrap(elem, wrapper) { 20 | var _a; 21 | (_a = elem.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(wrapper, elem); 22 | wrapper.appendChild(elem); 23 | } 24 | // unwrap undoes the operation done by the wrap function. 25 | export function unwrap(elem, wrapper) { 26 | var _a, _b; 27 | (_a = wrapper.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(elem, wrapper); 28 | (_b = wrapper.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(wrapper); 29 | } 30 | -------------------------------------------------------------------------------- /layouts/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ partial "head/icon.html" . }} 10 | {{ template "_internal/opengraph.html" . }} 11 | {{ template "_internal/twitter_cards.html" . }} 12 | {{- with .Site.Params.telegramChannel -}} 13 | 14 | {{ end -}} 15 | {{ partial "schema_jsonld.html" . }} 16 | {{ range .AlternativeOutputFormats -}} 17 | {{ printf `` .Rel .MediaType.Type .Permalink $.Title | safeHTML }} 18 | {{ end -}} 19 | {{ if .IsTranslated }} 20 | {{- range .AllTranslations -}} 21 | 22 | {{ end -}} 23 | {{ end }} 24 | {{ with .Site.Params.googleVerification }} 25 | 26 | {{ end }} 27 | {{ if .Site.Params.enableZoomJS }} 28 | 29 | {{ end }} 30 | {{ $styles := slice }} 31 | {{ $styles = $styles | append (resources.Get "scss/main.scss" | toCSS) (resources.Get "scss/custom.scss" | toCSS) }} 32 | {{ $styles = $styles | append (resources.Get "css/syntax.css") (resources.Get "css/notice.css") (resources.Get "css/custom.css") }} 33 | {{ $styles = $styles | resources.Concat "styles.css" | minify | fingerprint }} 34 | 35 | 40 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_range.css: -------------------------------------------------------------------------------- 1 | input[type='range'] { 2 | margin: 10px 0; 3 | padding: 10px 0; 4 | background: transparent; 5 | } 6 | 7 | input[type='range']:focus { 8 | outline: none; 9 | } 10 | 11 | input[type='range']::-webkit-slider-runnable-track { 12 | width: 100%; 13 | height: 9.5px; 14 | transition: 0.2s; 15 | background: var(--background); 16 | border-radius: 3px; 17 | } 18 | 19 | input[type='range']::-webkit-slider-thumb { 20 | box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; 21 | height: 20px; 22 | width: 20px; 23 | border-radius: 50%; 24 | background: var(--border); 25 | -webkit-appearance: none; 26 | margin-top: -7px; 27 | } 28 | 29 | input[type='range']:focus::-webkit-slider-runnable-track { 30 | background: var(--background); 31 | } 32 | 33 | input[type='range']::-moz-range-track { 34 | width: 100%; 35 | height: 9.5px; 36 | transition: 0.2s; 37 | background: var(--background); 38 | border-radius: 3px; 39 | } 40 | 41 | input[type='range']::-moz-range-thumb { 42 | box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; 43 | height: 20px; 44 | width: 20px; 45 | border-radius: 50%; 46 | background: var(--border); 47 | } 48 | 49 | input[type='range']::-ms-track { 50 | width: 100%; 51 | height: 9.5px; 52 | background: transparent; 53 | border-color: transparent; 54 | border-width: 16px 0; 55 | color: transparent; 56 | } 57 | 58 | input[type='range']::-ms-fill-lower { 59 | background: var(--background); 60 | border: 0.2px solid #010101; 61 | border-radius: 3px; 62 | box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; 63 | } 64 | 65 | input[type='range']::-ms-fill-upper { 66 | background: var(--background); 67 | border: 0.2px solid #010101; 68 | border-radius: 3px; 69 | box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; 70 | } 71 | 72 | input[type='range']::-ms-thumb { 73 | box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; 74 | border: 1px solid #000; 75 | height: 20px; 76 | width: 20px; 77 | border-radius: 50%; 78 | background: var(--border); 79 | } 80 | 81 | input[type='range']:focus::-ms-fill-lower { 82 | background: var(--background); 83 | } 84 | 85 | input[type='range']:focus::-ms-fill-upper { 86 | background: var(--background); 87 | } 88 | -------------------------------------------------------------------------------- /layouts/partials/schema_jsonld.html: -------------------------------------------------------------------------------- 1 | {{ if .IsHome }} 2 | 31 | {{ else if and .IsPage (ne .Params.layout "archives") }} 32 | 68 | {{ end }} 69 | -------------------------------------------------------------------------------- /layouts/_default/baseof.amp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ if .Params.hasIframe -}} 7 | 8 | {{ end -}} 9 | 10 | 11 | {{- block "title" . }} 12 | {{ if .IsHome }}{{ else }}{{ if .Title }}{{ .Title }} | {{ end }}{{ end }}{{ site.Title }} 13 | {{- end }} 14 | 15 | 16 | 17 | 18 | 19 | {{ template "_internal/opengraph.html" . }} 20 | {{ template "_internal/twitter_cards.html" . }} 21 | {{ partial "schema_jsonld.html" . }} 22 | 23 | 30 | 31 | 32 |
{{- partial "header.html" . -}}
33 |
34 | {{- block "main" . }} 35 | {{- end }} 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /assets/css/notice.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/martignoni/hugo-notice */ 2 | .notice { 3 | --title-color: #fff; 4 | --title-background-color: #6be; 5 | --content-color: #444; 6 | --content-background-color: #e7f2fa; 7 | } 8 | 9 | .notice.info { 10 | --title-background-color: #fb7; 11 | --content-background-color: #fec; 12 | } 13 | 14 | .notice.tip { 15 | --title-background-color: #5a5; 16 | --content-background-color: #efe; 17 | } 18 | 19 | .notice.warning { 20 | --title-background-color: #c33; 21 | --content-background-color: #fee; 22 | } 23 | 24 | /* Dark theme */ 25 | @media (prefers-color-scheme: dark) { 26 | .notice { 27 | --title-color: #fff; 28 | --title-background-color: #069; 29 | --content-color: #ddd; 30 | --content-background-color: #023; 31 | } 32 | 33 | .notice.info { 34 | --title-background-color: #a50; 35 | --content-background-color: #420; 36 | } 37 | 38 | .notice.tip { 39 | --title-background-color: #363; 40 | --content-background-color: #121; 41 | } 42 | 43 | .notice.warning { 44 | --title-background-color: #800; 45 | --content-background-color: #400; 46 | } 47 | } 48 | 49 | html.dark-mode .notice { 50 | --title-color: #fff; 51 | --title-background-color: #069; 52 | --content-color: #ddd; 53 | --content-background-color: #023; 54 | } 55 | 56 | html.dark-mode .notice.info { 57 | --title-background-color: #a50; 58 | --content-background-color: #420; 59 | } 60 | 61 | html.dark-mode .notice.tip { 62 | --title-background-color: #363; 63 | --content-background-color: #121; 64 | } 65 | 66 | html.dark-mode .notice.warning { 67 | --title-background-color: #800; 68 | --content-background-color: #400; 69 | } 70 | 71 | /* Content */ 72 | .notice { 73 | padding: 18px; 74 | line-height: 24px; 75 | margin-bottom: 24px; 76 | border-radius: 4px; 77 | color: var(--content-color); 78 | background: var(--content-background-color); 79 | } 80 | 81 | .notice p:last-child { 82 | margin-bottom: 0; 83 | } 84 | 85 | /* Title */ 86 | .notice-title { 87 | margin: -18px -18px 12px; 88 | padding: 4px 18px; 89 | border-radius: 4px 4px 0 0; 90 | font-weight: 700; 91 | color: var(--title-color); 92 | background: var(--title-background-color); 93 | } 94 | 95 | /* Icon */ 96 | .icon-notice { 97 | display: inline-flex; 98 | align-self: center; 99 | margin-right: 8px; 100 | } 101 | 102 | .icon-notice img, 103 | .icon-notice svg { 104 | height: 1em; 105 | width: 1em; 106 | fill: currentColor; 107 | } 108 | 109 | .icon-notice img, 110 | .icon-notice.baseline svg { 111 | top: 0.125em; 112 | position: relative; 113 | } 114 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_misc.css: -------------------------------------------------------------------------------- 1 | img, 2 | video { 3 | max-width: 100%; 4 | height: auto; 5 | } 6 | 7 | hr { 8 | border: none; 9 | border-top: 1px solid var(--border); 10 | } 11 | 12 | table { 13 | border-collapse: collapse; 14 | margin-bottom: 10px; 15 | width: 100%; 16 | table-layout: fixed; 17 | } 18 | 19 | table caption { 20 | text-align: left; 21 | } 22 | 23 | td, 24 | th { 25 | padding: 6px; 26 | text-align: left; 27 | vertical-align: top; 28 | word-wrap: break-word; 29 | } 30 | 31 | thead { 32 | border-bottom: 1px solid var(--border); 33 | } 34 | 35 | tfoot { 36 | border-top: 1px solid var(--border); 37 | } 38 | 39 | tbody tr:nth-child(even) { 40 | background-color: var(--background); 41 | } 42 | 43 | tbody tr:nth-child(even) button { 44 | background-color: var(--background-alt); 45 | } 46 | 47 | tbody tr:nth-child(even) button:hover { 48 | background-color: var(--background-body); 49 | } 50 | 51 | ::-webkit-scrollbar { 52 | height: 10px; 53 | width: 10px; 54 | } 55 | 56 | ::-webkit-scrollbar-track { 57 | background: var(--background); 58 | border-radius: 6px; 59 | } 60 | 61 | ::-webkit-scrollbar-thumb { 62 | background: var(--scrollbar-thumb); 63 | border-radius: 6px; 64 | } 65 | 66 | ::-webkit-scrollbar-thumb:hover { 67 | background: var(--scrollbar-thumb-hover); 68 | } 69 | 70 | ::selection { 71 | background-color: var(--selection); 72 | color: var(--text-bright); 73 | } 74 | 75 | details { 76 | display: flex; 77 | flex-direction: column; 78 | align-items: flex-start; 79 | background-color: var(--background-alt); 80 | padding: 10px 10px 0; 81 | margin: 1em 0; 82 | border-radius: 6px; 83 | overflow: hidden; 84 | } 85 | 86 | details[open] { 87 | padding: 10px; 88 | } 89 | 90 | details > :last-child { 91 | margin-bottom: 0; 92 | } 93 | 94 | details[open] summary { 95 | margin-bottom: 10px; 96 | } 97 | 98 | summary { 99 | display: list-item; 100 | background-color: var(--background); 101 | padding: 10px; 102 | margin: -10px -10px 0; 103 | cursor: pointer; 104 | outline: none; 105 | } 106 | 107 | summary:hover, 108 | summary:focus { 109 | text-decoration: underline; 110 | } 111 | 112 | details > :not(summary) { 113 | margin-top: 0; 114 | } 115 | 116 | summary::-webkit-details-marker { 117 | color: var(--text-main); 118 | } 119 | 120 | dialog { 121 | background-color: var(--background-alt); 122 | color: var(--text-main); 123 | border: none; 124 | border-radius: 6px; 125 | border-color: var(--border); 126 | padding: 10px 30px; 127 | } 128 | 129 | dialog > header:first-child { 130 | background-color: var(--background); 131 | border-radius: 6px 6px 0 0; 132 | margin: -10px -30px 10px; 133 | padding: 10px; 134 | text-align: center; 135 | } 136 | 137 | dialog::backdrop { 138 | background: #0000009c; 139 | backdrop-filter: blur(4px); 140 | } 141 | 142 | footer { 143 | border-top: 1px solid var(--border); 144 | padding-top: 10px; 145 | color: var(--text-muted); 146 | } 147 | 148 | body > footer { 149 | margin-top: 40px; 150 | } 151 | -------------------------------------------------------------------------------- /assets/scss/water/parts/_forms.css: -------------------------------------------------------------------------------- 1 | button, 2 | select, 3 | input[type='submit'], 4 | input[type='reset'], 5 | input[type='button'], 6 | input[type='checkbox'], 7 | input[type='range'], 8 | input[type='radio'] { 9 | cursor: pointer; 10 | } 11 | 12 | input, 13 | select { 14 | display: block; 15 | } 16 | 17 | [type='checkbox'], 18 | [type='radio'] { 19 | display: initial; 20 | } 21 | 22 | input, 23 | button, 24 | textarea, 25 | select { 26 | color: var(--form-text); 27 | background-color: var(--background); 28 | font-family: inherit; 29 | font-size: inherit; 30 | margin-right: 6px; 31 | margin-bottom: 6px; 32 | padding: 10px; 33 | border: none; 34 | border-radius: 6px; 35 | outline: none; 36 | } 37 | 38 | button, 39 | input[type='submit'], 40 | input[type='reset'], 41 | input[type='button'] { 42 | background-color: var(--button-base); 43 | padding-right: 30px; 44 | padding-left: 30px; 45 | } 46 | 47 | button:hover, 48 | input[type='submit']:hover, 49 | input[type='reset']:hover, 50 | input[type='button']:hover { 51 | background: var(--button-hover); 52 | } 53 | 54 | input[type='color'] { 55 | min-height: 2rem; 56 | padding: 8px; 57 | cursor: pointer; 58 | } 59 | 60 | input[type='checkbox'], 61 | input[type='radio'] { 62 | height: 1em; 63 | width: 1em; 64 | } 65 | 66 | input[type='radio'] { 67 | border-radius: 100%; 68 | } 69 | 70 | input { 71 | vertical-align: top; 72 | } 73 | 74 | label { 75 | vertical-align: middle; 76 | margin-bottom: 4px; 77 | display: inline-block; 78 | } 79 | 80 | input:not([type='checkbox']):not([type='radio']), 81 | input[type='range'], 82 | select, 83 | button, 84 | textarea { 85 | -webkit-appearance: none; 86 | } 87 | 88 | textarea { 89 | display: block; 90 | margin-right: 0; 91 | box-sizing: border-box; 92 | resize: vertical; 93 | } 94 | 95 | textarea:not([cols]) { 96 | width: 100%; 97 | } 98 | 99 | textarea:not([rows]) { 100 | min-height: 40px; 101 | height: 140px; 102 | } 103 | 104 | select { 105 | background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; 106 | padding-right: 35px; 107 | } 108 | 109 | select::-ms-expand { 110 | display: none; 111 | } 112 | 113 | select[multiple] { 114 | padding-right: 10px; 115 | background-image: none; 116 | overflow-y: auto; 117 | } 118 | 119 | input:focus, 120 | select:focus, 121 | button:focus, 122 | textarea:focus { 123 | box-shadow: 0 0 0 2px var(--focus); 124 | } 125 | 126 | input[type='checkbox']:active, 127 | input[type='radio']:active, 128 | input[type='submit']:active, 129 | input[type='reset']:active, 130 | input[type='button']:active, 131 | input[type='range']:active, 132 | button:active { 133 | transform: translateY(2px); 134 | } 135 | 136 | input:disabled, 137 | select:disabled, 138 | button:disabled, 139 | textarea:disabled { 140 | cursor: not-allowed; 141 | opacity: 0.5; 142 | } 143 | 144 | ::placeholder { 145 | color: var(--form-placeholder); 146 | } 147 | 148 | fieldset { 149 | border: 1px var(--focus) solid; 150 | border-radius: 6px; 151 | margin: 0; 152 | margin-bottom: 12px; 153 | padding: 10px; 154 | } 155 | 156 | legend { 157 | font-size: 0.9em; 158 | font-weight: 600; 159 | } 160 | -------------------------------------------------------------------------------- /config.toml.example: -------------------------------------------------------------------------------- 1 | baseURL = "example.com" 2 | defaultContentLanguage = "zh-Hans" 3 | defaultContentLanguageInSubdir = false 4 | title = "Someone's Blog" 5 | theme = "futu" 6 | enableRobotsTXT = true 7 | enableEmoji = true 8 | enableGitInfo = true 9 | hasCJKLanguage = true 10 | #disqusShortname = "" 11 | #googleAnalytics = "" 12 | 13 | [imaging] 14 | quality = 75 15 | 16 | [sitemap] 17 | changefreq = "weekly" 18 | priority = 0.5 19 | filename = "sitemap.xml" 20 | 21 | [params] 22 | keywords = [] 23 | description = "" 24 | ampLogo = "" 25 | # Site creation time 26 | since = "2016" 27 | paginate = 10 28 | # The date format to use; for a list of valid formats, see https://gohugo.io/functions/format/ 29 | dateFormatToUse = "2006/01/02" 30 | # Whether display TOC or not. 31 | toc = true 32 | enableRelated = true 33 | googleVerification = "" 34 | enableEmojiZoomJS = true 35 | 36 | [params.git] 37 | repo = "https://github.com/yuanji-dev/futu" 38 | branch = "main" 39 | 40 | [params.author] 41 | name = "Someone" 42 | 43 | [mediaTypes] 44 | [mediaTypes."text/plain"] 45 | suffixes = ["md"] 46 | 47 | [outputFormats.Markdown] 48 | mediaType = "text/markdown" 49 | isPlainText = true 50 | isHTML = false 51 | 52 | [outputs] 53 | home = ["HTML", "RSS"] 54 | page = ["HTML", "AMP", "Markdown"] 55 | section = ["HTML", "RSS", "Markdown"] 56 | taxonomy = ["HTML", "RSS"] 57 | taxonomyTerm = ["HTML"] 58 | 59 | [markup] 60 | [markup.tableOfContents] 61 | startLevel = 2 62 | [markup.goldmark.renderer] 63 | unsafe = true 64 | [markup.goldmark.parser] 65 | wrapStandAloneImageWithinParagraph = false 66 | [markup.highlight] 67 | anchorLineNos = true 68 | codeFences = true 69 | guessSyntax = true 70 | hl_Lines = '' 71 | lineAnchors = '' 72 | lineNoStart = 1 73 | lineNos = true 74 | lineNumbersInTable = false 75 | noClasses = false 76 | noHl = false 77 | style = 'monokai' 78 | tabWidth = 4 79 | 80 | [languages] 81 | [languages.zh-Hans] 82 | title = "一个人的博客" 83 | languageCode = 'zh-Hans' 84 | languageName = '简体中文' 85 | # https://gohugo.io/content-management/menus/ 86 | [languages.zh-Hans.menu] 87 | [[languages.zh-Hans.menu.main]] 88 | name = "首页" 89 | weight = 10 90 | identifier = "home" 91 | url = "/" 92 | [[languages.zh-Hans.menu.main]] 93 | name = "归档" 94 | weight = 20 95 | identifier = "archives" 96 | url = "/archives/" 97 | [[languages.zh-Hans.menu.main]] 98 | name = "标签" 99 | weight = 30 100 | identifier = "tags" 101 | url = "/tags/" 102 | [[languages.zh-Hans.menu.main]] 103 | name = "关于" 104 | weight = 40 105 | identifier = "about" 106 | url = "/about/" 107 | 108 | [languages.en] 109 | title = "Someone's Blog" 110 | languageCode = 'en' 111 | languageName = 'English' 112 | [[languages.en.menu.main]] 113 | name = "Home" 114 | weight = 10 115 | identifier = "home" 116 | url = "/" 117 | [[languages.en.menu.main]] 118 | name = "Archives" 119 | weight = 20 120 | identifier = "archives" 121 | url = "/archives/" 122 | [[languages.en.menu.main]] 123 | name = "Tags" 124 | weight = 30 125 | identifier = "tags" 126 | url = "/tags/" 127 | [[languages.en.menu.main]] 128 | name = "About" 129 | weight = 40 130 | identifier = "about" 131 | url = "/about/" 132 | -------------------------------------------------------------------------------- /layouts/partials/header.html: -------------------------------------------------------------------------------- 1 | {{ if .IsHome }} 2 |

{{ .Site.Title }}

3 | {{ else }} 4 | {{ .Site.Title }} 5 | {{ end }} 6 |

7 | 15 | 19 | {{ i18n "verified" }} 23 |

24 | 68 | -------------------------------------------------------------------------------- /assets/js/disqusloader.js: -------------------------------------------------------------------------------- 1 | /* 2 | disqusLoader.js v1.0 3 | A JavaScript plugin for lazy-loading Disqus comments widget. 4 | - 5 | By Osvaldas Valutis, www.osvaldas.info 6 | Available for use under the MIT License 7 | */ 8 | 9 | ;( function( window, document, index ) 10 | { 11 | 'use strict'; 12 | 13 | var extendObj = function( defaults, options ) 14 | { 15 | var prop, extended = {}; 16 | for( prop in defaults ) 17 | if( Object.prototype.hasOwnProperty.call( defaults, prop )) 18 | extended[ prop ] = defaults[ prop ]; 19 | 20 | for( prop in options ) 21 | if( Object.prototype.hasOwnProperty.call( options, prop )) 22 | extended[ prop ] = options[ prop ]; 23 | 24 | return extended; 25 | }, 26 | getOffset = function( el ) 27 | { 28 | var rect = el.getBoundingClientRect(); 29 | return { top: rect.top + document.body.scrollTop, left: rect.left + document.body.scrollLeft }; 30 | }, 31 | loadScript = function( url, callback ) 32 | { 33 | var script = document.createElement( 'script' ); 34 | script.src = url; 35 | script.async = true; 36 | script.setAttribute( 'data-timestamp', +new Date()); 37 | script.addEventListener( 'load', function() 38 | { 39 | if( typeof callback === 'function' ) 40 | callback(); 41 | }); 42 | ( document.head || document.body ).appendChild( script ); 43 | }, 44 | throttle = function(a,b){var c,d;return function(){var e=this,f=arguments,g=+new Date;c&&g window.innerHeight * laziness || winST - offset - instance.offsetHeight - ( window.innerHeight * laziness ) > 0 ) 64 | return true; 65 | 66 | var tmp = document.getElementById( 'disqus_thread' ); 67 | if( tmp ) tmp.removeAttribute( 'id' ); 68 | instance.setAttribute( 'id', 'disqus_thread' ); 69 | instance.disqusLoaderStatus = 'loaded'; 70 | 71 | if( scriptStatus == 'loaded' ) 72 | { 73 | DISQUS.reset({ reload: true, config: disqusConfig }); 74 | } 75 | else // unloaded | loading 76 | { 77 | window.disqus_config = disqusConfig; 78 | if( scriptStatus == 'unloaded' ) 79 | { 80 | scriptStatus = 'loading'; 81 | loadScript( scriptUrl, function() 82 | { 83 | scriptStatus = 'loaded'; 84 | }); 85 | } 86 | } 87 | }; 88 | 89 | window.addEventListener( 'scroll', throttle( throttleTO, init )); 90 | window.addEventListener( 'resize', throttle( throttleTO, init )); 91 | 92 | window.disqusLoader = function( element, options ) 93 | { 94 | options = extendObj( 95 | { 96 | laziness: 1, 97 | throttle: 250, 98 | scriptUrl: false, 99 | disqusConfig: false, 100 | 101 | }, options ); 102 | 103 | laziness = options.laziness + 1; 104 | throttleTO = options.throttle; 105 | disqusConfig = options.disqusConfig; 106 | scriptUrl = scriptUrl === false ? options.scriptUrl : scriptUrl; // set it only once 107 | 108 | if( typeof element === 'string' ) instance = document.querySelector( element ); 109 | else if( typeof element.length === 'number' ) instance = element[ 0 ]; 110 | else instance = element; 111 | 112 | if (instance) instance.disqusLoaderStatus = 'unloaded'; 113 | 114 | init(); 115 | }; 116 | 117 | }( window, document, 0 )); 118 | -------------------------------------------------------------------------------- /static/sitemap.xsl: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | XML Sitemap 12 | 13 | 65 | 66 | 67 |
68 |

XML Sitemap

69 |

70 | This is a sitemap generated by Hugo to allow search engines to discover this blog's content. 71 |

72 |

73 | The xsl style copy from Ghost. 74 |

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 97 | 100 | 103 | 106 | 107 | 108 | 109 |
URL ( total)PrioCh. Freq.Last Modified
90 | 91 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 101 | 102 | 104 | 105 |
110 |
111 | 112 | 113 | 114 |
115 |
-------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "water/builds/water"; 2 | @import "share_buttons"; 3 | 4 | body { 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, "PingFang SC", 6 | "Hiragino Sans GB", STHeiti, "Microsoft YaHei", sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 7 | 8 | img { 9 | display: block; 10 | margin: 0 auto; 11 | } 12 | 13 | table { 14 | margin-bottom: 0; 15 | 16 | &:not([class]) { 17 | display: block; 18 | white-space: nowrap; 19 | border-spacing: 0; 20 | border-collapse: collapse; 21 | overflow-x: auto; 22 | max-width: 100%; 23 | text-align: left; 24 | vertical-align: top; 25 | } 26 | 27 | caption { 28 | text-align: center; 29 | font-weight: 600; 30 | } 31 | } 32 | 33 | code, 34 | kbd, 35 | var, 36 | samp { 37 | font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; 38 | font-style: normal; 39 | } 40 | 41 | > header > nav ul { 42 | padding: 0; 43 | 44 | li { 45 | display: inline-block; 46 | margin-right: 1em; 47 | margin-bottom: 0.25em; 48 | } 49 | } 50 | 51 | footer { 52 | border-top: medium none currentcolor; 53 | text-align: center; 54 | font-size: 0.8em; 55 | } 56 | 57 | audio, 58 | video { 59 | width: 100%; 60 | max-width: 100%; 61 | } 62 | 63 | figure { 64 | margin: 1em 0 0.5em; 65 | padding: 0; 66 | 67 | + p { 68 | margin-top: 0.5em; 69 | } 70 | 71 | figcaption { 72 | opacity: 0.65; 73 | font-size: 0.85em; 74 | text-align: center; 75 | } 76 | } 77 | 78 | hr { 79 | height: 1px; 80 | margin: 2em 0; 81 | border: 0; 82 | background: var(--border); 83 | } 84 | 85 | details { 86 | display: block; 87 | overflow: visible; 88 | background-color: var(--background); 89 | padding: 0 1em; 90 | 91 | nav { 92 | margin: 0.5em 0; 93 | 94 | ul { 95 | margin: 0; 96 | padding-left: 1em; 97 | 98 | li { 99 | display: list-item; 100 | margin-right: 1em; 101 | margin-bottom: 0.25em; 102 | } 103 | } 104 | } 105 | 106 | summary { 107 | margin: 0; 108 | } 109 | 110 | &[open] { 111 | padding: 0 1em 1em; 112 | } 113 | } 114 | } 115 | 116 | // article list styles 117 | article.home-section { 118 | + article.home-section { 119 | margin-top: 2em; 120 | } 121 | } 122 | 123 | ul.pages-list { 124 | list-style: none; 125 | margin: 1em 0; 126 | padding: 0; 127 | 128 | li { 129 | margin: 0.5em 0; 130 | display: flex; 131 | flex-direction: column-reverse; 132 | 133 | @media (min-width: "800px") { 134 | flex-direction: row; 135 | justify-content: space-between; 136 | align-items: center; 137 | } 138 | 139 | small { 140 | font-size: 0.75em; 141 | } 142 | } 143 | } 144 | 145 | // header styles 146 | .header-verification { 147 | font-size: 1.5em; 148 | color: var(--text-bright); 149 | font-weight: 600; 150 | margin-bottom: 0; 151 | margin-top: 24px; 152 | display: block; 153 | 154 | &::after { 155 | content: ""; 156 | display: inline-block; 157 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%231d9bf0' viewBox='0 0 16 16'%3E%3Cpath d='M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01-.622-.636zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708z'/%3E%3C/svg%3E") 158 | no-repeat; 159 | width: 1em; 160 | height: 1em; 161 | margin-left: 5px; 162 | vertical-align: -15%; 163 | } 164 | } 165 | 166 | .header-official { 167 | margin-top: 0; 168 | color: var(--text-muted); 169 | 170 | svg { 171 | vertical-align: -10%; 172 | margin-right: 0.5em; 173 | } 174 | } 175 | 176 | // dark mode styles 177 | #mode-switch { 178 | vertical-align: -10%; 179 | padding: 0; 180 | font: inherit; 181 | background: 0 0; 182 | border: 0; 183 | } 184 | 185 | html.dark-mode #moon { 186 | display: none; 187 | } 188 | 189 | html:not(.dark-mode) #sun { 190 | display: none; 191 | } 192 | 193 | // other cleanup for water.css 194 | a[href^="mailto\:"]::before { 195 | content: ""; 196 | } 197 | 198 | a[href^="tel\:"]::before { 199 | content: ""; 200 | } 201 | 202 | a[href^="sms\:"]::before { 203 | content: ""; 204 | } 205 | 206 | // fix zoom.js overlay in dark mode 207 | .zoom-overlay { 208 | background: var(--background-body); 209 | } 210 | -------------------------------------------------------------------------------- /assets/css/syntax.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --chroma-hl-background: #e1dbd5; 3 | } 4 | :root.dark-mode { 5 | --chroma-hl-background: #39374a; 6 | } 7 | /* Background */ .bg { color: #575279; background-color: #faf4ed; } 8 | /* PreWrapper */ .chroma { color: #575279; } 9 | /* Other */ .chroma .x { } 10 | /* Error */ .chroma .err { color: #b4637a } 11 | /* CodeLine */ .chroma .cl { } 12 | /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } 13 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 14 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } 15 | /* LineHighlight */ .chroma .hl { background-color: var(--chroma-hl-background); } 16 | /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 17 | /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } 18 | /* Line */ .chroma .line { display: flex; } 19 | /* Keyword */ .chroma .k { color: #286983 } 20 | /* KeywordConstant */ .chroma .kc { color: #286983 } 21 | /* KeywordDeclaration */ .chroma .kd { color: #286983 } 22 | /* KeywordNamespace */ .chroma .kn { color: #907aa9 } 23 | /* KeywordPseudo */ .chroma .kp { color: #286983 } 24 | /* KeywordReserved */ .chroma .kr { color: #286983 } 25 | /* KeywordType */ .chroma .kt { color: #286983 } 26 | /* Name */ .chroma .n { color: #d7827e } 27 | /* NameAttribute */ .chroma .na { color: #d7827e } 28 | /* NameBuiltin */ .chroma .nb { color: #d7827e } 29 | /* NameBuiltinPseudo */ .chroma .bp { color: #d7827e } 30 | /* NameClass */ .chroma .nc { color: #56949f } 31 | /* NameConstant */ .chroma .no { color: #ea9d34 } 32 | /* NameDecorator */ .chroma .nd { color: #797593 } 33 | /* NameEntity */ .chroma .ni { color: #d7827e } 34 | /* NameException */ .chroma .ne { color: #286983 } 35 | /* NameFunction */ .chroma .nf { color: #d7827e } 36 | /* NameFunctionMagic */ .chroma .fm { color: #d7827e } 37 | /* NameLabel */ .chroma .nl { color: #d7827e } 38 | /* NameNamespace */ .chroma .nn { color: #d7827e } 39 | /* NameOther */ .chroma .nx { } 40 | /* NameProperty */ .chroma .py { color: #d7827e } 41 | /* NameTag */ .chroma .nt { color: #d7827e } 42 | /* NameVariable */ .chroma .nv { color: #d7827e } 43 | /* NameVariableClass */ .chroma .vc { color: #d7827e } 44 | /* NameVariableGlobal */ .chroma .vg { color: #d7827e } 45 | /* NameVariableInstance */ .chroma .vi { color: #d7827e } 46 | /* NameVariableMagic */ .chroma .vm { color: #d7827e } 47 | /* Literal */ .chroma .l { color: #ea9d34 } 48 | /* LiteralDate */ .chroma .ld { color: #ea9d34 } 49 | /* LiteralString */ .chroma .s { color: #ea9d34 } 50 | /* LiteralStringAffix */ .chroma .sa { color: #ea9d34 } 51 | /* LiteralStringBacktick */ .chroma .sb { color: #ea9d34 } 52 | /* LiteralStringChar */ .chroma .sc { color: #ea9d34 } 53 | /* LiteralStringDelimiter */ .chroma .dl { color: #ea9d34 } 54 | /* LiteralStringDoc */ .chroma .sd { color: #ea9d34 } 55 | /* LiteralStringDouble */ .chroma .s2 { color: #ea9d34 } 56 | /* LiteralStringEscape */ .chroma .se { color: #286983 } 57 | /* LiteralStringHeredoc */ .chroma .sh { color: #ea9d34 } 58 | /* LiteralStringInterpol */ .chroma .si { color: #ea9d34 } 59 | /* LiteralStringOther */ .chroma .sx { color: #ea9d34 } 60 | /* LiteralStringRegex */ .chroma .sr { color: #ea9d34 } 61 | /* LiteralStringSingle */ .chroma .s1 { color: #ea9d34 } 62 | /* LiteralStringSymbol */ .chroma .ss { color: #ea9d34 } 63 | /* LiteralNumber */ .chroma .m { color: #ea9d34 } 64 | /* LiteralNumberBin */ .chroma .mb { color: #ea9d34 } 65 | /* LiteralNumberFloat */ .chroma .mf { color: #ea9d34 } 66 | /* LiteralNumberHex */ .chroma .mh { color: #ea9d34 } 67 | /* LiteralNumberInteger */ .chroma .mi { color: #ea9d34 } 68 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ea9d34 } 69 | /* LiteralNumberOct */ .chroma .mo { color: #ea9d34 } 70 | /* Operator */ .chroma .o { color: #797593 } 71 | /* OperatorWord */ .chroma .ow { color: #797593 } 72 | /* Punctuation */ .chroma .p { color: #797593 } 73 | /* Comment */ .chroma .c { color: #9893a5 } 74 | /* CommentHashbang */ .chroma .ch { color: #9893a5 } 75 | /* CommentMultiline */ .chroma .cm { color: #9893a5 } 76 | /* CommentSingle */ .chroma .c1 { color: #9893a5 } 77 | /* CommentSpecial */ .chroma .cs { color: #9893a5 } 78 | /* CommentPreproc */ .chroma .cp { color: #9893a5 } 79 | /* CommentPreprocFile */ .chroma .cpf { color: #9893a5 } 80 | /* Generic */ .chroma .g { } 81 | /* GenericDeleted */ .chroma .gd { color: #b4637a } 82 | /* GenericEmph */ .chroma .ge { font-style: italic } 83 | /* GenericError */ .chroma .gr { } 84 | /* GenericHeading */ .chroma .gh { } 85 | /* GenericInserted */ .chroma .gi { color: #56949f } 86 | /* GenericOutput */ .chroma .go { } 87 | /* GenericPrompt */ .chroma .gp { } 88 | /* GenericStrong */ .chroma .gs { font-weight: bold } 89 | /* GenericSubheading */ .chroma .gu { color: #907aa9 } 90 | /* GenericTraceback */ .chroma .gt { } 91 | /* GenericUnderline */ .chroma .gl { } 92 | /* TextWhitespace */ .chroma .w { } 93 | -------------------------------------------------------------------------------- /static/zoom.js/dist/zoom.js: -------------------------------------------------------------------------------- 1 | import { toOffset, usableWidth } from "./common.js"; 2 | import { ZoomImage } from "./zoom-image.js"; 3 | // Values for the currently active zoomed instance. 4 | // There can be at most one. 5 | let activeZoomImage = null; 6 | let initialScrollPos = null; 7 | let initialTouchPos = null; 8 | let closeScrollDelta = null; 9 | let closeTouchDelta = null; 10 | function openActiveZoom(img, c) { 11 | if (activeZoomImage !== null) { 12 | activeZoomImage.dismissImmediate(); 13 | } 14 | if (img.naturalWidth === 0 || img.naturalHeight === 0) { 15 | // Spec: "These attributes return the intrinsic dimensions of the 16 | // image, or zero if the dimensions are not known." 17 | // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-image-dev 18 | return; 19 | } 20 | const tooNarrow = img.width >= usableWidth(document.documentElement, toOffset(c.padding)); 21 | activeZoomImage = new ZoomImage(img, toOffset(tooNarrow ? c.paddingNarrow : c.padding)); 22 | initialScrollPos = null; 23 | initialTouchPos = null; 24 | closeScrollDelta = c.dismissScrollDelta; 25 | closeTouchDelta = c.dismissTouchDelta; 26 | activeZoomImage.zoom(); 27 | addCloseListeners(); 28 | } 29 | function closeActiveZoom() { 30 | if (activeZoomImage === null) { 31 | return; 32 | } 33 | removeCloseListeners(); 34 | activeZoomImage.onDismissComplete(() => { activeZoomImage = null; }); 35 | activeZoomImage.dismiss(); 36 | closeScrollDelta = null; 37 | closeTouchDelta = null; 38 | initialScrollPos = null; 39 | initialTouchPos = null; 40 | } 41 | function addCloseListeners() { 42 | document.addEventListener("scroll", handleDocumentScroll, { passive: true }); 43 | document.addEventListener("keyup", handleDocumentKeyup, { passive: true }); 44 | document.addEventListener("touchstart", handleDocumentTouchStart, { passive: true }); 45 | document.addEventListener("click", handleDocumentClick, { passive: true, capture: true }); 46 | } 47 | function removeCloseListeners() { 48 | // NOTE: in removeEventListener calls, for the options parameter it is 49 | // necessary only for the "capture" property to match. The other 50 | // properties, such as "passive", don't matter. Accordingly even the 51 | // type defintions don't allow for other properties to be specified. 52 | document.removeEventListener("scroll", handleDocumentScroll); 53 | document.removeEventListener("keyup", handleDocumentKeyup); 54 | document.removeEventListener("touchstart", handleDocumentTouchStart); 55 | document.removeEventListener("click", handleDocumentClick, { capture: true }); 56 | } 57 | function handleDocumentScroll() { 58 | if (closeScrollDelta === null) { 59 | // Should not happen since it is assigned in openActiveZoom. 60 | throw "null closeScrollDelta"; 61 | } 62 | if (initialScrollPos === null) { 63 | initialScrollPos = window.scrollY; 64 | return; 65 | } 66 | const deltaY = Math.abs(initialScrollPos - window.scrollY); 67 | if (deltaY < closeScrollDelta) { 68 | return; 69 | } 70 | closeActiveZoom(); 71 | } 72 | function handleDocumentKeyup(e) { 73 | if (e.code !== "Escape") { 74 | return; 75 | } 76 | closeActiveZoom(); 77 | } 78 | function handleDocumentTouchStart(e) { 79 | if (e.touches.length === 0) { 80 | return; 81 | } 82 | initialTouchPos = e.touches[0].pageY; 83 | document.addEventListener("touchmove", handleDocumentTouchMove); 84 | } 85 | function handleDocumentTouchMove(e) { 86 | if (closeTouchDelta === null) { 87 | // Should not happen since it is assigned in openActiveZoom. 88 | throw "null closeTouchDelta"; 89 | } 90 | if (initialTouchPos === null) { 91 | // Should not happen (see touchstart handler), but guard anyway. 92 | // This guard is also required to show TypeScript that the 93 | // variable is non-null below. 94 | return; 95 | } 96 | if (e.touches.length === 0) { 97 | return; 98 | } 99 | if (Math.abs(e.touches[0].pageY - initialTouchPos) < closeTouchDelta) { 100 | return; 101 | } 102 | closeActiveZoom(); 103 | document.removeEventListener("touchmove", handleDocumentTouchMove); 104 | } 105 | function handleDocumentClick(e) { 106 | e.stopPropagation(); 107 | closeActiveZoom(); 108 | } 109 | export const defaultConfig = { 110 | padding: 40, 111 | paddingNarrow: 20, 112 | dismissScrollDelta: 15, 113 | dismissTouchDelta: 10, 114 | }; 115 | // zoom zooms the specified image. 116 | // 117 | // The image will not be zoomed if its naturalWidth and naturalHeight properties 118 | // are 0 (usually because the values are unavailable). 119 | export function zoom(img, cfg = defaultConfig) { 120 | openActiveZoom(img, cfg !== undefined ? cfg : defaultConfig); 121 | } 122 | // dismissZoom programmatically dismisses the presently active zoom. It is a 123 | // no-op if there is no zoom active at the time of the call. 124 | export function dismissZoom() { 125 | closeActiveZoom(); 126 | } 127 | -------------------------------------------------------------------------------- /static/zoom.js/dist/zoom-image.js: -------------------------------------------------------------------------------- 1 | import { unwrap, usableHeight, usableWidth, viewportHeight, viewportWidth, wrap } from "./common.js"; 2 | // scaleFactor returns the scale factor with which the image should be 3 | // transformed. The tw and th parameters specify the maximum usable width and 4 | // height in the viewport. 5 | function scaleFactor(img, tw, th) { 6 | const maxScaleFactor = img.naturalWidth / img.width; 7 | const ir = img.naturalWidth / img.naturalHeight; 8 | const tr = tw / th; 9 | if (img.naturalWidth < tw && img.naturalHeight < th) { 10 | return maxScaleFactor; 11 | } 12 | if (ir < tr) { 13 | return (th / img.naturalHeight) * maxScaleFactor; 14 | } 15 | return (tw / img.naturalWidth) * maxScaleFactor; 16 | } 17 | // ZoomImage manages a single zoom and dismiss lifecycle 18 | // on a element. 19 | export class ZoomImage { 20 | constructor(img, offset) { 21 | this.dismissCompleteNotified = false; 22 | this.dismissCompleteCallbacks = []; 23 | // necessary because dismissModifyDOM() cannot be safely called multiple 24 | // times. 25 | this.dismissModifiedDOM = false; 26 | this.img = img; 27 | this.oldTransform = img.style.transform; 28 | this.wrapper = ZoomImage.makeWrapper(); 29 | this.overlay = ZoomImage.makeOverlay(); 30 | this.offset = offset; 31 | } 32 | static makeOverlay() { 33 | const ret = document.createElement("div"); 34 | ret.classList.add("zoom-overlay"); 35 | return ret; 36 | } 37 | static makeWrapper() { 38 | const ret = document.createElement("div"); 39 | ret.classList.add("zoom-img-wrapper"); 40 | return ret; 41 | } 42 | static elemOffset(elem, wnd, docElem) { 43 | const rect = elem.getBoundingClientRect(); 44 | return { 45 | top: rect.top + wnd.scrollY - docElem.clientTop, 46 | left: rect.left + wnd.scrollX - docElem.clientLeft 47 | }; 48 | } 49 | hackForceRepaint() { 50 | const x = this.img.naturalWidth; 51 | return x; 52 | } 53 | zoomModifyDOM() { 54 | this.img.classList.add("zoom-img"); 55 | wrap(this.img, this.wrapper); 56 | document.body.appendChild(this.overlay); 57 | } 58 | dismissModifyDOM() { 59 | document.body.removeChild(this.overlay); 60 | unwrap(this.img, this.wrapper); 61 | this.img.classList.remove("zoom-img"); 62 | } 63 | zoomAnimate(scale) { 64 | const imageOffset = ZoomImage.elemOffset(this.img, window, document.documentElement); 65 | const wx = window.scrollX + (viewportWidth(document.documentElement) / 2); 66 | const wy = window.scrollY + (viewportHeight(document.documentElement) / 2); 67 | const ix = imageOffset.left + (this.img.width / 2); 68 | const iy = imageOffset.top + (this.img.height / 2); 69 | // In img.style.transform, use "scale3d()", not "scale()". There 70 | // is an issue in macOS Safari version 16.3 (18614.4.6.1.6) and 71 | // possibly other versions, if the latter is used. The element 72 | // becomes invisible when zoomed in. 73 | // 74 | // In practice, the issue occurs more often (only?) with 75 | // elements near the top of the page being zoomed. 76 | // 77 | // As a side note, during debugging, the incorrectly invisible 78 | // element reappeared if the "scale()" value is <= 1. The issue 79 | // occurs only for "scale()" value > 1. 80 | // 81 | // In any case, "scale3d()" fixes the issue. 82 | this.img.style.transform = `scale3d(${scale},${scale},${scale})`; 83 | this.wrapper.style.transform = `translate3d(${wx - ix}px, ${wy - iy}px, 0)`; 84 | document.body.classList.add("zoom-overlay-open"); 85 | } 86 | // NOTE: This method is idempotent, and can be called multiple times 87 | // safely. 88 | dismissAnimate() { 89 | document.body.classList.remove("zoom-overlay-open"); 90 | this.img.style.transform = this.oldTransform; 91 | this.wrapper.style.transform = "none"; 92 | } 93 | zoom() { 94 | this.zoomModifyDOM(); 95 | // repaint before animating. 96 | // TODO: is this necessary? 97 | this.hackForceRepaint(); 98 | this.zoomAnimate(scaleFactor(this.img, usableWidth(document.documentElement, this.offset), usableHeight(document.documentElement, this.offset))); 99 | } 100 | onDismissComplete(f) { 101 | this.dismissCompleteCallbacks.push(f); 102 | } 103 | notifyDismissComplete() { 104 | if (this.dismissCompleteNotified) { 105 | return; 106 | } 107 | this.dismissCompleteCallbacks.forEach(f => f()); 108 | this.dismissCompleteNotified = true; 109 | } 110 | dismiss() { 111 | this.img.addEventListener("transitionend", () => { 112 | document.body.classList.remove("zoom-overlay-transitioning"); 113 | if (!this.dismissModifiedDOM) { 114 | this.dismissModifyDOM(); 115 | this.dismissModifiedDOM = true; 116 | } 117 | this.notifyDismissComplete(); 118 | }, { once: true }); 119 | document.body.classList.add("zoom-overlay-transitioning"); 120 | this.dismissAnimate(); 121 | } 122 | dismissImmediate() { 123 | this.dismissAnimate(); 124 | document.body.classList.remove("zoom-overlay-transitioning"); 125 | if (!this.dismissModifiedDOM) { 126 | this.dismissModifyDOM(); 127 | this.dismissModifiedDOM = true; 128 | } 129 | this.notifyDismissComplete(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /static/zoom.js/README.md: -------------------------------------------------------------------------------- 1 | # zoom.js 2 | 3 | An image zooming plugin, as seen on older versions of [medium.com][medium]. This 4 | project is a port of [`fat/zoom.js`][fat] but has no jQuery or Bootstrap 5 | dependencies. 6 | 7 | Version 4 is written in TypeScript, has a new API, includes typings, and has no 8 | dependencies. 9 | 10 | npm package: [https://www.npmjs.com/package/@nishanths/zoom.js][npm] 11 | 12 | ## Branches and versions 13 | 14 | No API backwards compatibility guarantees even within the same major version. 15 | 16 | * **v4**: The default branch. It contains code for version 4, which is the 17 | current major version. 18 | * **master**: Frozen and no longer maintained. The final version on this branch 19 | is 3.1.0. 20 | 21 | ## Demo 22 | 23 | [https://nishanths.github.io/zoom.js][demo] 24 | 25 | Zoom on an image by clicking on it. 26 | 27 | Dismiss the zoom by either clicking again on the image, clicking the overlay 28 | around the image, scrolling away, or hitting the `esc` key. 29 | 30 | ## Usage 31 | 32 | Install the package: 33 | 34 | ``` 35 | npm i @nishanths/zoom.js 36 | ``` 37 | 38 | Link the `src/zoom.css` file in your application: 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | Import and use symbols from the package: 45 | 46 | ```ts 47 | import { zoom } from "@nishanths/zoom.js" 48 | ``` 49 | 50 | Note that the `package.json` for the package specifies the `module` property but 51 | not the `main` property. You may need a module-aware tool to correctly include 52 | the package in your bundle. For further reading, see this Stack Overflow 53 | [answer](https://stackoverflow.com/a/47537198/3309046) as a starting point. 54 | 55 | ## Building locally 56 | 57 | To build the package locally, run the following from the root directory 58 | of the repo: 59 | 60 | ``` 61 | % npm install 62 | % make build 63 | ``` 64 | 65 | This should produce a `dist` directory. The js files in the `dist` 66 | directory are ES modules. 67 | 68 | ## Documentation 69 | 70 | ### API 71 | 72 | ```ts 73 | // Config is the configuration provided to the zoom function. 74 | export type Config = { 75 | // padding defines the horizontal space and the vertical space around 76 | // the zoomed image. 77 | padding: number 78 | 79 | // paddingNarrow is similar to the padding property, except that it is 80 | // used if the viewport width is too narrow, such that the use of the 81 | // larger padding property may produce poor results. 82 | // 83 | // paddingNarrow should be <= padding, however this is not validated. 84 | paddingNarrow: number 85 | 86 | // dismissScrollDelta defines the vertical scrolling threshold at which 87 | // the zoomed image is dismissed by user interaction. The value is the pixel 88 | // difference between the original vertical scroll position and the 89 | // subsequent vertical scroll positions. 90 | dismissScrollDelta: number 91 | 92 | // dismissTouchDelta defines the vertical touch movement threshold at 93 | // which the zoomed image is dismissed by user interactoin. The value is the 94 | // pixel difference between the initial vertical touch position and 95 | // subsequent vertical touch movements. 96 | dismissTouchDelta: number 97 | } 98 | 99 | export const defaultConfig: Config = { 100 | padding: 40, 101 | paddingNarrow: 20, 102 | dismissScrollDelta: 15, 103 | dismissTouchDelta: 10, 104 | } 105 | 106 | // zoom zooms the specified image. 107 | // 108 | // The image will not be zoomed if its naturalWidth and naturalHeight properties 109 | // are 0 (usually because the values are unavailable). 110 | export function zoom(img: HTMLImageElement, cfg: Config = defaultConfig): void 111 | 112 | // dismissZoom programmatically dismisses the presently active zoom. It is a 113 | // no-op if there is no zoom active at the time of the call. 114 | export function dismissZoom(): void 115 | ``` 116 | 117 | ### Examples 118 | 119 | The following TypeScript program makes all existing `` elements on the page 120 | zoomable. Images are zoomed when they are clicked. 121 | 122 | ```ts 123 | import { zoom } from "@nishanths/zoom.js" 124 | 125 | function setupZoom(img: HTMLImageElement) { 126 | img.classList.add("zoom-cursor") 127 | img.addEventListener("click", () => { zoom(img) }) 128 | } 129 | 130 | const imgs = [...document.querySelectorAll("img")] 131 | imgs.forEach(img => { setupZoom(img) }) 132 | ``` 133 | 134 | The following TypeScript program customizes only certain properties of a 135 | `Config`, keeping the defaults for the other properties. 136 | 137 | ```ts 138 | import { Config, defaultConfig } from "@nishanths/zoom.js" 139 | 140 | const customConfig: Config = { 141 | ...defaultConfig, 142 | padding: 30, 143 | } 144 | ``` 145 | 146 | ### Notes 147 | 148 | All CSS class names used by the package are prefixed with `zoom-`. 149 | 150 | Add the class name `zoom-cursor` to a zoomable `` element to use an 151 | [`zoom-in` cursor][zoom-in-cursor] instead of the default cursor for the 152 | image. 153 | 154 | The program appends the DOM node for the overlay element, which appears when an 155 | image is zoomed, to the end of `document.body`. 156 | 157 | While an image is zoomed, the program listens for `click` events on 158 | `document.body` with `useCapture` set to `true`, and the handler function calls 159 | `e.stopPropagation()`. This may interfere with other `click` event handlers on 160 | the page. The event listener is removed when the zoom is dismissed. 161 | 162 | When an image is zoomed, its `transform` style is replaced with a new value that 163 | is necessary for zooming. The old `transform` is restored when the zoom is 164 | dismissed. 165 | 166 | ### Browser compatibility 167 | 168 | I think any popular web browser versions released after 2016 should be supported 169 | by this package. Please read the source code for exact details. 170 | 171 | ## License 172 | 173 | The software in this repository is based on the original [`fat/zoom.js`][fat] 174 | project. The copyright notices and license notices from the original project are 175 | present in the `LICENSE.original` file at the root of this repository. 176 | 177 | New source code and modifications in this repository are licensed under an MIT 178 | license. See the `LICENSE` file at the root of the repository. 179 | 180 | [fat]: https://github.com/fat/zoom.js 181 | [medium]: https://medium.com 182 | [demo]: https://nishanths.github.io/zoom.js 183 | [zoom-in-cursor]: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor 184 | [npm]: https://www.npmjs.com/package/@nishanths/zoom.js 185 | -------------------------------------------------------------------------------- /layouts/partials/share_buttons.html: -------------------------------------------------------------------------------- 1 | {{ if (.Params.enableShare | default (.Site.Params.enableShare | default false )) }} 2 | 149 | {{ end }} 150 | --------------------------------------------------------------------------------