diff --git a/.drone.yml b/.drone.yml index 7f16f3c1..2ee2533a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,11 +7,13 @@ clone: steps: - name: build - image: ruby:3.4 + image: cimg/ruby:3.4.9-node environment: JEKYLL_ENV: production BUNDLE_PATH: vendor/bundle commands: + - npm config set registry http://mirrors.cloud.tencent.com/npm + - npm install - bundle config mirror.https://rubygems.org https://mirrors.cloud.tencent.com/rubygems - bundle install --verbose - bundle exec jekyll build --trace --verbose @@ -22,6 +24,8 @@ steps: path: /drone/src/vendor - name: jekyll-cache path: /drone/src/.jekyll-cache + - name: node_modules + path: /drone/src/node_modules when: branch: [main] @@ -35,3 +39,6 @@ volumes: - name: jekyll-cache host: path: /home/ubuntu/docs.hmcl.net/cache/jekyll-cache +- name: node_modules + host: + path: /home/ubuntu/docs.hmcl.net/cache/node_modules diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 99cae981..afa036ff 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -225,12 +225,14 @@ jobs: git fetch https://github.com/${{ needs.preview-create-init.outputs.repository }} ${{ needs.preview-create-init.outputs.sha }} git merge --squash ${{ needs.preview-create-init.outputs.sha }} --no-edit git commit -m "Merge #${{ env.GITHUB_PR_NUMBER }} for preview build" + - name: Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" - bundler-cache: ${{ env.PREVIEW_WATCH }} - cache-version: PR-${{ env.GITHUB_PR_NUMBER }} - name: Jekyll Build run: | echo "url: https://${{ needs.preview-create-init.outputs.domain }}" > _action.yml @@ -239,7 +241,7 @@ jobs: echo "destination: /home/runner/site" >> _action.yml echo "preview:" >> _action.yml echo " pr-number: ${{ env.GITHUB_PR_NUMBER }}" >> _action.yml - ${{ env.PREVIEW_WATCH }} || bundle install --jobs 4 + npm install && bundle install --jobs 4 bundle exec jekyll build --trace --config _config.yml,_action.yml - id: upload-site name: Upload Site diff --git a/.gitignore b/.gitignore index f60b43ac..4b2e8308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,18 @@ -_site -.sass-cache -.jekyll-cache -.jekyll-metadata -vendor -Gemfile.lock +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata/ + +/.idea/ + +/_site/ + +/vendor/ +/node_modules/ + .bundle -/.idea/ \ No newline at end of file + +/Gemfile.lock +/package-lock.json +/yarn.lock +/pnpm-lock.yaml +/bun.lock diff --git a/Gemfile b/Gemfile index e4e9405f..dcdd1823 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,4 @@ gem "wdm", "0.2.0", :platforms => [:windows] gem "http_parser.rb", "0.8.0", :platforms => [:jruby] # plugin dependencies -gem "webp-ffi", "0.4.0" if ENV["ENABLE_WEBP_AUTO_CONVERSION"] == "true" || ENV["DRONE"] == "true" -gem "mini_racer", "0.20.0" if ENV["ENABLE_EMBEDDED_V8"] == "true" || ENV["DRONE"] == "true" -gem "terser", "1.2.7" +gem "open3", "0.2.1" diff --git a/_config.yml b/_config.yml index e76e9a43..78bf0ddb 100644 --- a/_config.yml +++ b/_config.yml @@ -8,8 +8,8 @@ # layouts_dir: _layouts # data_dir: _data # includes_dir: _includes -# cache_dir: .jekyll-cache -disable_disk_cache: true +cache_dir: .jekyll-cache/v260607 +# disable_disk_cache: false sass: # sass_dir: _sass # minimal-mistakes @@ -50,6 +50,13 @@ include: exclude: - README.md - LICENSE + - tsconfig.json + - tsconfig.node.json + - package.json + - package-lock.json + - yarn.lock + - pnpm-lock.yaml + - bun.lock keep_files: [] encoding: "utf-8" # markdown_ext: "markdown,mkdown,mkdn,mkd,md" @@ -328,9 +335,6 @@ feed: post_process: terser: /assets/js/main.min.js: - - /assets/js/settings.js - - /assets/js/theme.js - - /assets/js/meta.js - /assets/js/vendor/jquery/jquery-3.6.0.js - /assets/js/plugins/gumshoe.js - /assets/js/plugins/jquery.ba-throttle-debounce.js @@ -339,14 +343,14 @@ post_process: - /assets/js/plugins/jquery.magnific-popup.js - /assets/js/plugins/smooth-scroll.js - /assets/js/plugins/jquery.auto-redirect.js + - /assets/js/extend.ts.js - /assets/js/_main.js remove_dirs: - /assets/images/ - - /assets/js/plugins/ + - /assets/js/lunr/ - /assets/js/vendor/ + - /assets/js/plugins/ remove_files: - - /assets/js/meta.js - /assets/js/_main.js - - /assets/js/theme.js - - /assets/js/settings.js + - /assets/js/extend.ts.js - /assets/js/main.min.js.map diff --git a/_launcher/shader.md b/_launcher/shader.md index 654a551e..b06a5615 100644 --- a/_launcher/shader.md +++ b/_launcher/shader.md @@ -24,7 +24,7 @@ authors: 在对应的游戏版本管理页面,点击 `自动安装` ,你会看到有个 `OptiFine` 的选项。 -![OptiFine_AutoInstaller](/assets/img/docs/shader/install_auto-16338577874692.png) +![OptiFine_AutoInstaller](/assets/img/docs/shader/install_auto.png) 点开之后选择合适的版本然后等待安装完成即可。 目前,如果要在 `Fabric` 使用 `OptiFine` ,需要通过 **方式四** 安装。 diff --git a/_plugins/kramdown_enhancer.rb b/_plugins/kramdown_enhancer.rb index 5ad64746..6b567a4d 100644 --- a/_plugins/kramdown_enhancer.rb +++ b/_plugins/kramdown_enhancer.rb @@ -1,8 +1,5 @@ -begin - require "webp-ffi" -rescue LoadError; end - module KramdownEnhancer + CACHE = Jekyll::Cache.new("KramdownEnhancer") GITHUB_LINK_REGEX = /\b(GP-\d+|GC-[0-9a-f]{7})\b/ BLOCKQUOTE_TYPES = { note: "notice--info", @@ -173,7 +170,6 @@ def relative_url(input) Jekyll::Hooks.register :site, :post_read do |site| KramdownEnhancer.baseurl = site.config["baseurl"] webp_list = [] - webp_enabled = defined?(WebP) site.each_site_file do |file| KramdownEnhancer.file[file.relative_path] = file if file.is_a?(Jekyll::StaticFile) @@ -182,9 +178,18 @@ def relative_url(input) destination = File.join(site.dest, url) if File.exist?(source) KramdownEnhancer.webp[file.url] = url - elsif webp_enabled && %w[.png .jpg .jpeg .tif .tiff].include?(file.extname.downcase) + elsif Jekyll.env == "production" && %w[.png .jpg .jpeg .tif .tiff].include?(file.extname.downcase) + source_base64 = Base64.encode64(File.read(file.path, mode: "rb")) + hash = Digest::SHA256.hexdigest(source_base64) + if KramdownEnhancer::CACHE.key?("webp_#{hash}") + Jekyll.logger.info "Kramdown Enhancer:", "[webp] Hit Cache #{url}" + else + destination_base64 = Script.call("webp", source: source_base64) + KramdownEnhancer::CACHE["webp_#{hash}"] = Base64.decode64(destination_base64) + Jekyll.logger.info "Kramdown Enhancer:", "[webp] Generated #{url}" + end FileUtils.mkdir_p(File.dirname(destination)) - WebP.encode(file.path, destination) + File.write(destination, KramdownEnhancer::CACHE["webp_#{hash}"], mode: "wb") webp_list.push(KramdownEnhancer::WebpFile.new(site, site.dest, File.dirname(url), File.basename(url))) KramdownEnhancer.webp[file.url] = url end diff --git a/_plugins/post_process.rb b/_plugins/post_process.rb index 396a83c7..344fae9b 100644 --- a/_plugins/post_process.rb +++ b/_plugins/post_process.rb @@ -1,51 +1,73 @@ -require "terser" - -ExecJS.runtime = ExecJS::Runtimes::MiniRacer if ExecJS::Runtimes::MiniRacer.available? - -Jekyll::Hooks.register :site, :post_write do |site| - config = site.config["post_process"] - next unless config - - terser = config["terser"] - if terser.is_a?(Hash) - terser.each do |terser_output, terser_inputs| - next unless terser_output.is_a?(String) && terser_inputs.is_a?(Array) - - terser_codes = [] - terser_inputs_all_exist = true - terser_inputs.each do |file| - destination = File.join(site.dest, file) - if File.exist?(destination) - terser_codes << File.read(destination, encoding: "UTF-8") - else - terser_inputs_all_exist = false - break - end - end - - if terser_inputs_all_exist - destination = File.join(site.dest, terser_output.to_s) - File.write(destination, Terser.compile(terser_codes.join(";"))) - Jekyll.logger.info "Post Process:", "terser #{terser_output}" - end - end - end - - remove_files = config["remove_files"] - if remove_files.is_a?(Array) - remove_files.each do |file| - destination = File.join(site.dest, file) - File.delete(destination) if File.exist?(destination) - Jekyll.logger.info "Post Process:", "remove_files #{file}" - end - end - - remove_dirs = config["remove_dirs"] - if remove_dirs.is_a?(Array) - remove_dirs.each do |dir| - destination = File.join(site.dest, dir) - FileUtils.rm_rf(destination) if File.directory?(destination) - Jekyll.logger.info "Post Process:", "remove_dirs #{dir}" - end - end -end +Jekyll::Hooks.register :site, :post_write do |site| + config = site.config["post_process"] + next unless config + + cache = nil + terser = config["terser"] + if terser.is_a?(Hash) + terser.each do |terser_output, terser_inputs| + next unless terser_output.is_a?(String) && terser_inputs.is_a?(Array) + + cache = Jekyll::Cache.new("PostProcess") unless cache + terser_codes = [] + terser_inputs_all_exist = true + terser_inputs.each do |file| + destination = File.join(site.dest, file) + if File.exist?(destination) + terser_codes << File.read(destination, encoding: "UTF-8") + else + terser_inputs_all_exist = false + break + end + end + + if terser_inputs_all_exist + destination = File.join(site.dest, terser_output.to_s) + code = terser_codes.join(";") + hash = Digest::SHA256.hexdigest(code) + unless cache.key?("terser_#{hash}") + cache["terser_#{hash}"] = Script.call("terser", code: code) + end + File.write(destination, cache["terser_#{hash}"]) + Jekyll.logger.info "Post Process:", "[terser] #{terser_output}" + end + end + end + + remove_files = config["remove_files"] + if remove_files.is_a?(Array) + remove_files.each do |file| + destination = File.join(site.dest, file) + File.delete(destination) if File.exist?(destination) + Jekyll.logger.info "Post Process:", "[remove_files] #{file}" + end + end + + remove_dirs = config["remove_dirs"] + if remove_dirs.is_a?(Array) + remove_dirs.each do |dir| + destination = File.join(site.dest, dir) + FileUtils.rm_rf(destination) if File.directory?(destination) + Jekyll.logger.info "Post Process:", "[remove_dirs] #{dir}" + end + end + + # clean history jekyll cache + jekyll_cache_dir = File.dirname(site.cache_dir) + current_cache_name = File.basename(site.cache_dir) + if File.basename(jekyll_cache_dir) == ".jekyll-cache" + Dir.entries(jekyll_cache_dir).select do |entry| + next if entry == "." || entry == ".." + entry_path = File.join(jekyll_cache_dir, entry) + if File.file?(entry_path) + File.delete(entry_path) + Jekyll.logger.info "Post Process:", "[clean_history_jekyll_cache] #{entry}" + elsif File.directory?(entry_path) + if entry < current_cache_name + FileUtils.rm_rf(entry_path) + Jekyll.logger.info "Post Process:", "[clean_history_jekyll_cache] #{entry}" + end + end + end + end +end diff --git a/_plugins/script.rb b/_plugins/script.rb new file mode 100644 index 00000000..02cd7e03 --- /dev/null +++ b/_plugins/script.rb @@ -0,0 +1,33 @@ +module Script + class << self + def init + begin + stdout, _, status = Open3.capture3("bun", "-v") + if status.success? + @runtime = "bun" + Jekyll.logger.info "Script:", "[init] #{@runtime} #{stdout}" + return + end + rescue + # Ignored + end + + stdout, stderr, status = Open3.capture3("node", "-v") + raise stderr unless status.success? + @runtime = "node" + Jekyll.logger.info "Script:", "[init] #{@runtime} #{stdout}" + end + + def call(name, param) + init unless @runtime + uuid = SecureRandom.uuid + stdout, stderr, status = Open3.capture3(@runtime, "_plugins/scripts/call.js", stdin_data: {"name": name, "param": param, "uuid": uuid}.to_json) + raise stderr unless status.success? + log, _, result = stdout.rpartition(uuid) + log.split("\n").map do |line| + Jekyll.logger.info "Script:", "[#{name}] #{line}" + end + JSON.parse(result) + end + end +end diff --git a/_plugins/scripts/call.js b/_plugins/scripts/call.js new file mode 100644 index 00000000..e634934a --- /dev/null +++ b/_plugins/scripts/call.js @@ -0,0 +1,13 @@ +let input = ""; + +process.stdin.on("data", (chunk) => { + input += chunk; +}); + +process.stdin.on("end", async () => { + const data = JSON.parse(input); + const mod = await import("./" + data.name + ".js"); + const result = await mod.default(data.param); + console.log(data.uuid); + console.log(JSON.stringify(result)); +}); diff --git a/_plugins/scripts/terser.js b/_plugins/scripts/terser.js new file mode 100644 index 00000000..63bf08db --- /dev/null +++ b/_plugins/scripts/terser.js @@ -0,0 +1,9 @@ +import { minify } from "terser"; + +export default async function ({ code }) { + const result = await minify(code, { + compress: false, + mangle: true + }); + return result.code; +} diff --git a/_plugins/scripts/typescript.js b/_plugins/scripts/typescript.js new file mode 100644 index 00000000..3e77cd64 --- /dev/null +++ b/_plugins/scripts/typescript.js @@ -0,0 +1,35 @@ +import ts from "typescript"; + +export default function ({ code }) { + const input = "index.ts"; + const output = "index.js"; + + const sources = new Map([[input, code]]); + const results = new Map(); + + ts.createProgram([input], { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ESNext, + noResolve: true, + }, { + getSourceFile: function (fileName, languageVersion, _onError) { + const sourceText = this.readFile(fileName); + return sourceText !== undefined + ? ts.createSourceFile(fileName, sourceText, languageVersion) + : undefined; + }, + getDefaultLibFileName: (defaultLibOptions) => "/" + ts.getDefaultLibFileName(defaultLibOptions), + writeFile: (fileName, content) => results.set(fileName, content), + getCurrentDirectory: () => "/", + getDirectories: (_path) => [], + fileExists: () => true, + readFile: (fileName) => sources.get(fileName), + getCanonicalFileName: (fileName) => fileName, + useCaseSensitiveFileNames: () => true, + getNewLine: () => "\n", + getEnvironmentVariable: () => "", + resolveModuleNames: () => [], + }).emit(); + + return results.get(output); +} diff --git a/_plugins/scripts/webp.js b/_plugins/scripts/webp.js new file mode 100644 index 00000000..de8bc00a --- /dev/null +++ b/_plugins/scripts/webp.js @@ -0,0 +1,9 @@ +import sharp from "sharp"; + +export default async function ({ source }) { + const buffer = new Buffer(source, "base64"); + if (global.Bun && Bun.Image) { + return new Blob(buffer).image().webp().toBase64(); + } + return (await sharp(buffer).webp().toBuffer()).toString("base64"); +} diff --git a/_plugins/typescript.rb b/_plugins/typescript.rb new file mode 100644 index 00000000..b695f9e6 --- /dev/null +++ b/_plugins/typescript.rb @@ -0,0 +1,19 @@ +class TypescriptConverter < Jekyll::Converter + CACHE = Jekyll::Cache.new("TypescriptConverter") + + def matches(ext) + ext =~ /^\.ts$/i + end + + def output_ext(ext) + "#{ext}.js" + end + + def convert(content) + hash = Digest::SHA256.hexdigest(content) + unless TypescriptConverter::CACHE.key?(hash) + TypescriptConverter::CACHE[hash] = Script.call("typescript", code: content) + end + TypescriptConverter::CACHE[hash] + end +end diff --git a/_plugins/version_sort_filter.rb b/_plugins/version_sort_filter.rb index 8b49d525..c9b732bf 100644 --- a/_plugins/version_sort_filter.rb +++ b/_plugins/version_sort_filter.rb @@ -14,7 +14,7 @@ def valid_string_array?(array, property) end def version_to_numbers(version_string) - version_string.split('.').map { |n| n.to_i } + version_string.split(".").map { |n| n.to_i } end end diff --git a/assets/img/docs/shader/install_auto-16338577874692.png b/assets/img/docs/shader/install_auto-16338577874692.png deleted file mode 100644 index 3ba28002..00000000 Binary files a/assets/img/docs/shader/install_auto-16338577874692.png and /dev/null differ diff --git a/assets/img/docs/shader/select_optifine-16338577687881.png b/assets/img/docs/shader/select_optifine-16338577687881.png deleted file mode 100644 index 05c8125e..00000000 Binary files a/assets/img/docs/shader/select_optifine-16338577687881.png and /dev/null differ diff --git a/assets/js/extend.ts b/assets/js/extend.ts new file mode 100644 index 00000000..f3ae9326 --- /dev/null +++ b/assets/js/extend.ts @@ -0,0 +1,251 @@ +/* @frontmatter +layout: null +*/ + +interface Window { + $: JQueryStatic; + appendMeta: (text: string, icon: string) => void; + hits: (tag: string) => void; + settings: { + get: (key: string) => string; + _get: ( + id: string, + setting: Setting, + keys: [name: string, childName?: string], + ) => string; + set: (key: string, value: string) => void; + format: ( + key: string, + ) => [ + id: string, + setting: Setting, + keys: [name: string, childName?: string], + ]; + onChange: ( + key: string | string[], + handler: (value: string) => void, + ) => void; + }; +} + +type RadioSetting = { + type: "radio"; + default: string; + options: string[]; +}; + +type MultiRadioSetting = { + type: "multi-radio"; + children: Record; + options: string[]; +}; + +type Setting = RadioSetting | MultiRadioSetting; + +type SettingSchema = Record; + +(() => { + const settingNamePrefix = "HMCL_DOCS_SETTINGS_"; + const settingCache: Record = {}; + const settingSchema: SettingSchema = /*{%comment%}*/ {}; /*{%endcomment%}*/ + /**{{'/'}}{{ site.data.settings | jsonify }};/**/ + const $settingEventBus = $({}); + + window.addEventListener("storage", ({ key, newValue }) => { + if ( + key !== null && + key.startsWith(settingNamePrefix) && + newValue !== null && + newValue !== settingCache[key] + ) { + settingCache[key] = newValue; + $settingEventBus.trigger(key, newValue); + } + }); + + const settings = (window.settings = { + format(key) { + const keys = key.toLocaleLowerCase().split("."); + if (keys.length > 2 || keys.length === 0) { + throw new Error("unknow key [" + key + "]"); + } + + const item = settingSchema[keys[0]]; + if (item === undefined) { + throw new Error("unknow key [" + key + "]"); + } + + let id = settingNamePrefix + keys[0].toUpperCase(); + if (item.type === "multi-radio") { + if (keys.length === 1) { + throw new Error("unknow key [" + key + "]"); + } + if (item.children[keys[1]] === undefined) { + throw new Error("unknow key [" + key + "]"); + } + id += "_" + keys[1].toUpperCase(); + } else if (keys.length === 2) { + throw new Error("unknow key [" + key + "]"); + } + return [id, item, keys as [name: string, childName?: string]]; + }, + set(key, value) { + const [id] = this.format(key); + if (settingCache[id] !== value) { + settingCache[id] = value; + localStorage.setItem(id, value); + $settingEventBus.trigger(id, value); + } + }, + get(key) { + const [id, config, keys] = this.format(key); + return this._get(id, config, keys); + }, + _get(id, setting, keys) { + if (settingCache[id] !== undefined) { + return settingCache[id]; + } + const storeValue = localStorage.getItem(id); + + if (setting.type === "multi-radio" || setting.type === "radio") { + if (storeValue !== null) { + for (const option of setting.options) { + if (storeValue === option) { + settingCache[id] = storeValue; + return storeValue; + } + } + } + + return setting.type === "radio" + ? setting.default + : setting.children[keys[1]!].default; + } else { + throw new Error("key not match type"); + } + }, + onChange(key, handler) { + if (Array.isArray(key)) { + for (const item of key) { + this.onChange(item, handler); + } + } else { + const [id, setting, keys] = this.format(key); + handler(this._get(id, setting, keys)); + $settingEventBus.on(id, (_, data) => handler(data)); + } + }, + }); + + const $skin = $("#skin"); + const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const applySkin = (skin: string) => { + $skin.attr( + "href", + "{{ '/assets/css/skins/' | relative_url }}" + skin + ".css", + ); + }; + + const autoSchemeHandler = () => { + applySkin( + settings.get("appearance_skin." + (darkQuery.matches ? "dark" : "light")), + ); + }; + + let currentModeIndex = 0; + const modeKeys = ["light", "dark", "auto"]; + const modeIcons = ["fa-sun", "fa-moon", "fa-computer"]; + + const $menu = $(".masthead .visible-links"); + let $switcher: JQuery | null = null; + if ($menu.length !== 0) { + $switcher = $("