Auto deploy pages
395
LICENSE
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
Attribution 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International Public License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution 4.0 International Public License ("Public License"). To the
|
||||||
|
extent this Public License may be interpreted as a contract, You are
|
||||||
|
granted the Licensed Rights in consideration of Your acceptance of
|
||||||
|
these terms and conditions, and the Licensor grants You such rights in
|
||||||
|
consideration of benefits the Licensor receives from making the
|
||||||
|
Licensed Material available under these terms and conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
d. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
f. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
g. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
i. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
j. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
k. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
4. If You Share Adapted Material You produce, the Adapter's
|
||||||
|
License You apply must not prevent recipients of the Adapted
|
||||||
|
Material from complying with this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material; and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public licenses.
|
||||||
|
Notwithstanding, Creative Commons may elect to apply one of its public
|
||||||
|
licenses to material it publishes and in those instances will be
|
||||||
|
considered the “Licensor.” The text of the Creative Commons public
|
||||||
|
licenses is dedicated to the public domain under the CC0 Public Domain
|
||||||
|
Dedication. Except for the limited purpose of indicating that material
|
||||||
|
is shared under a Creative Commons public license or as otherwise
|
||||||
|
permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the public
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
307
categories/index.html
Normal file
6181
css/index.css
Normal file
22
css/minimal.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.pace {
|
||||||
|
-webkit-pointer-events: none;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pace-inactive {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pace .pace-progress {
|
||||||
|
background: #9c9c9c77;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
top: 0;
|
||||||
|
right: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
0
css/var.css
Normal file
BIN
img/404.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
img/avatar.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
img/butterfly-icon.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
img/cover.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
img/error-page.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
img/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
img/friend_404.gif
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
img/index.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
img/top.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
943
js/main.js
Normal file
@ -0,0 +1,943 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
let headerContentWidth, $nav
|
||||||
|
let mobileSidebarOpen = false
|
||||||
|
|
||||||
|
const adjustMenu = init => {
|
||||||
|
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
||||||
|
|
||||||
|
if (init) {
|
||||||
|
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
||||||
|
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
||||||
|
headerContentWidth = blogInfoWidth + menusWidth
|
||||||
|
$nav = document.getElementById('nav')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||||
|
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化header
|
||||||
|
const initAdjust = () => {
|
||||||
|
adjustMenu(true)
|
||||||
|
$nav.classList.add('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
// sidebar menus
|
||||||
|
const sidebarFn = {
|
||||||
|
open: () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
|
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
|
||||||
|
document.getElementById('sidebar-menus').classList.add('open')
|
||||||
|
mobileSidebarOpen = true
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
btf.overflowPaddingR.remove()
|
||||||
|
btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
|
||||||
|
document.getElementById('sidebar-menus').classList.remove('open')
|
||||||
|
mobileSidebarOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首頁top_img底下的箭頭
|
||||||
|
*/
|
||||||
|
const scrollDownInIndex = () => {
|
||||||
|
const handleScrollToDest = () => {
|
||||||
|
btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const $scrollDownEle = document.getElementById('scroll-down')
|
||||||
|
$scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代碼
|
||||||
|
* 只適用於Hexo默認的代碼渲染
|
||||||
|
*/
|
||||||
|
const addHighlightTool = () => {
|
||||||
|
const highLight = GLOBAL_CONFIG.highlight
|
||||||
|
if (!highLight) return
|
||||||
|
|
||||||
|
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||||
|
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||||
|
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
||||||
|
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
|
||||||
|
|
||||||
|
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||||
|
|
||||||
|
const isPrismjs = plugin === 'prismjs'
|
||||||
|
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||||
|
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
||||||
|
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
|
||||||
|
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
|
||||||
|
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
|
||||||
|
|
||||||
|
const alertInfo = (ele, text) => {
|
||||||
|
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||||||
|
btf.snackbarShow(text)
|
||||||
|
} else {
|
||||||
|
ele.textContent = text
|
||||||
|
ele.style.opacity = 1
|
||||||
|
setTimeout(() => { ele.style.opacity = 0 }, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = async (text, ctx) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
alertInfo(ctx, GLOBAL_CONFIG.copy.success)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy: ', err)
|
||||||
|
alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// click events
|
||||||
|
const highlightCopyFn = (ele, clickEle) => {
|
||||||
|
const $buttonParent = ele.parentNode
|
||||||
|
$buttonParent.classList.add('copy-true')
|
||||||
|
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
|
||||||
|
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||||
|
if (!codeElement) return
|
||||||
|
copy(codeElement.innerText, clickEle.previousElementSibling)
|
||||||
|
$buttonParent.classList.remove('copy-true')
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightShrinkFn = ele => ele.classList.toggle('closed')
|
||||||
|
|
||||||
|
const codeFullpage = (item, clickEle) => {
|
||||||
|
const wrapEle = item.closest('figure.highlight')
|
||||||
|
const isFullpage = wrapEle.classList.toggle('code-fullpage')
|
||||||
|
|
||||||
|
document.body.style.overflow = isFullpage ? 'hidden' : ''
|
||||||
|
clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage)
|
||||||
|
clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightToolsFn = e => {
|
||||||
|
const $target = e.target.classList
|
||||||
|
const currentElement = e.currentTarget
|
||||||
|
if ($target.contains('expand')) highlightShrinkFn(currentElement)
|
||||||
|
else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target)
|
||||||
|
else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandCode = e => e.currentTarget.classList.toggle('expand-done')
|
||||||
|
|
||||||
|
// 獲取隱藏狀態下元素的真實高度
|
||||||
|
const getActualHeight = item => {
|
||||||
|
const hiddenElements = new Map()
|
||||||
|
|
||||||
|
const fix = () => {
|
||||||
|
let current = item
|
||||||
|
while (current !== document.body && current != null) {
|
||||||
|
if (window.getComputedStyle(current).display === 'none') {
|
||||||
|
hiddenElements.set(current, current.getAttribute('style') || '')
|
||||||
|
}
|
||||||
|
current = current.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = 'visibility: hidden !important; display: block !important;'
|
||||||
|
hiddenElements.forEach((originalStyle, elem) => {
|
||||||
|
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const restore = () => {
|
||||||
|
hiddenElements.forEach((originalStyle, elem) => {
|
||||||
|
if (originalStyle === '') elem.removeAttribute('style')
|
||||||
|
else elem.setAttribute('style', originalStyle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fix()
|
||||||
|
const height = item.offsetHeight
|
||||||
|
restore()
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEle = (lang, item) => {
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
|
||||||
|
if (isShowTool) {
|
||||||
|
const hlTools = document.createElement('div')
|
||||||
|
hlTools.className = `highlight-tools ${highlightShrinkClass}`
|
||||||
|
hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle
|
||||||
|
btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
|
||||||
|
fragment.appendChild(hlTools)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
|
||||||
|
const ele = document.createElement('div')
|
||||||
|
ele.className = 'code-expand-btn'
|
||||||
|
ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
|
||||||
|
btf.addEventListenerPjax(ele, 'click', expandCode)
|
||||||
|
fragment.appendChild(ele)
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||||
|
}
|
||||||
|
|
||||||
|
$figureHighlight.forEach(item => {
|
||||||
|
let langName = ''
|
||||||
|
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
|
||||||
|
|
||||||
|
if (!highlightLang) {
|
||||||
|
createEle('', item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrismjs) {
|
||||||
|
langName = item.getAttribute('data-language') || 'Code'
|
||||||
|
} else {
|
||||||
|
langName = item.getAttribute('class').split(' ')[1]
|
||||||
|
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||||
|
}
|
||||||
|
createEle(`<div class="code-lang">${langName}</div>`, item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PhotoFigcaption
|
||||||
|
*/
|
||||||
|
const addPhotoFigcaption = () => {
|
||||||
|
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||||
|
document.querySelectorAll('#article-container img').forEach(item => {
|
||||||
|
const altValue = item.title || item.alt
|
||||||
|
if (!altValue) return
|
||||||
|
const ele = document.createElement('div')
|
||||||
|
ele.className = 'img-alt is-center'
|
||||||
|
ele.textContent = altValue
|
||||||
|
item.insertAdjacentElement('afterend', ele)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightbox
|
||||||
|
*/
|
||||||
|
const runLightbox = () => {
|
||||||
|
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* justified-gallery 圖庫排版
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fetchUrl = async url => {
|
||||||
|
const response = await fetch(url)
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
const runJustifiedGallery = (item, data, isButton = false, tabs) => {
|
||||||
|
const dataLength = data.length
|
||||||
|
|
||||||
|
const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, {
|
||||||
|
gap: 5,
|
||||||
|
isConstantSize: true,
|
||||||
|
sizeRange: [150, 600],
|
||||||
|
// useResizeObserver: true,
|
||||||
|
// observeChildren: true,
|
||||||
|
useTransform: true
|
||||||
|
// useRecycle: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const replaceDq = str => str.replace(/"/g, '"') // replace double quotes to "
|
||||||
|
|
||||||
|
const getItems = (nextGroupKey, count) => {
|
||||||
|
const nextItems = []
|
||||||
|
const startCount = (nextGroupKey - 1) * count
|
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
const num = startCount + i
|
||||||
|
if (num >= dataLength) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = data[num]
|
||||||
|
const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : ''
|
||||||
|
const title = item.title ? `title="${replaceDq(item.title)}"` : ''
|
||||||
|
|
||||||
|
nextItems.push(`<div class="item">
|
||||||
|
<img src="${item.url}" data-grid-maintained-target="true" ${alt + title} />
|
||||||
|
</div>`)
|
||||||
|
}
|
||||||
|
return nextItems
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonText = GLOBAL_CONFIG.infinitegrid.buttonText
|
||||||
|
const addButton = item => {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.innerHTML = buttonText + '<i class="fa-solid fa-arrow-down"></i>'
|
||||||
|
|
||||||
|
button.addEventListener('click', e => {
|
||||||
|
e.target.closest('button').remove()
|
||||||
|
btf.setLoading.add(item)
|
||||||
|
appendItem(ig.getGroups().length + 1, 10)
|
||||||
|
}, { once: true })
|
||||||
|
|
||||||
|
item.insertAdjacentElement('afterend', button)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendItem = (nextGroupKey, count) => {
|
||||||
|
ig.append(getItems(nextGroupKey, count), nextGroupKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxGroupKey = Math.ceil(dataLength / 10)
|
||||||
|
let isLayoutHidden = false
|
||||||
|
|
||||||
|
const completeFn = e => {
|
||||||
|
if (tabs) {
|
||||||
|
const parentNode = item.parentNode
|
||||||
|
|
||||||
|
if (isLayoutHidden) {
|
||||||
|
parentNode.style.visibility = 'visible'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.offsetHeight === 0) {
|
||||||
|
parentNode.style.visibility = 'hidden'
|
||||||
|
isLayoutHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updated, isResize, mounted } = e
|
||||||
|
if (!updated.length || !mounted.length || isResize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.loadLightbox(item.querySelectorAll('img:not(.medium-zoom-image)'))
|
||||||
|
|
||||||
|
if (ig.getGroups().length === maxGroupKey) {
|
||||||
|
btf.setLoading.remove(item)
|
||||||
|
!tabs && ig.off('renderComplete', completeFn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isButton) {
|
||||||
|
btf.setLoading.remove(item)
|
||||||
|
addButton(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestAppendFn = btf.debounce(e => {
|
||||||
|
const nextGroupKey = (+e.groupKey || 0) + 1
|
||||||
|
appendItem(nextGroupKey, 10)
|
||||||
|
|
||||||
|
if (nextGroupKey === maxGroupKey) {
|
||||||
|
ig.off('requestAppend', requestAppendFn)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
btf.setLoading.add(item)
|
||||||
|
ig.on('renderComplete', completeFn)
|
||||||
|
|
||||||
|
if (isButton) {
|
||||||
|
appendItem(1, 10)
|
||||||
|
} else {
|
||||||
|
ig.on('requestAppend', requestAppendFn)
|
||||||
|
ig.renderItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxSendOnce', () => { ig.destroy() })
|
||||||
|
}
|
||||||
|
|
||||||
|
const addJustifiedGallery = async (ele, tabs = false) => {
|
||||||
|
if (!ele.length) return
|
||||||
|
const init = async () => {
|
||||||
|
for (const item of ele) {
|
||||||
|
if (btf.isHidden(item) || item.classList.contains('loaded')) continue
|
||||||
|
|
||||||
|
const isButton = item.getAttribute('data-button') === 'true'
|
||||||
|
const children = item.firstElementChild
|
||||||
|
const text = children.textContent
|
||||||
|
children.textContent = ''
|
||||||
|
item.classList.add('loaded')
|
||||||
|
try {
|
||||||
|
const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text)
|
||||||
|
runJustifiedGallery(children, content, isButton, tabs)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Gallery data parsing failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof InfiniteGrid === 'function') {
|
||||||
|
init()
|
||||||
|
} else {
|
||||||
|
await btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rightside scroll percent
|
||||||
|
*/
|
||||||
|
const rightsideScrollPercent = currentTop => {
|
||||||
|
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||||
|
const goUpElement = document.getElementById('go-up')
|
||||||
|
|
||||||
|
if (scrollPercent < 95) {
|
||||||
|
goUpElement.classList.add('show-percent')
|
||||||
|
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
||||||
|
} else {
|
||||||
|
goUpElement.classList.remove('show-percent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滾動處理
|
||||||
|
*/
|
||||||
|
const scrollFn = () => {
|
||||||
|
const $rightside = document.getElementById('rightside')
|
||||||
|
const innerHeight = window.innerHeight + 56
|
||||||
|
let initTop = 0
|
||||||
|
const $header = document.getElementById('page-header')
|
||||||
|
const isChatBtn = typeof chatBtn !== 'undefined'
|
||||||
|
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||||
|
|
||||||
|
// 檢查文檔高度是否小於視窗高度
|
||||||
|
const checkDocumentHeight = () => {
|
||||||
|
if (document.body.scrollHeight <= innerHeight) {
|
||||||
|
$rightside.classList.add('rightside-show')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果文檔高度小於視窗高度,直接返回
|
||||||
|
if (checkDocumentHeight()) return
|
||||||
|
|
||||||
|
// find the scroll direction
|
||||||
|
const scrollDirection = currentTop => {
|
||||||
|
const result = currentTop > initTop // true is down & false is up
|
||||||
|
initTop = currentTop
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
let flag = ''
|
||||||
|
const scrollTask = btf.throttle(() => {
|
||||||
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
|
const isDown = scrollDirection(currentTop)
|
||||||
|
if (currentTop > 56) {
|
||||||
|
if (flag === '') {
|
||||||
|
$header.classList.add('nav-fixed')
|
||||||
|
$rightside.classList.add('rightside-show')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
if (flag !== 'down') {
|
||||||
|
$header.classList.remove('nav-visible')
|
||||||
|
isChatBtn && window.chatBtn.hide()
|
||||||
|
flag = 'down'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (flag !== 'up') {
|
||||||
|
$header.classList.add('nav-visible')
|
||||||
|
isChatBtn && window.chatBtn.show()
|
||||||
|
flag = 'up'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flag = ''
|
||||||
|
if (currentTop === 0) {
|
||||||
|
$header.classList.remove('nav-fixed', 'nav-visible')
|
||||||
|
}
|
||||||
|
$rightside.classList.remove('rightside-show')
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowPercent && rightsideScrollPercent(currentTop)
|
||||||
|
checkDocumentHeight()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toc,anchor
|
||||||
|
*/
|
||||||
|
const scrollFnToDo = () => {
|
||||||
|
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||||
|
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||||
|
const $article = document.getElementById('article-container')
|
||||||
|
|
||||||
|
if (!($article && (isToc || isAnchor))) return
|
||||||
|
|
||||||
|
let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
|
||||||
|
|
||||||
|
if (isToc) {
|
||||||
|
const $cardTocLayout = document.getElementById('card-toc')
|
||||||
|
$cardToc = $cardTocLayout.querySelector('.toc-content')
|
||||||
|
$tocLink = $cardToc.querySelectorAll('.toc-link')
|
||||||
|
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||||
|
isExpand = $cardToc.classList.contains('is-expand')
|
||||||
|
|
||||||
|
// toc元素點擊
|
||||||
|
const tocItemClickFn = e => {
|
||||||
|
const target = e.target.closest('.toc-link')
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
|
||||||
|
if (window.innerWidth < 900) {
|
||||||
|
$cardTocLayout.classList.remove('open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
|
||||||
|
|
||||||
|
autoScrollToc = item => {
|
||||||
|
const sidebarHeight = $cardToc.clientHeight
|
||||||
|
const itemOffsetTop = item.offsetTop
|
||||||
|
const itemHeight = item.clientHeight
|
||||||
|
const scrollTop = $cardToc.scrollTop
|
||||||
|
const offset = itemOffsetTop - scrollTop
|
||||||
|
const middlePosition = (sidebarHeight - itemHeight) / 2
|
||||||
|
|
||||||
|
if (offset !== middlePosition) {
|
||||||
|
$cardToc.scrollTop = scrollTop + (offset - middlePosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理 hexo-blog-encrypt 事件
|
||||||
|
$cardToc.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
// find head position & add active class
|
||||||
|
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||||
|
let detectItem = ''
|
||||||
|
|
||||||
|
const findHeadPosition = top => {
|
||||||
|
if (top === 0) return false
|
||||||
|
|
||||||
|
let currentId = ''
|
||||||
|
let currentIndex = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < $articleList.length; i++) {
|
||||||
|
const ele = $articleList[i]
|
||||||
|
if (top > btf.getEleTop(ele) - 80) {
|
||||||
|
const id = ele.id
|
||||||
|
currentId = id ? '#' + encodeURI(id) : ''
|
||||||
|
currentIndex = i
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectItem === currentIndex) return
|
||||||
|
|
||||||
|
if (isAnchor) btf.updateAnchor(currentId)
|
||||||
|
|
||||||
|
detectItem = currentIndex
|
||||||
|
|
||||||
|
if (isToc) {
|
||||||
|
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
||||||
|
|
||||||
|
if (currentId) {
|
||||||
|
const currentActive = $tocLink[currentIndex]
|
||||||
|
currentActive.classList.add('active')
|
||||||
|
|
||||||
|
setTimeout(() => autoScrollToc(currentActive), 0)
|
||||||
|
|
||||||
|
if (!isExpand) {
|
||||||
|
let parent = currentActive.parentNode
|
||||||
|
while (!parent.matches('.toc')) {
|
||||||
|
if (parent.matches('li')) parent.classList.add('active')
|
||||||
|
parent = parent.parentNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main of scroll
|
||||||
|
const tocScrollFn = btf.throttle(() => {
|
||||||
|
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||||
|
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||||
|
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
||||||
|
}
|
||||||
|
findHeadPosition(currentTop)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThemeChange = mode => {
|
||||||
|
const globalFn = window.globalFn || {}
|
||||||
|
const themeChange = globalFn.themeChange || {}
|
||||||
|
if (!themeChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(themeChange).forEach(key => {
|
||||||
|
const themeChangeFn = themeChange[key]
|
||||||
|
if (['disqus', 'disqusjs'].includes(key)) {
|
||||||
|
setTimeout(() => themeChangeFn(mode), 300)
|
||||||
|
} else {
|
||||||
|
themeChangeFn(mode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rightside
|
||||||
|
*/
|
||||||
|
const rightSideFn = {
|
||||||
|
readmode: () => { // read mode
|
||||||
|
const $body = document.body
|
||||||
|
const newEle = document.createElement('button')
|
||||||
|
|
||||||
|
const exitReadMode = () => {
|
||||||
|
$body.classList.remove('read-mode')
|
||||||
|
newEle.remove()
|
||||||
|
newEle.removeEventListener('click', exitReadMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
$body.classList.add('read-mode')
|
||||||
|
newEle.type = 'button'
|
||||||
|
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
||||||
|
newEle.addEventListener('click', exitReadMode)
|
||||||
|
$body.appendChild(newEle)
|
||||||
|
},
|
||||||
|
darkmode: () => { // switch between light and dark mode
|
||||||
|
const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
|
||||||
|
if (willChangeMode === 'dark') {
|
||||||
|
btf.activateDarkMode()
|
||||||
|
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
|
||||||
|
} else {
|
||||||
|
btf.activateLightMode()
|
||||||
|
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
|
||||||
|
}
|
||||||
|
btf.saveToLocal.set('theme', willChangeMode, 2)
|
||||||
|
handleThemeChange(willChangeMode)
|
||||||
|
},
|
||||||
|
'rightside-config': item => { // Show or hide rightside-hide-btn
|
||||||
|
const hideLayout = item.firstElementChild
|
||||||
|
if (hideLayout.classList.contains('show')) {
|
||||||
|
hideLayout.classList.add('status')
|
||||||
|
setTimeout(() => {
|
||||||
|
hideLayout.classList.remove('status')
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLayout.classList.toggle('show')
|
||||||
|
},
|
||||||
|
'go-up': () => { // Back to top
|
||||||
|
btf.scrollToDest(0, 500)
|
||||||
|
},
|
||||||
|
'hide-aside-btn': () => { // Hide aside
|
||||||
|
const $htmlDom = document.documentElement.classList
|
||||||
|
const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
|
||||||
|
btf.saveToLocal.set('aside-status', saveStatus, 2)
|
||||||
|
$htmlDom.toggle('hide-aside')
|
||||||
|
},
|
||||||
|
'mobile-toc-button': (p, item) => { // Show mobile toc
|
||||||
|
const tocEle = document.getElementById('card-toc')
|
||||||
|
tocEle.style.transition = 'transform 0.3s ease-in-out'
|
||||||
|
|
||||||
|
const tocEleHeight = tocEle.clientHeight
|
||||||
|
const btData = item.getBoundingClientRect()
|
||||||
|
|
||||||
|
const tocEleBottom = window.innerHeight - btData.bottom - 30
|
||||||
|
if (tocEleHeight > tocEleBottom) {
|
||||||
|
tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
tocEle.classList.toggle('open')
|
||||||
|
tocEle.addEventListener('transitionend', () => {
|
||||||
|
tocEle.style.cssText = ''
|
||||||
|
}, { once: true })
|
||||||
|
},
|
||||||
|
'chat-btn': () => { // Show chat
|
||||||
|
window.chatBtnFn()
|
||||||
|
},
|
||||||
|
translateLink: () => { // switch between traditional and simplified chinese
|
||||||
|
window.translateFn.translatePage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('rightside').addEventListener('click', e => {
|
||||||
|
const $target = e.target.closest('[id]')
|
||||||
|
if ($target && rightSideFn[$target.id]) {
|
||||||
|
rightSideFn[$target.id](e.currentTarget, $target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* menu
|
||||||
|
* 側邊欄sub-menu 展開/收縮
|
||||||
|
*/
|
||||||
|
const clickFnOfSubMenu = () => {
|
||||||
|
const handleClickOfSubMenu = e => {
|
||||||
|
const target = e.target.closest('.site-page.group')
|
||||||
|
if (!target) return
|
||||||
|
target.classList.toggle('hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
const menusItems = document.querySelector('#sidebar-menus .menus_items')
|
||||||
|
menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机端目录点击
|
||||||
|
*/
|
||||||
|
const openMobileMenu = () => {
|
||||||
|
const toggleMenu = document.getElementById('toggle-menu')
|
||||||
|
if (!toggleMenu) return
|
||||||
|
btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 複製時加上版權信息
|
||||||
|
*/
|
||||||
|
const addCopyright = () => {
|
||||||
|
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
||||||
|
|
||||||
|
const handleCopy = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const copyFont = window.getSelection(0).toString()
|
||||||
|
let textFont = copyFont
|
||||||
|
if (copyFont.length > limitCount) {
|
||||||
|
textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
|
||||||
|
}
|
||||||
|
if (e.clipboardData) {
|
||||||
|
return e.clipboardData.setData('text', textFont)
|
||||||
|
} else {
|
||||||
|
return window.clipboardData.setData('text', textFont)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.addEventListener('copy', handleCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 網頁運行時間
|
||||||
|
*/
|
||||||
|
const addRuntime = () => {
|
||||||
|
const $runtimeCount = document.getElementById('runtimeshow')
|
||||||
|
if ($runtimeCount) {
|
||||||
|
const publishDate = $runtimeCount.getAttribute('data-publishDate')
|
||||||
|
$runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最後一次更新時間
|
||||||
|
*/
|
||||||
|
const addLastPushDate = () => {
|
||||||
|
const $lastPushDateItem = document.getElementById('last-push-date')
|
||||||
|
if ($lastPushDateItem) {
|
||||||
|
const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
|
||||||
|
$lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* table overflow
|
||||||
|
*/
|
||||||
|
const addTableWrap = () => {
|
||||||
|
const $table = document.querySelectorAll('#article-container table')
|
||||||
|
if (!$table.length) return
|
||||||
|
|
||||||
|
$table.forEach(item => {
|
||||||
|
if (!item.closest('.highlight')) {
|
||||||
|
btf.wrap(item, 'div', { class: 'table-wrap' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tag-hide
|
||||||
|
*/
|
||||||
|
const clickFnOfTagHide = () => {
|
||||||
|
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
||||||
|
if (!hideButtons.length) return
|
||||||
|
hideButtons.forEach(item => item.addEventListener('click', e => {
|
||||||
|
const currentTarget = e.currentTarget
|
||||||
|
currentTarget.classList.add('open')
|
||||||
|
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||||
|
}, { once: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabsFn = () => {
|
||||||
|
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
||||||
|
if (!navTabsElements.length) return
|
||||||
|
|
||||||
|
const setActiveClass = (elements, activeIndex) => {
|
||||||
|
elements.forEach((el, index) => {
|
||||||
|
el.classList.toggle('active', index === activeIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNavClick = e => {
|
||||||
|
const target = e.target.closest('button')
|
||||||
|
if (!target || target.classList.contains('active')) return
|
||||||
|
|
||||||
|
const navItems = [...e.currentTarget.children]
|
||||||
|
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
||||||
|
const indexOfButton = navItems.indexOf(target)
|
||||||
|
setActiveClass(navItems, indexOfButton)
|
||||||
|
e.currentTarget.classList.remove('no-default')
|
||||||
|
setActiveClass(tabContents, indexOfButton)
|
||||||
|
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToTopClick = tabElement => e => {
|
||||||
|
if (e.target.closest('button')) {
|
||||||
|
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navTabsElements.forEach(tabElement => {
|
||||||
|
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
||||||
|
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCardCategory = () => {
|
||||||
|
const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
|
||||||
|
if (!cardCategory) return
|
||||||
|
|
||||||
|
const handleToggleBtn = e => {
|
||||||
|
const target = e.target
|
||||||
|
if (target.nodeName === 'I') {
|
||||||
|
e.preventDefault()
|
||||||
|
target.parentNode.classList.toggle('expand')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchComments = () => {
|
||||||
|
const switchBtn = document.getElementById('switch-btn')
|
||||||
|
if (!switchBtn) return
|
||||||
|
|
||||||
|
let switchDone = false
|
||||||
|
const postComment = document.getElementById('post-comment')
|
||||||
|
const handleSwitchBtn = () => {
|
||||||
|
postComment.classList.toggle('move')
|
||||||
|
if (!switchDone && typeof loadOtherComment === 'function') {
|
||||||
|
switchDone = true
|
||||||
|
loadOtherComment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPostOutdateNotice = () => {
|
||||||
|
const { limitDay, messagePrev, messageNext, position } = GLOBAL_CONFIG.noticeOutdate
|
||||||
|
const diffDay = btf.diffDate(GLOBAL_CONFIG_SITE.postUpdate)
|
||||||
|
if (diffDay >= limitDay) {
|
||||||
|
const ele = document.createElement('div')
|
||||||
|
ele.className = 'post-outdate-notice'
|
||||||
|
ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
|
||||||
|
const $targetEle = document.getElementById('article-container')
|
||||||
|
if (position === 'top') {
|
||||||
|
$targetEle.insertBefore(ele, $targetEle.firstChild)
|
||||||
|
} else {
|
||||||
|
$targetEle.appendChild(ele)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lazyloadImg = () => {
|
||||||
|
window.lazyLoadInstance = new LazyLoad({
|
||||||
|
elements_selector: 'img',
|
||||||
|
threshold: 0,
|
||||||
|
data_src: 'lazy-src'
|
||||||
|
})
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxComplete', () => {
|
||||||
|
window.lazyLoadInstance.update()
|
||||||
|
}, 'lazyload')
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativeDate = selector => {
|
||||||
|
selector.forEach(item => {
|
||||||
|
item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
|
||||||
|
item.style.display = 'inline'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const justifiedIndexPostUI = () => {
|
||||||
|
const recentPostsElement = document.getElementById('recent-posts')
|
||||||
|
if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
|
||||||
|
gap: { horizontal: 10, vertical: 20 },
|
||||||
|
useTransform: true,
|
||||||
|
useResizeObserver: true
|
||||||
|
})
|
||||||
|
masonryItem.renderItems()
|
||||||
|
btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
|
||||||
|
}
|
||||||
|
|
||||||
|
typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unRefreshFn = () => {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
adjustMenu(false)
|
||||||
|
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
const menuMask = document.getElementById('menu-mask')
|
||||||
|
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||||
|
|
||||||
|
clickFnOfSubMenu()
|
||||||
|
GLOBAL_CONFIG.islazyload && lazyloadImg()
|
||||||
|
GLOBAL_CONFIG.copyright !== undefined && addCopyright()
|
||||||
|
|
||||||
|
if (GLOBAL_CONFIG.autoDarkmode) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (btf.saveToLocal.get('theme') !== undefined) return
|
||||||
|
e.matches ? handleThemeChange('dark') : handleThemeChange('light')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forPostFn = () => {
|
||||||
|
addHighlightTool()
|
||||||
|
addPhotoFigcaption()
|
||||||
|
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
||||||
|
runLightbox()
|
||||||
|
scrollFnToDo()
|
||||||
|
addTableWrap()
|
||||||
|
clickFnOfTagHide()
|
||||||
|
tabsFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshFn = () => {
|
||||||
|
initAdjust()
|
||||||
|
justifiedIndexPostUI()
|
||||||
|
|
||||||
|
if (GLOBAL_CONFIG_SITE.isPost) {
|
||||||
|
GLOBAL_CONFIG.noticeOutdate !== undefined && addPostOutdateNotice()
|
||||||
|
GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
|
||||||
|
} else {
|
||||||
|
GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
|
||||||
|
GLOBAL_CONFIG.runtime && addRuntime()
|
||||||
|
addLastPushDate()
|
||||||
|
toggleCardCategory()
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex()
|
||||||
|
scrollFn()
|
||||||
|
|
||||||
|
forPostFn()
|
||||||
|
switchComments()
|
||||||
|
openMobileMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
|
||||||
|
refreshFn()
|
||||||
|
unRefreshFn()
|
||||||
|
|
||||||
|
// 處理 hexo-blog-encrypt 事件
|
||||||
|
window.addEventListener('hexo-blog-decrypt', e => {
|
||||||
|
forPostFn()
|
||||||
|
window.translateFn.translateInitialization()
|
||||||
|
Object.values(window.globalFn.encrypt).forEach(fn => {
|
||||||
|
fn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
173
js/search/algolia.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
const { algolia } = GLOBAL_CONFIG
|
||||||
|
const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia
|
||||||
|
|
||||||
|
if (!appId || !apiKey || !indexName) {
|
||||||
|
return console.error('Algolia setting is invalid!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const $searchMask = document.getElementById('search-mask')
|
||||||
|
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||||
|
|
||||||
|
const animateElements = show => {
|
||||||
|
const action = show ? 'animateIn' : 'animateOut'
|
||||||
|
const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s'
|
||||||
|
const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s'
|
||||||
|
btf[action]($searchMask, maskAnimation)
|
||||||
|
btf[action]($searchDialog, dialogAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixSafariHeight = () => {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
$searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
btf.overflowPaddingR.add()
|
||||||
|
animateElements(true)
|
||||||
|
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
||||||
|
|
||||||
|
const handleEscape = event => {
|
||||||
|
if (event.code === 'Escape') {
|
||||||
|
closeSearch()
|
||||||
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscape)
|
||||||
|
fixSafariHeight()
|
||||||
|
window.addEventListener('resize', fixSafariHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSearch = () => {
|
||||||
|
btf.overflowPaddingR.remove()
|
||||||
|
animateElements(false)
|
||||||
|
window.removeEventListener('resize', fixSafariHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchClickFn = () => {
|
||||||
|
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchFnOnce = () => {
|
||||||
|
$searchMask.addEventListener('click', closeSearch)
|
||||||
|
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutContent = (content) => {
|
||||||
|
if (!content) return ''
|
||||||
|
const firstOccur = content.indexOf('<mark>')
|
||||||
|
let start = firstOccur - 30
|
||||||
|
let end = firstOccur + 120
|
||||||
|
let pre = ''
|
||||||
|
let post = ''
|
||||||
|
|
||||||
|
if (start <= 0) {
|
||||||
|
start = 0
|
||||||
|
end = 140
|
||||||
|
} else {
|
||||||
|
pre = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end > content.length) {
|
||||||
|
end = content.length
|
||||||
|
} else {
|
||||||
|
post = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${pre}${content.substring(start, end)}${post}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableDiv = [
|
||||||
|
document.getElementById('algolia-hits'),
|
||||||
|
document.getElementById('algolia-pagination'),
|
||||||
|
document.querySelector('#algolia-info .algolia-stats')
|
||||||
|
]
|
||||||
|
|
||||||
|
const search = instantsearch({
|
||||||
|
indexName,
|
||||||
|
searchClient: algoliasearch(appId, apiKey),
|
||||||
|
searchFunction (helper) {
|
||||||
|
disableDiv.forEach(item => {
|
||||||
|
item.style.display = helper.state.query ? '' : 'none'
|
||||||
|
})
|
||||||
|
if (helper.state.query) helper.search()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const widgets = [
|
||||||
|
instantsearch.widgets.configure({ hitsPerPage }),
|
||||||
|
instantsearch.widgets.searchBox({
|
||||||
|
container: '#algolia-search-input',
|
||||||
|
showReset: false,
|
||||||
|
showSubmit: false,
|
||||||
|
placeholder: languages.input_placeholder,
|
||||||
|
showLoadingIndicator: true
|
||||||
|
}),
|
||||||
|
instantsearch.widgets.hits({
|
||||||
|
container: '#algolia-hits',
|
||||||
|
templates: {
|
||||||
|
item (data) {
|
||||||
|
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
||||||
|
const result = data._highlightResult
|
||||||
|
const content = result.contentStripTruncate
|
||||||
|
? cutContent(result.contentStripTruncate.value)
|
||||||
|
: result.contentStrip
|
||||||
|
? cutContent(result.contentStrip.value)
|
||||||
|
: result.content
|
||||||
|
? cutContent(result.content.value)
|
||||||
|
: ''
|
||||||
|
return `
|
||||||
|
<a href="${link}" class="algolia-hit-item-link">
|
||||||
|
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
||||||
|
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||||
|
</a>`
|
||||||
|
},
|
||||||
|
empty (data) {
|
||||||
|
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
instantsearch.widgets.stats({
|
||||||
|
container: '#algolia-info > .algolia-stats',
|
||||||
|
templates: {
|
||||||
|
text (data) {
|
||||||
|
const stats = languages.hits_stats
|
||||||
|
.replace(/\$\{hits}/, data.nbHits)
|
||||||
|
.replace(/\$\{time}/, data.processingTimeMS)
|
||||||
|
return `<hr>${stats}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
instantsearch.widgets.poweredBy({
|
||||||
|
container: '#algolia-info > .algolia-poweredBy'
|
||||||
|
}),
|
||||||
|
instantsearch.widgets.pagination({
|
||||||
|
container: '#algolia-pagination',
|
||||||
|
totalPages: 5,
|
||||||
|
templates: {
|
||||||
|
first: '<i class="fas fa-angle-double-left"></i>',
|
||||||
|
last: '<i class="fas fa-angle-double-right"></i>',
|
||||||
|
previous: '<i class="fas fa-angle-left"></i>',
|
||||||
|
next: '<i class="fas fa-angle-right"></i>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
search.addWidgets(widgets)
|
||||||
|
search.start()
|
||||||
|
searchClickFn()
|
||||||
|
searchFnOnce()
|
||||||
|
|
||||||
|
window.addEventListener('pjax:complete', () => {
|
||||||
|
if (!btf.isHidden($searchMask)) closeSearch()
|
||||||
|
searchClickFn()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (window.pjax) {
|
||||||
|
search.on('render', () => {
|
||||||
|
window.pjax.refresh(document.getElementById('algolia-hits'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
360
js/search/local-search.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* 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 += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||||
|
} else {
|
||||||
|
resultItem += `<div 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></a>`
|
||||||
|
})
|
||||||
|
|
||||||
|
resultItem += '</div>'
|
||||||
|
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 } = GLOBAL_CONFIG.localSearch
|
||||||
|
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-wrap')
|
||||||
|
const $loadingStatus = document.getElementById('loading-status')
|
||||||
|
const isXml = !path.endsWith('json')
|
||||||
|
|
||||||
|
const inputEventFunction = () => {
|
||||||
|
if (!localSearch.isfetched) return
|
||||||
|
let searchText = input.value.trim().toLowerCase()
|
||||||
|
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||||
|
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
|
||||||
|
const keywords = searchText.split(/[-\s]+/)
|
||||||
|
const container = document.getElementById('local-search-results')
|
||||||
|
let resultItems = []
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
// Perform local searching
|
||||||
|
resultItems = localSearch.getResultItems(keywords)
|
||||||
|
}
|
||||||
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
|
container.textContent = ''
|
||||||
|
statsItem.textContent = ''
|
||||||
|
} else if (resultItems.length === 0) {
|
||||||
|
container.textContent = ''
|
||||||
|
const statsDiv = document.createElement('div')
|
||||||
|
statsDiv.className = 'search-result-stats'
|
||||||
|
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||||
|
statsItem.innerHTML = statsDiv.outerHTML
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
||||||
|
|
||||||
|
container.innerHTML = `<div class="search-result-list">${resultItems.map(result => result.item).join('')}</div>`
|
||||||
|
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||||
|
window.pjax && window.pjax.refresh(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadingStatus.textContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('search:loaded', () => {
|
||||||
|
const $loadDataItem = document.getElementById('loading-database')
|
||||||
|
$loadDataItem.nextElementSibling.style.display = 'block'
|
||||||
|
$loadDataItem.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
searchClickFn()
|
||||||
|
searchFnOnce()
|
||||||
|
|
||||||
|
// pjax
|
||||||
|
window.addEventListener('pjax:complete', () => {
|
||||||
|
!btf.isHidden($searchMask) && closeSearch()
|
||||||
|
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||||
|
searchClickFn()
|
||||||
|
})
|
||||||
|
})
|
||||||
117
js/tw_cn.js
Normal file
297
js/utils.js
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
(() => {
|
||||||
|
const btfFn = {
|
||||||
|
debounce: (func, wait = 0, immediate = false) => {
|
||||||
|
let timeout
|
||||||
|
return (...args) => {
|
||||||
|
const later = () => {
|
||||||
|
timeout = null
|
||||||
|
if (!immediate) func(...args)
|
||||||
|
}
|
||||||
|
const callNow = immediate && !timeout
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(later, wait)
|
||||||
|
if (callNow) func(...args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
throttle: function (func, wait, options = {}) {
|
||||||
|
let timeout, context, args
|
||||||
|
let previous = 0
|
||||||
|
|
||||||
|
const later = () => {
|
||||||
|
previous = options.leading === false ? 0 : new Date().getTime()
|
||||||
|
timeout = null
|
||||||
|
func.apply(context, args)
|
||||||
|
if (!timeout) context = args = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttled = (...params) => {
|
||||||
|
const now = new Date().getTime()
|
||||||
|
if (!previous && options.leading === false) previous = now
|
||||||
|
const remaining = wait - (now - previous)
|
||||||
|
context = this
|
||||||
|
args = params
|
||||||
|
if (remaining <= 0 || remaining > wait) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = null
|
||||||
|
}
|
||||||
|
previous = now
|
||||||
|
func.apply(context, args)
|
||||||
|
if (!timeout) context = args = null
|
||||||
|
} else if (!timeout && options.trailing !== false) {
|
||||||
|
timeout = setTimeout(later, remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return throttled
|
||||||
|
},
|
||||||
|
|
||||||
|
overflowPaddingR: {
|
||||||
|
add: () => {
|
||||||
|
const paddingRight = window.innerWidth - document.body.clientWidth
|
||||||
|
|
||||||
|
if (paddingRight > 0) {
|
||||||
|
document.body.style.paddingRight = `${paddingRight}px`
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||||
|
if (menuElement) {
|
||||||
|
menuElement.style.paddingRight = `${paddingRight}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: () => {
|
||||||
|
document.body.style.paddingRight = ''
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||||
|
if (menuElement) {
|
||||||
|
menuElement.style.paddingRight = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
snackbarShow: (text, showAction = false, duration = 2000) => {
|
||||||
|
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
|
||||||
|
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
|
||||||
|
Snackbar.show({
|
||||||
|
text,
|
||||||
|
backgroundColor: bg,
|
||||||
|
showAction,
|
||||||
|
duration,
|
||||||
|
pos: position,
|
||||||
|
customClass: 'snackbar-css'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
diffDate: (inputDate, more = false) => {
|
||||||
|
const dateNow = new Date()
|
||||||
|
const datePost = new Date(inputDate)
|
||||||
|
const diffMs = dateNow - datePost
|
||||||
|
const diffSec = diffMs / 1000
|
||||||
|
const diffMin = diffSec / 60
|
||||||
|
const diffHour = diffMin / 60
|
||||||
|
const diffDay = diffHour / 24
|
||||||
|
const diffMonth = diffDay / 30
|
||||||
|
const { dateSuffix } = GLOBAL_CONFIG
|
||||||
|
|
||||||
|
if (!more) return Math.floor(diffDay)
|
||||||
|
|
||||||
|
if (diffMonth > 12) return datePost.toISOString().slice(0, 10)
|
||||||
|
if (diffMonth >= 1) return `${Math.floor(diffMonth)} ${dateSuffix.month}`
|
||||||
|
if (diffDay >= 1) return `${Math.floor(diffDay)} ${dateSuffix.day}`
|
||||||
|
if (diffHour >= 1) return `${Math.floor(diffHour)} ${dateSuffix.hour}`
|
||||||
|
if (diffMin >= 1) return `${Math.floor(diffMin)} ${dateSuffix.min}`
|
||||||
|
return dateSuffix.just
|
||||||
|
},
|
||||||
|
|
||||||
|
loadComment: (dom, callback) => {
|
||||||
|
if ('IntersectionObserver' in window) {
|
||||||
|
const observerItem = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
callback()
|
||||||
|
observerItem.disconnect()
|
||||||
|
}
|
||||||
|
}, { threshold: [0] })
|
||||||
|
observerItem.observe(dom)
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToDest: (pos, time = 500) => {
|
||||||
|
const currentPos = window.scrollY
|
||||||
|
const isNavFixed = document.getElementById('page-header').classList.contains('fixed')
|
||||||
|
if (currentPos > pos || isNavFixed) pos = pos - 70
|
||||||
|
|
||||||
|
if ('scrollBehavior' in document.documentElement.style) {
|
||||||
|
window.scrollTo({
|
||||||
|
top: pos,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now()
|
||||||
|
const animate = currentTime => {
|
||||||
|
const timeElapsed = currentTime - startTime
|
||||||
|
const progress = Math.min(timeElapsed / time, 1)
|
||||||
|
window.scrollTo(0, currentPos + (pos - currentPos) * progress)
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
},
|
||||||
|
|
||||||
|
animateIn: (ele, animation) => {
|
||||||
|
ele.style.display = 'block'
|
||||||
|
ele.style.animation = animation
|
||||||
|
},
|
||||||
|
|
||||||
|
animateOut: (ele, animation) => {
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
ele.style.display = ''
|
||||||
|
ele.style.animation = ''
|
||||||
|
ele.removeEventListener('animationend', handleAnimationEnd)
|
||||||
|
}
|
||||||
|
ele.addEventListener('animationend', handleAnimationEnd)
|
||||||
|
ele.style.animation = animation
|
||||||
|
},
|
||||||
|
|
||||||
|
wrap: (selector, eleType, options) => {
|
||||||
|
const createEle = document.createElement(eleType)
|
||||||
|
for (const [key, value] of Object.entries(options)) {
|
||||||
|
createEle.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
selector.parentNode.insertBefore(createEle, selector)
|
||||||
|
createEle.appendChild(selector)
|
||||||
|
},
|
||||||
|
|
||||||
|
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
|
||||||
|
|
||||||
|
getEleTop: ele => {
|
||||||
|
let actualTop = ele.offsetTop
|
||||||
|
let current = ele.offsetParent
|
||||||
|
|
||||||
|
while (current !== null) {
|
||||||
|
actualTop += current.offsetTop
|
||||||
|
current = current.offsetParent
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualTop
|
||||||
|
},
|
||||||
|
|
||||||
|
loadLightbox: ele => {
|
||||||
|
const service = GLOBAL_CONFIG.lightbox
|
||||||
|
|
||||||
|
if (service === 'medium_zoom') {
|
||||||
|
mediumZoom(ele, { background: 'var(--zoom-bg)' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service === 'fancybox') {
|
||||||
|
Array.from(ele).forEach(i => {
|
||||||
|
if (i.parentNode.tagName !== 'A') {
|
||||||
|
const dataSrc = i.dataset.lazySrc || i.src
|
||||||
|
const dataCaption = i.title || i.alt || ''
|
||||||
|
btf.wrap(i, 'a', { href: dataSrc, 'data-fancybox': 'gallery', 'data-caption': dataCaption, 'data-thumb': dataSrc })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!window.fancyboxRun) {
|
||||||
|
Fancybox.bind('[data-fancybox]', {
|
||||||
|
Hash: false,
|
||||||
|
Thumbs: {
|
||||||
|
showOnStart: false
|
||||||
|
},
|
||||||
|
Images: {
|
||||||
|
Panzoom: {
|
||||||
|
maxScale: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Carousel: {
|
||||||
|
transition: 'slide'
|
||||||
|
},
|
||||||
|
Toolbar: {
|
||||||
|
display: {
|
||||||
|
left: ['infobar'],
|
||||||
|
middle: [
|
||||||
|
'zoomIn',
|
||||||
|
'zoomOut',
|
||||||
|
'toggle1to1',
|
||||||
|
'rotateCCW',
|
||||||
|
'rotateCW',
|
||||||
|
'flipX',
|
||||||
|
'flipY'
|
||||||
|
],
|
||||||
|
right: ['slideshow', 'thumbs', 'close']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.fancyboxRun = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setLoading: {
|
||||||
|
add: ele => {
|
||||||
|
const html = `
|
||||||
|
<div class="loading-container">
|
||||||
|
<div class="loading-item">
|
||||||
|
<div></div><div></div><div></div><div></div><div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
ele.insertAdjacentHTML('afterend', html)
|
||||||
|
},
|
||||||
|
remove: ele => {
|
||||||
|
ele.nextElementSibling.remove()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAnchor: anchor => {
|
||||||
|
if (anchor !== window.location.hash) {
|
||||||
|
if (!anchor) anchor = location.pathname
|
||||||
|
const title = GLOBAL_CONFIG_SITE.title
|
||||||
|
window.history.replaceState({
|
||||||
|
url: location.href,
|
||||||
|
title
|
||||||
|
}, title, anchor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getScrollPercent: (() => {
|
||||||
|
let docHeight, winHeight, headerHeight, contentMath
|
||||||
|
|
||||||
|
return (currentTop, ele) => {
|
||||||
|
if (!docHeight || ele.clientHeight !== docHeight) {
|
||||||
|
docHeight = ele.clientHeight
|
||||||
|
winHeight = window.innerHeight
|
||||||
|
headerHeight = ele.offsetTop
|
||||||
|
contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollPercent = (currentTop - headerHeight) / contentMath
|
||||||
|
return Math.max(0, Math.min(100, Math.round(scrollPercent * 100)))
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
|
||||||
|
addEventListenerPjax: (ele, event, fn, option = false) => {
|
||||||
|
ele.addEventListener(event, fn, option)
|
||||||
|
btf.addGlobalFn('pjaxSendOnce', () => {
|
||||||
|
ele.removeEventListener(event, fn, option)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
removeGlobalFnEvent: (key, parent = window) => {
|
||||||
|
const globalFn = parent.globalFn || {}
|
||||||
|
const keyObj = globalFn[key]
|
||||||
|
if (!keyObj) return
|
||||||
|
|
||||||
|
Object.keys(keyObj).forEach(i => keyObj[i]())
|
||||||
|
|
||||||
|
delete globalFn[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.btf = { ...window.btf, ...btfFn }
|
||||||
|
})()
|
||||||
325
links/index.html
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
<!DOCTYPE html><html lang="zh-CN" data-theme="dark"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>友链 | 時痕</title><meta name="author" content="Linloir"><meta name="copyright" content="Linloir"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#0d0d0d"><meta name="description" content="我、技术、生活与值得分享的一切">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="友链">
|
||||||
|
<meta property="og:url" content="https://blog.linloir.cn/links/">
|
||||||
|
<meta property="og:site_name" content="時痕">
|
||||||
|
<meta property="og:description" content="我、技术、生活与值得分享的一切">
|
||||||
|
<meta property="og:locale" content="zh_CN">
|
||||||
|
<meta property="og:image" content="https://blog.linloir.cn/img/cover.jpg">
|
||||||
|
<meta property="article:published_time" content="2024-10-10T15:30:51.000Z">
|
||||||
|
<meta property="article:modified_time" content="2025-04-20T08:17:20.922Z">
|
||||||
|
<meta property="article:author" content="Linloir">
|
||||||
|
<meta property="article:tag" content="Linloir, blog, technology, life, share, Linloir's Blog, 時痕, 霖落, 博客, 技术, 生活, 分享">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:image" content="https://blog.linloir.cn/img/cover.jpg"><link rel="shortcut icon" href="/img/avatar.png"><link rel="canonical" href="https://blog.linloir.cn/links/"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.min.css" media="print" onload="this.media='all'"><script>
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const saveToLocal = {
|
||||||
|
set: (key, value, ttl) => {
|
||||||
|
if (!ttl) return
|
||||||
|
const expiry = Date.now() + ttl * 86400000
|
||||||
|
localStorage.setItem(key, JSON.stringify({ value, expiry }))
|
||||||
|
},
|
||||||
|
get: key => {
|
||||||
|
const itemStr = localStorage.getItem(key)
|
||||||
|
if (!itemStr) return undefined
|
||||||
|
const { value, expiry } = JSON.parse(itemStr)
|
||||||
|
if (Date.now() > expiry) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.btf = {
|
||||||
|
saveToLocal,
|
||||||
|
getScript: (url, attr = {}) => new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = url
|
||||||
|
script.async = true
|
||||||
|
Object.entries(attr).forEach(([key, val]) => script.setAttribute(key, val))
|
||||||
|
script.onload = script.onreadystatechange = () => {
|
||||||
|
if (!script.readyState || /loaded|complete/.test(script.readyState)) resolve()
|
||||||
|
}
|
||||||
|
script.onerror = reject
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}),
|
||||||
|
getCSS: (url, id) => new Promise((resolve, reject) => {
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = url
|
||||||
|
if (id) link.id = id
|
||||||
|
link.onload = link.onreadystatechange = () => {
|
||||||
|
if (!link.readyState || /loaded|complete/.test(link.readyState)) resolve()
|
||||||
|
}
|
||||||
|
link.onerror = reject
|
||||||
|
document.head.appendChild(link)
|
||||||
|
}),
|
||||||
|
addGlobalFn: (key, fn, name = false, parent = window) => {
|
||||||
|
if (!true && key.startsWith('pjax')) return
|
||||||
|
const globalFn = parent.globalFn || {}
|
||||||
|
globalFn[key] = globalFn[key] || {}
|
||||||
|
if (name && globalFn[key][name]) return
|
||||||
|
globalFn[key][name || Object.keys(globalFn[key]).length] = fn
|
||||||
|
parent.globalFn = globalFn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const activateDarkMode = () => {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark')
|
||||||
|
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||||
|
document.querySelector('meta[name="theme-color"]').setAttribute('content', 'undefined')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const activateLightMode = () => {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light')
|
||||||
|
if (document.querySelector('meta[name="theme-color"]') !== null) {
|
||||||
|
document.querySelector('meta[name="theme-color"]').setAttribute('content', 'undefined')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.activateDarkMode = activateDarkMode
|
||||||
|
btf.activateLightMode = activateLightMode
|
||||||
|
|
||||||
|
const theme = saveToLocal.get('theme')
|
||||||
|
|
||||||
|
const mediaQueryDark = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const mediaQueryLight = window.matchMedia('(prefers-color-scheme: light)')
|
||||||
|
|
||||||
|
if (theme === undefined) {
|
||||||
|
if (mediaQueryLight.matches) activateLightMode()
|
||||||
|
else if (mediaQueryDark.matches) activateDarkMode()
|
||||||
|
else {
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
const isNight = hour <= 6 || hour >= 18
|
||||||
|
isNight ? activateDarkMode() : activateLightMode()
|
||||||
|
}
|
||||||
|
mediaQueryDark.addEventListener('change', () => {
|
||||||
|
if (saveToLocal.get('theme') === undefined) {
|
||||||
|
e.matches ? activateDarkMode() : activateLightMode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
theme === 'light' ? activateLightMode() : activateDarkMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const asideStatus = saveToLocal.get('aside-status')
|
||||||
|
if (asideStatus !== undefined) {
|
||||||
|
document.documentElement.classList.toggle('hide-aside', asideStatus === 'hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const detectApple = () => {
|
||||||
|
if (/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)) {
|
||||||
|
document.documentElement.classList.add('apple')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detectApple()
|
||||||
|
|
||||||
|
})()
|
||||||
|
</script><script>const GLOBAL_CONFIG = {
|
||||||
|
root: '/',
|
||||||
|
algolia: undefined,
|
||||||
|
localSearch: undefined,
|
||||||
|
translate: {"defaultEncoding":2,"translateDelay":0,"msgToTraditionalChinese":"繁","msgToSimplifiedChinese":"简"},
|
||||||
|
noticeOutdate: undefined,
|
||||||
|
highlight: {"plugin":"highlight.js","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false,"highlightFullpage":false,"highlightMacStyle":false},
|
||||||
|
copy: {
|
||||||
|
success: '复制成功',
|
||||||
|
error: '复制失败',
|
||||||
|
noSupport: '浏览器不支持'
|
||||||
|
},
|
||||||
|
relativeDate: {
|
||||||
|
homepage: false,
|
||||||
|
post: false
|
||||||
|
},
|
||||||
|
runtime: '',
|
||||||
|
dateSuffix: {
|
||||||
|
just: '刚刚',
|
||||||
|
min: '分钟前',
|
||||||
|
hour: '小时前',
|
||||||
|
day: '天前',
|
||||||
|
month: '个月前'
|
||||||
|
},
|
||||||
|
copyright: undefined,
|
||||||
|
lightbox: 'fancybox',
|
||||||
|
Snackbar: {"chs_to_cht":"已切换为繁体中文","cht_to_chs":"已切换为简体中文","day_to_night":"已切换为深色模式","night_to_day":"已切换为浅色模式","bgLight":"#49b1f5","bgDark":"#1f1f1f","position":"top-center"},
|
||||||
|
infinitegrid: {
|
||||||
|
js: 'https://cdn.jsdelivr.net/npm/@egjs/infinitegrid/dist/infinitegrid.min.js',
|
||||||
|
buttonText: '加载更多'
|
||||||
|
},
|
||||||
|
isPhotoFigcaption: false,
|
||||||
|
islazyload: false,
|
||||||
|
isAnchor: true,
|
||||||
|
percent: {
|
||||||
|
toc: false,
|
||||||
|
rightside: false,
|
||||||
|
},
|
||||||
|
autoDarkmode: true
|
||||||
|
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
|
||||||
|
title: '友链',
|
||||||
|
isPost: false,
|
||||||
|
isHome: false,
|
||||||
|
isHighlightShrink: false,
|
||||||
|
isToc: false,
|
||||||
|
postUpdate: '2025-04-20 16:17:20'
|
||||||
|
}</script><meta name="generator" content="Hexo 7.3.0"></head><body><script>window.paceOptions = {
|
||||||
|
restartOnPushState: false
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxSend', () => {
|
||||||
|
Pace.restart()
|
||||||
|
}, 'pace_restart')
|
||||||
|
|
||||||
|
</script><link rel="stylesheet" href="/css/minimal.css"/><script src="https://cdn.jsdelivr.net/npm/pace-js/pace.min.js"></script><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="/img/avatar.png" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">0</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">0</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">0</div></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-th"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></div></div></div></div><div class="page type-link" id="body-wrap"><header class="not-home-page" id="page-header" style="background-image: url(/img/top.jpg);"><nav id="nav"><span id="blog-info"><a class="nav-site-title" href="/"><span class="site-name">時痕</span></a></span><div id="menus"><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-th"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 归档</span></a></div></div><div id="toggle-menu"><span class="site-page"><i class="fas fa-bars fa-fw"></i></span></div></div></nav><div id="page-site-info"><h1 id="site-title">友链</h1></div></header><main class="layout" id="content-inner"><div id="page"><div id="article-container"><div class="flink"><h2 id="关于我"><a href="#关于我" class="headerlink" title="关于我"></a>关于我</h2> <div class="flink-list">
|
||||||
|
<div class="flink-list-item">
|
||||||
|
<a href="https://github.com/Linloir" title="GitHub" target="_blank">
|
||||||
|
<div class="flink-item-icon">
|
||||||
|
<img class="no-lightbox" src="undefined" onerror='this.onerror=null;this.src="/img/friend_404.gif"' alt="GitHub" />
|
||||||
|
</div>
|
||||||
|
<div class="flink-item-name">GitHub</div>
|
||||||
|
<div class="flink-item-desc" title="undefined">undefined</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flink-list-item">
|
||||||
|
<a href="https://space.bilibili.com/57762388" title="BiliBili" target="_blank">
|
||||||
|
<div class="flink-item-icon">
|
||||||
|
<img class="no-lightbox" src="undefined" onerror='this.onerror=null;this.src="/img/friend_404.gif"' alt="BiliBili" />
|
||||||
|
</div>
|
||||||
|
<div class="flink-item-name">BiliBili</div>
|
||||||
|
<div class="flink-item-desc" title="undefined">undefined</div>
|
||||||
|
</a>
|
||||||
|
</div></div></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info is-center"><div class="avatar-img"><img src="/img/avatar.png" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar"/></div><div class="author-info-name">Linloir</div><div class="author-info-description">我、技术、生活与值得分享的一切</div><div class="site-data"><a href="/archives/"><div class="headline">文章</div><div class="length-num">0</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">0</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">0</div></a></div><a id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/xxxxxx"><i class="fab fa-github"></i><span>Follow Me</span></a><div class="card-info-social-icons"><a class="social-icon" href="https://github.com/Linloir" target="_blank" title="GitHub"><i class="fab fa-github"></i></a><a class="social-icon" href="mailto:jonathanzhang.st@gmail.com" target="_blank" title="Email"><i class="fas fa-envelope"></i></a></div></div><div class="sticky_layout"><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>最新文章</span></div><div class="aside-list"></div></div><div class="card-widget card-archives"></div><div class="card-widget card-webinfo"><div class="item-headline"><i class="fas fa-chart-line"></i><span>网站信息</span></div><div class="webinfo"><div class="webinfo-item"><div class="item-name">文章数目 :</div><div class="item-count">0</div></div><div class="webinfo-item"><div class="item-name">本站总字数 :</div><div class="item-count">0</div></div><div class="webinfo-item"><div class="item-name">本站访客数 :</div><div class="item-count" id="busuanzi_value_site_uv"><i class="fa-solid fa-spinner fa-spin"></i></div></div><div class="webinfo-item"><div class="item-name">本站总浏览量 :</div><div class="item-count" id="busuanzi_value_site_pv"><i class="fa-solid fa-spinner fa-spin"></i></div></div><div class="webinfo-item"><div class="item-name">最后更新时间 :</div><div class="item-count" id="last-push-date" data-lastPushDate="2025-04-20T08:17:38.470Z"><i class="fa-solid fa-spinner fa-spin"></i></div></div></div></div></div></div></main><footer id="footer" style="background: transparent;"><div id="footer-wrap"><div class="copyright">©2022 - 2025 By Linloir</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div><div class="footer_custom_text">Wirtten with Love ❤</div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="translateLink" type="button" title="简繁转换">简</button><button id="darkmode" type="button" title="日间和夜间模式切换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside-config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button id="go-up" type="button" title="回到顶部"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="/js/tw_cn.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/instant.page/instantpage.min.js" type="module"></script><script src="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.js"></script><script>(() => {
|
||||||
|
const panguFn = () => {
|
||||||
|
if (typeof pangu === 'object') pangu.autoSpacingPage()
|
||||||
|
else {
|
||||||
|
btf.getScript('https://cdn.jsdelivr.net/npm/pangu/dist/browser/pangu.min.js')
|
||||||
|
.then(() => {
|
||||||
|
pangu.autoSpacingPage()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panguInit = () => {
|
||||||
|
if (true){
|
||||||
|
GLOBAL_CONFIG_SITE.isPost && panguFn()
|
||||||
|
} else {
|
||||||
|
panguFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('pjaxComplete', panguInit, 'pangu')
|
||||||
|
document.addEventListener('DOMContentLoaded', panguInit)
|
||||||
|
})()</script><div class="js-pjax"><script>(async () => {
|
||||||
|
const showKatex = () => {
|
||||||
|
document.querySelectorAll('#article-container .katex').forEach(el => el.classList.add('katex-show'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.katex_js_css) {
|
||||||
|
window.katex_js_css = true
|
||||||
|
await btf.getCSS('https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css')
|
||||||
|
if (false) {
|
||||||
|
await btf.getScript('https://cdn.jsdelivr.net/npm/katex/dist/contrib/copy-tex.min.js')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showKatex()
|
||||||
|
})()</script><script>(() => {
|
||||||
|
const runMermaid = ele => {
|
||||||
|
window.loadMermaid = true
|
||||||
|
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default'
|
||||||
|
|
||||||
|
ele.forEach((item, index) => {
|
||||||
|
const mermaidSrc = item.firstElementChild
|
||||||
|
const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n`
|
||||||
|
const mermaidID = `mermaid-${index}`
|
||||||
|
const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent
|
||||||
|
|
||||||
|
const renderFn = mermaid.render(mermaidID, mermaidDefinition)
|
||||||
|
const renderMermaid = svg => {
|
||||||
|
mermaidSrc.insertAdjacentHTML('afterend', svg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mermaid v9 and v10 compatibility
|
||||||
|
typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeToMermaid = () => {
|
||||||
|
const codeMermaidEle = document.querySelectorAll('pre > code.mermaid')
|
||||||
|
if (codeMermaidEle.length === 0) return
|
||||||
|
|
||||||
|
codeMermaidEle.forEach(ele => {
|
||||||
|
const preEle = document.createElement('pre')
|
||||||
|
preEle.className = 'mermaid-src'
|
||||||
|
preEle.hidden = true
|
||||||
|
preEle.textContent = ele.textContent
|
||||||
|
const newEle = document.createElement('div')
|
||||||
|
newEle.className = 'mermaid-wrap'
|
||||||
|
newEle.appendChild(preEle)
|
||||||
|
ele.parentNode.replaceWith(newEle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMermaid = () => {
|
||||||
|
if (true) codeToMermaid()
|
||||||
|
const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap')
|
||||||
|
if ($mermaid.length === 0) return
|
||||||
|
|
||||||
|
const runMermaidFn = () => runMermaid($mermaid)
|
||||||
|
btf.addGlobalFn('themeChange', runMermaidFn, 'mermaid')
|
||||||
|
window.loadMermaid ? runMermaidFn() : btf.getScript('https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js').then(runMermaidFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
btf.addGlobalFn('encrypt', loadMermaid, 'mermaid')
|
||||||
|
window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid)
|
||||||
|
})()</script></div><script id="canvas_nest" defer="defer" color="165,165,165" opacity="0.8" zIndex="-1" count="99" mobile="false" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/canvas-nest.min.js"></script><script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script><script>(() => {
|
||||||
|
const pjaxSelectors = ["head > title","#config-diff","#body-wrap","#rightside-config-hide","#rightside-config-show",".js-pjax"]
|
||||||
|
|
||||||
|
window.pjax = new Pjax({
|
||||||
|
elements: 'a:not([target="_blank"])',
|
||||||
|
selectors: pjaxSelectors,
|
||||||
|
cacheBust: false,
|
||||||
|
analytics: false,
|
||||||
|
scrollRestoration: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const triggerPjaxFn = (val) => {
|
||||||
|
if (!val) return
|
||||||
|
Object.values(val).forEach(fn => fn())
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('pjax:send', () => {
|
||||||
|
// removeEventListener
|
||||||
|
btf.removeGlobalFnEvent('pjaxSendOnce')
|
||||||
|
btf.removeGlobalFnEvent('themeChange')
|
||||||
|
|
||||||
|
// reset readmode
|
||||||
|
const $bodyClassList = document.body.classList
|
||||||
|
if ($bodyClassList.contains('read-mode')) $bodyClassList.remove('read-mode')
|
||||||
|
|
||||||
|
triggerPjaxFn(window.globalFn.pjaxSend)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('pjax:complete', () => {
|
||||||
|
btf.removeGlobalFnEvent('pjaxCompleteOnce')
|
||||||
|
document.querySelectorAll('script[data-pjax]').forEach(item => {
|
||||||
|
const newScript = document.createElement('script')
|
||||||
|
const content = item.text || item.textContent || item.innerHTML || ""
|
||||||
|
Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
|
||||||
|
newScript.appendChild(document.createTextNode(content))
|
||||||
|
item.parentNode.replaceChild(newScript, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
triggerPjaxFn(window.globalFn.pjaxComplete)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('pjax:error', e => {
|
||||||
|
if (e.request.status === 404) {
|
||||||
|
pjax.loadUrl('/404')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()</script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>
|
||||||