a new start

This commit is contained in:
2025-10-25 12:39:30 +02:00
commit c97cadef78
726 changed files with 454051 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
_site
node_modules
vendor
.bundle
.sass-cache
.jekyll-metadata
.jekyll-cache
.ruby-lsp
*.gem
#<removed/js>
#<removed/css>
# assets
assets/js

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
16.19

7
.prettierignore Normal file
View File

@@ -0,0 +1,7 @@
package.json
*.md
*.html
*.scss
*.css
*.yml
_js/lib

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 120,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

1
.rubocop.yml Normal file
View File

@@ -0,0 +1 @@
inherit_from: ../../.rubocop.yml

93
.scripts/build-css.js Executable file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
const { basename, dirname, format, relative, resolve } = require("path");
const { readdir, stat, readFile, writeFile, mkdir } = require("fs").promises;
const dedent = require("dedent");
const ENC = "utf-8";
const INLINE_REGEX = /(.*)\s*\/\/\s*inline\s*$/gimu;
const DEFER_REGEX = /(.*)\s*\/\/\s*link\s*$/gimu;
const INLINE_BLOCK_REGEX =
/\/\/\s*>*\s*<{3,}\s*inline([\s\S]*?)\/\/\s*>{3,}.*/gimu;
const DEFER_BLOCK_REGEX =
/\/\/\s*>*\s*<{3,}\s*link([\s\S]*?)\/\/\s*>{3,}.*/gimu;
function genHeader(filename) {
return dedent`
// THIS FILE IS AUTOGENERATED, DO NOT MODIFY!
//
// To change the contents of this file,
// edit \`${filename}\`
// and run \`npm run build:css\`.
//
// During development you can run \`npm run watch:css\`
// to continuosly rebuild this file.
`;
}
// <https://stackoverflow.com/a/45130990/870615>
async function getFiles(dir) {
const subdirs = await readdir(dir);
const files = await Promise.all(
subdirs.map(async (subdir) => {
const res = resolve(dir, subdir);
return (await stat(res)).isDirectory() ? getFiles(res) : res;
}),
);
return files.reduce((a, f) => a.concat(f), []);
}
(async function main() {
try {
const files =
process.argv.length > 2 ? [process.argv[2]] : await getFiles("_sass");
await Promise.all(
files
.filter((f) => f.endsWith(".pre.scss"))
.map(async (file) => {
const content = await readFile(file, ENC);
const name = basename(file, ".pre.scss");
const filename = format({ name, ext: ".scss" });
const dir = dirname(file);
const inline = content
.replace(INLINE_REGEX, "$1")
.replace(INLINE_BLOCK_REGEX, "$1")
.replace(DEFER_REGEX, "// $1")
.replace(DEFER_BLOCK_REGEX, "");
const defer = content
.replace(DEFER_REGEX, "$1")
.replace(DEFER_BLOCK_REGEX, "$1")
.replace(INLINE_REGEX, "// $1")
.replace(INLINE_BLOCK_REGEX, "");
const path = relative(resolve(), dirname(file));
const header = genHeader([path, basename(file)].join("/"));
await Promise.all([
mkdir(resolve(dir, "__inline__"), { recursive: true }),
mkdir(resolve(dir, "__link__"), { recursive: true }),
]);
return Promise.all([
writeFile(
resolve(dir, "__inline__", filename),
header + "\n\n" + inline,
ENC,
),
writeFile(
resolve(dir, "__link__", filename),
header + "\n\n" + defer,
ENC,
),
]);
}),
);
process.exit(0);
} catch (e) {
console.error(e); // eslint-disable-line
process.exit(1);
}
})();

108
.scripts/version.js Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
const { resolve } = require("path");
const fs = require("fs");
const { readdir, rename, unlink, readFile, writeFile, access } = fs.promises;
const { promisify } = require("util");
const exec = promisify(require("child_process").exec);
const vPrev = require("../assets/version.json").version;
const vNext = require("../package.json").version;
const ENC = "utf-8";
const FILES = [
"./jekyll-theme-pivoine.gemspec",
"./_includes/body/scripts.html",
"./_includes/body/footer.html",
"./_includes/head/meta-static.html",
"./_includes/head/links-static.html",
"./_includes/head/styles-inline.html",
"./_includes/head/styles-no-inline.html",
"./_includes/header.txt",
"./_layouts/compress.html",
"./_js/lib/version.js",
].map((f) => resolve(f));
/**
* @param {string} dir
* @returns {Promise<string[]>}
* @see https://stackoverflow.com/a/45130990/870615
*/
async function getFiles(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(
dirents.map((dirent) => {
const res = resolve(dir, dirent.name);
return dirent.isDirectory() ? getFiles(res) : [res];
}),
);
return Array.prototype.concat(...files);
}
(async function main() {
try {
const prev = vPrev.replace(/\./g, "\\.");
const prevRegExp = new RegExp(prev, "g");
// const args = await Promise.all([
// getFiles("./hyde/_posts"),
// getFiles("./pivoine/_posts"),
// getFiles("./_projects"),
// getFiles("./docs"),
// ]);
const args = [];
const files = Array.prototype.concat.call(FILES, ...args);
const pFiles = Promise.all(
files
.filter(([f]) => !f.startsWith("."))
.map((f) => [f, readFile(f, ENC)])
.map(async ([f, p]) => {
const content = await p;
// if (f.includes("CHANGELOG")) {
// const pattern = new RegExp(`([^v])${prev}`, "g");
// return [f, content.replace(pattern, `$1${vNext}`)];
// }
return [f, content.replace(prevRegExp, vNext)];
})
.map(async (p) => {
const [f, content] = await p;
return writeFile(f, content, ENC);
}),
);
const pUnlink = Promise.all(
(await getFiles("./assets/js"))
.filter((f) => f.match(/assets\/js\/(.*)pivoine-(.*)/i))
.map(unlink),
);
const pJSCSS = rename(
resolve(`./assets/css/pivoine-${vPrev}.css`),
resolve(`./assets/css/pivoine-${vNext}.css`),
);
await Promise.all([pUnlink, pFiles, pJSCSS]);
await writeFile(
"./assets/version.json",
JSON.stringify({ version: vNext, prevVersion: vPrev }, null, 2),
);
try {
await access("../.scripts/version.js", fs.constants.X_OK);
await exec("../.scripts/version.js");
} catch (e) {
console.warn(e);
}
process.exit(0);
} catch (e) {
console.error(e); // eslint-disable-line
process.exit(1);
}
})();

62
Gemfile Normal file
View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
source 'https://rubygems.org'
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem 'jekyll', '~> 4.4'
# If you are part of the ["Customers" team](https://github.com/orgs/hydecorp/teams/pro-customers),
# you can fetch the theme from a private repository.
# See [Deploy in the pivoine Docs](https://pivoine.com/docs/deploy) for details.
# gem "jekyll-theme-pivoine", git: "https://github.com/hydecorp/pivoine-pro", tag: "pro/v9.2.0"
# IMPORTANT: The followign gem is used to compile math formulas to
# KaTeX during site building.
#
# There are a couple of things to know about this gem:
# * It is not supported on GitHub Pages.
# You have to build the site on your machine before uploading to GitHub,
# or use a more permissive cloud building tool such as Netlify.
# * You need some kind of JavaScript runtime on your machine.
# Usually installing NodeJS will suffice.
# For details, see <https://github.com/kramdown/math-katex#documentation>
#
# If you're using the MathJax math engine instead, free to remove the line below:
gem 'kramdown-math-katex'
# A JavaScript runtime for Ruby that helps with running the katex gem above.
gem 'duktape'
# Required for `jekyll serve` in Ruby 3
gem 'webrick'
# Uncomment when using the `--lsi` option for `jekyll build`
gem "classifier-reborn"
group :jekyll_plugins do
gem 'jekyll-sass-converter'
gem 'jekyll-compose'
gem 'jekyll-default-layout'
gem 'jekyll-feed'
gem 'jekyll-include-cache'
gem 'jekyll-last-modified-at'
gem 'jekyll-optional-front-matter'
gem 'jekyll-paginate-v2'
gem 'jekyll-readme-index'
gem 'jekyll-redirect-from'
gem 'jekyll-relative-links'
gem 'jekyll-seo-tag'
gem 'jekyll-sitemap'
gem 'jekyll-titles-from-headings'
end
gem 'tzinfo-data' if Gem.win_platform?
gem 'wdm' if Gem.win_platform?

217
Gemfile.lock Normal file
View File

@@ -0,0 +1,217 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.3.0)
bigdecimal (3.2.2)
classifier-reborn (2.3.0)
fast-stemmer (~> 1.0)
matrix (~> 0.4)
colorator (1.1.0)
concurrent-ruby (1.3.5)
csv (3.3.5)
duktape (2.7.0.0)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
execjs (2.10.0)
fast-stemmer (1.0.2)
ffi (1.17.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86-linux-gnu)
ffi (1.17.2-x86-linux-musl)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
forwardable-extended (2.6.0)
google-protobuf (4.31.1)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-aarch64-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-aarch64-linux-musl)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-arm64-darwin)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86-linux-musl)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-darwin)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-linux-musl)
bigdecimal
rake (>= 13)
http_parser.rb (0.8.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jekyll (4.4.1)
addressable (~> 2.4)
base64 (~> 0.2)
colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
json (~> 2.6)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (~> 0.3, >= 0.3.6)
pathutil (~> 0.9)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-compose (0.12.0)
jekyll (>= 3.7, < 5.0)
jekyll-default-layout (0.1.5)
jekyll (>= 3.0, < 5.0)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-last-modified-at (1.3.2)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate-v2 (3.0.0)
jekyll (>= 3.0, < 5.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.7.0)
jekyll (>= 3.3, < 5.0)
jekyll-sass-converter (3.1.0)
sass-embedded (~> 1.75)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
json (2.13.2)
katex (0.10.0)
execjs (~> 2.8)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-math-katex (1.0.1)
katex (~> 0.4)
kramdown (~> 2.0)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
matrix (0.4.3)
mercenary (0.4.0)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (6.0.2)
rake (13.3.0)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.4.1)
rouge (4.6.0)
safe_yaml (1.0.5)
sass-embedded (1.89.2)
google-protobuf (~> 4.31)
rake (>= 13)
sass-embedded (1.89.2-aarch64-linux-android)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-aarch64-linux-gnu)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-aarch64-linux-musl)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-arm-linux-androideabi)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-arm-linux-gnueabihf)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-arm-linux-musleabihf)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-arm64-darwin)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-riscv64-linux-android)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-riscv64-linux-gnu)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-riscv64-linux-musl)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-x86_64-darwin)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-x86_64-linux-android)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-x86_64-linux-gnu)
google-protobuf (~> 4.31)
sass-embedded (1.89.2-x86_64-linux-musl)
google-protobuf (~> 4.31)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.6.0)
webrick (1.9.1)
PLATFORMS
aarch64-linux-android
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-androideabi
arm-linux-gnu
arm-linux-gnueabihf
arm-linux-musl
arm-linux-musleabihf
arm64-darwin
riscv64-linux-android
riscv64-linux-gnu
riscv64-linux-musl
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux-android
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
classifier-reborn
duktape
jekyll (~> 4.4)
jekyll-compose
jekyll-default-layout
jekyll-feed
jekyll-include-cache
jekyll-last-modified-at
jekyll-optional-front-matter
jekyll-paginate-v2
jekyll-readme-index
jekyll-redirect-from
jekyll-relative-links
jekyll-sass-converter
jekyll-seo-tag
jekyll-sitemap
jekyll-titles-from-headings
kramdown-math-katex
webrick
BUNDLED WITH
2.7.1

253
_config.yml Normal file
View File

@@ -0,0 +1,253 @@
lang: en
title: Valknar's
copyright: © 2025 Valknar. All rights reserved.
description: Valknar's blog. Coding, Art, and Life.
tagline: Coding, Art, and Life
logo: /assets/img/logo.svg
favicon: /assets/icons/favicon.svg
author:
# Used by `jekyll-feed`:
name: Sebastian Krüger
email: valknar@pivoine.art
# Used by `jekyll-seo-tag`:
twitter: bordeaux1981
url: https://pivoine.art
baseurl: ''
exclude:
- packages
- gfx
include:
- _pages
twitter:
username: bordeaux1981
github:
username: valknarogg
collections:
posts:
output: true
permalink: /:categories/:slug
accent_image: /assets/bg/hypertown.webp
accent_video: /assets/bg/hypertown.mp4
accent_color: rgb(255, 168, 202)
theme_color: rgb(255,255,255)
menu:
legal:
- title: About
url: /about
- title: Imprint
url: /imprint
timezone: Europe/Berlin
featured_categories:
- slug: Palina
title: Palina
url: /palina
- slug: Odinsland
title: Odinsland
url: /odinsland
- slug: Devilish
title: Devilish
url: /devilish
- slug: Souls
title: Souls
url: /souls
- slug: Sketches
title: Sketches
url: /sketches
- slug: Music
title: Music
url: /music
featured_tags:
- slug: Hyperloop
title: Hyperloop
url: /hyperloop
lsi: true
markdown: kramdown
highlighter: rouge
kramdown:
syntax_highlighter: rouge
permalink: pretty
plugins:
- jekyll-default-layout
- jekyll-feed
- jekyll-optional-front-matter
- jekyll-paginate-v2
- jekyll-readme-index
- jekyll-redirect-from
- jekyll-relative-links
- jekyll-seo-tag
- jekyll-sitemap
- jekyll-titles-from-headings
- jekyll-include-cache
- jekyll-last-modified-at
pagination:
# Site-wide kill switch, disabled here it doesn't run at all
enabled: true
# Set to 'true' to enable pagination debugging. This can be enabled in the site config or only for individual pagination pages
debug: false
# The default document collection to paginate if nothing is specified ('posts' is default)
collection: all
# How many objects per paginated page, used to be `paginate` (default: 0, means all)
per_page: 5
# The permalink structure for the paginated pages (this can be any level deep)
permalink: '/page/:num/' # Pages are index.html inside this folder (default)
sort_field: 'date'
sort_reverse: true
autopages:
# Site-wide kill switch, disable here and it doesn't run at all
enabled: false
google_fonts: false
pivoine:
# Configure the order of complementary content on blog posts
post_addons: [about, newsletter, related, random, comments]
# Configure the order of complementary content on project pages
project_addons: [about, newsletter, other, comments]
# Set to `true` if you don't want to show an icon indicating external links
no_mark_external: true
# Set to `true` if third party plugins fail to work with dynamically loaded pages
no_push_state: false
# Set to `true` if you want to disable the drawer
no_drawer: false
# Set to `true` if you don't to use the auto-hiding (JavaScript based) navbar.
# Note that this will not hide the navbar completely, only replace it with a static one.
# Use custom css to hide completely, e.g. `#_navbar { display: none }`.
no_navbar: false
# Set to true to disable the built-in search functionality.
# Note that search is disabled during local use to save on build time.
# Run Jekyll with the `JEKYLL_ENV` environment variable set to `production` to enable.
no_search: false
# Set to `true` if you do not want parts of the css inlined in <head/>
# This will increase site build speed dramatically!
no_inline_css: false
# Set to `true` if you don't intend on changing the accent color on a per-page basis.
# This will increase site build speed!
no_page_style: false
# Code blocks and tables "break" the layout by spanning the full available width.
# Set this to true if you want them to be the same width as other content.
no_break_layout: true
# Set to `true` to disable the dynamic Table of Contents on large screens.
no_toc: false
# When set to `true`, will not extend the content in the "third column" on large screens.
# Instead, all content will remains within the center column.
# Note that this will not affect the Table of Contents, use `no_toc` instead.
no_third_column: false
# Set to `true` if you don't like oversized headlines on large screens.
no_large_headings: false
# Set to `true` if you do not want to expose your resume and projects
# in machine-readable formats.
no_structured_data: false
# You can set this to `true` if you don't want to set the `theme-color` meta tag,
# This only affects the meta tag, not the color specified in the app manifest.
no_theme_color: false
# Disable the breadcrumbs above the title
no_breadcrumbs: false
# Set to `true` when building with the `--lsi` option.
# The net effect is to use the Jekyll-provided `site.related_posts` variable.
use_lsi: true
# When using Google Analytics, set to `true` to display a cookie notice banner.
# When enabled, no user-related data will be stored until the user gives consent.
cookies_banner: false
# Set to `true` if you would like to add a "Powered by pivoine" link in the footer.
# Note that this setting has no effect when using the free version.
advertise: false
# Buyers of the PRO version can opt to hide all dates from the theme.
# Frequent consumers of online content will know that nothing devalues a post like
# seeing an old date.
hide_dates: false
# Similarly, showing last modified date can devalue a post if it is too far in the past.
hide_last_modified: true
# Note that dark mode only works in the PRO version of pivoine.
dark_mode:
# Set to `true` to always use the dark theme.
always: false
# Set to `true` to use the dark theme based on visitors' preference (OS setting).
dynamic: true
# Set to `true` to allow visitors to switch between light and dark mode.
icon: true
search:
icon: true
sound: true
umami:
script: https://umami.pivoine.art/script.js
id: 26158eb8-e0ae-4985-aa91-2f4a652f8ccb
# ⚡️ DANGER ZONE ⚡️
# ----------------
# This is an _experimental_ feature.
# Only use if you know what Service Workers are and how they can impact your site!
offline:
enabled: false
cache_version: 13
precache_assets:
- /assets/img/swipe.svg
optional_front_matter:
remove_originals: true
readme_index:
remove_originals: true
with_frontmatter: true
relative_links:
collections: true
titles_from_headings:
strip_title: true
collections: true
compress_html:
clippings: []
comments: []
endings: []
ignore:
envs: []
blanklines: false
profile: false
startings: []
sass:
style: compressed

31
_data/authors.yml Normal file
View File

@@ -0,0 +1,31 @@
# The primary author of this blog.
# Must be the same as `author` in `_config.yml`.
# Change `qwtel` to your shortname.
sk:
name: Valknar
email: valknark@pivoine.art
# Used at the bottom of each page and at the top of the `about` layout
# Markdown enabled, can use multiple paragraphs (enabled by `|`)
about: |
I am Valknar.
Life doesn't make sense without you...
# This photo will be used in the about section
picture:
path: /assets/img/me.webp
# srcset is optional, but can be used to provide higher res versions for retina displays
# srcset:
# 1x: /assets/img/me.webp
# 2x: /assets/img/me@2x.webp
# Social media icons in sidebar
# Comment/uncommet to show/hide
# Rearrange to change the order in which they appear
social:
# github: valknarogg
email: valknar@pivoine.art
twitter: bordeaux1981
sexy: https://sexy.pivoine.art
code: valknar

1500
_data/countries.yml Normal file

File diff suppressed because it is too large Load Diff

270
_data/social.yml Normal file
View File

@@ -0,0 +1,270 @@
# This file contains meta data for various social media sites
email:
name: Email
icon: icon-mail
prepend: 'mailto:'
amazon:
name: Amazon
icon: icon-amazon
google:
name: Google
icon: icon-google
google-plus:
name: Google+
icon: icon-google-plus
prepend: 'https://plus.google.com/'
google-drive:
name: Google Drive
icon: icon-google-drive
facebook:
name: Facebook
icon: icon-facebook
prepend: 'https://facebook.com/'
instagram:
name: Instagram
icon: icon-instagram
prepend: 'https://instagram.com/'
whatsapp:
name: WhatsApp
icon: icon-whatsapp
spotify:
name: Spotify
icon: icon-spotify
prepend: 'https://play.spotify.com/'
telegram:
name: Telegram
icon: icon-telegram
twitter:
name: X / Twitter
icon: icon-twitter
prepend: 'https://x.com/'
twitter-old:
name: Twitter
icon: icon-twitter-old
prepend: 'https://twitter.com/'
x:
name: X
icon: icon-twitter
prepend: 'https://x.com/'
vine:
name: Vine
icon: icon-vine
prepend: 'https://vine.com/'
vk:
name: VK
icon: icon-vk
prepend: 'https://vk.com/'
renren:
name: 人人网
icon: icon-renren
sina-weibo:
name: Sina Weibo
icon: icon-sina-weibo
rss:
name: RSS
icon: icon-rss2
youtube:
name: YouTube
icon: icon-youtube
prepend: 'https://www.youtube.com/channel/'
twitch:
name: Twitch
icon: icon-twitch
prepend: 'https://www.twitch.tv/'
vimeo:
name: Vimeo
icon: icon-vimeo
prepend: 'https://www.vimeo.com/'
lanyrd:
name: Lanyrd
icon: icon-lanyrd
flickr:
name: Flickr
icon: icon-flickr2
prepend: 'https://flickr.com/people/'
dribbble:
name: Dribbble
icon: icon-dribbble
prepend: 'https://dribbble.com/'
behance:
name: Behance
icon: icon-behance
prepend: 'https://www.behance.net/'
deviantart:
name: DeviantArt
icon: icon-deviantart
prepend: 'https://'
append: '.deviantart.com'
500px:
name: 500px
icon: icon-500px
prepend: 'https://500px.com/'
steam:
name: Steam
icon: icon-steam
prepend: 'http://steamcommunity.com/profiles/'
dropbox:
name: Dropbox
icon: icon-dropbox
ondrive:
name: OneDrive
icon: icon-ondrive
github:
name: GitHub
icon: icon-github
prepend: 'https://github.com/'
npm:
name: npm
icon: icon-npm
prepend: 'https://npmjs.com/~'
basecamp:
name: Basecamp
icon: icon-basecamp
trello:
name: Trello
icon: icon-trello
prepend: 'https://trello.com/'
wordpress:
name: WordPress
icon: icon-wordpress
joomla:
name: Joomla!
icon: icon-joomla
ello:
name: Ello
icon: icon-ello
prepend: 'https://ello.co/'
blogger:
name: Blogger
icon: icon-blogger
prepend: 'https://www.blogger.com/profile/'
tumblr:
name: Tumblr
icon: icon-tumblr
prepend: 'https://'
append: '.tumblr.com'
yahoo:
name: Yahoo
icon: icon-yahoo
soundcloud:
name: SoundCloud
icon: icon-soundcloud
prepend: 'https://soundcloud.com/'
skype:
name: Skype
icon: icon-skype
reddit:
name: reddit
icon: icon-reddit
prepend: 'https://www.reddit.com/user/'
hackernews:
name: Hacker News
icon: icon-hackernews
prepend: 'https://news.ycombinator.com/user?id='
wikipedia:
name: Wikipedia
icon: icon-wikipedia
linkedin:
name: LinkedIn
icon: icon-linkedin2
prepend: 'https://www.linkedin.com/in/'
lastfm:
name: Last.fm
icon: icon-lastfm
prepend: 'http://www.last.fm/user/'
delicious:
name: Delicious
icon: icon-delicious
prepend: 'http://del.icio.us/'
stumbleupon:
name: StumbleUpon
icon: icon-stumbleupon
prepend: 'http://www.stumbleupon.com/stumbler/'
stackoverflow:
name: Stack Overflow
icon: icon-stackoverflow
prepend: 'http://stackoverflow.com/users/'
pinterest:
name: Pinterest
icon: icon-pinterest2
prepend: 'https://www.pinterest.com/'
xing:
name: XING
icon: icon-xing2
prepend: 'https://www.xing.com/profile/'
flattr:
name: Flattr
icon: icon-flattr
foursquare:
name: Foursquare
icon: icon-foursquare
prepend: 'https://foursquare.com/user/'
yelp:
name: Yelp
icon: icon-yelp
prepend: 'https://www.yelp.com/user_details?userid='
paypal:
name: PayPal
icon: icon-paypal
prepend: 'https://www.paypal.me/'
keybase:
name: Keybase
icon: icon-key
prepend: 'https://keybase.io/'
download:
name: Download
icon: icon-box-add
signal:
name: Signal
icon: icon-signal
prepend: https://signal.group/
threads:
name: Threads
icon: icon-threads
prepend: https://threads.net/
playstation:
name: PlayStation
icon: icon-playstation
messenger:
name: Messenger
icon: icon-messenger
prepend: https://messenger.com/
stripe:
name: Stripe
icon: icon-stripe
slack:
name: Slack
icon: icon-slack
gitlab:
name: GitLab
icon: icon-gitlab
prepend: 'https://gitlab.com/'
line:
name: Line
icon: icon-line
prepend: https://line.me/
medium:
name: Medium
icon: icon-medium
prepend: 'https://medium.com/'
xbox:
name: Xbox
icon: icon-xbox
prepend: https://xbox.com/play/user/
wechat:
name: WeChat
icon: icon-wechat
discord:
name: Discord
icon: icon-discord
prepend: https://discord.com/invite/
mastodon:
name: Mastodon
icon: icon-mastodon
sexy:
name: sexy.pivoine.art
icon: gi gi-duality-mask
code:
name: code.pivoine.art
icon: gi gi-book-cover
prepend: https://code.pivoine.art/

64
_data/sounds.yml Normal file
View File

@@ -0,0 +1,64 @@
-
slug: hyperloop
artist: Valknar
title: Hyperloop
default: true
tracks:
- Hyperloop I.mp3
- Hyperloop II.mp3
- Hyperloop III.mp3
- Hyperloop IV.mp3
- Hyperloop IX.mp3
- Hyperloop V.mp3
- Hyperloop VI.mp3
- Hyperloop VII.mp3
- Hyperloop VIII.mp3
- Hyperloop X.mp3
- Hyperloop XI.mp3
- Hyperloop XII.mp3
- Hyperloop.mp3
-
slug: massive
artist: Valknar
title: Massive
tracks:
- Massive.mp3
- Sudden Death.mp3
- Strength And Honour.mp3
- Drums Of War.mp3
- Drums Of War (Remix).mp3
- Massive (Remix).mp3
- The Moon (Remix).mp3
-
slug: pass-the-glock
artist: Terror Squad
title: Pass The Glock
tracks:
- Pass The Glock - Bulletproof Remix.mp3
-
slug: detroit
artist: Valknar
title: Detroit
tracks:
- Detroit I.mp3
- Detroit II.mp3
- Detroit III.mp3
- Detroit IV.mp3
- Detroit V.mp3
- Detroit VI.mp3
- Detroit VII.mp3
- Detroit VIII.mp3
-
slug: stuttgart
artist: Valknar
title: Stuttgart
tracks:
- Stuttgart.mp3
- Stuttgart (Remix).mp3
-
slug: changed-her-mind-again
artist: Cold Stares
title: Changed Her Mind Again... - Breaks Remix
tracks:
- Changed Her Mind Again... - Breaks Remix.mp3

154
_data/strings.yml Normal file
View File

@@ -0,0 +1,154 @@
# Strings
# =======================================================================================
# This file contains all soft-coded strings of the theme.
# You can edit them here if you want to change the wording of certain phrases,
# e.g. maybe you prefer "Fetching…" to "Loading…", etc.
#
# You can also use this to "translate" the theme into different langauges,
# but note that this may not work in all situations,
# especially for languages that differ significantly from English.
# Free version
# ---------------------------------------------------------------------------------------
home: Home
pages: Pages
posts: Posts
about: About
related_posts: Related Posts
other_projects: Other Projects
comments: Comments
jump_to: Jump to
navigation: Navigation
social: Social
links: Links
links_icon: 'icon-link'
pagination: Pagination
newer: Newer
older: Older
forward: Forward
back: Back
permalink: Permalink
permalink_icon: 'content-hash'
templates: Templates (for web app)
present: present
toc: Table of Contents
note: Note
description: Description
search: Search
search_placeholder: Type something…
loading: Loading…
redirecting: Redirecting…
download: Download
continue_reading: >
Continue reading <!--post_title-->
cookies_banner:
text: >
This site uses cookies. [Cookies Policy](/cookies-policy/).
okay: Okay
not_found:
title: '404: Page Not Found'
message: |
Sorry, we've misplaced that URL or it's pointing to something that doesn't exist.
<!--home_link--> to try finding it again.
home_link: Head back home
offline:
title: 'You are Offline'
message: |
Sorry, but you appear to be offline and this page has not been prepared for offline viewing.
<!--home_link--> to try finding another page.
home_link: Head back home
error:
title: Error
message: |
Sorry, an error occurred while loading <!--link-->.
# Separators
# ---------------------------------------------------------------------------------------
posted: 'Posted'
last_modified_at: 'Last modified at'
last_modified_at_icon: 'icon-history'
colon: ':'
separator: '|'
category_start: 'in '
category_spearator: ' / '
tag_start: 'on '
tag_separator: ', '
from_to_separator: ''
breadcrumbs_home: 'home'
breadcrumbs_separator: '/'
# Date formats
# ---------------------------------------------------------------------------------------
date_formats:
post: '%d %b %Y'
last_modified_at: '%Y-%m-%d'
related_post: '%d %b %Y'
list_group_by: '%Y'
list_entry: '%d %b'
project: '%Y'
projects_group_by: '%Y'
resume: '%b %Y'
# PRO Version
# ---------------------------------------------------------------------------------------
# If you don't own the PRO version of pivoine, modifying these will have no effect.
welcome:
more_projects: See <!--projects_title--> for more
more_posts: See <!--posts_title--> for more
resume:
location: Location
location_separator: ', '
email: Email
phone: Phone
website: Website
work: Experience
work_title: <!--position--> at <!--company-->
highlights: Highlights
volunteer: Volunteer
volunteer_title: <!--position--> at <!--organization-->
education: Education
education_title: <!--study_type--> in <!--area--> from <!--institution--> with GPA of <!--gpa-->
courses: Courses
awards: Awards
awards_title: <!--title--> from <!--awarder-->
publications: Publications
publications_title: <!--name--> by <!--publisher-->
references: References
languages: Languages
fluency: Fluency
skills: Skills
level: Level
interests: Interests
summary: Summary
keywords: Keywords
print: Print
download_pdf: PDF
download_vcf: vCard
download_json: JSON
icons:
fallback: icon-link
location: icon-location
born: icon-quill
citizenship: icon-flag
maritalStatus: icon-man-woman
email: icon-envelop
phone: icon-phone
website: icon-home3
work: icon-briefcase
volunteer: icon-earth
education: icon-library
awards: icon-trophy
publications: icon-book
references: icon-quotes-right
languages: icon-bubbles
skills: icon-wrench
interests: icon-heart

26
_data/variables.yml Normal file
View File

@@ -0,0 +1,26 @@
---
root_font_size: 15 #px
root_font_size_medium: 16 #px
root_font_size_large: 17 #px
root_font_size_print: 8 #pt
root_line_height: 1.75
font_weight: 400 #=normal
font_weight_bold: 700 #=bold
font_weight_heading: 900 #=black (heavy)
content_width: 42 #rem
content_width_2: 48 #rem
content_width_5: 54 #rem
sidebar_width: 21 #rem
border_radius: 0.5 #rem
break_point_1: 42 #em
break_point_2: 54 #em
break_point_3: 64 #em
break_point_4: 72 #em
break_point_5: 86 #em
break_point_font_large: 124 #em

9
_includes/base-classes Normal file
View File

@@ -0,0 +1,9 @@
{%- capture classes -%}
{% if site.pivoine.dark_mode.always %}dark-mode{% endif %}
{% if site.pivoine.no_toc %} no-toc{% endif %}
{% if site.pivoine.no_break_layout %} no-break-layout{% endif %}
{% if site.pivoine.no_large_headings %} no-large-headings{% endif %}
{% if site.pivoine.no_third_column %} no-third-column{% endif %}
{% if site.pivoine.no_drawer %} no-drawer{% endif %}
{%- endcapture -%}
{{- classes | strip_newlines | strip -}}

View File

@@ -0,0 +1,43 @@
{% if site.google_analytics %}
<script>!function(w, d) {
w.ga=w.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
/*{% if site.pivoine.cookies_banner %}*/
if (navigator.CookiesOK) {
ga('create', '{{ site.google_analytics }}', 'auto');
} else if (d.cookie.indexOf("hy--cookies-ok=true") > -1) {
ga('create', '{{ site.google_analytics }}', {
'storage': 'none',
'clientId': localStorage ? localStorage.getItem('ga--client-id') : undefined
});
} else {
ga('create', '{{ site.google_analytics }}', {
'storage': 'none'
});
ga('set', 'forceSSL', true);
ga('set', 'anonymizeIp', true);
}
/*{% else %}*/
ga('create', '{{ site.google_analytics }}', 'auto');
/*{% endif %}*/
var pushStateEl = d.getElementById('_pushState');
var timeoutId;
pushStateEl.addEventListener('hy-push-state-load', function() {
w.clearTimeout(timeoutId);
timeoutId = w.setTimeout(function() {
ga('set', 'page', w.location.pathname);
ga('send', 'pageview');
}, 500);
});
d.addEventListener('hy--cookies-ok', function () {
w.ga(function(tracker) {
w.ga("set", "anonymizeIp", undefined);
localStorage && localStorage.setItem("ga--client-id", tracker.get("clientId"));
});
});
w.loadJSDeferred('https://www.google-analytics.com/analytics.js');
}(window, document);</script>
{% endif %}

View File

@@ -0,0 +1,22 @@
<nav id="breadcrumbs" class="screen-only"><ul>
{% assign crumbs = include.url | remove:'index.html' | split: '/' %}
{% if crumbs.size > 1 %}
<li><a href="{{ '/' | relative_url }}">{{ site.data.strings.breadcrumbs_home | default:'home' }}</a></li>
{% for crumb in crumbs offset: 1 %}
{%- if crumb != "page" -%}
<li>
{% unless forloop.last %}
<span>{{ site.data.strings.breadcrumbs_separator | default:'/' }}</span>
{% assign crumb_limit = forloop.index | plus: 1 %}
{% capture href %}{% for crumb1 in crumbs limit: crumb_limit %}{{ crumb1 | append: '/' }}{% endfor %}{% endcapture %}
<a href="{{ href | relative_url }}">{{ crumb | url_decode }}</a>
{% else %}
<span>{{ site.data.strings.breadcrumbs_separator | default:'/' }}</span>
<span>{{ crumbs | reverse | first | url_decode }}</span>
{% endunless %}
</li>
{%- endif -%}
{% endfor %}
{% endif %}
</ul></nav>

View File

@@ -0,0 +1,6 @@
{% if page.comments %}
<aside class="comments related" role="complementary">
<h2 class="hr-bottom">{{ site.data.strings.comments | default:"Comments" }}</h2>
{% include my-comments.html %}
</aside>
{% endif %}

View File

@@ -0,0 +1,18 @@
{% if site.copyright.size > 0 or site.legal.size > 0 or site.pivoine.advertise %}
<footer class="content" role="contentinfo">
<hr/>
{% if site.copyright.size > 0 %}
<p><small class="copyright">{{ site.copyright | markdownify | replace:'<p>','' | replace:'</p>','' }}</small></p>
{% endif %}
{% if site.legal.size > 0 %}
<nav class="legal"><small>
{% for node in site.legal %}
{% assign url = node.url | default: node.href %}
<a class="heading flip-title" href="{% include_cached smart-url url=url %}">{{ node.name | default:node.title }}</a>
{% unless forloop.last %}{{ site.data.strings.separator | default:'|' }}{% endunless %}
{% endfor %}
</small></nav>
{% endif %}
<hr class="sr-only"/>
</footer>
{% endif %}

23
_includes/body/index.html Normal file
View File

@@ -0,0 +1,23 @@
{% include_cached components/dark-mode.html %}
<hy-push-state
id="_pushState"
replace-selector="#_main"
link-selector="a[href]:not([href^='{{ assets_url }}']):not(.external):not(.no-push-state)"
script-selector="script"
duration="500"
hashchange
>
{% capture sidebar %}{% include_cached body/sidebar.html cover=page.cover invert=page.invert_sidebar theme_color=page.theme_color image=image color=color video=video %}{% endcapture %}
{% if page.cover %}{{ sidebar }}{% endif %}
{% include_cached body/menu.html %}
{% include body/main.html %}
{% unless page.cover %}{{ sidebar }}{% endunless %}
</hy-push-state>
{% unless page.redirect %}
{% include_cached body/scripts.html %}
{% include my-body.html %}
{% endunless %}
{% include_cached templates/index.html %}

9
_includes/body/main.html Normal file
View File

@@ -0,0 +1,9 @@
<main
id="_main"
class="content layout-{{ page.layout }}"
role="main"
>
{% unless site.pivoine.no_breadcrumbs %}{% include body/breadcrumbs.html url=page.url %}{% endunless %}
{{ content }}
{% include_cached body/footer.html %}
</main>

18
_includes/body/menu.html Normal file
View File

@@ -0,0 +1,18 @@
<div id="_navbar" class="navbar fixed-top">
<div class="content">
<span class="sr-only">{{ site.data.strings.jump_to | default:"Jump to" }}{{ site.data.strings.colon | default:":" }}</span>
<div class="nav-btn-bar">
<a id="_menu" class="nav-btn no-hover" href="#_drawer--opened">
<span class="sr-only">{{ site.data.strings.navigation | default:"Navigation" }}</span>
<span class="icon-menu"></span>
</a>
<div class="nav-insert-marker"></div>
{% if site.pivoine.search.icon %}
<hm-search class="nav-span" hidden placeholder="{{ site.data.strings.search | default:"Search" }}"></hm-search>
{% elsif site.pivoine.search.always %}
<hm-search class="nav-span" placeholder="{{ site.data.strings.search | default:"Search" }}"></hm-search>
{% endif %}
</div>
</div>
</div>
<hr class="sr-only" hidden />

41
_includes/body/nav.html Normal file
View File

@@ -0,0 +1,41 @@
<span class="sr-only">{{ site.data.strings.navigation | default:"Navigation" }}{{ site.data.strings.colon | default:":" }}</span>
<ul>
{% if site.menu %}
{% for node in site.menu %}
{% assign url = node.url | default: node.href %}
<li>
<a
{% if forloop.first %}id="_drawer--opened"{% endif %}
href="{% include_cached smart-url url=url %}"
class="sidebar-nav-item {% if node.external %}external{% endif %}"
{% if node.rel %}rel="{{ node.rel }}"{% endif %}
>
{{ node.name | default:node.title }}
</a>
</li>
{% endfor %}
{% else %}
{% assign pages = site.html_pages | where: "menu", true %}
{% assign documents = site.documents | where: "menu", true %}
{% assign nodes = pages | concat: documents | sort: "order" %}
{% for node in nodes %}
{% unless node.redirect_to %}
<li>
<a
{% if forloop.first %}id="_navigation"{% endif %}
href="{{ node.url | relative_url }}"
class="sidebar-nav-item"
{% if node.rel %}rel="{{ node.rel }}"{% endif %}
>
{{ node.title }}
</a>
</li>
{% else %}
<li>
<a href="{{ node.redirect_to }}" class="sidebar-nav-item external">{{ node.title }}</a>
</li>
{% endunless %}
{% endfor %}
{% endif %}
</ul>

View File

@@ -0,0 +1,7 @@
<!--[if gt IE 10]><!---->
<script nomodule>{% include scripts/nomodule.min.js %}</script>
<script src="{{ '/assets/js/pivoine-1.0.0.js' | relative_url }}" type="module"></script>
<script src="{{ '/assets/js/LEGACY-pivoine-1.0.0.js' | relative_url }}" nomodule defer></script>
{% include my-scripts.html %}
{% include body/analytics.html %}
<!--<![endif]-->

View File

@@ -0,0 +1,21 @@
{% assign image = include.image %}
{% assign video = include.video %}
{% assign color = include.color %}
{% assign theme_color = include.theme_color %}
{% if image.background %}
{% capture bg_style %}background:{{ image.style }}{% endcapture %}
{% capture bg_class %}sidebar-bg {% if image.overlay %}sidebar-overlay{% endif %}{% endcapture %}
{% else %}
{% assign bg_color = theme_color | default:site.theme_color | default:color %}
{% capture bg_style %}background-color:{{ bg_color }};{% if image != 'none' %}background-image:url({% include_cached smart-url url=image %}){% endif %};overflow: hidden;{% endcapture %}
{% capture bg_class %}sidebar-bg {% if image != 'none' %}sidebar-overlay{% endif %}{% endcapture %}
{% endif %}
<div class="{{ bg_class }}" style="{{ bg_style }}">
{% if video %}
<video id="_video" style="min-width: 100%; height: 100%; object-fit: cover" autoplay muted loop>
<source src="{% include_cached smart-url url=video %}" type="video/mp4">
</video>
{% endif %}
</div>

View File

@@ -0,0 +1,33 @@
<div class="sidebar-sticky">
<div class="sidebar-about">
{% if site.logo %}
<a class="no-hover" href="{{ '/' | relative_url }}" tabindex="-1">
<img src="{% include_cached smart-url url=site.logo %}" class="logo" alt="{{ site.short_title | default:site.title }}" width="120" height="120" loading="lazy" />
</a>
{% endif %}
<a class="sidebar-title" href="{{ '/' | relative_url }}"><h2 class="h1">{{ site.short_title | default:site.title }}</h2></a>
{% assign text = site.tagline | default:site.description %}
{% if text %}
<p class="{% if text.size > 100 %}fine{% endif %}">
{{ text | markdownify | replace:'<p>','' | replace:'</p>','' }}
</p>
{% endif %}
</div>
<nav class="sidebar-nav heading" role="navigation">
{% include body/nav.html %}
</nav>
{% assign author = site.data.authors.first[1] | default:site.author %}
<div class="sidebar-social">
{% include components/social.html author=author %}
</div>
{% if site.pivoine.sound %}
{% assign sounds = site.data.sounds | where: 'default', true %}
<div class="sound-player" data-featured="{{ sounds.first | jsonify | escape }}">
<canvas class="hidden"></canvas>
<a>[ SOUND ON ]</a>
<audio hidden controls="false"></audio>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,14 @@
<hy-drawer
id="_drawer"
class="{% if include.cover %}cover{% endif %}"
side="left"
threshold="10"
noscroll
{% if include.cover %}opened{% endif %}
>
<header id="_sidebar" class="sidebar{% if include.invert %} invert{% endif %}" role="banner">
{% include_cached body/sidebar-bg.html image=include.image color=include.color theme_color=include.theme_color video=include.video %}
{% include_cached body/sidebar-sticky.html %}
</header>
</hy-drawer>
<hr class="sr-only" hidden />

View File

@@ -0,0 +1,8 @@
{% assign author = site.data.authors[include.author] | default:site.data.authors.first[1] | default:site.author %}
{% if author.about %}
<aside class="about related mt4 mb4" role="complementary">
{% assign about_heading = site.data.strings.about | default:"About" %}
{% include components/author.html author=author heading=about_heading heading_tag='h2' %}
</aside>
{% endif %}

View File

@@ -0,0 +1,24 @@
{% assign plugins = site.plugins | default:site.gems %}
<div class="author mt4">
{% assign author = include.author %}
{% if author.picture %}
{% include_cached components/hy-img.html class="avatar" img=author.picture alt=author.name width="120" height="120" %}
{% elsif plugins contains 'jekyll-avatar' %}
{% assign avatar = author.social.github | default:author.github.username | default:author.github %}
{% include components/avatar-tag.html user=avatar %}
{% endif %}
{% assign heading_tag = include.heading_tag | default:'h2' %}
{% assign heading_id = include.heading_id %}
<{{ heading_tag }} {% if heading_id %}id="{{ heading_id }}"{% endif %} class="page-title hr-bottom">
{{ include.heading | default:author.name }}
</{{ heading_tag }}>
{{ author.about | markdownify }}
<div class="sidebar-social">
{% include components/social.html author=author %}
</div>
</div>

View File

@@ -0,0 +1,4 @@
{% comment %}<!--
Including `avatar` in a partial prevents a parse error when `jekyll-avatar` is not included.
-->{% endcomment %}
{% avatar user=include.user size=128 %}

View File

@@ -0,0 +1,7 @@
{% if site.pivoine.dark_mode.dynamic %}
<script>
window._sunrise = {{ site.pivoine.dark_mode.sunrise | default:"6" }};
window._sunset = {{ site.pivoine.dark_mode.sunset | default:"18" }};
{% include scripts/dark-mode.min.js %}
</script>
{% endif %}

View File

@@ -0,0 +1,6 @@
{% if site.clap_button %}
<clap-button class="mb6" {% if include.hidden == true %}hidden{% endif %}></clap-button>
{% if include.hidden == true %}<hr class="dingbat related mb6" />{% endif %}
{% else %}
<hr class="dingbat related mb6" />
{% endif %}

View File

@@ -0,0 +1,29 @@
{% assign sources = '' %}
{% if include.img.src or include.img.path %}
{% assign srcset = null %}
{% if include.img.srcset %}
{% capture srcset %}{% for hash in include.img.srcset %}{% assign tmp = hash[1] %}{% include_cached smart-url url=tmp %} {{ hash[0] }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% endif %}
{% assign src = include.img.src | default:include.img.path %}
{% capture sources %}
src="{% include_cached smart-url url=src %}"
{% if srcset %}srcset="{{ srcset | strip }}"{% endif %}
{% if include.sizes %}sizes="{{ include.sizes | replace:' ', '' }}"{% endif %}
{% endcapture %}
{% else %}
{% capture sources %}
src="{% include_cached smart-url url=include.img %}"
{% endcapture %}
{% endif %}
<img
{{ sources }}
{% if include.alt %}alt="{{ include.alt }}"{% endif %}
{% if include.class %}class="{{ include.class }}"{% endif %}
{% if include.property %}property="{{ include.property }}"{% endif %}
{% if include.width %}width="{{ include.width }}"{% endif %}
{% if include.height %}height="{{ include.height }}"{% endif %}
{% if include.width and include.height %}loading="lazy"{% endif %}
/>

View File

@@ -0,0 +1,3 @@
{% assign src = include.iframe.src | default:include.iframe.path %}
<iframe src="{% include_cached smart-url url=src %}" width="100%" frameborder="no" scrolling="no" allowfullscreen="true" style="width: 100%; aspect-ratio: calc(16/9);"></iframe>

View File

@@ -0,0 +1,12 @@
{% if include.href.size > 0 %}
<a
class="{{ include.class }} {{ include.a_class }}"
href="{{ include.href }}"
{% if include.rel %}rel="{{ include.rel }}"{% endif %}
{% if include.property %}property="{{ include.property }}"{% endif %}
>
{{- include.title -}}
</a>
{% else %}
<span class="{{ include.class }} {{ include.span_class }}">{{ include.title }}</span>
{% endif %}

View File

@@ -0,0 +1,14 @@
{% assign alt = include.alt %}
{% unless alt %}{% capture alt %}<div class="hr pb0"></div>{% endcapture %}{% endunless %}
{% if include.text.size > 0 %}
{% unless include.hide %}
<p class="{{ include.class | default:'note-sm' }}" {% if include.property %}property="{{ include.property }}"{% endif %}>
{{ include.text | markdownify | replace:"<p>","" | replace:"</p>","" }}
</p>
{% else %}
{{ alt }}
{% endunless %}
{% else %}
{{ alt }}
{% endif %}

View File

@@ -0,0 +1,15 @@
<h2 class="sr-only">{{ site.data.strings.pagination | default:"Pagination" }}</h2>
<nav class="pagination heading clearfix" role="navigation">
<ul>
<li class="pagination-item older" >
{% assign next_title = site.data.strings.older | default:"Older" %}
{% assign next_href = paginator.next_page_path | relative_url %}
{% include components/link.html rel="next" title=next_title href=next_href %}
</li>
<li class="pagination-item newer" >
{% assign prev_title = site.data.strings.newer | default:"Newer" %}
{% assign prev_href = paginator.previous_page_path | relative_url %}
{% include components/link.html rel="prev" title=prev_title href=prev_href %}
</li>
</ul>
</nav>

View File

@@ -0,0 +1,7 @@
{% assign post = include.post %}
{% assign format = include.format | default:site.data.strings.date_formats.related_post | default:"%d %b %Y" %}
<li class="h4">
<a href="{{ post.url | relative_url }}" class="flip-title"><span>{{ post.title }}</span></a>
<time class="faded fine" datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date:format }}</time>
</li>

View File

@@ -0,0 +1,102 @@
{% assign post = include.post %} {% assign no_link_title = include.no_link_title %} {% assign no_excerpt =
include.no_excerpt %} {% assign hide_image = include.hide_image %} {% assign hide_description = include.hide_description
%} {% assign show_gallery = include.show_gallery %} {% assign show_video = include.show_video %} {% assign show_iframe =
include.show_iframe %} {% assign hide_dates = include.hide_dates | default:site.pivoine.hide_dates %} {% assign
hide_posted_in = include.hide_posted_in %}
<article id="post{{ post.id | replace:'/','-' }}" class="page post mb6" role="article">
<header>
<h1 class="post-title flip-project-title">
{% unless no_link_title %}<a href="{{ post.url | relative_url }}" class="flip-title"
>{% endunless %} {{ post.title }} {% unless no_link_title %}</a
>{% endunless %}
</h1>
{%- unless hide_posted_in -%}
<div class="post-date">
{% capture foobar %} {%- unless hide_dates -%} {%- assign post_format = site.data.strings.date_formats.post |
default:"%d %b %Y" -%}
<time datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date:post_format }}</time>
{%- else -%} {{- site.data.strings.posted | default:"Posted" -}} {%- endunless -%} {{ ' ' }} {%- assign
category_start = site.data.strings.category_start | default:"in " -%} {%- assign category_separator =
site.data.strings.category_separator | default:" / " -%} {%- include components/tag-list.html tags=post.categories
meta=site.featured_categories start_with=category_start separator=category_separator -%} {{ ' ' }} {%- assign
tag_start = site.data.strings.tag_start | default:"on " -%} {%- assign tag_separator =
site.data.strings.tag_separator | default:", " -%} {%- include components/tag-list.html tags=post.tags
meta=site.featured_tags start_with=tag_start separator=tag_separator -%} {% endcapture %}
<span class="ellipsis mr1"> {{ foobar }} </span>
{% unless hide_dates or site.pivoine.hide_last_modified or post.hide_last_modified %} {% if post.last_modified_at
%} {% assign d1 = post.date | date:"%Y-%m-%d" %} {% assign d2 = post.last_modified_at | date:"%Y-%m-%d" %} {% if
d1 != d2 %} {% assign label = site.data.strings.last_modified_at | default:"Last modified at" %} {% assign
last_modified_at_format = site.data.strings.date_formats.last_modified_at | default:"%Y-%m-%d" %}
<span
class="ellipsis"
data-tippy-content="{{ label }}{{ site.data.strings.colon }} {{ post.last_modified_at | date:post_format }}"
>
<span class="sr-only">{{ label }}{{ site.data.strings.colon }}</span>
<span class="{{ site.data.strings.last_modified_icon | default:'icon-history' }}"></span>
<time datetime="{{ post.last_modified_at | date_to_xmlschema }}"
>{{ post.last_modified_at | date:last_modified_at_format }}</time
>
</span>
{% endif %} {% endif %} {% endunless %}
</div>
{%- endunless -%} {% assign alt = false %} {% unless hide_image %}{% if post.image %} {% unless no_link_title %}<a
href="{{ post.url | relative_url }}"
class="no-hover no-print-link {% unless post.hide_image %}flip-project{% endunless %}"
tabindex="-1"
>{% endunless %}
<div class="img-wrapper lead aspect-ratio flip-project-img {% unless no_link_title %}sixteen-nine{% endunless %}">
<div>
{% if show_video %} {% include_cached components/video.html video=post.video poster=post.image %} {% elsif
show_iframe %} {% include_cached components/iframe.html iframe=post.iframe %} {% else %} {% include_cached
components/hy-img.html img=post.image alt=post.title %} {% endif %}
</div>
</div>
{% unless no_link_title %}</a
>{% endunless %} {% assign alt = '' %} {% endif %}{% endunless %} {% include components/message.html
text=post.description hide=hide_description alt=alt %}
</header>
{% if no_excerpt %} {% if post.gallery %}
<div class="gallery-wrapper lead">
{% for image in post.gallery %}
<a class="gallery-item" href="{{ image | relative_url }}" data-fslightbox target="_blank">
{% if post.video_gallery %} {% assign i = image | replace: "mp4", "webp" %} {% include_cached
components/hy-img.html img=i alt=post.title %} {% else %} {% include_cached components/hy-img.html img=image
alt=post.title %} {% endif %}
</a>
{% endfor %}
</div>
{% endif %} {% if post.sound %} {% assign sounds = site.data.sounds | where: 'slug', page.sound %}
<div class="sound-wrapper" data-featured="{{ sounds.first | jsonify | escape }}">
<table class="stretch-table dl-table">
<tbody>
{% for track in sounds.first.tracks %}
<tr>
<td style="text-align: left">
<a class="sound-item" href="/assets/sounds/{{ page.sound }}/{{ track }}" target="_blank">
{{ sounds.first.artist }} - {{ track | replace: ".mp3", "" }}
</a>
</td>
<td style="text-align: right">
<a download href="/assets/sounds/{{ page.sound }}/{{ track }}" target="_blank">
{{ site.data.strings.download }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %} {{ post.content }} {% else %} {% capture post_title %}<a
class="heading flip-title"
href="{{ post.url | relative_url }}"
>{{ post.title }}</a
>{% endcapture %} {% assign text = site.data.strings.continue_reading | default:"Continue reading
<!--post_title-->" %}
<footer>
<p class="read-more">{{ text | replace:"<!--post_title-->", post_title }}</p>
</footer>
{% endif %}
</article>

View File

@@ -0,0 +1,35 @@
{% assign post = page %}
{% if page.related_posts %}
{% if major >= 4 and minor >= 1 %}
{% assign related_posts = site.posts | where_exp:"post", "page.related_posts contains post.path or page.related_posts contains post.url" %}
{% else %}
{% assign related_posts_1 = site.posts | where_exp:"post", "page.related_posts contains post.path" %}
{% assign related_posts_2 = site.posts | where_exp:"post", "page.related_posts contains post.url" %}
{% assign related_posts = related_posts_1 | concat:related_posts_2 %}
{% endif %}
{% elsif site.pivoine.use_lsi or site.use_lsi %}
{% assign related_posts = site.related_posts %}
{% elsif post.categories.first %}
{% assign related_posts = site.categories[post.categories.first] | where_exp:"post", "post.url != page.url" %}
{% elsif post.tags.first %}
{% assign related_posts = site.tags[post.tags.first] | where_exp:"post", "post.url != page.url" %}
{% else %}
{% assign related_posts = site.related_posts %}
{% endif %}
{% if related_posts.size > 0 %}
<aside class="related mb4" role="complementary">
<h2 class="hr-bottom">{{ site.data.strings.related_posts | default:"Related Posts" }}</h2>
<ul class="related-posts">
{% for post in related_posts limit:3 %}
{% if post %}
{% include_cached components/post-list-item.html post=post %}
{% else %}
<li>Post with path <code>{{ post_path }}</code> not found.</li>
{% endif %}
{% endfor %}
</ul>
</aside>
{% endif %}

View File

@@ -0,0 +1,40 @@
{% assign platform = include.platform | downcase %}
{% assign username = include.username %}
{% if username.size > 0 %}
{% assign = data_social = site.data.social[platform] | default:site.data_social[platform] %}
{% assign name = data_social.name | default:include.platform %}
{% assign icon = data_social.icon | default:'icon-link' %}
{% assign app = data_social.append %}
{% assign prep = data_social.prepend %}
{% unless data_social %}
{% if platform == "email" %}
{% assign name = "Email" %}
{% assign icon = "icon-mail" %}
{% assign prep = "mailto:" %}
{% elsif platform == "twitter" %}
{% assign name = "Twitter" %}
{% assign icon = "icon-twitter" %}
{% assign prep = "https://twitter.com/" %}
{% elsif platform == "github" %}
{% assign name = "GitHub" %}
{% assign icon = "icon-github" %}
{% assign prep = "https://github.com/" %}
{% endif %}
{% endunless %}
{% if username contains "//" or username contains "mailto:" %}
{% assign url = username %}
{% else %}
{% assign url = username | prepend:prep | append:app %}
{% endif %}
<li>
<a href="{{ url }}" title="{{ name }}" class="no-mark-external">
<span class="{{ icon }}"></span>
<span class="sr-only">{{ name }}</span>
</a>
</li>
{% endif %}

View File

@@ -0,0 +1,23 @@
<span class="sr-only">{{ site.data.strings.social | default:"Social" }}{{ site.data.strings.colon }}</span>
<ul>
{% if include.author.social %}
{% for link in include.author.social %}
{% include components/social-list-item.html platform=link.first username=link.last %}
{% endfor %}
{% else %}
{% assign twitter_username = author.twitter.username | default: author.twitter | default:site.twitter.username | default:site.twitter | default:site.twitter_username %}
{% if twitter_username %}
{% include components/social-list-item.html platform="twitter" username=twitter_username %}
{% endif %}
{% assign github_username = author.github.username | default: author.github | default:site.github.username | default:site.github | default:site.github_username %}
{% if github_username %}
{% include components/social-list-item.html platform="github" username=github_username %}
{% endif %}
{% assign email = author.email | default: site.email %}
{% if email %}
{% include components/social-list-item.html platform="email" username=email %}
{% endif %}
{% endif %}
</ul>

View File

@@ -0,0 +1,30 @@
{%- assign tags = include.tags -%}
{%- assign meta = include.meta -%}
{%- assign start_with = include.start_with -%}
{%- assign separator = include.separator -%}
{%- assign end_with = include.end_with -%}
{%- assign content = '' -%}
{%- if tags.size > 0 -%}
{%- assign content = start_with -%}
{%- for tag_slug in tags -%}
{%- capture iter_separator -%}{% if forloop.last %}{{ end_with }}{% else %}{{ separator }}{% endif %}{%- endcapture -%}
{%- if major >= 4 and minor >= 1 %}
{%- assign tag = meta | find: "slug", tag_slug -%}
{%- else -%}
{%- assign tag = meta | where: "slug", tag_slug | first -%}
{%- endif -%}
{%- if tag -%}
{%- capture content_temp -%}{{ content }}<a href="{{ tag.url | relative_url }}" class="flip-title">{{ tag.title }}</a>{{ iter_separator }}{%- endcapture -%}
{%- else -%}
{%- capture content_temp -%}{{ content }}<span>{{ tag_slug | capitalize }}</span>{{ iter_separator }}{%- endcapture -%}
{%- endif -%}
{%- assign content = content_temp -%}
{%- endfor -%}
{%- endif -%}
{{- content -}}

View File

@@ -0,0 +1,31 @@
{% assign src = include.video.src | include.video.path | default:include.video %}
{% assign poster = include.video.poster | default:include.poster %}
<media-controller>
<video
slot="media"
src="{% include_cached smart-url url=src %}"
{% if poster %}poster="{% include_cached smart-url url=poster %}"{% endif %}
{% if include.video.class %}class="{{ include.video.class }}"{% endif %}
{% if include.video.property %}property="{{ include.video.property }}"{% endif %}
{% if include.video.width %}width="{{ include.video.width }}"{% endif %}
{% if include.video.height %}height="{{ include.video.height }}"{% endif %}
{% if include.video.width and include.video.height %}loading="lazy"{% endif %}
{% if include.video.autoplay %}autoplay{% endif %}
{% if include.video.loop %}loop{% endif %}
{% if include.video.muted %}muted{% endif %}
{% if include.video.preload %}preload="{{ include.video.preload }}"{% endif %}
{% if include.video.crossorigin %}crossorigin="{{ include.video.crossorigin }}"{% endif %}
></video>
<media-loading-indicator slot="centered-chrome" noautohide></media-loading-indicator>
<media-control-bar>
<media-play-button></media-play-button>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<media-time-display></media-time-display>
<media-time-range></media-time-range>
<media-duration-display></media-duration-display>
<media-playback-rate-button></media-playback-rate-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>

View File

@@ -0,0 +1 @@
{% capture to_scssify %}{% include styles/inline.scss %}{% endcapture %}{{ to_scssify | scssify }}

View File

@@ -0,0 +1 @@
{% feed_meta %}

15
_includes/head/index.html Normal file
View File

@@ -0,0 +1,15 @@
{% assign google_fonts = site.google_fonts %}
{% assign font_heading = site.font_heading %}
{% assign font = site.font %}
{% include head/meta.html %}
{% include_cached head/meta-static.html %}
{% include head/links.html lang=page.lang %}
{% include_cached head/links-static.html %}
{% include_cached head/scripts.html %}
{% include_cached head/styles.html layout=page.layout color=color theme_color=theme_color %}
{% include my-head.html %}

View File

@@ -0,0 +1,30 @@
{% if plugins contains 'jekyll-feed' %}{% include head/feed-tag.html %}{% endif %}
<link rel="icon" type="image/png" href="/assets/icons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg" />
<link rel="shortcut icon" href="/assets/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
<link rel="manifest" href="/assets/icons/site.webmanifest" />
{% if site.google_fonts %}
{%- capture google_fonts_url %}{{ site.google_fonts_url | default:'https://fonts.googleapis.com' }}{%- endcapture -%}
<link rel="dns-prefetch" href="{{ google_fonts_url }}" />
{%- if google_fonts_url == 'https://fonts.googleapis.com' -%}
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
{%- endif -%}
{% endif %}
{% if site.google_analytics %}
<link rel="dns-prefetch" href="https://www.google-analytics.com" />
{% endif %}
{% if site.kramdown.math_engine == 'katex' %}
{% capture katex_url %}{{ 'assets/bower_components/katex/dist/katex.min.css' | relative_url }}{% endcapture %}
<link rel="dns-prefetch" href="{{ katex_url }}" id="_katexPreload" />
<noscript><link rel="stylesheet" href="{{ katex_url }}"></noscript>
{% endif %}
{% assign disqus = site.disqus | default:site.disqus_shortname %}
{% if disqus %}
<link rel="dns-prefetch" href="https://{{ disqus }}.disqus.com" id="_hrefDisqus" />
{% endif %}

View File

@@ -0,0 +1,2 @@
{% assign lang = include.lang | default:site.lang | default:'en' %}
<link rel="alternate" href="{{ page.url | absolute_url }}" hreflang="{{ lang | downcase | replace:'_','-' }}" />

View File

@@ -0,0 +1,13 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="{{ site.title }}" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="application-name" content="{{ site.title }}" />
<meta name="generator" content="pivoine v1.0.0" />

30
_includes/head/meta.html Normal file
View File

@@ -0,0 +1,30 @@
{% if page.noindex or page.no_index or page.sitemap == false %}
<meta name="robots" content="noindex" />
{% endif %}
{% unless page.redirect %}
{% if plugins contains 'jekyll-seo-tag' %}
{% include head/seo-tag.html %}
{% else %}
{% include head/seo-fallback.html %}
{% endif %}
{% if site.keywords.size > 0 or page.keywords.size > 0 %}
<meta name="keywords" content="{{ page.keywords | default:site.keywords | join:',' }}" />
{% endif %}
{% else %}
<meta http-equiv="refresh" content="0; url={{ page.redirect.to }}" />
<title>{{ site.data.strings.redirecting | default:"Redirecting..." }}</title>
{% endunless %}
{% if site.pivoine.dark_mode.dynamic %}
<meta name="color-scheme" content="dark light" />
{% elsif site.pivoine.dark_mode.always %}
<meta name="color-scheme" content="dark" />
{% else %}
<meta name="color-scheme" content="light" />
{% endif %}
{% unless site.pivoine.no_theme_color %}
<meta name="theme-color" content="{{ theme_color | default:'rgb(8,46,57)' }}" />
{% endunless %}

View File

@@ -0,0 +1,4 @@
<style id="_pageStyle">
{% capture page_style %}{% include_cached styles/page-style.scss color=include.color theme_color=include.theme_color %}{% endcapture %}
{{ page_style | scssify }}
</style>

View File

@@ -0,0 +1,24 @@
<script>{% include scripts/load-js.min.js %}{% include scripts/loadCSS.min.js %}{% include scripts/cssrelpreload.min.js %}!function(w) {
w._baseURL = '{{ "/" | relative_url }}';
w._publicPath = '{{ "/assets/js/" | relative_url }}';
w._noPushState = {{ site.pivoine.no_push_state | default:site.disable_push_state | default:false }};
w._noDrawer = {{ site.pivoine.no_drawer | default:site.disable_drawer | default:false }};
w._noNavbar = {{ site.pivoine.no_navbar | default:false }};
w._noToc = {{ site.pivoine.no_toc | default:false }};
w._noSearch = {{ site.pivoine.no_search | default:false }};
w._search = {
DATA_URL: '{{ "/assets/sitedata.json?no-cache" | relative_url }}',
STORAGE_KEY: 'mini-search{{ "/" | relative_url }}',
INDEX_KEY: 'index--{{ site.time | date_to_xmlschema }}',
};
w._clapButton = {% if site.clap_button or jekyll.environment != 'production' %}true{% else %}false{% endif %};
}(window);</script>
{% if site.kramdown.math_engine == 'mathjax' %}
<script async src="{{ 'assets/bower_components/MathJax/es5/tex-mml-chtml.js' | relative_url }}" id="_MathJax"></script>
{% endif %}
{% if jekyll.environment != 'production' and site.pivoine.umami %}
<script defer src="{{ site.pivoine.umami.script }}" data-website-id="{{ site.pivoine.umami.id }}"></script>
{% endif %}

View File

@@ -0,0 +1,13 @@
{% assign strings = site.data.strings %}
{% capture title %}
{% if page.url == "/" %}
{{ site.title }}{% if site.tagline %} {{ strings.separator | default:"|" }} {{ site.tagline }}{% endif %}
{% elsif page.title.size > 0 %}
{{ page.title }} {{ strings.separator | default:"|" }} {{ site.title }}
{% else %}
{{ site.title }}
{% endif %}
{% endcapture %}
<title>{{ title | strip }}</title>
<meta name="description" content="{{ page.description | default:page.excerpt | default:site.description | markdownify | strip_html }}" />
<link rel="canonical" href="{{ page.url | absolute_url }}" />

View File

@@ -0,0 +1,18 @@
{% comment %}<!--
Including `seo` in a partial prevents a parse error when `jekyll-seo-tag` is not included
-->{% endcomment %}
{% seo %}
{% if page.accent_video %}
<meta property="og:video" content="{{ page.accent_video | absolute_url }}" />
{% endif %}
{% if site.pivoine.sound and page.sound %}
<meta property="og:type" content="music.album" />
{% assign sounds = site.data.sounds | where: 'slug', page.sound %}
{% for track in sounds.first.tracks %}
{% assign track_url = "/assets/sounds/" | append:page.sound | append:"/" | append: track" %}
<meta property="music:song" content="{{ track_url | absolute_url }}" />
<meta property="music:song:track" content="{{ forloop.index + 1 }}" />
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,25 @@
{% assign google_fonts = site.google_fonts %}
{% capture style_url %}{{ 'assets/css/pivoine-1.0.0.css' | relative_url }}{% endcapture %}
{% capture icons_url %}{{ 'assets/icomoon/style.css' | relative_url }}{% endcapture %}
{% capture game_icons_url %}{{ 'assets/game-icons/css/game-icons.css' | relative_url }}{% endcapture %}
{% if google_fonts %}
{% capture fonts_url %}{{ site.google_fonts_url | default:'https://fonts.googleapis.com' }}/css?family={{ google_fonts | uri_escape }}&display=swap{% endcapture %}
{% endif %}
<link rel="preload" as="style" href="{{ style_url }}" id="_stylePreload" />
<link rel="preload" as="style" href="{{ icons_url }}" id="_iconsPreload" />
<link rel="preload" as="style" href="{{ game_icons_url }}" id="_gameIconsPreload" />
{% if google_fonts %}<link rel="preload" as="style" href="{{ fonts_url }}" id="_fontsPreload" />{% endif %}
<script>
setRel('_stylePreload');
setRel('_iconsPreload');
setRel('_gameIconsPreload');
/*{% if google_fonts %}*/setRel('_fontsPreload');/*{% endif %}*/
</script>
<noscript>
<link rel="stylesheet" href="{{ style_url }}" />
<link rel="stylesheet" href="{{ icons_url }}" />
<link rel="stylesheet" href="{{ game_icons_url }}" />
{% if google_fonts %}<link rel="stylesheet" href="{{ fonts_url }}" />{% endif %}
</noscript>

View File

@@ -0,0 +1,4 @@
{% assign layout = include.layout %}
<style id="_styleInline">
{% include_cached head/css/inline %}
</style>

View File

@@ -0,0 +1,12 @@
{% assign google_fonts = site.google_fonts %}
{% capture style_url %}{{ 'assets/css/pivoine-1.0.0.css' | relative_url }}{% endcapture %}
{% capture icons_url %}{{ 'assets/icomoon/style.css' | relative_url }}{% endcapture %}
{% capture game_icons_url %}{{ 'assets/game-icons/css/game-icons.css' | relative_url }}{% endcapture %}
{% if google_fonts %}
{% capture fonts_url %}{{ site.google_fonts_url | default:'https://fonts.googleapis.com' }}/css?family={{ google_fonts | uri_escape }}&display=swap{% endcapture %}
{% endif %}
<link rel="stylesheet" href="{{ style_url }}" id="_stylePreload" />
<link rel="stylesheet" href="{{ icons_url }}" id="_iconsPreload" />
<link rel="stylesheet" as="style" href="{{ game_icons_url }}" id="_gameIconsPreload" />
{% if google_fonts %}<link rel="stylesheet" href="{{ fonts_url }}" id="_fontsPreload" />{% endif %}

View File

@@ -0,0 +1,12 @@
<!--[if gt IE 8]><!---->
{% if site.pivoine.no_inline_css or jekyll.environment == 'development' %}
{% include_cached head/styles-no-inline.html %}
{% else %}
{% include_cached head/styles-layout.html layout=include.layout %}
{% include_cached head/styles-inline.html %}
{% endif %}
{% unless site.pivoine.no_page_style %}
{% include_cached head/page-style.html color=include.color theme_color=include.theme_color %}
{% endunless %}
<!--<![endif]-->

12
_includes/header.txt Normal file
View File

@@ -0,0 +1,12 @@
/*************************************************************
*
*
* ____ ____ .__ __ /\
* \ \ / /____ | | | | __ ____ _____ ______)/ ______
* \ Y /\__ \ | | | |/ // \\__ \\_ __ \/ ___/
* \ / / __ \| |_| <| | \/ __ \| | \/\___ \
* \___/ (____ /____/__|_ \___| (____ /__| /____ >
* \/ \/ \/ \/ \/
*
*
*************************************************************/

View File

@@ -0,0 +1,2 @@
<!-- This file is only here to prevent an error when using pivoine with Jekyll's default content. -->
GitHub

7
_includes/if-non-null Normal file
View File

@@ -0,0 +1,7 @@
{% capture maybe %}{% include {{ include.try }} %}{% endcapture %}
{% assign maybe = maybe | strip_newlines %}
{% if maybe.size > 0 %}
{{ maybe }}
{% elsif include.fallback %}
{% include {{ include.fallback }} %}
{% endif %}

42
_includes/my-body.html Normal file
View File

@@ -0,0 +1,42 @@
{% comment %}
<!--
Example code for using Matamo as alternative analytics solution.
-->
{% if site.matomo_analytics %}
<script>
var _paq = _paq || [];
{% if site.matomo_analytics.no_cookies %}
_paq.push(['disableCookies']);
{% endif %}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '{{site.matomo_analytics.site_id}}']);
var pushStateEl = document.getElementById('_pushState');
var timeStartLoadPage, referer, timeItTookToLoadPage;
pushStateEl.addEventListener('hy-push-state-start', function() {
timeStartLoadPage = new Date().getTime();
referer = window.location.toString();
});
pushStateEl.addEventListener('hy-push-state-ready', function() {
timeItTookToLoadPage = new Date().getTime() - timeStartLoadPage;
});
pushStateEl.addEventListener('hy-push-state-after', function() {
_paq.push(['setReferrerUrl', referer]);
_paq.push(['setCustomUrl', window.location.toString()]);
_paq.push(['setDocumentTitle', document.title]);
_paq.push(['deleteCustomVariables', 'page']);
_paq.push(['setGenerationTimeMs', timeItTookToLoadPage]);
_paq.push(['trackPageView']);
});
window.loadJSDeferred('{{site.matomo_analytics.root}}piwik.js');
</script>
{% endif %}
{% endcomment %}

View File

@@ -0,0 +1,24 @@
{% assign disqus = site.disqus | default:site.disqus_shortname %}
{% if disqus %}
<div id="disqus_thread"></div>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>
<script>!function(w, d) {
if (d.getElementById("disqus_thread")) {
if (w.DISQUS) {
w.DISQUS.reset({
reload: true,
config() {
this.page.url = w.location.href;
this.page.title = d.title;
},
});
} else {
w.disqus_config = function disqusConfig() {
this.page.url = w.location.href;
this.page.title = d.title;
};
w.loadJSDeferred(d.getElementById("_hrefDisqus").href + '/embed.js');
}
}
}(window, document);</script>
{% endif %}

13
_includes/my-head.html Normal file
View File

@@ -0,0 +1,13 @@
{% comment %}
<!--
Using `preload` so that page rendering doesn't get blocked.
Fill in `href`. You may have to adjust `as` and `onload`/`rel`, depending on content.
-->
<link href="<path/to/content.css>" rel="preload" as="style" onload="this.rel='stylesheet'">
<!--
Fallback in case JavaScript isn't enabled.
Fill in `href`. You may have to adjust `rel`.
-->
<noscript><link href="<path/to/content.css>" rel="stylesheet"></noscript>
{% endcomment %}

55
_includes/my-scripts.html Normal file
View File

@@ -0,0 +1,55 @@
{% comment %}
<!--
Example code for the CloudFlare mail protection script.
CloudFlare will inject this on every page, but due to pivoine's push state approach to page loading,
it will only run on the initial page.
The snippet below will run the code on every `hy-push-state-load` event instead.
-->
<script>
document.getElementById('_pushState').addEventListener('hy-push-state-load', function (e) {
function e(e){
(console.error?console.error:console.log).call(console,e)
}
function t(e){
return l.innerHTML='<a href="'+e.replace(/"/g,"&quot;")+'"></a>',l.childNodes[0].getAttribute("href")
}
function r(e,t){
var r=e.substr(t,2);return parseInt(r,16)
}
function n(e,n){
for(var o="",c=r(e,n),a=n+2;a<e.length;a+=2){
var l=r(e,a)^c;
o+=String.fromCharCode(l)
}
return t(o)
}
var o="/cdn-cgi/l/email-protection#",
c=".__cf_email__",
a="data-cfemail",
l=document.createElement("div");
!function(){
for(var t=document.getElementsByTagName("a"),r=0;r<t.length;r++)
try{
var c=t[r],a=c.href.indexOf(o);
a>-1&&(c.href="mailto:"+n(c.href,a+o.length))
}catch(t){
e(t)
}
}(),
function(){
for(var t=document.querySelectorAll(c),r=0;r<t.length;r++)
try{
var o=t[r],l=n(o.getAttribute(a),0),i=document.createTextNode(l);
o.parentNode.replaceChild(i,o)
}catch(t){
e(t)
}
}()
});
</script>
{% endcomment %}

38
_includes/scripts/cssrelpreload.min.js vendored Normal file
View File

@@ -0,0 +1,38 @@
!(function (a) {
if (a.loadCSS) {
var b = (loadCSS.relpreload = {});
if (
((b.support = function () {
try {
return a.document.createElement("link").relList.supports("preload");
} catch (b) {
return !1;
}
}),
(b.poly = function () {
for (
var b = a.document.getElementsByTagName("link"), c = 0;
c < b.length;
c++
) {
var d = b[c];
"preload" === d.rel &&
"style" === d.getAttribute("as") &&
(a.loadCSS(d.href, d, d.getAttribute("media")), (d.rel = null));
}
}),
!b.support())
) {
b.poly();
var c = a.setInterval(b.poly, 300);
a.addEventListener &&
a.addEventListener("load", function () {
b.poly(), a.clearInterval(c);
}),
a.attachEvent &&
a.attachEvent("onload", function () {
a.clearInterval(c);
});
}
}
})(this);

View File

@@ -0,0 +1,11 @@
!(function (window, document) {
var LM = "light-mode";
var DM = "dark-mode";
var h = new Date().getHours();
if ("matchMedia" in window && window.matchMedia("(prefers-color-scheme)"))
return;
var m = h <= window._sunrise || h >= window._sunset ? DM : LM;
var n = m === DM ? LM : DM;
document.body.classList.add(m);
document.body.classList.remove(n);
})(window, document);

9
_includes/scripts/dark-mode.min.js vendored Normal file
View File

@@ -0,0 +1,9 @@
((e, s) => {
var d = "light-mode",
a = "dark-mode",
o = new Date().getHours();
("matchMedia" in e && e.matchMedia("(prefers-color-scheme)")) ||
((e = (o = o <= e._sunrise || o >= e._sunset ? a : d) == a ? d : a),
s.body.classList.add(o),
s.body.classList.remove(e));
})(window, document);

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2017 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Compress via uglify:
// uglifyjs load-js.js -c -m > load-js.min.js
!(function (window, document) {
"use strict";
function addEvent(el, type, cb, opts) {
if (el.addEventListener) el.addEventListener(type, cb, opts);
else if (el.attachEvent) el.attachEvent("on" + type, cb);
else el["on" + type] = cb;
}
window.loadJS = function (src, cb) {
var script = document.createElement("script");
script.src = src;
if (cb) addEvent(script, "load", cb, { once: true });
var ref = document.scripts[0];
ref.parentNode.insertBefore(script, ref);
return script;
};
window._loaded = false;
window.loadJSDeferred = function (src, cb) {
var script = document.createElement("script");
script.src = src;
function insert() {
window._loaded = true;
if (cb) addEvent(script, "load", cb, { once: true });
var ref = document.scripts[0];
ref.parentNode.insertBefore(script, ref);
}
if (window._loaded) insert();
else addEvent(window, "load", insert, { once: true });
return script;
};
window.setRel = window.setRelStylesheet = function (id) {
var link = document.getElementById(id);
function set() {
this.rel = "stylesheet";
}
addEvent(link, "load", set, { once: true });
};
})(window, document);

35
_includes/scripts/load-js.min.js vendored Normal file
View File

@@ -0,0 +1,35 @@
((r, a) => {
function d(e, t, n, o) {
e.addEventListener
? e.addEventListener(t, n, o)
: e.attachEvent
? e.attachEvent("on" + t, n)
: (e["on" + t] = n);
}
(r.loadJS = function (e, t) {
var n = a.createElement("script"),
e = ((n.src = e), t && d(n, "load", t, { once: !0 }), a.scripts[0]);
return e.parentNode.insertBefore(n, e), n;
}),
(r._loaded = !1),
(r.loadJSDeferred = function (e, t) {
var n = a.createElement("script");
function o() {
(r._loaded = !0), t && d(n, "load", t, { once: !0 });
var e = a.scripts[0];
e.parentNode.insertBefore(n, e);
}
return (n.src = e), r._loaded ? o() : d(r, "load", o, { once: !0 }), n;
}),
(r.setRel = r.setRelStylesheet =
function (e) {
d(
a.getElementById(e),
"load",
function () {
this.rel = "stylesheet";
},
{ once: !0 },
);
});
})(window, document);

44
_includes/scripts/loadCSS.min.js vendored Normal file
View File

@@ -0,0 +1,44 @@
!(function (a) {
"use strict";
var b = function (b, c, d) {
function e(a) {
return h.body
? a()
: void setTimeout(function () {
e(a);
});
}
function f() {
i.addEventListener && i.removeEventListener("load", f),
(i.media = d || "all");
}
var g,
h = a.document,
i = h.createElement("link");
if (c) g = c;
else {
var j = (h.body || h.getElementsByTagName("head")[0]).childNodes;
g = j[j.length - 1];
}
var k = h.styleSheets;
(i.rel = "stylesheet"),
(i.href = b),
(i.media = "only x"),
e(function () {
g.parentNode.insertBefore(i, c ? g : g.nextSibling);
});
var l = function (a) {
for (var b = i.href, c = k.length; c--; ) if (k[c].href === b) return a();
setTimeout(function () {
l(a);
});
};
return (
i.addEventListener && i.addEventListener("load", f),
(i.onloadcssdefined = l),
l(f),
i
);
};
"undefined" != typeof exports ? (exports.loadCSS = b) : (a.loadCSS = b);
})("undefined" != typeof global ? global : this);

View File

@@ -0,0 +1,25 @@
// `script[nomodule]` polyfill for Safari 10.1.
// Source: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
(function () {
var check = document.createElement("script");
if (!("noModule" in check) && "onbeforeload" in check) {
var support = false;
document.addEventListener(
"beforeload",
function (e) {
if (e.target === check) {
support = true;
} else if (!e.target.hasAttribute("nomodule") || !support) {
return;
}
e.preventDefault();
},
true,
);
check.type = "module";
check.src = ".";
document.head.appendChild(check);
check.remove();
}
})();

20
_includes/scripts/nomodule.min.js vendored Normal file
View File

@@ -0,0 +1,20 @@
(() => {
var t,
n = document.createElement("script");
!("noModule" in n) &&
"onbeforeload" in n &&
((t = !1),
document.addEventListener(
"beforeload",
function (e) {
if (e.target === n) t = !0;
else if (!e.target.hasAttribute("nomodule") || !t) return;
e.preventDefault();
},
!0,
),
(n.type = "module"),
(n.src = "."),
document.head.appendChild(n),
n.remove());
})();

10
_includes/smart-url Normal file
View File

@@ -0,0 +1,10 @@
{%- assign url = include.url -%}
{%- assign ch1 = url | slice:0 -%}
{%- if url contains '://' -%}
{{ url }}
{%- elsif ch1 == '/' -%}
{%- assign url = url | remove_first:site.baseurl -%}
{{ url | relative_url }}
{%- else -%}
{{ url }}
{%- endif -%}

1
_includes/smart-url.txt Normal file
View File

@@ -0,0 +1 @@
{% comment %} Retained for backwards compatibility. Use smart-url (without .txt) instead. {% endcomment %}{% include smart-url url=include.url %}

View File

@@ -0,0 +1,18 @@
.note:before {
content: "{{ site.data.strings.note | default:'Note' }}";
}
.page > header > .note-sm:before {
content: "{{ site.data.strings.description | default:'Description' }}";
}
#markdown-toc:before {
content: "{{ site.data.strings.toc | default:'Table of Contents' }}";
}
.layout-resume .note-sm:before {
content: "{{ site.data.strings.resume.summary | default:'Summary' }}";
}
{% if site.pivoine.no_page_style %}
{% assign color = site.accent_color | default:'rgb(79,177,186)' %}
{% assign theme_color = site.theme_color | default:'rgb(8,46,57)' %}
{% include_cached styles/page-style.scss color=color theme_color=theme_color %}
{% endif %}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
{% include styles/variables.scss %}
@import "variables";
@import "my-variables";
@import "mixins";
{% if site.pivoine.dark_mode.dynamic %}
@import "pro/dark-mode-dynamic";
{% else %}
@import "pro/dark-mode";
{% endif %}
@import "html";
@import "pooleparty/__inline__/base";
@import "pooleparty/__inline__/type";
@import "pooleparty/__inline__/table";
@import "pooleparty/__inline__/footer";
@import "pooleparty/__inline__/footnotes";
@import "pooleparty/__inline__/code";
@import "pooleparty/__inline__/posts";
@import "pooleparty/__inline__/related";
@import "pooleparty/__inline__/read-more";
@import "pooleparty/__inline__/message";
@import "pooleparty/__inline__/pagination";
@import "pivoine/__inline__/base";
@import "pivoine/__inline__/utilities";
@import "pivoine/__inline__/links";
@import "pivoine/__inline__/images";
@import "pivoine/__inline__/sidebar";
@import "pivoine/__inline__/social";
@import "pivoine/__inline__/menu";
@import "pivoine/__inline__/toc";
@import "pivoine/__inline__/content";
@import "pivoine/__inline__/avatar";
@import "pivoine/__inline__/katex";
@import "pivoine/__inline__/footer";
@import "pivoine/__inline__/sound";
@import "my-inline";
{% include styles/common.scss %}

View File

@@ -0,0 +1,19 @@
@use "sass:color";
@use "sass:math";
{% assign color = include.color %}
{% assign theme_color = include.theme_color %}
html {
--accent-color: {{ color }};
--accent-color-faded: color.adjust({{ color }}, $alpha: -0.5);
--accent-color-highlight: color.adjust({{ color }}, $alpha: -0.9);
--accent-color-darkened: color.adjust({{ color }}, $lightness: -7.5%);
{% if site.github and site.pivoine.dart_sass_2_compat != true %}
--dark-mode-body-bg: #{hsl(color.channel({{ theme_color }}, "hue", $space: hsl), math.div(color.channel({{ theme_color }}, "saturation", $space: hsl), 8), 17.5%)};
--dark-mode-border-color: #{hsl(color.channel({{ theme_color }}, "hue", $space: hsl), math.div(color.channel({{ theme_color }}, "saturation", $space: hsl), 8), 22.5%)};
{% else %}
--dark-mode-body-bg: #{hsl(hue({{ theme_color }}), calc(saturation({{ theme_color }}) / 8), 17.5%)};
--dark-mode-border-color: #{hsl(hue({{ theme_color }}), calc(saturation({{ theme_color }}) / 8), 22.5%)};
{% endif %}
}

116
_includes/styles/style.scss Normal file
View File

@@ -0,0 +1,116 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
{% include styles/variables.scss %}
@import "variables";
@import "my-variables";
@import "mixins";
{% if site.pivoine.dark_mode.dynamic %}
@import "pro/dark-mode-dynamic";
{% else %}
@import "pro/dark-mode";
{% endif %}
@import "reboot-mod";
{% unless site.pivoine.no_inline_css or jekyll.environment == 'development' %}
@import "pooleparty/__link__/base";
@import "pooleparty/__link__/type";
@import "pooleparty/__link__/table";
@import "pooleparty/__link__/footer";
@import "pooleparty/__link__/footnotes";
@import "pooleparty/__link__/code";
@import "pooleparty/__link__/posts";
@import "pooleparty/__link__/related";
@import "pooleparty/__link__/read-more";
@import "pooleparty/__link__/message";
@import "pooleparty/__link__/pagination";
@import "pivoine/__link__/base";
@import "pivoine/__link__/utilities";
@import "pivoine/__link__/links";
@import "pivoine/__link__/images";
@import "pivoine/__link__/sidebar";
@import "pivoine/__link__/search";
@import "pivoine/__link__/social";
@import "pivoine/__link__/menu";
@import "pivoine/__link__/toc";
@import "pivoine/__link__/content";
@import "pivoine/__link__/avatar";
@import "pivoine/__link__/katex";
@import "pivoine/__link__/footer";
{% unless site.pivoine.no_mark_external or site.no_mark_external %}
@import "pivoine/__link__/mark-external";
{% endunless %}
{% unless site.pivoine.no_break_layout %}
@import "pivoine/__link__/break-layout";
{% endunless %}
@import "my-style";
{% else %}
@import "html";
@import "pooleparty/_base.pre.scss";
@import "pooleparty/_type.pre.scss";
@import "pooleparty/_table.pre.scss";
@import "pooleparty/_footer.pre.scss";
@import "pooleparty/_footnotes.pre.scss";
@import "pooleparty/_code.pre.scss";
@import "pooleparty/_posts.pre.scss";
@import "pooleparty/_related.pre.scss";
@import "pooleparty/_read-more.pre.scss";
@import "pooleparty/_message.pre.scss";
@import "pooleparty/_pagination.pre.scss";
@import "pivoine/_base.pre.scss";
@import "pivoine/_utilities.pre.scss";
@import "pivoine/_links.pre.scss";
@import "pivoine/_images.pre.scss";
@import "pivoine/_sidebar.pre.scss";
@import "pivoine/_search.pre.scss";
@import "pivoine/_social.pre.scss";
@import "pivoine/_menu.pre.scss";
@import "pivoine/_toc.pre.scss";
@import "pivoine/_content.pre.scss";
@import "pivoine/_avatar.pre.scss";
@import "pivoine/_katex.pre.scss";
@import "pivoine/_footer.pre.scss";
@import "pivoine/_sound.pre.scss";
{% unless site.pivoine.no_mark_external or site.no_mark_external %}
@import "pivoine/_mark-external.pre.scss";
{% endunless %}
{% unless site.pivoine.no_break_layout %}
@import "pivoine/_break-layout.pre.scss";
{% endunless %}
@import "my-inline";
@import "my-style";
{% include styles/common.scss %}
{% endunless %}
@import "spinner";
@import "tippy";
@import "syntax";
@import "pro/syntax-dark";
{% if site.pivoine.dark_mode.dynamic %}
@import "pro/dark-mode-dynamic-syntax";
{% endif %}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2020 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// {% assign vars = site.data.variables %}
// {% assign ui_font = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif' %}
// {% assign ui_font_code = 'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Mono", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Consolas", "Courier New", monospace' %}
@use "sass:math";
$font-family: {{ vars.font | default:site.font | default:ui_font }};
$font-family-heading: {{ vars.font_heading | default:site.font_heading | default:ui_font }};
$code-font-family: {{ vars.font_code | default:site.font_code | default:ui_font_code }};
$theme-color: {{ vars.theme_color | default:site.theme_color | default:'rgb(8,46,57)' }};
$root-font-size: {{ vars.root_font_size | default:15 }}px;
$root-font-size-medium: {{ vars.root_font_size_medium | default:16 }}px;
$root-font-size-large: {{ vars.root_font_size_large | default:17 }}px;
$root-font-size-print: {{ vars.root_font_size_print | default:8 }}pt;
$root-line-height: {{ vars.root_line_height | default:1.75 }};
$font-weight: {{ vars.font_weight | default:400 }};
$font-weight-bold: {{ vars.font_weight_bold | default:700 }};
$font-weight-heading: {{ vars.font_weight_heading | default:900 }};
$content-width: {{ vars.content_width | default:42 }}rem;
$content-width-2: {{ vars.content_width_2 | default:48 }}rem;
$content-width-5: {{ vars.content_width_5 | default:54 }}rem;
$content-padding: {{ vars.content_padding | default:1 }}rem;
$sidebar-width: {{ vars.sidebar_width | default:21 }}rem;
$break-point-1: {{ vars.break_point_1 | default:42 }}em;
$break-point-2: {{ vars.break_point_2 | default:54 }}em;
$break-point-3: {{ vars.break_point_3 | default:64 }}em;
$break-point-4: {{ vars.break_point_4 | default:72 }}em;
$break-point-5: {{ vars.break_point_5 | default:86 }}em;
$border-radius: {{ vars.border_radius | default:0.5 }}rem;
$break-point-font-large: {{ vars.break_point_font_large | default:124 }}em;
$content-margin-3: 3rem;
$content-margin-5: 4rem;
$gh-pages-compat: {% if site.github and site.pivoine.dart_sass_2_compat != true %}true{% else %}false{% endif %};
// TODO: doc
{% if site.github and site.pivoine.dart_sass_2_compat != true %}
$half-content: (math.div($content-width-5, 2) + $content-margin-5);
{% else %}
$half-content: calc(math.div($content-width-5, 2) + $content-margin-5);
{% endif %}
// The sidebar width starts adjusting dynamically when the content is at the center of the window.
// This is the case when the window size has a `min-width` of content area + twice the sidebar (left, right):
$break-point-dynamic: $content-width-5 + (2 * $content-margin-5) + (2 * $sidebar-width);

View File

@@ -0,0 +1,8 @@
<template id="_animation-template">
<div class="animation-main fixed-top">
{% unless site.pivoine.no_breadcrumbs %}{% include body/breadcrumbs.html url="/" %}{% endunless %}
<div class="content">
<div class="page"></div>
</div>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template id="_dark-mode-template">
<button id="_dark-mode" class="nav-btn no-hover" >
<span class="sr-only">{{ site.data.strings.dark_mode | default:"Dark Mode" }}</span>
<span class="icon-brightness-contrast"></span>
</button>
</template>

View File

@@ -0,0 +1,10 @@
<template id="_error-template">
<div class="page">
<h1 class="page-title">{{ strings.error.title | default:"Error" }}</h1>
{% capture link %}<a class="this-link" href=""></a>{% endcapture %}
{% assign text = strings.error.message | default:"Sorry, an error occurred while loading: <!--link-->." %}
<p class="lead">
{{ text | replace:"<!--link-->",link }}
</p>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<div hidden>
{% assign strings = site.data.strings %}
<h2 class="sr-only">{{ strings.templates | default: "Templates"}}{{ strings.colon | default:":" }}</h2>
{% include templates/animation.html %}
{% include templates/loading.html %}
{% include templates/error.html %}
{% include templates/permalink.html %}
{% if site.pivoine.dark_mode.icon %}
{% include templates/dark-mode.html %}
{% endif %}
{% if site.pivoine.search.icon %}
{% include templates/search.html %}
{% endif %}
</div>

View File

@@ -0,0 +1,6 @@
<template id="_loading-template">
<div class="loading nav-btn fr">
<span class="sr-only">{{ strings.loading | default:"Loading…" }}</span>
<span class="icon-cog"></span>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template id="_permalink-template">
<a href="#" class="permalink">
<span class="sr-only">{{ strings.permalink | default:"Permalink" }}</span>
<span class="{{ strings.permalink_icon | default:"content-hash" }}"></span>
</a>
</template>

View File

@@ -0,0 +1,6 @@
<template id="_search-template">
<button id="_search" class="nav-btn no-hover" >
<span class="sr-only">{{ site.data.strings.search | default:"Search" }}</span>
<span class="icon-search"></span>
</button>
</template>

1491
_js/lib/modernizr-custom.js Normal file

File diff suppressed because it is too large Load Diff

75
_js/lib/version.js Normal file
View File

@@ -0,0 +1,75 @@
console.log(`
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠈⠁⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⢀⣠⡶⠟⠀⠀⠀⠀⢀⣀⠤⠀⠀⠀⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠤⠀⠀⠀⠀⠉⠀⠀⠀⣀⣤⠶⠛⠋⠀⠒⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⠶⠞⣋⡥⠔⣀⠄⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠒⠉⠉⡠⠤⠖⠒⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⣠⠴⠂⠀⠈⠁⠀⡤⠾⠿⠛⠉⠀⢀⣴⠶⠖⣋⠀⣀⡠⠄⢀⣠⡶⢀⣀⡠⠀⠀⠠⠖⠚⠉⠁⠀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠊⠁⡴⠾⠁⠈⠁⢀⣠⠖⢁⡀⠀⢀⡠⠄⠀⣀⣀⣠⠄⠀⠞⠛⠉⠁⠴⠾⠛⠛⠛⠋⢁⣤⣤⡶⠖⠋⠀⠀⠀⠐⠚⠋⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⢀⠄⠀⠀⠀⠀⠀⢀⣤⠶⠛⠁⣈⠅⠀⣀⡤⠶⠚⣋⠽⠋⠀⣀⣠⠄⠀⢀⣠⣤⠶⠚⠉⠠⠞⠛⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠒⠚⠛⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢒⢁⡤⢀⡤⠀⠀⣠⠖⠀⠚⣉⣡⣀⠤⠊⠁⠒⠉⠁⠀⠀⣈⣤⣴⡾⠿⠋⠁⣠⠾⡛⠉⠀⢀⡀⠀⠀⠀⠀⠀⠀⢀⡠⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡀⠀⠀⠀⠀⠤⠤⠤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⣱⣯⠞⣁⣤⠐⣫⠴⠊⠀⠚⣛⣉⠁⠀⠴⠚⠉⠤⠴⠾⠿⢿⣿⠗⠀⢀⣠⠔⣲⣿⠴⠖⠋⠁⠀⠄⠀⣀⣀⠠⠚⢉⣠⣴⠶⠚⠁⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⡵⢁⡾⠛⠁⣨⣿⡷⠊⣁⠀⣀⣴⠟⠉⠀⣀⠤⠔⠲⠶⠛⠃⠀⠀⠋⠥⠒⠟⠋⣠⠞⢉⡠⠀⣀⠀⢀⣤⠶⠛⠉⠀⠀⠐⠉⠁⠀⠀⠀⠀⠀⢀⣤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠂⠁⢀⠔⠁⡰⠢⠊⠴⠟⠚⣩⡷⠛⠁⠠⠖⠊⠉⣀⠤⠀⢀⣤⣶⣾⣿⠟⠋⢀⣠⠴⠊⠀⠀⣡⠴⠊⠁⠀⠀⠀⠀⠀⠀⠀⣀⡠⠄⠀⠀⠀⠀⣀⣀⣀⡀⠀⠀⢀⣠⣴⠶⠟⠋⠁⠀⠀⠀⣀⣉⣉⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠡⠟⡴⣡⠋⢀⡜⠁⠀⣠⠔⠂⢀⣁⠤⠀⣠⣴⣦⠖⠋⠀⠀⠚⠋⠁⠚⠉⠀⣀⡤⠤⠒⠂⠀⠐⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠒⠉⠁⠀⠀⠀⠀⠀⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠁⢰⢃⣞⠔⢁⡴⠋⠀⡠⢊⠔⠈⡀⢉⡄⣠⡾⢛⡉⡠⣠⠴⣣⣶⣿⡥⢾⣿⠭⠃⠀⠀⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠐⠀⠀⢀⣀⡤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠁⠀⠃⢈⠋⡀⠏⠀⣀⢈⡴⢋⠴⢋⢴⠿⣿⡿⣾⠋⠞⠉⠁⠚⠉⠉⠁⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡶⠋⠀⠀⣀⠤⢀⣠⠔⠊⣉⣤⠔⠀⠀⣀⠤⠄⠀⠀⠀⠐⠒⠚⠓⢒⣲⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠇⠀⠀⣠⠂⠔⠁⣠⠐⡽⠈⠀⢀⡔⠁⠀⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣀⠀⠀⠀⠀⠀⢠⢞⡁⡠⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠾⠋⠁⠀⠀⣀⣾⣿⣿⠟⠁⢀⠾⠫⠀⠀⠀⠀⠀⣠⣤⣶⣶⣶⣶⣿⣽⣿⣟⣋⡉⠉⠛⠒⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⡟⢀⢀⣧⡇⠀⠀⠀⠠⠊⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠀⠠⡜⢿⣿⣿⣦⠀⠀⡆⢰⣱⣫⣮⡴⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⢞⣫⠿⠋⠁⠀⠀⠁⠀⠀⠀⠀⠀⠀⣉⣩⣽⣿⠿⠿⠟⠛⠛⠉⢉⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣾⣿⠤⢾⣾⣿⣷⣾⣠⣴⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⠀⠀⠀⠀⣿⣿⣿⠀⢀⠇⣼⣽⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠐⠊⠁⠀⣂⠭⠒⡩⠔⢊⡀⠀⠀⠀⢀⣤⡶⣞⣥⣶⠿⠟⠛⠛⣒⣚⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣄⣀⠀⠀⠐⠒⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣩⣴⣶⣶⡽⣿⡿⣿⣾⣷⡅⠀⠀⢀⡄⠀⠀⠀⢠⠀⠹⣿⣷⣤⣤⣼⣿⣿⠟⢀⡜⣰⠿⣾⠏⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠀⠀⠔⢊⣡⠔⠋⣠⣶⣿⣾⣟⣥⣾⣿⣿⣿⡿⠿⠟⠛⢛⣉⣩⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠶⠾⣿⣿⣿⣶⣶⣤⡤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣂⠛⢙⣾⣿⣽⣿⠗⣲⣋⠄⠁⠡⠠⣀⠑⠠⢈⠛⠿⠿⠿⠛⢁⡠⠞⣈⣴⣟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⢀⡠⠖⠋⢁⣴⡿⣿⣿⣿⣿⠿⠟⠻⠛⣉⣡⣴⣶⠾⠛⠁⠐⠒⠲⢿⣿⣿⡿⠿⠿⠛⠛⠷⠦⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣸⣿⣿⣿⣿⣿⣿⡯⣅⠨⣾⣿⣿⣿⣯⣭⣟⣔⠄⠀⠈⡩⡙⠓⠢⠬⠉⠀⠈⠉⠁⠀⣬⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠊⠁⠀⠠⠖⠛⢉⡨⣵⣾⠿⣂⣠⣴⣶⣿⣿⣿⣥⡤⠤⣀⣠⣤⣶⣶⣶⣶⣶⣶⣶⣶⣤⣤⣤⣀⡀⠀⠠⢤⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣯⡹⣿⣿⣟⠁⠀⠈⠙⠢⣽⣷⣿⡿⢿⣷⣾⣽⣿⣫⣴⣎⣴⣭⣀⣀⡀⠀⢀⣀⡠⠶⠿⠓⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠄⠀⠀⢀⣠⢴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡛⠛⠛⠛⠛⠛⠋⠉⠁⠀⠀⠀⠀⠀⠀⠘⠿⣛⠛⠶⠦⢤⣄⡀⠀⠙⠷⣶⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⡿⢀⣿⢿⣿⡄⠀⠀⠀⢄⣲⣟⡵⠪⠏⠞⠟⠉⠋⠛⠻⠿⠿⣿⣿⣿⡿⠾⠶⠖⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠒⠂⠈⠁⠀⠀⢄⣺⠿⠛⣉⡩⢽⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠛⠛⣛⣀⣤⣴⣶⣾⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠉⠂⠀⠀⠀⠀⠁⠀⠀⠈⠓⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⡿⠁⠈⢿⣿⣟⠿⣦⣤⣽⣿⣿⡥⠂⠀⠀⠀⣠⣴⣶⠿⠟⣛⣫⠭⠵⠒⠒⠒⣋⣉⣩⣥⣤⣄⡀⠀⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠊⠁⣀⡤⠤⢶⣾⣿⡿⠿⠿⠛⠛⠛⠒⠀⠀⠀⠀⠀⠀⠤⣾⣿⣿⣿⢿⣿⣟⡻⠿⣭⣭⣉⣙⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣽⡿⠁⠀⠀⠈⠻⠊⡻⠶⣬⣭⣭⣵⣶⠶⠶⣚⣛⣭⠴⠖⠛⠉⢁⣀⣒⣾⣿⣿⣿⣿⣿⣿⣿⣿⣶⣒⣂⣉⣉⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣉⣀⣀⣀⠤⠶⣿⣿⣷⣶⣶⣦⣤⣄⣀⠉⠛⠛⠛⠻⠿⠷⠦⠌⠉⠑⢂⠈⢙⡛⠿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠿⣷⣾⠿⠋⠟⠉⠀⣀⣤⠶⠟⠉⠽⠶⣿⣿⣿⣿⣶⣶⣾⣿⣿⣿⣿⠿⠯⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⢛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⢀⣤⣶⢶⣽⣦⡉⠻⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢀⣤⣶⠟⠋⠉⠀⢀⣠⣤⣤⣉⠩⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣦⣤⣤⣤⣤⡤⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠶⢶⣶⣶⠶⢶⠶⢭⣿⣿⣟⠿⢿⣷⣬⡉⢭⣭⣥⣀⠀⠀⠀⠉⠛⠻⠿⠿⠿⣦⡀⠉⠛⢿⣧⡀⠀⠀⠀⠀⠀⢄⣠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠊⠀⠀⠚⠉⠀⠀⠀⠀⠀⠀⠈⠛⠍⠫⢛⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣍⣉⡉⠉⠉⠁⠐⠒⠂⠀⠀⠀⠀⠀⠀⠀⠀⠒⠀⠀⠀⠀⠀⠀⠉⠓⠶⢤⣤⣈⡙⠛⠿⠦⣄⠀⠈⠀⣽⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⢠⡀⠈⠀⠀⠀⠈⠙⠆⠀⠀⠀⠀⠀⠻⣍⠛⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⣀⢤⣻⣿⣮⣙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣯⡛⠿⢟⠅⡀⠀⠀⠀⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⢄⣠⣌⣿⣿⣷⣤⣀⡠⢤⣀⣩⣶⣯⣉⠛⠛⠷⠄⠀⠀⠀⠈⠢⢽⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠔⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⠀⡈⢷⣹⡻⣿⣿⣿⢿⣿⣿⡿⣯⣛⠙⢿⣿⣿⡷⣤⠑⡠⡀⢄⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⣿⣿⣿⣿⢿⣿⣷⣌⠙⢿⣿⣿⣷⣄⠀⠀⡀⠀⠀⠀⠀⠀⠙⠆⠀⠀⠀⠀⠢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠹⠟⢿⣌⢾⣿⣿⣿⣷⣄⢌⣿⣮⣿⣷⣝⣆⠁⠘⢌⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠢⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⡙⠿⣧⡈⠓⠙⢷⣄⠙⣿⣯⠛⢧⡀⢌⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠹⠊⠟⣿⢿⣻⡿⣿⣿⣿⣿⣿⢻⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣶⣄⠁⠀⠀⠀⠙⢧⠈⠻⣇⠀⠁⠀⠑⢿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣦⣴⣿⣿⣿⣶⣦⣀⠀⠀⠀⠀⠀⠀⠀⣷⣸⠀⠀⠀⠀⠀⠀⠸⠈⢷⠁⡿⡟⡏⢇⢻⡇⠏⠃⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⢀⡉⠻⣄⠀⠀⠀⠀⠀⠀⠀⢲⣄⠈⢿⣿⣦⡀⠀⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠙⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⢹⣷⣠⡀⢾⣿⠀⠀⠀⠠⠀⢀⠀⣀⣈⣤⣥⢁⣷⡜⡄⠃⠀⠀⠀⠀⠀⠀⠀⢀⠀⡈⣆⢢⣧⠀⠡⡀⠀⠸⣿⣶⡄⡀⠀⠀⠀⠀⠀⠀⠀⢿⣷⣤⣝⢿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠄⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣫⣭⣻⡿⣿⢙⣛⠿⣿⣿⣿⣿⣷⣾⣿⣿⣿⣦⠻⠀⣼⠀⣡⣾⣿⣿⣿⣿⢟⡅⠈⠀⠇⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⣇⣿⣾⡞⣿⡇⡄⠘⡄⠀⢹⢿⣿⣮⠀⠀⠀⠀⠀⠀⠀⠀⠻⣿⣿⣧⡙⢿⣷⡀⠀⠀⠀⢢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣶⣾⣿⣿⣿⣿⣿⣶⣄⢻⣿⣮⡙⠟⣿⣏⣿⣿⣿⣷⣻⣧⢸⣏⣾⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⠀⠀⢷⣿⣿⣿⣿⣿⣿⣿⡀⠸⡄⢦⣷⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣄⠙⢧⠀⠀⠀⠀⢳⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣣⣶⣶⣬⣍⡻⣿⣿⢙⣿⣿⣿⣆⠈⢿⢹⣿⣿⣿⢁⣿⣇⢻⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⢀⠀⢰⠀⢀⠀⢰⢰⢸⣿⣿⢿⢹⣿⣿⣿⡇⡆⣿⣼⣿⣿⣿⣟⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠏⢿⣧⠀⠀⠀⢠⠀⠈⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⣢⣶⣶⣶⣄⢣⣿⣿⣿⣿⡿⣛⣎⠉⣼⣿⣿⣶⣝⢧⠘⣿⣿⣿⣫⣌⠻⡿⢸⣿⣿⣿⣿⣿⠿⣋⠀⠀⠀⠀⠀⣴⠀⣰⡾⠀⣸⡆⠀⣸⣿⣿⣿⠈⢢⣿⣿⠹⡇⡇⣿⣿⣿⣿⢿⣿⣹⠇⠀⠀⠀⠀⢆⠀⠀⠀⠀⠀⠀⠹⣧⠀⠀⠈⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⠀⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⡟⡙⠨⡕⢪⠢⠀⣿⣿⣿⣿⣾⣄⠀⢻⣿⣿⣿⣿⣷⠁⠸⢸⣿⣿⣿⣿⣿⣿⡇⠀⠀⢀⣾⠇⠀⡟⢡⣷⣿⣿⢠⣿⣿⣧⢹⡄⣾⣿⣿⡇⠃⠁⣿⣇⢻⣯⢣⢿⡏⠀⠀⠀⠀⢷⣄⢳⣄⠀⠀⠀⠀⠀⠘⡆⠀⠀⢸⣿⡄⠀⠀⠀⠘⡄⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⢿⣿⠿⠀⡯⠿⢅⣶⣾⣤⡀⣿⣿⣿⣿⣿⣿⣷⡌⠿⡟⣿⣿⣿⡇⠀⣾⣿⣿⣿⣿⣿⣿⠃⠀⠀⠎⠁⠀⠀⠀⢸⣿⣿⡟⣾⣿⣿⣿⠈⣷⣿⣿⣿⡇⠀⠀⣿⣿⠈⣿⠈⠘⣿⠀⠀⠀⢸⡜⣿⣷⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⡀⠀⠀⠀⢱⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠘⣶⣴⣶⣶⣶⣦⣤⣀⡈⠻⣟⢉⣛⡛⠧⠈⠀⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠇⣿⣿⢸⡇⣸⣿⠿⣛⢿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⢸⢿⡟⣼⢿⣿⣿⣿⠀⢿⣿⣿⣿⠀⠀⠀⣿⡿⠀⢿⠀⠀⢹⠀⠀⠀⠈⣿⡜⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠸⣻⢿⣿⣿⣿⣿⣿⠛⢽⡷⡌⢸⣿⡿⠗⠆⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⠸⠀⠀⣿⠇⣩⣶⣶⣭⣾⣿⣆⠹⠋⠀⠀⠀⠀⠀⠀⢀⡄⠀⠈⡼⣹⠏⣾⣿⣿⣿⡀⢸⣿⡿⠁⠀⠀⠀⢸⠇⠀⠈⠀⣄⠈⠀⠀⠀⠀⢹⣷⢹⣿⣿⣿⡇⣄⢀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣘⣿⣿⣿⣿⣿⣿⣧⠻⣿⡄⢈⣵⣯⣷⡢⡀⣿⣿⣿⣿⣿⣿⣿⣿⡿⡇⣿⣿⠂⣾⢦⠀⠙⢸⣿⠿⣿⣿⣿⣿⣿⠀⠀⢀⣀⠀⠀⢠⢃⣾⡤⠀⠀⢃⡟⠀⣿⣿⣿⣿⠁⢸⡿⠁⠀⠀⠀⠀⠈⠀⠀⠀⠀⣿⡀⠀⠀⠀⠀⠈⣿⠀⢿⣿⣿⡷⢿⠸⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠈⢙⡻⠿⠿⣿⣿⣿⣿⣧⢻⣿⡈⣿⣿⣿⣿⣧⢿⣿⣿⣿⣿⣿⣝⣗⠁⠀⣿⡿⠀⠛⠈⢀⣠⣌⣿⣿⣿⣿⣿⣿⣿⢀⣶⠏⠀⠀⠀⡠⢿⣿⡴⠁⠀⣼⠁⠀⣿⣿⣿⣿⠀⠈⠀⠀⡄⠀⠀⡄⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⢹⠀⠈⣿⣿⡇⢸⠄⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣶⡩⡝⢿⣿⣷⣝⠣⠹⣿⣿⣿⣿⣯⠻⣿⣿⣿⣿⣿⡌⠀⠀⠘⢁⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠸⠀⠀⠀⢀⠜⢠⣿⡿⣡⢣⣷⡟⠀⠀⣿⢟⣿⡿⠀⠀⠀⣴⠃⠀⠀⣷⠀⠀⠀⠀⠀⢸⣿⠀⠀⡄⠀⠀⠀⠀⠀⢸⣿⡇⠸⠀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠺⣿⣿⣷⣝⢷⣬⣛⡻⢷⣦⣙⠿⠿⠟⠿⡷⠌⠊⠛⠻⠌⠁⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢻⣿⠏⠀⠀⠀⠀⢠⠏⠀⡾⠙⠁⢡⣿⣿⢃⡄⠀⠏⣼⣿⠇⢀⡆⣸⠏⠀⠀⣰⣿⠀⠀⠀⠀⠀⠘⡇⠀⢰⡇⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣷⡹⢿⡟⣣⣤⣤⣝⣻⣿⣿⠿⣿⣿⣶⣤⣀⡀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠞⢁⡄⠀⠀⠀⠀⠎⠀⢸⠁⠀⣰⣿⡿⣿⣾⡇⠀⣸⣿⡟⢀⣾⢷⠏⠀⠀⢠⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀⡾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⠶⣝⡞⠻⣿⣿⣿⣿⣿⣿⣶⣭⣉⣛⠛⠉⢠⣿⣿⣿⣿⣿⣿⣿⣿⡿⠏⠀⠠⢴⣿⣅⠀⠀⠀⠀⠀⠀⡏⠀⠠⢋⡿⠁⣿⣿⡇⢀⣿⣿⠁⣾⠟⠈⠀⠀⠀⣾⡿⢁⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣤⣴⣶⣶⣶⣶⣶⣄⡀⠀⠀⠀⠈⠙⠻⠿⣿⣿⣿⡿⠿⠟⠋⠀⠈⠛⠿⠿⠿⠿⠛⠛⠋⢀⣠⣼⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⢀⠀⠀⢸⠁⠀⣿⣿⠇⣾⣿⠃⠀⣿⠀⠀⠀⠀⢠⣿⣵⠃⠀⠀⠀⠀⠀⠀⠀⢰⣿⠃⢀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⡤⣀⣀⠤⣀⣤⣴⣶⡶⣿⣿⣛⣉⡥⠀⣠⡴⠊⠀⣠⣾⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⣾⡆⠀⠀⠀⠀⣿⠿⢸⣿⠃⠀⠀⡇⠀⠀⠀⠀⣾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⣼⠃⣴⠇⠀⠀⠀⠀⠀⠀⠀⢰⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⣠⠾⢋⣠⣴⣾⣿⣿⣿⣿⣿⣷⠆⠀⠀⢠⠎⠀⠀⠀⣸⠟⠀⠀⠀⠀⠀⠸⠀⣾⠃⠀⠀⠀⠁⠀⠀⠀⠘⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣡⡾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣨⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⣽⣿⡿⣫⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠰⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⠁⠀⢀⡴⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣨⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣬⣯⣿⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⡌⠙⠿⠿⣿⣿⡿⠟⠉⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠉⠻⠟⠿⢛⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⢏⣿⣿⣿⣿⣿⣿⣿⢿⡟⣿⠟⡡⠊⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠱⣿⣻⣟⣿⠿⠋⠁⠨⣿⣿⣿⣿⣿⡿⠟⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡄⠀⠀⠂⠀⠀⠀⠈⠀⠐⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⣿⠿⠋⠉⠀⠀⠀⠀⠀⠀⠠⢾⣶⣿⢂⣀⠀⠀⠀⠀⠀⠀⡠⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⡠⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠈⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠂⢀⣠⣤⠔⠀⠀⠀⠀⠀⡰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠚⠁⢀⣠⠯⠟⠁⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡤⠖⠂⠀⠀⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
`);

40
_js/src/clap-button.js Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2020 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import "broadcastchannel-polyfill";
import { webComponentsReady, stylesheetReady } from "./common";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
).then(
() =>
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
),
]),
]);
await webComponentsReady;
await stylesheetReady;
if (process.env.GET_CLAPS_API && !window.GET_CLAPS_API)
window.GET_CLAPS_API = process.env.GET_CLAPS_API;
import(/* webpackMode: "eager" */ "@getclaps/button");
})();

202
_js/src/common.js Normal file
View File

@@ -0,0 +1,202 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Observable, of } from "rxjs";
// HACK: Temporary MS Edge fix
// TODO: Move rx-element into separate file or module
export {
getScrollHeight,
getScrollLeft,
getScrollTop,
} from "@hydecorp/component/lib/util";
export { fromMediaQuery, fetchRx } from "@hydecorp/component/lib/creators";
export { subscribeWhen, filterWhen } from "@hydecorp/component/lib/operators";
export { createIntersectionObservable } from "@hydecorp/component/lib/observers";
const style = getComputedStyle(document.documentElement);
export const BREAK_POINT_3 = `(min-width: ${style.getPropertyValue("--break-point-3")})`;
export const BREAK_POINT_DYNAMIC = `(min-width: ${style.getPropertyValue("--break-point-dynamic")})`;
export const CONTENT_WIDTH_5 = parseFloat(
style.getPropertyValue("--content-width-5"),
);
export const CONTENT_MARGIN_5 = parseFloat(
style.getPropertyValue("--content-margin-5"),
);
export const DRAWER_WIDTH = parseFloat(
style.getPropertyValue("--sidebar-width"),
);
export const HALF_CONTENT = parseFloat(
style.getPropertyValue("--half-content"),
);
// Check the user agent for Safari and iOS Safari, to give them some special treatment...
const ua = navigator.userAgent.toLowerCase();
export const isSafari = ua.indexOf("safari") > 0 && ua.indexOf("chrome") < 0;
export const isMobile = ua.indexOf("mobile") > 0;
export const isMobileSafari = isSafari && isMobile;
export const isUCBrowser = ua.indexOf("ucbrowser") > 0;
export const isFirefox = ua.indexOf("firefox") > 0;
export const isFirefoxIOS = ua.indexOf("fxios") > 0 && ua.indexOf("safari") > 0;
export const hasCSSOM =
"attributeStyleMap" in Element.prototype && "CSS" in window && CSS.number;
export const webComponentsReady = new Promise((res) => {
if ("customElements" in window) res(true);
else document.addEventListener("WebComponentsReady", res);
});
// FIXME: Replace with something more robust!?
export const stylesheetReady = new Promise(function checkCSS(
res,
rej,
retries = 30,
) {
const drawerEl = document.querySelector("hy-drawer");
if (!drawerEl) res(true);
else if (getComputedStyle(drawerEl).getPropertyValue("--hy-drawer-width"))
res(true);
else if (retries <= 0) rej(Error("Stylesheet not loaded within 10 seconds"));
else setTimeout(() => checkCSS(res, rej, retries - 1), 1000 / 3);
});
export const once = (el, eventName) =>
new Promise((res) => el.addEventListener(eventName, res, { once: true }));
export const timeout = (t) => new Promise((res) => setTimeout(res, t));
// Takes an array of Modernizr feature tests and makes sure they all pass.
export function hasFeatures(features) {
if (!window.Modernizr) return true;
return [...features].every((feature) => {
const hasFeature = window.Modernizr[feature];
if (!hasFeature && process.env.DEBUG)
console.warn(`Feature '${feature}' missing!`);
return hasFeature;
});
}
// Some functions to hide and show content.
export function show() {
this.style.display = "block";
this.style.visibility = "visible";
}
export function hide() {
this.style.display = "none";
this.style.visibility = "hidden";
}
export function unshow() {
this.style.display = "";
this.style.visibility = "";
}
export const unhide = unshow;
// Same as `el.innerHTML = ''`, but not quite so hacky.
export function empty() {
while (this?.firstChild) this.removeChild(this.firstChild);
}
/**
* An observable wrapper for the WebAnimations API.
* Will return an observable that emits once when the animation finishes.
* @param {HTMLElement|null} el
* @param {AnimationKeyFrame | AnimationKeyFrame[] | null} effect
* @param {number|AnimationEffectTiming} timing
* @returns {Observable<Event>}
*/
export function animate(el, effect, timing) {
if (!el) return of(new CustomEvent("finish"));
return Observable.create((observer) => {
const anim = el.animate(effect, timing);
anim.addEventListener("finish", (e) => {
observer.next(e);
requestAnimationFrame(() => {
requestAnimationFrame(() => observer.complete());
});
});
return () => {
if (anim.playState !== "finished") anim.cancel();
};
});
}
/**
* @param {string} templateId
* @returns {HTMLElement|null}
*/
export function importTemplate(templateId) {
const template = document.getElementById(templateId);
return template && document.importNode(template.content, true);
}
export const body = document.body || document.documentElement;
export const rem = (units) =>
units * parseFloat(getComputedStyle(body).fontSize);
export const getViewWidth = () => window.innerWidth || body.clientWidth;
export const getViewHeight = () => window.innerHeight || body.clientHeight;
/**
* @template Q
* @template S
* @param {Worker} worker
* @param {Q} message
* @returns {Promise<S>}
*/
export function postMessage(worker, message) {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
worker.postMessage(message, [messageChannel.port2]);
});
}
const promisifyLoad = (loadFn) => (href) =>
new Promise((r) => loadFn(href).addEventListener("load", r));
/** @type {(href: string) => Promise<Event>} */
export const loadJS = promisifyLoad(window.loadJS);
/** @type {(href: string) => Promise<Event>} */
export const loadCSS = promisifyLoad(window.loadCSS);
/**
* @param {ArrayLike<Element>} els
* @param {IntersectionObserverInit} [options]
* @returns {Promise<IntersectionObserverEntry>}
*/
export function intersectOnce(els, options) {
return new Promise((res) => {
const io = new IntersectionObserver((entries) => {
if (entries.some((x) => x.isIntersecting)) {
els.forEach((el) => io.unobserve(el));
res(entries.find((x) => x.isIntersecting));
}
}, options);
els.forEach((el) => io.observe(el));
});
}

166
_js/src/cross-fader.js Normal file
View File

@@ -0,0 +1,166 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { EMPTY, of } from "rxjs";
import { catchError, finalize, map, switchMap } from "rxjs/operators";
import { animate, fetchRx } from "./common";
const RE_CSS_URL = /url\s*\(['"]?(([^'"\\]|\\.)*)['"]?\)/u;
/** @param {Document} doc */
const calcHash = (doc) => {
const sidebar = doc.getElementById("_sidebar");
const sidebarBg = sidebar?.querySelector(".sidebar-bg");
const pageStyle = doc.getElementById("_pageStyle");
const source = doc.getElementById("source");
// const rule = Array.from(pageStyle?.sheet?.rules ?? []).find(r => r.selectorText === 'html');
// const accentColor = rule?.style.getPropertyValue('--accent-color') ?? '';
// const themeColor = rule?.style.getPropertyValue('--theme-color') ?? '';
return [
pageStyle?.innerText?.trim(),
sidebar?.classList,
sidebarBg?.classList,
sidebarBg?.style.backgroundImage,
source?.src,
].join("\n");
};
/**
* Consider a URL external if either the protocol, hostname or port is different.
* @param {URL} param0
* @param {Location=} location
*/
function isExternal({ protocol, host }, location = window.location) {
return protocol !== location.protocol || host !== location.host;
}
const objectURLs = new WeakMap();
export class CrossFader {
/** @param {number} fadeDuration */
constructor(fadeDuration) {
this.sidebar = document.getElementById("_sidebar");
this.fadeDuration = fadeDuration;
this.prevHash = calcHash(document);
this.themeColorEl = document.querySelector('meta[name="theme-color"]');
}
/** @param {Document} newDocument */
fetchImage2(newDocument) {
const sidebarBg = newDocument.querySelector(".sidebar-bg");
const video = sidebarBg?.querySelector("source");
const { backgroundImage = "" } = sidebarBg?.style ?? {};
const result = RE_CSS_URL.exec(backgroundImage);
const videoUrl = video?.src;
if (!result) {
return of("");
}
const url = new URL(result[1], window.location.origin);
return fetchRx(url.href, {
method: "GET",
headers: { Accept: "image/*" },
...(isExternal(url) ? { mode: "cors" } : {}),
}).pipe(
switchMap((r) => r.blob()),
map((blob) => [URL.createObjectURL(blob), videoUrl]),
catchError(() => of(url.href)),
);
}
/** @param {Document} newDocument */
fetchImage(newDocument) {
const hash = calcHash(newDocument);
if (hash === this.prevHash) return EMPTY;
return this.fetchImage2(newDocument).pipe(
map(([objectUrl, videoUrl]) => {
/** @type {HTMLDivElement} */
const div =
newDocument.querySelector(".sidebar-bg") ??
document.createElement("div");
if (objectUrl) {
div.style.backgroundImage = `url(${objectUrl})`;
objectURLs.set(div, objectUrl);
}
const video = div.querySelector("video");
if (video && videoUrl) {
video.querySelector("source").src = videoUrl;
}
return [div, hash, newDocument];
}),
);
}
/** @param {Document} newDocument */
updateStyle(newDocument) {
const classList = newDocument.getElementById("_sidebar")?.classList;
if (classList) this.sidebar.setAttribute("class", classList);
if (this.themeColorEl) {
const themeColor = newDocument.head.querySelector(
'meta[name="theme-color"]',
)?.content;
if (themeColor) {
window.setTimeout(() => {
if (this.themeColorEl) {
this.themeColorEl.content = themeColor;
}
}, 250);
}
}
try {
const pageStyle = document.getElementById("_pageStyle");
const newPageStyle = newDocument.getElementById("_pageStyle");
if (!newPageStyle) return;
pageStyle?.parentNode?.replaceChild(newPageStyle, pageStyle);
} catch (e) {
if (process.env.DEBUG) console.error(e);
}
}
/**
* @param {[HTMLDivElement]} param0
* @param {[HTMLDListElement, string, Document]} param1
*/
fade([prevDiv], [div, hash, newDocument]) {
prevDiv?.parentNode?.insertBefore(div, prevDiv.nextElementSibling);
this.updateStyle(newDocument);
// Only update the prev hash after we're actually in the fade stage
this.prevHash = hash;
return animate(div, [{ opacity: 0 }, { opacity: 1 }], {
duration: this.fadeDuration,
easing: "ease",
}).pipe(
finalize(() => {
if (objectURLs.has(prevDiv))
URL.revokeObjectURL(objectURLs.get(prevDiv));
prevDiv?.parentNode?.removeChild(prevDiv);
div.querySelector("video").play();
}),
);
}
}

81
_js/src/dark-mode.js Normal file
View File

@@ -0,0 +1,81 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
import { importTemplate, stylesheetReady, once } from "./common";
const SEL_NAVBAR_BTN_BAR = "#_navbar > .content > .nav-btn-bar";
(async () => {
await stylesheetReady;
const darkMode = importTemplate("_dark-mode-template");
if (darkMode) {
const navbarEl = document.querySelector(SEL_NAVBAR_BTN_BAR);
navbarEl?.insertBefore(
darkMode,
navbarEl.querySelector(".nav-insert-marker"),
);
const metaEl = document.querySelector('meta[name="color-scheme"]');
let tId;
const navbarBtn = document.getElementById("_dark-mode");
navbarBtn?.addEventListener("click", (e) => {
e.preventDefault();
clearTimeout(tId);
const list = document.body.classList;
if (
list.contains("dark-mode") ||
("_sunset" in window &&
!list.contains("light-mode") &&
matchMedia("(prefers-color-scheme: dark)").matches)
) {
list.remove("dark-mode");
list.add("light-mode");
tId = setTimeout(() => {
if (metaEl) metaEl.content = "light";
document.documentElement.style.colorScheme = "light";
}, 250);
navbarBtn.dispatchEvent(
new CustomEvent("pivoine-dark-mode-toggle", {
detail: false,
bubbles: true,
}),
);
} else {
list.remove("light-mode");
list.add("dark-mode");
tId = setTimeout(() => {
if (metaEl) metaEl.content = "dark";
document.documentElement.style.colorScheme = "dark";
}, 250);
navbarBtn.dispatchEvent(
new CustomEvent("pivoine-dark-mode-toggle", {
detail: true,
bubbles: true,
}),
);
}
});
await once(document, "click");
const styleSheets = Array.from(document.styleSheets);
const inlineSheet = styleSheets.find(
(s) => s.ownerNode?.id === "_styleInline",
);
const linkSheet = styleSheets.find(
(s) => s.ownerNode?.id === "_stylePreload",
);
const setRule = (sheet) => {
if (!sheet) return;
const rule = Array.from(sheet.rules).find((rule) =>
rule.selectorText.startsWith(".color-transition"),
);
if (rule)
rule.style.transition =
"background-color 1s ease, border-color 1s ease";
};
setRule(inlineSheet);
setRule(linkSheet);
}
})();

275
_js/src/drawer.js Normal file
View File

@@ -0,0 +1,275 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { fromEvent, merge, NEVER, combineLatest } from "rxjs";
import {
distinctUntilChanged,
map,
filter,
startWith,
switchMap,
tap,
throttleTime,
withLatestFrom,
} from "rxjs/operators";
import {
BREAK_POINT_3,
BREAK_POINT_DYNAMIC,
isSafari,
isMobile,
isMobileSafari,
hasCSSOM,
webComponentsReady,
stylesheetReady,
getScrollTop,
getViewWidth,
fromMediaQuery,
} from "./common";
(async () => {
await Promise.all([
...("customElements" in window
? []
: [
import(
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
).then(
() =>
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
),
]),
...("ResizeObserver" in window
? []
: [
import(
/* webpackChunkName: "resize-observer" */ "./polyfills/resize-observer"
),
]),
]);
await webComponentsReady;
await stylesheetReady;
const MOBILE = 1;
const DESKTOP = 2;
const LARGE_DESKTOP = 3;
const subscribeWhen = (p$) => (source) => {
if (process.env.DEBUG && !p$) throw Error();
return p$.pipe(switchMap((p) => (p ? source : NEVER)));
};
// Determines the range from which to draw the drawer in pixels, counted from the left edge.
// It depends on the browser, e.g. Safari has a native gesture when sliding form the side,
// so we ignore the first 35 pixels (roughly the range for the native gesture),
// to avoid triggering both gestures.
function getRange(drawerWidth, size) {
if (size >= DESKTOP) return [0, drawerWidth];
if (isMobileSafari) return [35, 150];
return [0, 150];
}
// The functions below add an svg graphic to the sidebar
// that indicate that the sidebar can be drawn using touch gestures.
function setupIcon(drawerEl) {
const img = document.getElementById("_hrefSwipeSVG");
if (img) {
const svg = document.createElement("img");
svg.id = "_swipe";
svg.src = img.href;
svg.alt = "Swipe image";
svg.addEventListener("click", () => drawerEl.close());
document.getElementById("_sidebar")?.appendChild(svg);
}
}
function removeIcon() {
const svg = document.getElementById("_swipe");
svg?.parentNode?.removeChild(svg);
}
const detectSize = () =>
window.matchMedia(BREAK_POINT_DYNAMIC).matches
? LARGE_DESKTOP
: window.matchMedia(BREAK_POINT_3).matches
? DESKTOP
: MOBILE;
// First we get hold of some DOM elements.
const drawerEl = document.getElementById("_drawer");
const sidebarEl = document.getElementById("_sidebar");
const contentEl = sidebarEl?.querySelector(".sidebar-sticky");
if (!drawerEl || !sidebarEl || !contentEl) return;
document.getElementById("_menu")?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
drawerEl.toggle();
});
sidebarEl
.querySelectorAll('a[href^="/"]:not(.external)')
.forEach((el) => el.addEventListener("click", () => drawerEl.close()));
if (isSafari) drawerEl.setAttribute("threshold", "0");
if (!isMobile) drawerEl.setAttribute("mouseevents", "");
const [tValue, oValue] = hasCSSOM
? [
new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))]),
CSS.number(1),
]
: [null, null];
const updateSidebar = (t, size, distance) => {
const value = distance * t;
const opacity = size >= DESKTOP ? 1 : 1 - t;
if (hasCSSOM) {
tValue[0].x.value = value;
oValue.value = opacity;
sidebarEl.attributeStyleMap.set("transform", tValue);
contentEl.attributeStyleMap.set("opacity", oValue);
} else {
sidebarEl.style.transform = `translateX(${value}px)`;
contentEl.style.opacity = opacity;
}
};
// A flag for the 3 major viewport sizes we support
const size$ = merge(
fromMediaQuery(window.matchMedia(BREAK_POINT_3)),
fromMediaQuery(window.matchMedia(BREAK_POINT_DYNAMIC)),
).pipe(startWith({}), map(detectSize));
// An observable keeping track of the drawer (peek) width.
const peekWidth$ = fromEvent(drawerEl, "peek-width-change").pipe(
map((e) => e.detail),
);
// An observable keeping track the viewport width
const viewWidth$ = fromEvent(window, "resize", { passive: true }).pipe(
startWith({}),
map(getViewWidth),
);
// An observable keeping track of the distance between
// the middle point of the screen and the middle point of the drawer.
const distance$ = combineLatest(peekWidth$, viewWidth$).pipe(
map(([drawerWidth, viewWidth]) => viewWidth / 2 - drawerWidth / 2),
);
const t$ = merge(
distance$.pipe(
map(() =>
drawerEl.opacity !== undefined ? 1 - drawerEl.opacity : opened ? 0 : 1,
),
),
fromEvent(drawerEl, "hy-drawer-move").pipe(
map(({ detail: { opacity } }) => {
return 1 - opacity;
}),
),
);
drawerEl.addEventListener("hy-drawer-prepare", () => {
sidebarEl.style.willChange = "transform";
contentEl.style.willChange = "opacity";
});
drawerEl.addEventListener("hy-drawer-transitioned", () => {
sidebarEl.style.willChange = "";
contentEl.style.willChange = "";
});
// Save scroll position before the drawer gets initialized.
const scrollTop = getScrollTop();
// Start the drawer in `opened` state when the cover class is present,
// and the user hasn't started scrolling already.
const opened =
drawerEl.classList.contains("cover") &&
scrollTop <= 0 &&
!(history.state && history.state.closedOnce);
if (!opened) {
if (!history.state) history.replaceState({}, document.title);
history.state.closedOnce = true;
drawerEl.removeAttribute("opened");
}
const opened$ = fromEvent(drawerEl, "hy-drawer-transitioned").pipe(
map((e) => e.detail),
distinctUntilChanged(),
tap((opened) => {
if (!opened) {
removeIcon();
if (!history.state) history.replaceState({}, document.title);
history.state.closedOnce = true;
}
}),
startWith(opened),
);
// We need the height of the drawer in case we need to reset the scroll position
const drawerHeight = opened ? null : drawerEl.getBoundingClientRect().height;
drawerEl.addEventListener(
"hy-drawer-init",
() => {
drawerEl.classList.add("loaded");
setupIcon(drawerEl);
if (drawerHeight && scrollTop >= drawerHeight) {
window.scrollTo(0, scrollTop - drawerHeight);
}
},
{ once: true },
);
await import(/* webpackMode: "eager" */ "@hydecorp/drawer");
window._drawer = drawerEl;
t$.pipe(
withLatestFrom(size$, distance$),
tap((args) => updateSidebar(...args)),
).subscribe();
// Keeping the drawer updated.
peekWidth$
.pipe(
withLatestFrom(size$),
map((args) => getRange(...args)),
tap((range) => {
drawerEl.range = range;
}),
)
.subscribe();
// Hacky way of letting the cover page close when scrolling
fromEvent(document, "wheel", { passive: false })
.pipe(
subscribeWhen(opened$),
filter((e) => e.deltaY > 0),
tap((e) => {
if (drawerEl.translateX > 0) e.preventDefault();
}),
throttleTime(500),
tap(() => drawerEl.close()),
)
.subscribe();
})();

78
_js/src/entry.js Normal file
View File

@@ -0,0 +1,78 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import "@babel/polyfill";
import "../lib/version";
import "../lib/modernizr-custom";
import { hasFeatures } from "./common";
__webpack_public_path__ = window._publicPath;
const BASELINE = ["classlist", "eventlistener", "queryselector", "template"];
const DARK_MODE_FEATURES = ["customproperties"];
const DRAWER_FEATURES = [
"customproperties",
"history",
"matchmedia",
"opacity",
];
const PUSH_STATE_FEATURES = [
"history",
"matchmedia",
"opacity",
"cssanimations",
"cssremunit",
"documentfragment",
];
const CLAP_BUTTON_FEATURES = [
"customproperties",
"cssanimations",
"cssremunit",
];
const TOC_FEATURES = ["matchmedia", "cssremunit"];
if (hasFeatures(BASELINE)) {
import(/* webpackMode: "eager" */ "./upgrades");
if (!window._noNavbar) import(/* webpackChunkName: "navbar" */ "./navbar");
if (!window._noLightbox)
import(/* webpackChunkName: "lightbox" */ "./lightbox");
if (!window._noSound) import(/* webpackChunkName: "sound" */ "./sound");
if (hasFeatures(DARK_MODE_FEATURES)) {
// import(/* webpackMode: "eager" */ './pro/cookies-banner');
import(/* webpackMode: "eager" */ "./dark-mode");
}
if (!window._noSearch) import(/* webpackChunkName: "search" */ "./search");
if (window._clapButton && hasFeatures(CLAP_BUTTON_FEATURES)) {
import(/* webpackChunkName: "clap-button" */ "./clap-button");
}
// A list of Modernizr tests that are required for the drawer to work.
if (!window._noDrawer && hasFeatures(DRAWER_FEATURES)) {
import(/* webpackChunkName: "drawer" */ "./drawer");
}
if (!window._noPushState && hasFeatures(PUSH_STATE_FEATURES)) {
import(/* webpackChunkName: "push-state" */ "./push-state");
}
// if (!window._noToc && hasFeatures(TOC_FEATURES)) {
// import(/* webpackChunkName: "toc" */ './pro/toc');
// }
}

29
_js/src/flip/index.js Normal file
View File

@@ -0,0 +1,29 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { merge } from "rxjs";
import { filter } from "rxjs/operators";
import { setupFLIPTitle } from "./title";
const FLIP_TYPES = ["title"];
export function setupFLIP(start$, ready$, fadeIn$, options) {
const other$ = start$.pipe(
filter(({ flipType }) => !FLIP_TYPES.includes(flipType)),
);
return merge(setupFLIPTitle(start$, ready$, fadeIn$, options), other$);
}

116
_js/src/flip/title.js Normal file
View File

@@ -0,0 +1,116 @@
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Observable, of, zip } from "rxjs";
import { tap, finalize, filter, map, switchMap } from "rxjs/operators";
import { animate, empty } from "../common";
const TITLE_SELECTOR = ".page-title, .post-title";
/**
* @param {Observable<any>} start$
* @param {Observable<any>} ready$
* @param {Observable<any>} fadeIn$
* @param {any} opts
*/
export function setupFLIPTitle(
start$,
ready$,
fadeIn$,
{ animationMain, settings },
) {
if (!animationMain) return start$;
const flip$ = start$.pipe(
filter(({ flipType }) => flipType === "title"),
switchMap(({ anchor }) => {
if (!anchor) return of({});
const title = document.createElement("h1");
title.classList.add("page-title");
title.textContent = anchor.textContent;
title.style.transformOrigin = "left top";
const page = animationMain.querySelector(".page");
if (!page) return of({});
empty.call(page);
page.appendChild(title);
animationMain.style.position = "fixed";
animationMain.style.opacity = 1;
const first = anchor.getBoundingClientRect();
const last = title.getBoundingClientRect();
const firstFontSize = parseInt(getComputedStyle(anchor).fontSize, 10);
const lastFontSize = parseInt(getComputedStyle(title).fontSize, 10);
const invertX = first.left - last.left;
const invertY = first.top - last.top;
const invertScale = firstFontSize / lastFontSize;
anchor.style.opacity = 0;
const transform = [
{
transform: `translate3d(${invertX}px, ${invertY}px, 0) scale(${invertScale})`,
},
{ transform: "translate3d(0, 0, 0) scale(1)" },
];
return animate(title, transform, settings).pipe(
tap({
complete() {
animationMain.style.position = "absolute";
},
}),
);
}),
);
start$
.pipe(
switchMap(({ flipType }) =>
zip(
ready$.pipe(
filter(() => flipType === "title"),
map(({ replaceEls: [main] }) => {
const title = main.querySelector(TITLE_SELECTOR);
if (title) title.style.opacity = 0;
return title;
}),
),
fadeIn$,
).pipe(
map(([x]) => x),
tap((title) => {
if (title) title.style.opacity = 1;
animationMain.style.opacity = 0;
}),
finalize(() => {
animationMain.style.opacity = 0;
const page = animationMain.querySelector(".page");
empty.call(page);
}),
),
),
)
.subscribe();
return flip$;
}

Some files were not shown because too many files have changed in this diff Show More