diff --git a/_config.yml b/_config.yml index 71240344..322041c8 100644 --- a/_config.yml +++ b/_config.yml @@ -86,7 +86,7 @@ whitelist: # Serving # detach: false # port: 4000 -# host: 127.0.0.1 +host: localhost baseurl: "" # show_dir_listing: false diff --git a/_includes/head/custom.html b/_includes/head/custom.html index 55de7508..fb80f270 100644 --- a/_includes/head/custom.html +++ b/_includes/head/custom.html @@ -1,2 +1,3 @@ - + + diff --git a/_plugins/auto-link.rb b/_plugins/auto-link.rb index a2d4e867..943712cc 100644 --- a/_plugins/auto-link.rb +++ b/_plugins/auto-link.rb @@ -1,30 +1,94 @@ +require "digest" require "nokogiri" require "addressable/uri" -Jekyll::Hooks.register [:pages, :documents], :post_convert do |doc| - next unless doc.output_ext == ".html" +module Jekyll + module CustomPlugin + class FindFile + @@find_file_cache = {} + def self.process(site, link) + return @@find_file_cache[link] if @@find_file_cache.key(link) + link_uri = Addressable::URI.parse(link) + if link_uri&.path + relative_path = link_uri.path[1..].strip + relative_path_with_leading_slash = Jekyll::PathManager.join("", relative_path) + site.each_site_file do |file| + if [file.relative_path, file.url].any? { |v| [relative_path, relative_path_with_leading_slash].include?(v) } + @@find_file_cache[file.url] = file + @@find_file_cache[file.relative_path] = file + return file + end + end + end + nil + end - site = doc.site - liquid_context = Liquid::Context.new({}, {}, { site: site }) + def self.ensure_leading_slash(input) + return input if input.nil? || input.empty? || input.start_with?("/") + "/#{input}" + end - process_uri = lambda do |path| - uri = Addressable::URI.parse(path) - if uri&.path - uri.path = Liquid::Template.parse("{% link #{uri.path[1..]} %}").render!(liquid_context) + def self.hash_file(file, short = true) + file_hash = file.is_a?(Jekyll::StaticFile) ? Digest::SHA256.file(file.path).hexdigest : Digest::SHA256.hexdigest(file.output) + short ? file_hash&.slice(0, 8) : file_hash + end end - uri.to_s - end - fragment = Nokogiri::HTML::DocumentFragment.parse(doc.content) - fragment.css("[src^=\"/assets/\"],[src^=\"/\"][src$=\".md\"],[src^=\"/\"][src*=\".md#\"]").each do |item| - if item["src"] - item["src"] = process_uri.call(item["src"]) + module HashFilter + def hash_file(link) + site = @context.registers[:site] + file = FindFile::process(site, link) + return nil if file.nil? + FindFile::hash_file(file) + end end - end - fragment.css("[href^=\"/assets/\"],[href^=\"/\"][href$=\".md\"],[href^=\"/\"][href*=\".md#\"]").each do |item| - if item["href"] - item["href"] = process_uri.call(item["href"]) + + Liquid::Template.register_filter(HashFilter) + + Jekyll::Hooks.register [:pages, :documents], :post_convert do |doc| + next unless doc.output_ext == ".html" + + site = doc.site + relative_url_cache = site.filter_cache[:relative_url] ||= {} + sanitized_baseurl = site.config["baseurl"].is_a?(String) ? FindFile::ensure_leading_slash(site.config["baseurl"].chomp("/")) : "" + fragment = Nokogiri::HTML::DocumentFragment.parse(doc.content) + %w[src href].each do |attribute| + fragment.css("[#{attribute}^=\"/\"]").each do |item| + file = FindFile::process(site, item[attribute]) + next if file.nil? + + next item[attribute] = relative_url_cache[file.url].dup if relative_url_cache.key?(file.url) + next item[attribute] = relative_url_cache[file.relative_path].dup if relative_url_cache.key?(file.relative_path) + + relative_url_cache[file.relative_path] = relative_url_cache[file.url] = FindFile::ensure_leading_slash(file.url).prepend(sanitized_baseurl) + item[attribute] = relative_url_cache[file.relative_path].dup + end + end + doc.content = fragment.to_html end + + Jekyll::Hooks.register :site, :post_render do |site| + sanitized_baseurl = site.config["baseurl"].is_a?(String) ? FindFile::ensure_leading_slash(site.config["baseurl"].chomp("/")) : "" + file_hash_map = {} + + site.each_site_file do |file| + next if file.url.end_with?("/") || file.url.end_with?(".html") + file_hash_map[file.url] = FindFile::hash_file(file) + end + + (site.pages + site.documents).each do |doc| + next unless doc.output_ext == ".html" + fragment = Nokogiri::HTML.parse(doc.output) + %w[src href].each do |attribute| + fragment.css("[#{attribute}]").each do |item| + next unless item[attribute].start_with?(sanitized_baseurl) + file_url = item[attribute].sub(/^#{Regexp.escape(sanitized_baseurl)}/, "") + next unless file_hash_map.key?(file_url) + item[attribute] += "?hash=#{file_hash_map[file_url]}" + end + end + doc.output = fragment.to_html + end + end end - doc.content = fragment.to_html end diff --git a/assets/css/main.scss b/assets/css/main.scss index 0f4452fc..a845151c 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -1,8 +1,2 @@ --- --- - -@charset "utf-8"; - -$sans-serif: -apple-system, BlinkMacSystemFont, "Roboto", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; -@import "minimal-mistakes/skins/default"; -@import "minimal-mistakes-plus"; diff --git a/assets/css/skins/default.scss b/assets/css/skins/default.scss index a845151c..0f4452fc 100644 --- a/assets/css/skins/default.scss +++ b/assets/css/skins/default.scss @@ -1,2 +1,8 @@ --- --- + +@charset "utf-8"; + +$sans-serif: -apple-system, BlinkMacSystemFont, "Roboto", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; +@import "minimal-mistakes/skins/default"; +@import "minimal-mistakes-plus"; diff --git a/assets/js/cache.js b/assets/js/cache.js new file mode 100644 index 00000000..32f7f238 --- /dev/null +++ b/assets/js/cache.js @@ -0,0 +1,44 @@ +--- +layout: null +permalink: /cache.js +--- +const CACHE_VERSION = 1; +const CACHE_NAME = "assets-cache-" + CACHE_VERSION; + +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("fetch", (event) => { + const request = event.request; + const url = new URL(request.url); + + const pathname = url.pathname; + if (pathname.endsWith(".woff2")) { + const referrer = new URL(request.referrer); + const hash = referrer.searchParams.get("hash"); + if (hash === null) return; + url.searchParams.set("hash", "referrer:" + hash); + } + + const hash = url.searchParams.get("hash"); + if (hash === null) return; + + event.respondWith( + caches.open(CACHE_NAME).then((cache) => { + return cache.match(request).then((cached) => { + if (cached === undefined) { + return cache.delete(request, { ignoreSearch: true }).then(() => { + return fetch(request).then((response) => { + if (response && response.status === 200) { + cache.put(request, response.clone()); + } + return response; + }); + }) + } + return cached; + }); + }) + ); +}); diff --git a/assets/js/theme.js b/assets/js/theme.js index 9a202792..439d5d5e 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -1,14 +1,28 @@ --- layout: null --- +{%- assign skins = site.data.settings.appearance_skin_light.options | concat: site.data.settings.appearance_skin_dark.options | uniq -%} +{%- capture hash -%} +{ +{%- for skin in skins -%} +"{{ skin }}":"{{ '/assets/css/skins/' | append: skin | append: '.css' | hash_file }}" +{%- unless forloop.last %},{% endunless -%} +{%- endfor -%} +} +{%- endcapture %} + window.addEventListener("DOMContentLoaded", function () { + var hash = {{ hash }}; var skinLink = document.getElementById("skin"); var darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + function applySkin(skin) { + skinLink.href = "{{ '/assets/css/skins/' | relative_url }}" + skin + ".css?hash=" + hash[skin]; + } function applyDarkSkin() { - skinLink.href = "{{ '/assets/css/skins/' | relative_url }}" + settings.get("appearance_skin_dark", "dark") + ".css"; + applySkin(settings.get("appearance_skin_dark", "dark")); } function applyLightSkin() { - skinLink.href = "{{ '/assets/css/skins/' | relative_url }}" + settings.get("appearance_skin_light", "default") + ".css"; + applySkin(settings.get("appearance_skin_light", "default")); } function autoSchemeHandler() { if (darkModeQuery.matches) {