Merge pull request '更新butterfly主题并更新hexo 8.1.0' (#1) from update-theme into master
Reviewed-on: https://git.biss.click.biss.click/biss/blog/pulls/1
This commit is contained in:
@@ -299,9 +299,6 @@ footer:
|
|||||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||||
<a href="/artitalk">说说</a> | <a href="https://status.biss.click/status/dashboard">网站监控</a>
|
<a href="/artitalk">说说</a> | <a href="https://status.biss.click/status/dashboard">网站监控</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
|
||||||
<a href="https://www.foreverblog.cn/" rel="noopener external nofollow noreferrer" target="_blank" > <img class="img-foreverblog" src="/image/footer/forever_logo_en_default_white.png" alt="" style="width:auto;height:21px;margin-top:6px"> </a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,7 +329,7 @@ footer:
|
|||||||
<div class="my-footer-content-column">
|
<div class="my-footer-content-column">
|
||||||
<ul id="menu-get-started" class="my-footer-menu-list">
|
<ul id="menu-get-started" class="my-footer-menu-list">
|
||||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||||
<a href="https://notbyai.fyi/" target="_blank" rel="external nofollow noopener noreferrer"><img class="img-not-ai" src="https://pic.biss.click/image/95a79ad4-5265-42d9-8ebc-0dde74928946.webp" alt="Written by Human, Not by AI"></a>
|
<a href="https://notbyai.fyi/" target="_blank" rel="external nofollow noopener noreferrer"><img class="img-not-ai" src="https://pic.biss.click/image/f1bb8627-9cdc-43b8-a609-d7fd10dcfd37.webp" alt="Written by Human, Not by AI"></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||||
<a href="/cc" ><img src="https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg" alt="署名-非商业性使用-相同方式共享 4.0 国际"></a>
|
<a href="/cc" ><img src="https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg" alt="署名-非商业性使用-相同方式共享 4.0 国际"></a>
|
||||||
|
|||||||
235
package-lock.json
generated
235
package-lock.json
generated
@@ -8,9 +8,9 @@
|
|||||||
"name": "hexo-site",
|
"name": "hexo-site",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.13.4",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.2.0",
|
||||||
"hexo": "^7.3.0",
|
"hexo": "^8.1.0",
|
||||||
"hexo-abbrlink": "^2.2.1",
|
"hexo-abbrlink": "^2.2.1",
|
||||||
"hexo-ai-summary-liushen": "^1.3.1",
|
"hexo-ai-summary-liushen": "^1.3.1",
|
||||||
"hexo-deployer-git": "^4.0.0",
|
"hexo-deployer-git": "^4.0.0",
|
||||||
@@ -407,12 +407,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "6.2.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
@@ -428,12 +431,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/archy": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/archy/-/archy-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -828,13 +825,6 @@
|
|||||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cuid": {
|
|
||||||
"version": "2.1.8",
|
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/cuid/-/cuid-2.1.8.tgz",
|
|
||||||
"integrity": "sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==",
|
|
||||||
"deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
@@ -1148,12 +1138,24 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-archy": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fast-archy/-/fast-archy-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-j8CNf0sCX92BogtUZAMPhMj7p3nv7xxeETwfpVWQNQocC5b21FAa6bUzfuhcNoEa4f2RtDe5U3uY0NriH9nA7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fast-equals/-/fast-equals-3.0.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fast-equals/-/fast-equals-3.0.3.tgz",
|
||||||
"integrity": "sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg==",
|
"integrity": "sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-text-table": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fast-text-table/-/fast-text-table-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KUwE3MizTzXwhrvTTEpWbug1ngV1zfjwzdxSkeWYGUoVGaaQoid+jxgg4zm4LB+OrtnD+X2xJFq7DCO3pc3fdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fetch-blob": {
|
"node_modules/fetch-blob": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
@@ -1452,41 +1454,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hexo": {
|
"node_modules/hexo": {
|
||||||
"version": "7.3.0",
|
"version": "8.1.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hexo/-/hexo-7.3.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hexo/-/hexo-8.1.1.tgz",
|
||||||
"integrity": "sha512-dOe8mzBKrvjubW5oBmyhcnQDpC+M2xmAMLae5K+o+SkHxyvAhShkS2VQZoTsOLIJKY6xilv7dzCjCvE7ol/NHQ==",
|
"integrity": "sha512-UnzT4ImjKzMMuVRsvudxrl7MkdZwKe4S9xJN5pQPK9c+K0G+fLFb9kB6CqZZwj2E5ne+QK0ls4XMKqTUbNR3RQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"abbrev": "^2.0.0",
|
"abbrev": "^3.0.0",
|
||||||
"archy": "^1.0.0",
|
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
|
"fast-archy": "^1.0.0",
|
||||||
|
"fast-text-table": "^1.0.1",
|
||||||
"hexo-cli": "^4.3.2",
|
"hexo-cli": "^4.3.2",
|
||||||
"hexo-front-matter": "^4.2.1",
|
"hexo-front-matter": "^4.2.1",
|
||||||
"hexo-fs": "^4.1.3",
|
"hexo-fs": "^5.0.0",
|
||||||
"hexo-i18n": "^2.0.0",
|
"hexo-i18n": "^2.0.0",
|
||||||
"hexo-log": "^4.0.1",
|
"hexo-log": "^4.1.0",
|
||||||
"hexo-util": "^3.3.0",
|
"hexo-util": "^4.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"js-yaml-js-types": "^1.0.0",
|
"js-yaml-js-types": "^1.0.1",
|
||||||
"micromatch": "^4.0.4",
|
"micromatch": "^4.0.8",
|
||||||
"moize": "^6.1.6",
|
"moize": "^6.1.6",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.46",
|
||||||
"nunjucks": "^3.2.3",
|
"nunjucks": "^3.2.4",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.1.1",
|
||||||
"pretty-hrtime": "^1.0.3",
|
"pretty-hrtime": "^1.0.3",
|
||||||
"resolve": "^1.22.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"strip-ansi": "^6.0.0",
|
|
||||||
"text-table": "^0.2.0",
|
|
||||||
"tildify": "^2.0.0",
|
"tildify": "^2.0.0",
|
||||||
"titlecase": "^1.1.3",
|
"titlecase": "^1.1.3",
|
||||||
"warehouse": "^5.0.1"
|
"warehouse": "^6.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"hexo": "bin/hexo"
|
"hexo": "bin/hexo"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2421,6 +2422,115 @@
|
|||||||
"integrity": "sha512-tbo2P9xRWEKQmRf7+XuPjx9It1MnaE26nA+EEb2DN39gK1x+26W7Nm4Iyp4AugQjBWYYDx7OLn4gp1WFxQpQew==",
|
"integrity": "sha512-tbo2P9xRWEKQmRf7+XuPjx9It1MnaE26nA+EEb2DN39gK1x+26W7Nm4Iyp4AugQjBWYYDx7OLn4gp1WFxQpQew==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/hexo/node_modules/abbrev": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/abbrev/-/abbrev-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/hexo-fs": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hexo-fs/-/hexo-fs-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-Zm4m21coQA7TtL9JL0oNZF+ypY/HbqwVdaqGqoKD+GEV7HH2Y1YObNBgl1o/lV1tzStC8f15AoRcHdS2jiWp0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "^3.7.2",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"graceful-fs": "^4.2.10",
|
||||||
|
"hexo-util": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/hexo-fs/node_modules/hexo-util": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hexo-util/-/hexo-util-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-YvGngXijE2muEh5L/VI4Fmjqb+/yAkmY+VuyhWVoRwQu1X7bmWodsfYRXX7CUYhi5LqsvH8FAe/yBW1+f6ZX4Q==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camel-case": "^4.1.2",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"highlight.js": "^11.6.0",
|
||||||
|
"htmlparser2": "^9.0.0",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
|
"strip-indent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/hexo-fs/node_modules/htmlparser2": {
|
||||||
|
"version": "9.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||||
|
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.1.0",
|
||||||
|
"entities": "^4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/hexo-util": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hexo-util/-/hexo-util-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-oXKXBs1HZ2Wu/eq0paAVqtCmAEcqJPZ4xxSVRuwAplm1hFU41Ul53WA5bmYMEz9Dp+OJ/SdchjXRmYlbGJdt3w==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camel-case": "^4.1.2",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"highlight.js": "^11.6.0",
|
||||||
|
"htmlparser2": "^10.0.0",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
|
"strip-indent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hexo/node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/highlight.js": {
|
"node_modules/highlight.js": {
|
||||||
"version": "11.11.1",
|
"version": "11.11.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/highlight.js/-/highlight.js-11.11.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
@@ -3152,6 +3262,24 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.4",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/negotiator/-/negotiator-0.6.4.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
@@ -3954,15 +4082,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "7.1.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^6.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-indent": {
|
"node_modules/strip-indent": {
|
||||||
@@ -4040,12 +4171,6 @@
|
|||||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/text-table": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/text-table/-/text-table-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/through2": {
|
"node_modules/through2": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/through2/-/through2-4.0.2.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/through2/-/through2-4.0.2.tgz",
|
||||||
@@ -4241,22 +4366,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/warehouse": {
|
"node_modules/warehouse": {
|
||||||
"version": "5.0.1",
|
"version": "6.0.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/warehouse/-/warehouse-5.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/warehouse/-/warehouse-6.0.0.tgz",
|
||||||
"integrity": "sha512-5BQEQP56bPY+cqocTho4syazuGgSoyKd0y3PsS2j8tGN10HH+CEfJSIY+KUw9D0k4jaVEFMXLz0KqCiUzTYb8A==",
|
"integrity": "sha512-eOlhyPp5HC951QVDgAeculpSxvfum4UZAbqXSzocqbdiziTseFJFYxmhsKqrQ3wrwgtzPGgkVN2rPL31YLQ0SA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"cuid": "^2.1.8",
|
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"hexo-log": "^4.0.1",
|
"hexo-log": "^4.0.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"jsonparse": "^1.3.1",
|
"jsonparse": "^1.3.1",
|
||||||
|
"nanoid": "^3.3.7",
|
||||||
"rfdc": "^1.3.0",
|
"rfdc": "^1.3.0",
|
||||||
"through2": "^4.0.2"
|
"through2": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
"server": "hexo server"
|
"server": "hexo server"
|
||||||
},
|
},
|
||||||
"hexo": {
|
"hexo": {
|
||||||
"version": "7.3.0"
|
"version": "8.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.13.4",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.2.0",
|
||||||
"hexo": "^7.3.0",
|
"hexo": "^8.1.0",
|
||||||
"hexo-abbrlink": "^2.2.1",
|
"hexo-abbrlink": "^2.2.1",
|
||||||
"hexo-ai-summary-liushen": "^1.3.1",
|
"hexo-ai-summary-liushen": "^1.3.1",
|
||||||
"hexo-deployer-git": "^4.0.0",
|
"hexo-deployer-git": "^4.0.0",
|
||||||
|
|||||||
202
themes/butterfly/LICENSE
Normal file
202
themes/butterfly/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
193
themes/butterfly/README.md
Normal file
193
themes/butterfly/README.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<div align="right">
|
||||||
|
<a title="中文" href="/README_CN.md">中文</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
|
||||||
|
|
||||||
|
# hexo-theme-butterfly
|
||||||
|
|
||||||
|
A modern, elegant and feature-rich theme for Hexo
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
📢 **Demo**: [Butterfly Official](https://butterfly.js.org/) | [CrazyWong's Blog](https://blog.crazywong.com/)
|
||||||
|
|
||||||
|
📖 **Documentation**: [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/) | [中文文档](https://butterfly.js.org/posts/21cfbf15/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### 💾 Installation
|
||||||
|
|
||||||
|
#### Method 1: Git Installation (Recommended)
|
||||||
|
|
||||||
|
> 💡 **Tip**: If GitHub access is slow in mainland China, you can use the [Gitee Mirror](https://gitee.com/immyw/hexo-theme-butterfly.git)
|
||||||
|
|
||||||
|
Execute in your Hexo blog root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install stable version (recommended)
|
||||||
|
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install development version (early access to new features)
|
||||||
|
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Method 2: NPM Installation
|
||||||
|
|
||||||
|
> ⚠️ **Note**: NPM installation only supports Hexo 5.0.0 and above
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install hexo-theme-butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚙️ Theme Configuration
|
||||||
|
|
||||||
|
1. **Enable Theme**: Modify your Hexo configuration file `_config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
theme: butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Dependencies**: If you haven't installed pug and stylus renderers, please run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install hexo-renderer-pug hexo-renderer-stylus --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Theme Features
|
||||||
|
|
||||||
|
### 🎨 Design Style
|
||||||
|
- [x] **Card-based Design** - Modern card-style layout
|
||||||
|
- [x] **Rounded/Square Design** - Customizable border styles
|
||||||
|
- [x] **Responsive Design** - Perfect adaptation to all screen sizes
|
||||||
|
- [x] **Two-column Layout** - Optimized reading experience
|
||||||
|
- [x] **Dark Mode** - Eye-friendly night mode
|
||||||
|
|
||||||
|
### 📝 Content Features
|
||||||
|
- [x] **Multi-level Menu** - Support for secondary navigation menus
|
||||||
|
- [x] **Reading Mode** - Focused article reading experience
|
||||||
|
- [x] **TOC Navigation** - Desktop and mobile TOC support
|
||||||
|
- [x] **Word Count** - Display article word count and reading time
|
||||||
|
- [x] **Related Articles** - Smart recommendation of related content
|
||||||
|
- [x] **Outdated Reminder** - Automatic article update status alerts
|
||||||
|
- [x] **Traditional/Simplified Chinese** - Support for Traditional and Simplified Chinese switching
|
||||||
|
- [x] **Tag Plugins** - Rich tag plugin support
|
||||||
|
|
||||||
|
### 🔍 Search & Navigation
|
||||||
|
- [x] **Multiple Search Options** - Algolia Search / Local Search / Docsearch
|
||||||
|
- [x] **Built-in 404** - Beautiful 404 error page
|
||||||
|
- [x] **Pjax Support** - Smooth page transition experience
|
||||||
|
|
||||||
|
### 🎨 Code Display
|
||||||
|
- [x] **Syntax Highlighting** - Built-in multiple themes (darker/pale night/light/ocean)
|
||||||
|
- [x] **Code Features** - Language display/fold expand/copy button/auto-wrap
|
||||||
|
- [x] **Math Formulas** - Support for Mathjax and Katex
|
||||||
|
|
||||||
|
### 💬 Social Interaction
|
||||||
|
- [x] **Multiple Comment Systems** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk etc.
|
||||||
|
- [x] **Dual Comments Support** - Enable two comment systems simultaneously
|
||||||
|
- [x] **Share Features** - Sharejs/Addtoany sharing components
|
||||||
|
- [x] **Live Chat** - Chatra/Tidio/Crisp instant messaging
|
||||||
|
|
||||||
|
### 📊 Analytics & Statistics
|
||||||
|
- [x] **Visit Statistics** - Busuanzi counter
|
||||||
|
- [x] **Site Analytics** - Google Analytics/Baidu Analytics/Cloudflare Analytics/Microsoft Clarity/Umami
|
||||||
|
- [x] **Webmaster Verification** - Major search engine verification
|
||||||
|
- [x] **Ad Support** - Google AdSense/custom ad slots
|
||||||
|
|
||||||
|
### 🎪 Visual Effects
|
||||||
|
- [x] **Typing Effects** - activate_power_mode animations
|
||||||
|
- [x] **Background Effects** - Static ribbons/dynamic ribbons/floating ribbons/Canvas Nest
|
||||||
|
- [x] **Mouse Effects** - Fireworks/hearts/text click effects
|
||||||
|
- [x] **Loading Animations** - Preloader and pace.js progress bars
|
||||||
|
- [x] **Image Effects** - Medium Zoom/Fancybox image lightbox
|
||||||
|
- [x] **Lazy Loading** - Image lazy loading optimization
|
||||||
|
|
||||||
|
### 🛠️ Advanced Features
|
||||||
|
- [x] **PWA Support** - Progressive Web App
|
||||||
|
- [x] **Copy Protection** - Disable text copying/copyright info append
|
||||||
|
- [x] **Theme Customization** - Custom site color schemes
|
||||||
|
- [x] **Chart Support** - Mermaid flowcharts/Chart.js data charts
|
||||||
|
- [x] **Music Notation** - ABCJS music notation support
|
||||||
|
- [x] **Music Player** - APlayer/Meting music playback
|
||||||
|
- [x] **Article Series** - Series article organization
|
||||||
|
- [x] **Instantpage** - Page preloading acceleration
|
||||||
|
- [x] **Snackbar** - Elegant notification messages
|
||||||
|
|
||||||
|
## 🤝 Contributors
|
||||||
|
|
||||||
|
Thanks to all the developers who have contributed to the Butterfly theme!
|
||||||
|
|
||||||
|
[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
|
||||||
|
|
||||||
|
## 📸 Screenshots
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## ⭐ Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
|
||||||
|
|
||||||
|
## 🤝 Building a Better Theme Together
|
||||||
|
|
||||||
|
We believe **the power of open source comes from everyone's participation**! Whether you're a developer, designer, or user, you can contribute to the development of the Butterfly theme.
|
||||||
|
|
||||||
|
### 💬 Get Help & Support
|
||||||
|
|
||||||
|
- 🐛 **Found a bug?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - Let's solve it together!
|
||||||
|
- 💡 **Have ideas?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - Share your creative ideas!
|
||||||
|
- 📚 **Learning to use?** → [Official Documentation](https://butterfly.js.org/) - Detailed usage guide
|
||||||
|
- 💬 **Real-time discussion?** → [Telegram Group](https://t.me/bu2fly) - Chat with community members
|
||||||
|
|
||||||
|
### 🎯 Contributing
|
||||||
|
|
||||||
|
Want to make Butterfly better? We welcome any form of contribution:
|
||||||
|
|
||||||
|
- **🔧 Code Contributions** - Fix bugs, add new features, optimize performance
|
||||||
|
- **📝 Documentation** - Improve docs, translate content, write tutorials
|
||||||
|
- **🎨 Design Suggestions** - UI/UX improvements, theme colors, icon design
|
||||||
|
- **🧪 Testing & Feedback** - Test new features, report issues, provide user experience
|
||||||
|
- **💰 Financial Support** - [Sponsor the Project](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - Support long-term development
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the [Apache 2.0](LICENSE) License.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
This theme is developed based on [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody). Thanks to the original author for their excellent work that provided inspiration and foundation!
|
||||||
|
|
||||||
|
Thanks to all friends who have contributed to the development of the Butterfly theme. Your support has made this theme continuously improve and progress.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**✨ If this theme helps you, please give us a ⭐ Star! ✨**
|
||||||
|
</div>
|
||||||
193
themes/butterfly/README_CN.md
Normal file
193
themes/butterfly/README_CN.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<div align="right">
|
||||||
|
<a title="English" href="/README.md">English</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="./source/img/butterfly-icon.png" width="150" height="150" alt="Butterfly Logo" />
|
||||||
|
|
||||||
|
# hexo-theme-butterfly
|
||||||
|
|
||||||
|
一個適用於 Hexo 的現代化、美觀且功能豐富的主題
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
📢 **在線預覽**: [Butterfly 官方](https://butterfly.js.org/) | [CrazyWong 博客](https://blog.crazywong.com/)
|
||||||
|
|
||||||
|
📖 **完整文檔**: [中文文檔](https://butterfly.js.org/posts/21cfbf15/) | [English Docs](https://butterfly.js.org/en/posts/butterfly-docs-en-get-started/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速開始
|
||||||
|
|
||||||
|
### 💾 安裝方式
|
||||||
|
|
||||||
|
#### 方式一:Git 安裝(推薦)
|
||||||
|
|
||||||
|
> 💡 **提示**: 如果您在中國大陸訪問 GitHub 速度較慢,可以使用 [Gitee 鏡像](https://gitee.com/immyw/hexo-theme-butterfly.git)
|
||||||
|
|
||||||
|
在您的 Hexo 博客根目錄下執行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安裝穩定版本(推薦)
|
||||||
|
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安裝開發版本(搶先體驗新功能)
|
||||||
|
git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式二:NPM 安裝
|
||||||
|
|
||||||
|
> ⚠️ **注意**: NPM 安裝方式僅支援 Hexo 5.0.0 及以上版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install hexo-theme-butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚙️ 主題配置
|
||||||
|
|
||||||
|
1. **啟用主題**: 修改您的 Hexo 配置檔案 `_config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
theme: butterfly
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安裝依賴**: 如果您尚未安裝 pug 和 stylus 渲染器,請執行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install hexo-renderer-pug hexo-renderer-stylus --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ 主題特色
|
||||||
|
|
||||||
|
### 🎨 設計風格
|
||||||
|
- [x] **卡片化設計** - 現代化的卡片式佈局
|
||||||
|
- [x] **圓角/直角設計** - 支援自訂邊框樣式
|
||||||
|
- [x] **響應式設計** - 完美適配各種螢幕尺寸
|
||||||
|
- [x] **雙欄佈局** - 優化的閱讀體驗
|
||||||
|
- [x] **深色模式** - 護眼的夜間模式
|
||||||
|
|
||||||
|
### 📝 內容功能
|
||||||
|
- [x] **多級選單** - 支援二級導航選單
|
||||||
|
- [x] **閱讀模式** - 專注的文章閱讀體驗
|
||||||
|
- [x] **目錄導航** - 電腦和手機雙端支援 TOC
|
||||||
|
- [x] **字數統計** - 顯示文章字數和閱讀時間
|
||||||
|
- [x] **相關文章** - 智能推薦相關內容
|
||||||
|
- [x] **過期提醒** - 自動提示文章更新狀態
|
||||||
|
- [x] **簡繁轉換** - 支援繁體中文和簡體中文切換
|
||||||
|
- [x] **標籤外掛** - 豐富的標籤外掛支持
|
||||||
|
|
||||||
|
### 🔍 搜尋與導航
|
||||||
|
- [x] **多種搜尋** - Algolia 搜尋 / 本地搜尋 / Docsearch
|
||||||
|
- [x] **內建 404** - 美觀的 404 錯誤頁面
|
||||||
|
- [x] **Pjax 支援** - 流暢的頁面切換體驗
|
||||||
|
|
||||||
|
### 🎨 程式碼展示
|
||||||
|
- [x] **語法高亮** - 內建多種主題(darker/pale night/light/ocean)
|
||||||
|
- [x] **程式碼功能** - 語言顯示/摺疊展開/複製按鈕/自動換行
|
||||||
|
- [x] **數學公式** - 支援 Mathjax 和 Katex
|
||||||
|
|
||||||
|
### 💬 社交互動
|
||||||
|
- [x] **多元評論系統** - Disqus/Gitalk/Valine/Waline/Twikoo/Giscus/Artalk 等
|
||||||
|
- [x] **雙評論支援** - 可同時啟用兩套評論系統
|
||||||
|
- [x] **分享功能** - Sharejs/Addtoany 分享套件
|
||||||
|
- [x] **線上客服** - Chatra/Tidio/Crisp 即時聊天
|
||||||
|
|
||||||
|
### 📊 數據分析
|
||||||
|
- [x] **訪問統計** - 不蒜子計數器
|
||||||
|
- [x] **網站分析** - Google Analytics/百度統計/Cloudflare Analytics/Microsoft Clarity/Umami
|
||||||
|
- [x] **站長驗證** - 各大搜尋引擎驗證
|
||||||
|
- [x] **廣告支援** - Google AdSense/自訂廣告位
|
||||||
|
|
||||||
|
### 🎪 視覺效果
|
||||||
|
- [x] **打字特效** - activate_power_mode 動畫
|
||||||
|
- [x] **背景特效** - 靜態彩帶/動態彩帶/飄帶效果/Canvas Nest
|
||||||
|
- [x] **滑鼠特效** - 煙花/愛心/文字點擊效果
|
||||||
|
- [x] **載入動畫** - Preloader 和 pace.js 進度條
|
||||||
|
- [x] **圖片效果** - Medium Zoom/Fancybox 圖片燈箱
|
||||||
|
- [x] **懶載入** - 圖片延遲載入優化
|
||||||
|
|
||||||
|
### 🛠️ 進階功能
|
||||||
|
- [x] **PWA 支援** - 漸進式網頁應用
|
||||||
|
- [x] **複製保護** - 可關閉文字複製/版權資訊追加
|
||||||
|
- [x] **主題定製** - 自訂網站配色方案
|
||||||
|
- [x] **圖表支援** - Mermaid 流程圖/Chart.js 數據圖表
|
||||||
|
- [x] **音樂符號** - ABCJS 音樂記譜法支援
|
||||||
|
- [x] **音樂播放器** - APlayer/Meting 音樂播放功能
|
||||||
|
- [x] **系列文章** - 系列文章組織功能
|
||||||
|
- [x] **Instantpage** - 頁面預載入加速
|
||||||
|
- [x] **Snackbar** - 優雅的提示訊息
|
||||||
|
|
||||||
|
## 🤝 貢獻者
|
||||||
|
|
||||||
|
感謝所有為 Butterfly 主題做出貢獻的開發者們!
|
||||||
|
|
||||||
|
[](https://github.com/jerryc127/hexo-theme-butterfly/graphs/contributors)
|
||||||
|
|
||||||
|
## 📸 主題截圖
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## ⭐ Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#jerryc127/hexo-theme-butterfly&Date)
|
||||||
|
|
||||||
|
## 🤝 一起構建更美好的主題
|
||||||
|
|
||||||
|
我們相信,**開源的力量來自於每一個人的參與**!無論您是開發者、設計師還是用戶,都可以為 Butterfly 主題的發展貢獻力量。
|
||||||
|
|
||||||
|
### 💬 獲取幫助與支援
|
||||||
|
|
||||||
|
- 🐛 **發現問題?** → [GitHub Issues](https://github.com/jerryc127/hexo-theme-butterfly/issues) - 讓我們一起解決!
|
||||||
|
- 💡 **有好想法?** → [GitHub Discussions](https://github.com/jerryc127/hexo-theme-butterfly/discussions) - 分享您的創意想法!
|
||||||
|
- 📚 **學習使用?** → [官方文檔](https://butterfly.js.org/) - 詳細的使用指南
|
||||||
|
- 💬 **即時討論?** → [Telegram 群組](https://t.me/bu2fly) - 與社群成員實時交流
|
||||||
|
|
||||||
|
### 🎯 參與貢獻
|
||||||
|
|
||||||
|
想要讓 Butterfly 變得更好嗎?我們歡迎您的任何形式的貢獻:
|
||||||
|
|
||||||
|
- **🔧 代碼貢獻** - 修復 Bug、添加新功能、優化性能
|
||||||
|
- **📝 文檔完善** - 改進文檔、翻譯內容、撰寫教程
|
||||||
|
- **🎨 設計建議** - UI/UX 改進、主題配色、圖示設計
|
||||||
|
- **🧪 測試反饋** - 測試新功能、回報問題、提供使用體驗
|
||||||
|
- **💰 資金支援** - [贊助項目](https://buy.stripe.com/3cs6rP6YA91sbbG5kk) - 支持長期發展
|
||||||
|
|
||||||
|
## 📄 授權條款
|
||||||
|
|
||||||
|
本專案採用 [Apache 2.0](LICENSE) 授權條款。
|
||||||
|
|
||||||
|
## 🙏 致敬與感謝
|
||||||
|
|
||||||
|
本主題基於 [hexo-theme-melody](https://github.com/Molunerfinn/hexo-theme-melody) 進行開發,感謝原作者的精彩創作為我們提供了靈感與基礎!
|
||||||
|
|
||||||
|
感謝所有為 Butterfly 主題發展做出貢獻的朋友們,是你們的支持讓這個主題能夠不斷完善與進步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**✨ 如果這個主題對您有幫助,請給我們一個 ⭐ Star!✨**
|
||||||
|
</div>
|
||||||
@@ -319,6 +319,7 @@ aside:
|
|||||||
# If set 0 will show all
|
# If set 0 will show all
|
||||||
limit: 40
|
limit: 40
|
||||||
color: false
|
color: false
|
||||||
|
custom_colors:
|
||||||
# Order of tags, random/name/length
|
# Order of tags, random/name/length
|
||||||
orderby: random
|
orderby: random
|
||||||
# Sort of order. 1, asc for ascending; -1, desc for descending
|
# Sort of order. 1, asc for ascending; -1, desc for descending
|
||||||
@@ -939,6 +940,10 @@ mermaid:
|
|||||||
theme:
|
theme:
|
||||||
light: default
|
light: default
|
||||||
dark: dark
|
dark: dark
|
||||||
|
# Enable "Open in New Tab" button to view diagram in a separate window
|
||||||
|
open_in_new_tab: true
|
||||||
|
# Enable zoom and pan interactions on diagrams
|
||||||
|
zoom_pan: true
|
||||||
|
|
||||||
# chartjs
|
# chartjs
|
||||||
# see https://www.chartjs.org/docs/latest/
|
# see https://www.chartjs.org/docs/latest/
|
||||||
|
|||||||
@@ -72,20 +72,16 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight = 'undefined'
|
let highlightProvider = config.syntax_highlighter || (config.highlight.enable ? 'highlight.js' : config.prismjs.enable ? 'prismjs' : null)
|
||||||
let syntaxHighlighter = config.syntax_highlighter
|
const { copy, language, height_limit, fullpage, macStyle, shrink } = theme.code_blocks
|
||||||
let highlightEnable = syntaxHighlighter ? ['highlight.js', 'prismjs'].includes(syntaxHighlighter) : (config.highlight.enable || config.prismjs.enable)
|
let highlight = JSON.stringify({
|
||||||
if (highlightEnable) {
|
plugin: highlightProvider,
|
||||||
const { copy, language, height_limit, fullpage, macStyle } = theme.code_blocks
|
|
||||||
highlight = JSON.stringify({
|
|
||||||
plugin: syntaxHighlighter ? syntaxHighlighter : config.highlight.enable ? 'highlight.js' : 'prismjs',
|
|
||||||
highlightCopy: copy,
|
highlightCopy: copy,
|
||||||
highlightLang: language,
|
highlightLang: language,
|
||||||
highlightHeightLimit: height_limit,
|
highlightHeightLimit: height_limit,
|
||||||
highlightFullpage: fullpage,
|
highlightFullpage: fullpage,
|
||||||
highlightMacStyle: macStyle
|
highlightMacStyle: macStyle
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
script.
|
script.
|
||||||
const GLOBAL_CONFIG = {
|
const GLOBAL_CONFIG = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mixin indexPostUI()
|
mixin indexPostUI()
|
||||||
- const indexLayout = theme.index_layout
|
- const indexLayout = theme.index_layout
|
||||||
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : ''
|
- const masonryLayoutClass = [6, 7].includes(indexLayout) ? 'masonry' : ''
|
||||||
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
||||||
.recent-post-items
|
.recent-post-items
|
||||||
each article, index in page.posts.data
|
each article, index in page.posts.data
|
||||||
@@ -8,17 +8,17 @@ mixin indexPostUI()
|
|||||||
- const link = article.link || article.path
|
- const link = article.link || article.path
|
||||||
- const title = article.title || _p('no_title')
|
- const title = article.title || _p('no_title')
|
||||||
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
- const leftOrRight = indexLayout === 3 ? (index % 2 === 0 ? 'left' : 'right') : (indexLayout === 2 ? 'right' : '')
|
||||||
- const post_cover = article.cover
|
- const postCover = article.cover
|
||||||
- const no_cover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
- const noCover = article.cover === false || !theme.cover.index_enable ? 'no-cover' : ''
|
||||||
|
|
||||||
if post_cover && theme.cover.index_enable
|
if postCover && theme.cover.index_enable
|
||||||
.post_cover(class=leftOrRight)
|
.post_cover(class=leftOrRight)
|
||||||
a(href=url_for(link) title=title)
|
a(href=url_for(link) title=title)
|
||||||
if article.cover_type === 'img'
|
if article.cover_type === 'img'
|
||||||
img.post-bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
img.post-bg(src=url_for(postCover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
|
||||||
else
|
else
|
||||||
div.post-bg(style=`background: ${post_cover}`)
|
div.post-bg(style=`background: ${postCover}`)
|
||||||
.recent-post-info(class=no_cover)
|
.recent-post-info(class=noCover)
|
||||||
a.article-title(href=url_for(link) title=title)
|
a.article-title(href=url_for(link) title=title)
|
||||||
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
if globalPageType === 'home' && (article.top || article.sticky > 0)
|
||||||
i.fas.fa-thumbtack.sticky
|
i.fas.fa-thumbtack.sticky
|
||||||
@@ -35,13 +35,13 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label=_p('post.updated')
|
span.article-meta-label=_p('post.updated')
|
||||||
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
|
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated))= date(article.updated, config.date_format)
|
||||||
else
|
else
|
||||||
- const data_type_updated = theme.post_meta.page.date_type === 'updated'
|
- const isUpdatedType = theme.post_meta.page.date_type === 'updated'
|
||||||
- const date_type = data_type_updated ? 'updated' : 'date'
|
- const dateType = isUpdatedType ? 'updated' : 'date'
|
||||||
- const date_icon = data_type_updated ? 'fas fa-history' : 'far fa-calendar-alt'
|
- const dateIcon = isUpdatedType ? 'fas fa-history' : 'far fa-calendar-alt'
|
||||||
- const date_title = data_type_updated ? _p('post.updated') : _p('post.created')
|
- const dateTitle = isUpdatedType ? _p('post.updated') : _p('post.created')
|
||||||
i(class=date_icon)
|
i(class=dateIcon)
|
||||||
span.article-meta-label= date_title
|
span.article-meta-label= dateTitle
|
||||||
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]))= date(article[date_type], config.date_format)
|
time(datetime=date_xml(article[dateType]) title=dateTitle + ' ' + full_date(article[dateType]))= date(article[dateType], config.date_format)
|
||||||
if theme.post_meta.page.categories && article.categories.data.length > 0
|
if theme.post_meta.page.categories && article.categories.data.length > 0
|
||||||
span.article-meta
|
span.article-meta
|
||||||
span.article-meta-separator |
|
span.article-meta-separator |
|
||||||
@@ -69,7 +69,10 @@ mixin indexPostUI()
|
|||||||
span.article-meta-label= ' ' + _p('card_post_count')
|
span.article-meta-label= ' ' + _p('card_post_count')
|
||||||
|
|
||||||
if theme.comments.card_post_count && theme.comments.use
|
if theme.comments.card_post_count && theme.comments.use
|
||||||
case theme.comments.use[0]
|
- const commentSystem = theme.comments.use[0]
|
||||||
|
- const commentLink = url_for(link) + '#post-comment'
|
||||||
|
|
||||||
|
case commentSystem
|
||||||
when 'Disqus'
|
when 'Disqus'
|
||||||
when 'Disqusjs'
|
when 'Disqusjs'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
@@ -77,30 +80,30 @@ mixin indexPostUI()
|
|||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Valine'
|
when 'Valine'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.valine-comment-count(data-xid=url_for(link))
|
span.valine-comment-count(data-xid=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Waline'
|
when 'Waline'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.waline-comment-count(data-path=url_for(link))
|
span.waline-comment-count(data-path=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Twikoo'
|
when 'Twikoo'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a.twikoo-count(href=url_for(link) + '#post-comment')
|
a.twikoo-count(href=commentLink)
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Facebook Comments'
|
when 'Facebook Comments'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
span.fb-comments-count(data-href=urlNoIndex(article.permalink))
|
||||||
when 'Remark42'
|
when 'Remark42'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
span.remark42__counter(data-url=urlNoIndex(article.permalink))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
when 'Artalk'
|
when 'Artalk'
|
||||||
+countBlockInIndex
|
+countBlockInIndex
|
||||||
a(href=url_for(link) + '#post-comment')
|
a(href=commentLink)
|
||||||
span.artalk-count(data-page-key=url_for(link))
|
span.artalk-count(data-page-key=url_for(link))
|
||||||
i.fa-solid.fa-spinner.fa-spin
|
i.fa-solid.fa-spinner.fa-spin
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
.tag-cloud-list.text-center
|
.tag-cloud-list.text-center
|
||||||
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'})
|
!=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors})
|
||||||
<div id="tags-chart" data-length="10" style="height: 300px; padding: 10px;"></div>
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ script.
|
|||||||
const options = { ...params, responsive: "resize" }
|
const options = { ...params, responsive: "resize" }
|
||||||
|
|
||||||
// Render the music score using ABCJS.renderAbc
|
// Render the music score using ABCJS.renderAbc
|
||||||
ABCJS.renderAbc(ele, ele.innerHTML, options)
|
ABCJS.renderAbc(ele, ele.textContent, options)
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,275 @@
|
|||||||
script.
|
script.
|
||||||
(() => {
|
(() => {
|
||||||
|
const parseViewBox = viewBox => {
|
||||||
|
if (!viewBox) return null
|
||||||
|
const parts = viewBox.trim().split(/[\s,]+/).map(n => Number(n))
|
||||||
|
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return null
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSvgViewBox = svg => {
|
||||||
|
const attr = parseViewBox(svg.getAttribute('viewBox'))
|
||||||
|
if (attr) return attr
|
||||||
|
|
||||||
|
// Fallback: use bbox to build a viewBox
|
||||||
|
try {
|
||||||
|
const bbox = svg.getBBox()
|
||||||
|
if (bbox && bbox.width && bbox.height) return [bbox.x, bbox.y, bbox.width, bbox.height]
|
||||||
|
} catch (e) {
|
||||||
|
// getBBox may fail on some edge cases; ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = Number(svg.getAttribute('width')) || 0
|
||||||
|
const h = Number(svg.getAttribute('height')) || 0
|
||||||
|
if (w > 0 && h > 0) return [0, 0, w, h]
|
||||||
|
return [0, 0, 100, 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSvgViewBox = (svg, vb) => {
|
||||||
|
svg.setAttribute('viewBox', `${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v))
|
||||||
|
|
||||||
|
const openSvgInNewTab = ({ source, initViewBox }) => {
|
||||||
|
const getClonedSvg = () => {
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
const template = document.createElement('template')
|
||||||
|
template.innerHTML = source.trim()
|
||||||
|
const svg = template.content.querySelector('svg')
|
||||||
|
return svg ? svg.cloneNode(true) : null
|
||||||
|
}
|
||||||
|
if (source && typeof source.cloneNode === 'function') {
|
||||||
|
return source.cloneNode(true)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = getClonedSvg()
|
||||||
|
if (!clone) return
|
||||||
|
if (initViewBox && initViewBox.length === 4) {
|
||||||
|
clone.setAttribute('viewBox', initViewBox.join(' '))
|
||||||
|
}
|
||||||
|
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
||||||
|
if (!clone.getAttribute('xmlns:xlink') && clone.outerHTML.includes('xlink:')) {
|
||||||
|
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
||||||
|
}
|
||||||
|
// inject background to match current theme
|
||||||
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
const bg = getComputedStyle(document.body).backgroundColor || (isDark ? '#1e1e1e' : '#ffffff')
|
||||||
|
if (!clone.style.background) clone.style.background = bg
|
||||||
|
|
||||||
|
const serializer = new XMLSerializer()
|
||||||
|
const svgSource = serializer.serializeToString(clone)
|
||||||
|
const htmlSource = `<!doctype html><html><head><meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
html, body { width: 100%; height: 100%; margin: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; }
|
||||||
|
svg { max-width: 100%; max-height: 100%; height: auto; width: auto; }
|
||||||
|
</style>
|
||||||
|
</head><body>${svgSource}</body></html>`
|
||||||
|
const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
window.open(url, '_blank', 'noopener')
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachMermaidViewerButton = wrap => {
|
||||||
|
let btn = wrap.querySelector('.mermaid-open-btn')
|
||||||
|
if (!btn) {
|
||||||
|
btn = document.createElement('button')
|
||||||
|
btn.type = 'button'
|
||||||
|
btn.className = 'mermaid-open-btn'
|
||||||
|
wrap.appendChild(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.innerHTML = '<i class="fa fa-search fa-fw" aria-hidden="true"></i>'
|
||||||
|
|
||||||
|
if (!btn.__mermaidViewerBound) {
|
||||||
|
btn.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')
|
||||||
|
if (!svg) return
|
||||||
|
const initViewBox = wrap.__mermaidInitViewBox
|
||||||
|
if (typeof svg === 'string') {
|
||||||
|
openSvgInNewTab({ source: svg, initViewBox })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openSvgInNewTab({ source: svg, initViewBox })
|
||||||
|
})
|
||||||
|
btn.__mermaidViewerBound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates)
|
||||||
|
const zoomAtPoint = (vb, factor, px, py) => {
|
||||||
|
const w = vb[2] * factor
|
||||||
|
const h = vb[3] * factor
|
||||||
|
const nx = px - (px - vb[0]) * factor
|
||||||
|
const ny = py - (py - vb[1]) * factor
|
||||||
|
return [nx, ny, w, h]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initMermaidGestures = wrap => {
|
||||||
|
const svg = wrap.querySelector('svg')
|
||||||
|
if (!svg) return
|
||||||
|
|
||||||
|
// Ensure viewBox exists so gestures always work
|
||||||
|
const initVb = getSvgViewBox(svg)
|
||||||
|
wrap.__mermaidInitViewBox = initVb
|
||||||
|
wrap.__mermaidCurViewBox = initVb.slice()
|
||||||
|
setSvgViewBox(svg, initVb)
|
||||||
|
|
||||||
|
// Avoid binding multiple times on themeChange/pjax
|
||||||
|
if (wrap.__mermaidGestureBound) return
|
||||||
|
wrap.__mermaidGestureBound = true
|
||||||
|
|
||||||
|
// Helper: map client (viewport) coordinate -> viewBox coordinate
|
||||||
|
const clientToViewBox = (clientX, clientY) => {
|
||||||
|
const rect = svg.getBoundingClientRect()
|
||||||
|
const vb = wrap.__mermaidCurViewBox || getSvgViewBox(svg)
|
||||||
|
const x = vb[0] + (clientX - rect.left) * (vb[2] / rect.width)
|
||||||
|
const y = vb[1] + (clientY - rect.top) * (vb[3] / rect.height)
|
||||||
|
return { x, y, rect, vb }
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
pointers: new Map(),
|
||||||
|
startVb: null,
|
||||||
|
startDist: 0,
|
||||||
|
startCenter: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clampVb = vb => {
|
||||||
|
const init = wrap.__mermaidInitViewBox || vb
|
||||||
|
const minW = init[2] * 0.1
|
||||||
|
const maxW = init[2] * 10
|
||||||
|
const minH = init[3] * 0.1
|
||||||
|
const maxH = init[3] * 10
|
||||||
|
vb[2] = clamp(vb[2], minW, maxW)
|
||||||
|
vb[3] = clamp(vb[3], minH, maxH)
|
||||||
|
return vb
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCurVb = vb => {
|
||||||
|
vb = clampVb(vb)
|
||||||
|
wrap.__mermaidCurViewBox = vb
|
||||||
|
setSvgViewBox(svg, vb)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerDown = e => {
|
||||||
|
// Allow only primary button for mouse
|
||||||
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||||||
|
svg.setPointerCapture(e.pointerId)
|
||||||
|
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||||
|
|
||||||
|
if (state.pointers.size === 1) {
|
||||||
|
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||||
|
} else if (state.pointers.size === 2) {
|
||||||
|
const pts = [...state.pointers.values()]
|
||||||
|
const dx = pts[0].x - pts[1].x
|
||||||
|
const dy = pts[0].y - pts[1].y
|
||||||
|
state.startDist = Math.hypot(dx, dy)
|
||||||
|
state.startVb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||||
|
state.startCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = e => {
|
||||||
|
if (!state.pointers.has(e.pointerId)) return
|
||||||
|
state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
||||||
|
|
||||||
|
// Pan with 1 pointer
|
||||||
|
if (state.pointers.size === 1 && state.startVb) {
|
||||||
|
const p = [...state.pointers.values()][0]
|
||||||
|
const prev = { x: e.clientX - e.movementX, y: e.clientY - e.movementY }
|
||||||
|
// movementX/Y unreliable on touch, compute from stored last position
|
||||||
|
const last = wrap.__mermaidLastSinglePointer || p
|
||||||
|
const dxClient = p.x - last.x
|
||||||
|
const dyClient = p.y - last.y
|
||||||
|
wrap.__mermaidLastSinglePointer = p
|
||||||
|
|
||||||
|
const { rect } = clientToViewBox(p.x, p.y)
|
||||||
|
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||||
|
const dx = dxClient * (vb[2] / rect.width)
|
||||||
|
const dy = dyClient * (vb[3] / rect.height)
|
||||||
|
setCurVb([vb[0] - dx, vb[1] - dy, vb[2], vb[3]])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinch zoom with 2 pointers
|
||||||
|
if (state.pointers.size === 2 && state.startVb && state.startDist > 0) {
|
||||||
|
const pts = [...state.pointers.values()]
|
||||||
|
const dx = pts[0].x - pts[1].x
|
||||||
|
const dy = pts[0].y - pts[1].y
|
||||||
|
const dist = Math.hypot(dx, dy)
|
||||||
|
if (!dist) return
|
||||||
|
const factor = state.startDist / dist // dist bigger => zoom in (viewBox smaller)
|
||||||
|
|
||||||
|
const cx = (pts[0].x + pts[1].x) / 2
|
||||||
|
const cy = (pts[0].y + pts[1].y) / 2
|
||||||
|
const centerClient = { x: cx, y: cy }
|
||||||
|
|
||||||
|
const pxy = clientToViewBox(centerClient.x, centerClient.y)
|
||||||
|
const cpx = pxy.x
|
||||||
|
const cpy = pxy.y
|
||||||
|
|
||||||
|
const vb = zoomAtPoint(state.startVb, factor, cpx, cpy)
|
||||||
|
setCurVb(vb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerUpOrCancel = e => {
|
||||||
|
state.pointers.delete(e.pointerId)
|
||||||
|
if (state.pointers.size === 0) {
|
||||||
|
state.startVb = null
|
||||||
|
state.startDist = 0
|
||||||
|
state.startCenter = null
|
||||||
|
wrap.__mermaidLastSinglePointer = null
|
||||||
|
} else if (state.pointers.size === 1) {
|
||||||
|
// reset single pointer baseline to avoid jump
|
||||||
|
wrap.__mermaidLastSinglePointer = [...state.pointers.values()][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wheel zoom (mouse/trackpad)
|
||||||
|
const onWheel = e => {
|
||||||
|
// ctrlKey on mac trackpad pinch; we treat both as zoom
|
||||||
|
e.preventDefault()
|
||||||
|
const delta = e.deltaY
|
||||||
|
const zoomFactor = delta > 0 ? 1.1 : 0.9
|
||||||
|
const { x, y } = clientToViewBox(e.clientX, e.clientY)
|
||||||
|
const vb = (wrap.__mermaidCurViewBox || getSvgViewBox(svg)).slice()
|
||||||
|
setCurVb(zoomAtPoint(vb, zoomFactor, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDblClick = () => {
|
||||||
|
const init = wrap.__mermaidInitViewBox
|
||||||
|
if (!init) return
|
||||||
|
wrap.__mermaidCurViewBox = init.slice()
|
||||||
|
setSvgViewBox(svg, init)
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.addEventListener('pointerdown', onPointerDown)
|
||||||
|
svg.addEventListener('pointermove', onPointerMove)
|
||||||
|
svg.addEventListener('pointerup', onPointerUpOrCancel)
|
||||||
|
svg.addEventListener('pointercancel', onPointerUpOrCancel)
|
||||||
|
svg.addEventListener('wheel', onWheel, { passive: false })
|
||||||
|
svg.addEventListener('dblclick', onDblClick)
|
||||||
|
}
|
||||||
|
|
||||||
const runMermaid = ele => {
|
const runMermaid = ele => {
|
||||||
window.loadMermaid = true
|
window.loadMermaid = true
|
||||||
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}'
|
||||||
|
|
||||||
ele.forEach((item, index) => {
|
ele.forEach((item, index) => {
|
||||||
const mermaidSrc = item.firstElementChild
|
const mermaidSrc = item.firstElementChild
|
||||||
|
|
||||||
|
// Clear old render (themeChange/pjax will rerun)
|
||||||
|
const oldSvg = item.querySelector('svg')
|
||||||
|
if (oldSvg) oldSvg.remove()
|
||||||
|
item.__mermaidGestureBound = false
|
||||||
|
|
||||||
const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
const config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}
|
||||||
if (!config.theme) {
|
if (!config.theme) {
|
||||||
config.theme = theme
|
config.theme = theme
|
||||||
@@ -17,8 +281,12 @@ script.
|
|||||||
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||||
const renderMermaid = svg => {
|
const renderMermaid = svg => {
|
||||||
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
||||||
|
if (!{theme.mermaid.zoom_pan}) initMermaidGestures(item)
|
||||||
|
item.__mermaidOriginalSvg = svg
|
||||||
|
if (!{theme.mermaid.open_in_new_tab}) attachMermaidViewerButton(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// mermaid v9 and v10 compatibility
|
// mermaid v9 and v10 compatibility
|
||||||
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ if theme.aside.card_tags.enable
|
|||||||
i.fas.fa-tags
|
i.fas.fa-tags
|
||||||
span= _p('aside.card_tags')
|
span= _p('aside.card_tags')
|
||||||
|
|
||||||
- let { limit, orderby, order } = theme.aside.card_tags
|
- let { limit, orderby, order, custom_colors } = theme.aside.card_tags
|
||||||
- limit = limit === 0 ? 0 : limit || 40
|
- limit = limit === 0 ? 0 : limit || 40
|
||||||
|
|
||||||
if theme.aside.card_tags.color
|
if theme.aside.card_tags.color
|
||||||
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index'})
|
.card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em', page: 'index', custom_colors: custom_colors})
|
||||||
else
|
else
|
||||||
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
.card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit, color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hexo-theme-butterfly",
|
"name": "hexo-theme-butterfly",
|
||||||
"version": "5.5.3",
|
"version": "5.5.4",
|
||||||
"description": "A Simple and Card UI Design theme for Hexo",
|
"description": "A Simple and Card UI Design theme for Hexo",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
abcjs_basic_js:
|
abcjs_basic_js:
|
||||||
name: abcjs
|
name: abcjs
|
||||||
file: dist/abcjs-basic-min.js
|
file: dist/abcjs-basic-min.js
|
||||||
version: 6.5.2
|
version: 6.6.0
|
||||||
activate_power_mode:
|
activate_power_mode:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
file: dist/activate-power-mode.min.js
|
file: dist/activate-power-mode.min.js
|
||||||
@@ -9,7 +9,7 @@ activate_power_mode:
|
|||||||
algolia_search:
|
algolia_search:
|
||||||
name: algoliasearch
|
name: algoliasearch
|
||||||
file: dist/lite/builds/browser.umd.js
|
file: dist/lite/builds/browser.umd.js
|
||||||
version: 5.46.0
|
version: 5.47.0
|
||||||
aplayer_css:
|
aplayer_css:
|
||||||
name: aplayer
|
name: aplayer
|
||||||
file: dist/APlayer.min.css
|
file: dist/APlayer.min.css
|
||||||
@@ -66,26 +66,26 @@ docsearch_css:
|
|||||||
name: '@docsearch/css'
|
name: '@docsearch/css'
|
||||||
other_name: docsearch-css
|
other_name: docsearch-css
|
||||||
file: dist/style.css
|
file: dist/style.css
|
||||||
version: 4.3.2
|
version: 4.5.3
|
||||||
docsearch_js:
|
docsearch_js:
|
||||||
name: '@docsearch/js'
|
name: '@docsearch/js'
|
||||||
other_name: docsearch-js
|
other_name: docsearch-js
|
||||||
file: dist/umd/index.js
|
file: dist/umd/index.js
|
||||||
version: 4.3.2
|
version: 4.5.3
|
||||||
egjs_infinitegrid:
|
egjs_infinitegrid:
|
||||||
name: '@egjs/infinitegrid'
|
name: '@egjs/infinitegrid'
|
||||||
other_name: egjs-infinitegrid
|
other_name: egjs-infinitegrid
|
||||||
file: dist/infinitegrid.min.js
|
file: dist/infinitegrid.min.js
|
||||||
version: 4.12.0
|
version: 4.13.0
|
||||||
fancybox:
|
fancybox:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.umd.js
|
file: dist/fancybox/fancybox.umd.js
|
||||||
version: 6.1.7
|
version: 6.1.9
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fancybox_css:
|
fancybox_css:
|
||||||
name: '@fancyapps/ui'
|
name: '@fancyapps/ui'
|
||||||
file: dist/fancybox/fancybox.css
|
file: dist/fancybox/fancybox.css
|
||||||
version: 6.1.7
|
version: 6.1.9
|
||||||
other_name: fancyapps-ui
|
other_name: fancyapps-ui
|
||||||
fireworks:
|
fireworks:
|
||||||
name: butterfly-extsrc
|
name: butterfly-extsrc
|
||||||
@@ -112,12 +112,12 @@ katex:
|
|||||||
name: katex
|
name: katex
|
||||||
file: dist/katex.min.css
|
file: dist/katex.min.css
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.27
|
version: 0.16.28
|
||||||
katex_copytex:
|
katex_copytex:
|
||||||
name: katex
|
name: katex
|
||||||
file: dist/contrib/copy-tex.min.js
|
file: dist/contrib/copy-tex.min.js
|
||||||
other_name: KaTeX
|
other_name: KaTeX
|
||||||
version: 0.16.27
|
version: 0.16.28
|
||||||
lazyload:
|
lazyload:
|
||||||
name: vanilla-lazyload
|
name: vanilla-lazyload
|
||||||
file: dist/lazyload.iife.min.js
|
file: dist/lazyload.iife.min.js
|
||||||
@@ -125,7 +125,7 @@ lazyload:
|
|||||||
mathjax:
|
mathjax:
|
||||||
name: mathjax
|
name: mathjax
|
||||||
file: tex-mml-chtml.js
|
file: tex-mml-chtml.js
|
||||||
version: 4.0.0
|
version: 4.1.0
|
||||||
medium_zoom:
|
medium_zoom:
|
||||||
name: medium-zoom
|
name: medium-zoom
|
||||||
file: dist/medium-zoom.min.js
|
file: dist/medium-zoom.min.js
|
||||||
@@ -190,7 +190,7 @@ twikoo:
|
|||||||
typed:
|
typed:
|
||||||
name: typed.js
|
name: typed.js
|
||||||
file: dist/typed.umd.js
|
file: dist/typed.umd.js
|
||||||
version: 2.1.0
|
version: 3.0.0
|
||||||
valine:
|
valine:
|
||||||
name: valine
|
name: valine
|
||||||
file: dist/Valine.min.js
|
file: dist/Valine.min.js
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ module.exports = {
|
|||||||
enable: true,
|
enable: true,
|
||||||
limit: 40,
|
limit: 40,
|
||||||
color: false,
|
color: false,
|
||||||
|
custom_colors: null,
|
||||||
orderby: 'random',
|
orderby: 'random',
|
||||||
order: 1,
|
order: 1,
|
||||||
sort_order: null
|
sort_order: null
|
||||||
@@ -522,7 +523,9 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
light: 'default',
|
light: 'default',
|
||||||
dark: 'dark'
|
dark: 'dark'
|
||||||
}
|
},
|
||||||
|
open_in_new_tab: true,
|
||||||
|
zoom_pan: true
|
||||||
},
|
},
|
||||||
chartjs: {
|
chartjs: {
|
||||||
enable: false,
|
enable: false,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ hexo.extend.helper.register('postDesc', data => {
|
|||||||
|
|
||||||
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
hexo.extend.helper.register('cloudTags', function (options = {}) {
|
||||||
const env = this
|
const env = this
|
||||||
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags' } = options
|
let { source, minfontsize, maxfontsize, limit, unit = 'px', orderby, order, page = 'tags', custom_colors } = options
|
||||||
|
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
source = source.limit(limit)
|
source = source.limit(limit)
|
||||||
@@ -36,15 +36,48 @@ hexo.extend.helper.register('cloudTags', function (options = {}) {
|
|||||||
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
return `rgb(${Math.max(r, 50)}, ${Math.max(g, 50)}, ${Math.max(b, 50)})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateStyle = (size, unit, page) => {
|
const normalizeColors = input => {
|
||||||
const colorStyle = page === 'tags' ? `background-color: ${getRandomColor()};` : `color: ${getRandomColor()};`
|
if (!input) return null
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const color = input.trim()
|
||||||
|
return color ? [color] : null
|
||||||
|
}
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const value = input[i]
|
||||||
|
if (value === null || value === undefined) continue
|
||||||
|
const color = String(value).trim()
|
||||||
|
if (!color) continue
|
||||||
|
result.push(color)
|
||||||
|
}
|
||||||
|
return result.length ? result : null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const userColors = normalizeColors(custom_colors)
|
||||||
|
|
||||||
|
const resolveColorClass = (idx) => `tag-color-${idx % userColors.length}`
|
||||||
|
|
||||||
|
const generateStyle = (size, unit, page, color) => {
|
||||||
|
const colorStyle = page === 'tags' ? `background-color: ${color};` : `color: ${color};`
|
||||||
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
|
return `font-size: ${parseFloat(size.toFixed(2))}${unit}; ${colorStyle}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.sort(orderby, order).map(tag => {
|
return source.sort(orderby, order).map((tag, idx) => {
|
||||||
const ratio = length ? sizeMap.get(tag.length) / length : 0
|
const ratio = length ? sizeMap.get(tag.length) / length : 0
|
||||||
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
const size = minfontsize + ((maxfontsize - minfontsize) * ratio)
|
||||||
const style = generateStyle(size, unit, page)
|
|
||||||
|
if (userColors && userColors.length) {
|
||||||
|
const colorClass = resolveColorClass(idx)
|
||||||
|
const color = userColors[idx % userColors.length]
|
||||||
|
const style = generateStyle(size, unit, page, color)
|
||||||
|
return `<a href="${env.url_for(tag.path)}" class="tag-cloud-item ${colorClass}" style="${style}">${tag.name}</a>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = getRandomColor()
|
||||||
|
const style = generateStyle(size, unit, page, color)
|
||||||
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
return `<a href="${env.url_for(tag.path)}" style="${style}">${tag.name}</a>`
|
||||||
}).join('')
|
}).join('')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ wordWrap = $highlight_enable && !$highlight_line_number && hexo-config('code_blo
|
|||||||
--hlscrollbar-bg: lighten(#121212, 5)
|
--hlscrollbar-bg: lighten(#121212, 5)
|
||||||
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
--hlexpand-bg: linear-gradient(180deg, rgba(lighten(#121212, 2), .6), rgba(lighten(#121212, 2), .9))
|
||||||
|
|
||||||
|
$scrollbar-style
|
||||||
|
// scrollbar - firefox
|
||||||
|
@-moz-document url-prefix()
|
||||||
|
scrollbar-color: var(--hlscrollbar-bg) transparent
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background: var(--hlscrollbar-bg)
|
||||||
|
|
||||||
if $highlight_enable
|
if $highlight_enable
|
||||||
@require 'highlight/index'
|
@require 'highlight/index'
|
||||||
|
|
||||||
@@ -88,6 +96,11 @@ $code-block
|
|||||||
&:hover
|
&:hover
|
||||||
border-bottom-color: var(--hl-color)
|
border-bottom-color: var(--hl-color)
|
||||||
|
|
||||||
|
&.default
|
||||||
|
pre
|
||||||
|
padding: 10px 20px
|
||||||
|
@extend $scrollbar-style
|
||||||
|
|
||||||
&.copy-true
|
&.copy-true
|
||||||
user-select: all
|
user-select: all
|
||||||
-webkit-user-select: all
|
-webkit-user-select: all
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
figure.highlight
|
figure.highlight
|
||||||
table
|
table
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
pre .deletion
|
pre .deletion
|
||||||
color: $highlight-deletion
|
color: $highlight-deletion
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ if $highlight_theme != false
|
|||||||
|
|
||||||
.container
|
.container
|
||||||
pre[class*='language-']
|
pre[class*='language-']
|
||||||
// scrollbar - firefox
|
@extend $scrollbar-style
|
||||||
@-moz-document url-prefix()
|
|
||||||
scrollbar-color: var(--hlscrollbar-bg) transparent
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
|
||||||
background: var(--hlscrollbar-bg)
|
|
||||||
|
|
||||||
&:not(.line-numbers)
|
&:not(.line-numbers)
|
||||||
padding: 10px 20px
|
padding: 10px 20px
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: var(--white)
|
color: var(--white)
|
||||||
font-size: 1.85em
|
font-size: 1.85em
|
||||||
|
@extend .limit-more-line
|
||||||
|
-webkit-line-clamp: 3
|
||||||
|
|
||||||
+minWidth768()
|
+minWidth768()
|
||||||
font-size: 2.85em
|
font-size: 2.85em
|
||||||
|
|||||||
@@ -37,13 +37,13 @@
|
|||||||
display: none
|
display: none
|
||||||
padding: 0 0 15px
|
padding: 0 0 15px
|
||||||
width: 100%
|
width: 100%
|
||||||
addBorderRadius()
|
|
||||||
|
|
||||||
.reward-all
|
.reward-all
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 20px 10px
|
padding: 20px 10px
|
||||||
background: var(--reward-pop)
|
background: var(--reward-pop)
|
||||||
|
addBorderRadius()
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|||||||
@@ -65,12 +65,55 @@ if hexo-config('waline.bg')
|
|||||||
|
|
||||||
if hexo-config('mermaid.enable')
|
if hexo-config('mermaid.enable')
|
||||||
.mermaid-wrap
|
.mermaid-wrap
|
||||||
|
position: relative
|
||||||
margin: 0 0 20px
|
margin: 0 0 20px
|
||||||
|
background: var(--card-bg)
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
if hexo-config('mermaid.open_in_new_tab')
|
||||||
|
.mermaid-open-btn
|
||||||
|
position: absolute
|
||||||
|
top: 8px
|
||||||
|
right: 8px
|
||||||
|
z-index: 2
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
padding: 0
|
||||||
|
width: 34px
|
||||||
|
height: 25px
|
||||||
|
border: none
|
||||||
|
border-radius: 20%
|
||||||
|
background: #D3D3D3
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, .15)
|
||||||
|
color: #fff
|
||||||
|
font-size: 0
|
||||||
|
line-height: 1
|
||||||
|
cursor: pointer
|
||||||
|
transition: background .2s ease, transform .2s ease
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: 16px
|
||||||
|
line-height: 1
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible
|
||||||
|
outline: none
|
||||||
|
background: #C0C0C0
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
& > svg
|
& > svg
|
||||||
|
max-width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
|
if hexo-config('mermaid.zoom_pan')
|
||||||
|
cursor: grab
|
||||||
|
user-select: none
|
||||||
|
touch-action: none
|
||||||
|
|
||||||
|
&:active
|
||||||
|
cursor: grabbing
|
||||||
|
|
||||||
if hexo-config('mermaid.code_write')
|
if hexo-config('mermaid.code_write')
|
||||||
pre > code.mermaid
|
pre > code.mermaid
|
||||||
display: none
|
display: none
|
||||||
|
|||||||
@@ -61,11 +61,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||||
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||||
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
||||||
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
|
const isNotHighlightJs = plugin !== 'highlight.js'
|
||||||
|
const isPrismjs = plugin === 'prismjs'
|
||||||
|
const $figureHighlight = isNotHighlightJs
|
||||||
|
? Array.from(document.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
|
||||||
|
: document.querySelectorAll('figure.highlight')
|
||||||
|
|
||||||
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||||
|
|
||||||
const isPrismjs = plugin === 'prismjs'
|
|
||||||
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||||
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
||||||
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
|
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
|
||||||
@@ -133,7 +136,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const highlightCopyFn = (ele, clickEle) => {
|
const highlightCopyFn = (ele, clickEle) => {
|
||||||
const $buttonParent = ele.parentNode
|
const $buttonParent = ele.parentNode
|
||||||
$buttonParent.classList.add('copy-true')
|
$buttonParent.classList.add('copy-true')
|
||||||
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
|
const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
|
||||||
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||||
if (!codeElement) return
|
if (!codeElement) return
|
||||||
copy(codeElement.innerText, clickEle)
|
copy(codeElement.innerText, clickEle)
|
||||||
@@ -213,20 +216,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fragment.appendChild(ele)
|
fragment.appendChild(ele)
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
$figureHighlight.forEach(item => {
|
$figureHighlight.forEach(item => {
|
||||||
let langName = ''
|
let langName = ''
|
||||||
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
|
if (isNotHighlightJs) {
|
||||||
|
const newClassName = isPrismjs ? 'prismjs' : 'default'
|
||||||
|
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
|
||||||
|
}
|
||||||
|
|
||||||
if (!highlightLang) {
|
if (!highlightLang) {
|
||||||
createEle('', item)
|
createEle('', item)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPrismjs) {
|
if (isNotHighlightJs) {
|
||||||
langName = item.getAttribute('data-language') || 'Code'
|
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
|
||||||
} else {
|
} else {
|
||||||
langName = item.getAttribute('class').split(' ')[1]
|
langName = item.getAttribute('class').split(' ')[1]
|
||||||
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||||
|
|||||||
567
themes/butterfly/source/js/search/local-search.js
Normal file
567
themes/butterfly/source/js/search/local-search.js
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
/**
|
||||||
|
* Refer to hexo-generator-searchdb
|
||||||
|
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
|
||||||
|
* Modified by hexo-theme-butterfly
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LocalSearch {
|
||||||
|
constructor ({
|
||||||
|
path = '',
|
||||||
|
unescape = false,
|
||||||
|
top_n_per_article = 1
|
||||||
|
}) {
|
||||||
|
this.path = path
|
||||||
|
this.unescape = unescape
|
||||||
|
this.top_n_per_article = top_n_per_article
|
||||||
|
this.isfetched = false
|
||||||
|
this.datas = null
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexByWord (words, text, caseSensitive = false) {
|
||||||
|
const index = []
|
||||||
|
const included = new Set()
|
||||||
|
|
||||||
|
if (!caseSensitive) {
|
||||||
|
text = text.toLowerCase()
|
||||||
|
}
|
||||||
|
words.forEach(word => {
|
||||||
|
if (this.unescape) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerText = word
|
||||||
|
word = div.innerHTML
|
||||||
|
}
|
||||||
|
const wordLen = word.length
|
||||||
|
if (wordLen === 0) return
|
||||||
|
let startPosition = 0
|
||||||
|
let position = -1
|
||||||
|
if (!caseSensitive) {
|
||||||
|
word = word.toLowerCase()
|
||||||
|
}
|
||||||
|
while ((position = text.indexOf(word, startPosition)) > -1) {
|
||||||
|
index.push({ position, word })
|
||||||
|
included.add(word)
|
||||||
|
startPosition = position + wordLen
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Sort index by position of keyword
|
||||||
|
index.sort((left, right) => {
|
||||||
|
if (left.position !== right.position) {
|
||||||
|
return left.position - right.position
|
||||||
|
}
|
||||||
|
return right.word.length - left.word.length
|
||||||
|
})
|
||||||
|
return [index, included]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge hits into slices
|
||||||
|
mergeIntoSlice (start, end, index) {
|
||||||
|
let item = index[0]
|
||||||
|
let { position, word } = item
|
||||||
|
const hits = []
|
||||||
|
const count = new Set()
|
||||||
|
while (position + word.length <= end && index.length !== 0) {
|
||||||
|
count.add(word)
|
||||||
|
hits.push({
|
||||||
|
position,
|
||||||
|
length: word.length
|
||||||
|
})
|
||||||
|
const wordEnd = position + word.length
|
||||||
|
|
||||||
|
// Move to next position of hit
|
||||||
|
index.shift()
|
||||||
|
while (index.length !== 0) {
|
||||||
|
item = index[0]
|
||||||
|
position = item.position
|
||||||
|
word = item.word
|
||||||
|
if (wordEnd > position) {
|
||||||
|
index.shift()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
hits,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
count: count.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight title and content
|
||||||
|
highlightKeyword (val, slice) {
|
||||||
|
let result = ''
|
||||||
|
let index = slice.start
|
||||||
|
for (const { position, length } of slice.hits) {
|
||||||
|
result += val.substring(index, position)
|
||||||
|
index = position + length
|
||||||
|
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
||||||
|
}
|
||||||
|
result += val.substring(index, slice.end)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultItems (keywords) {
|
||||||
|
const resultItems = []
|
||||||
|
this.datas.forEach(({ title, content, url }) => {
|
||||||
|
// The number of different keywords included in the article.
|
||||||
|
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||||
|
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
|
||||||
|
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
|
||||||
|
|
||||||
|
// Show search results
|
||||||
|
const hitCount = indexOfTitle.length + indexOfContent.length
|
||||||
|
if (hitCount === 0) return
|
||||||
|
|
||||||
|
const slicesOfTitle = []
|
||||||
|
if (indexOfTitle.length !== 0) {
|
||||||
|
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
|
||||||
|
}
|
||||||
|
|
||||||
|
let slicesOfContent = []
|
||||||
|
while (indexOfContent.length !== 0) {
|
||||||
|
const item = indexOfContent[0]
|
||||||
|
const { position } = item
|
||||||
|
// Cut out 120 characters. The maxlength of .search-input is 80.
|
||||||
|
const start = Math.max(0, position - 20)
|
||||||
|
const end = Math.min(content.length, position + 100)
|
||||||
|
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort slices in content by included keywords' count and hits' count
|
||||||
|
slicesOfContent.sort((left, right) => {
|
||||||
|
if (left.count !== right.count) {
|
||||||
|
return right.count - left.count
|
||||||
|
} else if (left.hits.length !== right.hits.length) {
|
||||||
|
return right.hits.length - left.hits.length
|
||||||
|
}
|
||||||
|
return left.start - right.start
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select top N slices in content
|
||||||
|
const upperBound = parseInt(this.top_n_per_article, 10)
|
||||||
|
if (upperBound >= 0) {
|
||||||
|
slicesOfContent = slicesOfContent.slice(0, upperBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultItem = ''
|
||||||
|
|
||||||
|
url = new URL(url, location.origin)
|
||||||
|
url.searchParams.append('highlight', keywords.join(' '))
|
||||||
|
|
||||||
|
if (slicesOfTitle.length !== 0) {
|
||||||
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||||
|
} else {
|
||||||
|
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
slicesOfContent.forEach(slice => {
|
||||||
|
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
|
||||||
|
})
|
||||||
|
|
||||||
|
resultItem += '</a></li>'
|
||||||
|
resultItems.push({
|
||||||
|
item: resultItem,
|
||||||
|
id: resultItems.length,
|
||||||
|
hitCount,
|
||||||
|
includedCount
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return resultItems
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData () {
|
||||||
|
const isXml = !this.path.endsWith('json')
|
||||||
|
fetch(this.path)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(res => {
|
||||||
|
// Get the contents from search data
|
||||||
|
this.isfetched = true
|
||||||
|
this.datas = isXml
|
||||||
|
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
|
||||||
|
title: element.querySelector('title').textContent,
|
||||||
|
content: element.querySelector('content').textContent,
|
||||||
|
url: element.querySelector('url').textContent
|
||||||
|
}))
|
||||||
|
: JSON.parse(res)
|
||||||
|
// Only match articles with non-empty titles
|
||||||
|
this.datas = this.datas.filter(data => data.title).map(data => {
|
||||||
|
data.title = data.title.trim()
|
||||||
|
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
|
||||||
|
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
// Remove loading animation
|
||||||
|
window.dispatchEvent(new Event('search:loaded'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight by wrapping node in mark elements with the given class name
|
||||||
|
highlightText (node, slice, className) {
|
||||||
|
const val = node.nodeValue
|
||||||
|
let index = slice.start
|
||||||
|
const children = []
|
||||||
|
for (const { position, length } of slice.hits) {
|
||||||
|
const text = document.createTextNode(val.substring(index, position))
|
||||||
|
index = position + length
|
||||||
|
const mark = document.createElement('mark')
|
||||||
|
mark.className = className
|
||||||
|
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
||||||
|
children.push(text, mark)
|
||||||
|
}
|
||||||
|
node.nodeValue = val.substring(index, slice.end)
|
||||||
|
children.forEach(element => {
|
||||||
|
node.parentNode.insertBefore(element, node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the search words provided in the url in the text
|
||||||
|
highlightSearchWords (body) {
|
||||||
|
const params = new URL(location.href).searchParams.get('highlight')
|
||||||
|
const keywords = params ? params.split(' ') : []
|
||||||
|
if (!keywords.length || !body) return
|
||||||
|
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
|
||||||
|
const allNodes = []
|
||||||
|
while (walk.nextNode()) {
|
||||||
|
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
|
||||||
|
}
|
||||||
|
allNodes.forEach(node => {
|
||||||
|
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
|
||||||
|
if (!indexOfNode.length) return
|
||||||
|
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
|
||||||
|
this.highlightText(node, slice, 'search-keyword')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Search
|
||||||
|
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
|
||||||
|
const enablePagination = pagination && pagination.enable
|
||||||
|
const localSearch = new LocalSearch({
|
||||||
|
path,
|
||||||
|
top_n_per_article,
|
||||||
|
unescape
|
||||||
|
})
|
||||||
|
|
||||||
|
const input = document.querySelector('.local-search-input input')
|
||||||
|
const statsItem = document.getElementById('local-search-stats')
|
||||||
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const isXml = !path.endsWith('json')
|
||||||
|
|
||||||
|
// Pagination variables (only initialize if pagination is enabled)
|
||||||
|
let currentPage = 0
|
||||||
|
const hitsPerPage = pagination.hitsPerPage || 10
|
||||||
|
|
||||||
|
let currentResultItems = []
|
||||||
|
|
||||||
|
if (!enablePagination) {
|
||||||
|
// If pagination is disabled, we don't need these variables
|
||||||
|
currentPage = undefined
|
||||||
|
currentResultItems = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache frequently used elements
|
||||||
|
const elements = {
|
||||||
|
get pagination () { return document.getElementById('local-search-pagination') },
|
||||||
|
get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide search results area
|
||||||
|
const toggleResultsVisibility = hasResults => {
|
||||||
|
if (enablePagination) {
|
||||||
|
elements.pagination.style.display = hasResults ? '' : 'none'
|
||||||
|
} else {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render search results for current page
|
||||||
|
const renderResults = (searchText, resultItems) => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
|
||||||
|
// Determine items to display based on pagination mode
|
||||||
|
const itemsToDisplay = enablePagination
|
||||||
|
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
|
||||||
|
: resultItems
|
||||||
|
|
||||||
|
// Handle empty page in pagination mode
|
||||||
|
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
|
||||||
|
currentPage = 0
|
||||||
|
renderResults(searchText, resultItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numbering to items
|
||||||
|
const numberedItems = itemsToDisplay.map((result, index) => {
|
||||||
|
const itemNumber = enablePagination
|
||||||
|
? currentPage * hitsPerPage + index + 1
|
||||||
|
: index + 1
|
||||||
|
return result.item.replace(
|
||||||
|
'<li class="local-search-hit-item">',
|
||||||
|
`<li class="local-search-hit-item" value="${itemNumber}">`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
|
||||||
|
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
|
||||||
|
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
|
||||||
|
// Handle pagination
|
||||||
|
if (enablePagination) {
|
||||||
|
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
|
||||||
|
renderPagination(currentPage, nbPages, searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasResults = resultItems.length > 0
|
||||||
|
toggleResultsVisibility(hasResults)
|
||||||
|
|
||||||
|
window.pjax && window.pjax.refresh(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render pagination
|
||||||
|
const renderPagination = (page, nbPages, query) => {
|
||||||
|
if (nbPages <= 1) {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
elements.paginationList.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.pagination.style.display = 'block'
|
||||||
|
|
||||||
|
const isFirstPage = page === 0
|
||||||
|
const isLastPage = page === nbPages - 1
|
||||||
|
|
||||||
|
// Responsive page display
|
||||||
|
const isMobile = window.innerWidth < 768
|
||||||
|
const maxVisiblePages = isMobile ? 3 : 5
|
||||||
|
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
|
||||||
|
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
|
||||||
|
|
||||||
|
// Adjust starting page to maintain max visible pages
|
||||||
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
|
startPage = Math.max(0, endPage - maxVisiblePages + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pagesHTML = ''
|
||||||
|
|
||||||
|
// Only add ellipsis and first page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && startPage > 0) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
|
||||||
|
</li>`
|
||||||
|
if (startPage > 1) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add middle page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const isSelected = i === page
|
||||||
|
if (isSelected) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
|
||||||
|
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
|
||||||
|
</li>`
|
||||||
|
} else {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add ellipsis and last page when there are many pages
|
||||||
|
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
|
||||||
|
if (endPage < nbPages - 2) {
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
|
||||||
|
<span class="ais-Pagination-link">...</span>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
pagesHTML += `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--page">
|
||||||
|
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbPages > 1) {
|
||||||
|
elements.paginationList.innerHTML = `
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isFirstPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
${pagesHTML}
|
||||||
|
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
|
||||||
|
${isLastPage
|
||||||
|
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
|
||||||
|
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
|
||||||
|
}
|
||||||
|
</li>`
|
||||||
|
} else {
|
||||||
|
elements.pagination.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search results and stats
|
||||||
|
const clearSearchResults = () => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
container.textContent = ''
|
||||||
|
statsItem.textContent = ''
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = []
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show no results message
|
||||||
|
const showNoResults = searchText => {
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
container.textContent = ''
|
||||||
|
const statsDiv = document.createElement('div')
|
||||||
|
statsDiv.className = 'search-result-stats'
|
||||||
|
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||||
|
statsItem.innerHTML = statsDiv.outerHTML
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = []
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputEventFunction = () => {
|
||||||
|
if (!localSearch.isfetched) return
|
||||||
|
let searchText = input.value.trim().toLowerCase()
|
||||||
|
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||||
|
|
||||||
|
if (searchText !== '') $loadingStatus.hidden = false
|
||||||
|
|
||||||
|
const keywords = searchText.split(/[-\s]+/)
|
||||||
|
let resultItems = []
|
||||||
|
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
resultItems = localSearch.getResultItems(keywords)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
|
clearSearchResults()
|
||||||
|
} else if (resultItems.length === 0) {
|
||||||
|
showNoResults(searchText)
|
||||||
|
} else {
|
||||||
|
// Sort results by relevance
|
||||||
|
resultItems.sort((left, right) => {
|
||||||
|
if (left.includedCount !== right.includedCount) {
|
||||||
|
return right.includedCount - left.includedCount
|
||||||
|
} else if (left.hitCount !== right.hitCount) {
|
||||||
|
return right.hitCount - left.hitCount
|
||||||
|
}
|
||||||
|
return right.id - left.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (enablePagination) {
|
||||||
|
currentResultItems = resultItems
|
||||||
|
currentPage = 0
|
||||||
|
}
|
||||||
|
renderResults(searchText, resultItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadingStatus.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadFlag = false
|
||||||
|
const $searchMask = document.getElementById('search-mask')
|
||||||
|
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||||
|
|
||||||
|
// fix safari
|
||||||
|
const fixSafariHeight = () => {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
|
btf.animateIn($searchMask, 'to_show 0.5s')
|
||||||
|
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
||||||
|
setTimeout(() => { input.focus() }, 300)
|
||||||
|
if (!loadFlag) {
|
||||||
|
!localSearch.isfetched && localSearch.fetchData()
|
||||||
|
input.addEventListener('input', inputEventFunction)
|
||||||
|
loadFlag = true
|
||||||
|
}
|
||||||
|
// shortcut: ESC
|
||||||
|
document.addEventListener('keydown', function f (event) {
|
||||||
|
if (event.code === 'Escape') {
|
||||||
|
closeSearch()
|
||||||
|
document.removeEventListener('keydown', f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fixSafariHeight()
|
||||||
|
window.addEventListener('resize', fixSafariHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = () => {
|
||||||
|
btf.overflowPaddingR.remove()
|
||||||
|
btf.animateOut($searchDialog, 'search_close .5s')
|
||||||
|
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||||
|
window.removeEventListener('resize', fixSafariHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchClickFn = () => {
|
||||||
|
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchFnOnce = () => {
|
||||||
|
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
|
$searchMask.addEventListener('click', closeSearch)
|
||||||
|
if (GLOBAL_CONFIG.localSearch.preload) {
|
||||||
|
localSearch.fetchData()
|
||||||
|
}
|
||||||
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
|
||||||
|
// Pagination event delegation - only add if pagination is enabled
|
||||||
|
if (enablePagination) {
|
||||||
|
elements.pagination.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
const link = e.target.closest('a[data-page]')
|
||||||
|
if (link) {
|
||||||
|
const page = parseInt(link.dataset.page, 10)
|
||||||
|
if (!isNaN(page) && currentResultItems.length > 0) {
|
||||||
|
currentPage = page
|
||||||
|
renderResults(input.value.trim().toLowerCase(), currentResultItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
toggleResultsVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('search:loaded', () => {
|
||||||
|
const $loadDataItem = document.getElementById('loading-database')
|
||||||
|
$loadDataItem.nextElementSibling.style.visibility = 'visible'
|
||||||
|
$loadDataItem.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
searchClickFn()
|
||||||
|
searchFnOnce()
|
||||||
|
|
||||||
|
// pjax
|
||||||
|
window.addEventListener('pjax:complete', () => {
|
||||||
|
!btf.isHidden($searchMask) && closeSearch()
|
||||||
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
searchClickFn()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user