├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── publish.yml │ └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_CN.md ├── _config.yml ├── languages ├── default.yml ├── en.yml ├── ja.yml ├── ko.yml ├── zh-CN.yml ├── zh-HK.yml └── zh-TW.yml ├── layout ├── archive.pug ├── category.pug ├── includes │ ├── additional-js.pug │ ├── footer.pug │ ├── head.pug │ ├── head │ │ ├── Open_Graph.pug │ │ ├── analytics.pug │ │ ├── config.pug │ │ ├── config_site.pug │ │ ├── google_adsense.pug │ │ ├── preconnect.pug │ │ ├── pwa.pug │ │ ├── site_verification.pug │ │ └── structured_data.pug │ ├── header │ │ ├── index.pug │ │ ├── menu_item.pug │ │ ├── nav.pug │ │ ├── post-info.pug │ │ └── social.pug │ ├── layout.pug │ ├── loading │ │ ├── fullpage-loading.pug │ │ ├── index.pug │ │ └── pace.pug │ ├── mixins │ │ ├── article-sort.pug │ │ └── indexPostUI.pug │ ├── page │ │ ├── 404.pug │ │ ├── categories.pug │ │ ├── default-page.pug │ │ ├── flink.pug │ │ ├── shuoshuo.pug │ │ └── tags.pug │ ├── pagination.pug │ ├── post │ │ ├── outdate-notice.pug │ │ ├── post-copyright.pug │ │ └── reward.pug │ ├── rightside.pug │ ├── sidebar.pug │ ├── third-party │ │ ├── abcjs │ │ │ ├── abcjs.pug │ │ │ └── index.pug │ │ ├── aplayer.pug │ │ ├── card-post-count │ │ │ ├── artalk.pug │ │ │ ├── disqus.pug │ │ │ ├── fb.pug │ │ │ ├── index.pug │ │ │ ├── remark42.pug │ │ │ ├── twikoo.pug │ │ │ ├── valine.pug │ │ │ └── waline.pug │ │ ├── chat │ │ │ ├── chatra.pug │ │ │ ├── crisp.pug │ │ │ ├── index.pug │ │ │ └── tidio.pug │ │ ├── comments │ │ │ ├── artalk.pug │ │ │ ├── disqus.pug │ │ │ ├── disqusjs.pug │ │ │ ├── facebook_comments.pug │ │ │ ├── giscus.pug │ │ │ ├── gitalk.pug │ │ │ ├── index.pug │ │ │ ├── js.pug │ │ │ ├── livere.pug │ │ │ ├── remark42.pug │ │ │ ├── twikoo.pug │ │ │ ├── utterances.pug │ │ │ ├── valine.pug │ │ │ └── waline.pug │ │ ├── effect.pug │ │ ├── math │ │ │ ├── chartjs.pug │ │ │ ├── index.pug │ │ │ ├── katex.pug │ │ │ ├── mathjax.pug │ │ │ └── mermaid.pug │ │ ├── newest-comments │ │ │ ├── artalk.pug │ │ │ ├── common.pug │ │ │ ├── disqus-comment.pug │ │ │ ├── github-issues.pug │ │ │ ├── index.pug │ │ │ ├── remark42.pug │ │ │ ├── twikoo-comment.pug │ │ │ ├── valine.pug │ │ │ └── waline.pug │ │ ├── pjax.pug │ │ ├── prismjs.pug │ │ ├── search │ │ │ ├── algolia.pug │ │ │ ├── docsearch.pug │ │ │ ├── index.pug │ │ │ └── local-search.pug │ │ ├── share │ │ │ ├── addtoany.pug │ │ │ ├── index.pug │ │ │ └── share-js.pug │ │ ├── subtitle.pug │ │ └── umami_analytics.pug │ └── widget │ │ ├── card_ad.pug │ │ ├── card_announcement.pug │ │ ├── card_archives.pug │ │ ├── card_author.pug │ │ ├── card_bottom_self.pug │ │ ├── card_categories.pug │ │ ├── card_newest_comment.pug │ │ ├── card_post_series.pug │ │ ├── card_post_toc.pug │ │ ├── card_recent_post.pug │ │ ├── card_tags.pug │ │ ├── card_top_self.pug │ │ ├── card_webinfo.pug │ │ └── index.pug ├── index.pug ├── page.pug ├── post.pug └── tag.pug ├── package.json ├── plugins.yml ├── scripts ├── common │ └── postDesc.js ├── events │ ├── 404.js │ ├── cdn.js │ ├── comment.js │ ├── init.js │ ├── merge_config.js │ ├── stylus.js │ └── welcome.js ├── filters │ ├── post_lazyload.js │ └── random_cover.js ├── helpers │ ├── aside_archives.js │ ├── aside_categories.js │ ├── getArchiveLength.js │ ├── inject_head_js.js │ ├── page.js │ ├── related_post.js │ └── series.js └── tag │ ├── button.js │ ├── chartjs.js │ ├── flink.js │ ├── gallery.js │ ├── hide.js │ ├── inlineImg.js │ ├── label.js │ ├── mermaid.js │ ├── note.js │ ├── score.js │ ├── series.js │ ├── tabs.js │ └── timeline.js └── source ├── css ├── _global │ ├── function.styl │ └── index.styl ├── _highlight │ ├── highlight.styl │ ├── highlight │ │ ├── diff.styl │ │ └── index.styl │ ├── prismjs │ │ ├── diff.styl │ │ ├── index.styl │ │ └── line-number.styl │ └── theme.styl ├── _layout │ ├── aside.styl │ ├── chat.styl │ ├── comments.styl │ ├── footer.styl │ ├── head.styl │ ├── loading.styl │ ├── pagination.styl │ ├── post.styl │ ├── relatedposts.styl │ ├── reward.styl │ ├── rightside.styl │ ├── sidebar.styl │ └── third-party.styl ├── _mode │ ├── darkmode.styl │ └── readmode.styl ├── _page │ ├── 404.styl │ ├── archives.styl │ ├── categories.styl │ ├── common.styl │ ├── flink.styl │ ├── homepage.styl │ ├── shuoshuo.styl │ └── tags.styl ├── _search │ ├── algolia.styl │ ├── index.styl │ └── local-search.styl ├── _tags │ ├── button.styl │ ├── gallery.styl │ ├── hexo.styl │ ├── hide.styl │ ├── inlineImg.styl │ ├── label.styl │ ├── note.styl │ ├── series.styl │ ├── tabs.styl │ └── timeline.styl ├── _third-party │ └── normalize.min.css ├── index.styl └── var.styl ├── img ├── 404.jpg ├── butterfly-icon.png ├── error-page.png ├── favicon.ico └── friend_404.gif └── js ├── main.js ├── search ├── algolia.js └── local-search.js ├── tw_cn.js └── utils.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://buy.stripe.com/3cs6rP6YA91sbbG5kk','https://jsd.012700.xyz/gh/jerryc127/CDN/Photo/wechat.jpg'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: '[Bug]: ' 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 重要:請依照該模板來提交 10 | Important: Please follow the template to create a new issue 11 | 12 | - type: input 13 | id: butterfly-ver 14 | attributes: 15 | label: 使用的 Butterfly 版本? | What version of Butterfly are you using? 16 | description: 檢視主題的 package.json | Check the theme's package.json 17 | validations: 18 | required: true 19 | 20 | - type: dropdown 21 | id: modify 22 | attributes: 23 | label: 是否修改過主題文件? | Has the theme files been modified? 24 | options: 25 | - 是 (Yes) 26 | - 否 (No) 27 | validations: 28 | required: true 29 | 30 | - type: dropdown 31 | id: browser 32 | attributes: 33 | label: 使用的瀏覽器? | What browser are you using? 34 | options: 35 | - Chrome 36 | - Edge 37 | - Safari 38 | - Opera 39 | - Other 40 | validations: 41 | required: true 42 | 43 | - type: dropdown 44 | id: platform 45 | attributes: 46 | label: 使用的系統? | What operating system are you using? 47 | options: 48 | - Windows 49 | - macOS 50 | - Linux 51 | - Android 52 | - iOS 53 | - Other 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: dependencies 59 | attributes: 60 | label: 依賴插件 | Package dependencies information 61 | description: 在 Hexo 根目錄下執行 `npm ls --depth 0` | Run `npm ls --depth 0` in Hexo root directory 62 | render: Text 63 | validations: 64 | required: true 65 | 66 | - type: textarea 67 | id: description 68 | attributes: 69 | label: 問題描述 | Describe the bug 70 | description: 請描述你的問題現象 | A clear and concise description of what the bug is. 71 | placeholder: 請儘量提供截圖來定位問題 | If applicable, add screenshots to help explain your problem 72 | value: 73 | validations: 74 | required: true 75 | 76 | - type: input 77 | id: website 78 | attributes: 79 | label: 出現問題的網站 | Website with the issue 80 | description: 請提供可復現問題的網站地址 | Please provide a website URL where the problem can be reproduced. 81 | placeholder: 請填寫具體的網址,不要填寫 localhost 網站 | Please provide a specific URL, do not use localhost. 82 | validations: 83 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions about Butterfly 4 | url: https://github.com/jerryc127/hexo-theme-butterfly/discussions 5 | about: 一些使用問題請到 Discussion 詢問。 Please ask questions in Discussion. 6 | 7 | - name: Butterfly Q&A 8 | url: https://butterfly.js.org/posts/98d20436/ 9 | about: Butterfly Q&A 10 | 11 | - name: Telegram 12 | url: https://t.me/bu2fly 13 | about: 'Official Telegram Group' 14 | 15 | - name: QQ 群 16 | url: https://jq.qq.com/?_wv=1027&k=KU9105XR 17 | about: '群號 1070540070' 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: '[Feature]: ' 4 | 5 | body: 6 | - type: textarea 7 | id: feature-request 8 | attributes: 9 | label: 想要的功能 | What feature do you want? 10 | description: 請描述你需要的新功能 | A clear and concise description of what the feature is. 11 | placeholder: 12 | value: 13 | validations: 14 | require: true -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '12.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm install 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v5 11 | with: 12 | days-before-issue-stale: 30 13 | days-before-pr-stale: -1 14 | days-before-close: 7 15 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 16 | close-pr-message: 'This issue has not seen any activity since it was marked stale. Closing.' 17 | stale-issue-label: 'Stale' 18 | exempt-issue-labels: 'pinned,bug,enhancement,documentation,Plan' 19 | operations-per-run: 1000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /languages/ja.yml: -------------------------------------------------------------------------------- 1 | footer: 2 | framework: フレームワーク 3 | theme: テーマ 4 | 5 | copy: 6 | success: コピー成功 7 | error: コピー失敗 8 | noSupport: ブラウザが対応していません 9 | 10 | page: 11 | articles: 記事一覧 12 | tag: タグ 13 | category: カテゴリ 14 | archives: アーカイブ 15 | 16 | card_post_count: コメント数 17 | 18 | no_title: タイトルなし 19 | 20 | post: 21 | created: 作成日 22 | updated: 更新日 23 | wordcount: 総文字数 24 | min2read: 読む時間 25 | min2read_unit: 分 26 | page_pv: 閲覧数 27 | comments: コメント数 28 | copyright: 29 | author: 著者 30 | link: リンク 31 | copyright_notice: 著作権表示 32 | copyright_content: 'このブログのすべての記事は、%s ライセンスの下で提供されており、特に明記されていない限り、すべての権利を留保します。転載時には出典を明記してください: %s。' 33 | recommend: 関連記事 34 | edit: 編集 35 | 36 | search: 37 | title: 検索 38 | load_data: データベースを読み込んでいます 39 | input_placeholder: 記事を検索 40 | algolia_search: 41 | hits_empty: '${query} の検索結果が見つかりませんでした。' 42 | hits_stats: '${hits} 件の結果が ${time}ms で見つかりました' 43 | local_search: 44 | hits_empty: '${query} の検索結果が見つかりませんでした。' 45 | hits_stats: '${hits} 件の記事が見つかりました' 46 | 47 | pagination: 48 | prev: 前へ 49 | next: 次へ 50 | 51 | comment: コメント 52 | 53 | aside: 54 | articles: 記事 55 | tags: タグ 56 | categories: カテゴリ 57 | card_announcement: お知らせ 58 | card_categories: カテゴリ 59 | card_tags: タグ 60 | card_archives: アーカイブ 61 | card_recent_post: 最近の記事 62 | card_webinfo: 63 | headline: サイト情報 64 | article_name: 記事数 65 | runtime: 66 | name: 稼働時間 67 | unit: 日 68 | last_push_date: 69 | name: 最終更新日 70 | site_wordcount: 総文字数 71 | site_uv_name: ユーザー数 72 | site_pv_name: ページビュー数 73 | more_button: もっと見る 74 | card_newest_comments: 75 | headline: 最新コメント 76 | loading_text: ローディング中... 77 | error: コメントを取得できませんでした。設定を確認してください。 78 | zero: コメントがありません 79 | image: 画像 80 | link: リンク 81 | code: コード 82 | card_toc: 目次 83 | card_post_series: シリーズ記事 84 | 85 | date_suffix: 86 | just: たった今 87 | min: 分前 88 | hour: 時間前 89 | day: 日前 90 | month: ヶ月前 91 | 92 | donate: 寄付 93 | share: 共有 94 | 95 | rightside: 96 | readmode_title: 読書モード 97 | translate_title: 簡体字と繁体字の切り替え 98 | night_mode_title: ライトモード/ダークモード切り替え 99 | back_to_top: トップに戻る 100 | toc: 目次 101 | scroll_to_comment: コメントへ移動 102 | setting: 設定 103 | aside: シングルカラムとダブルカラムの切り替え 104 | chat: チャット 105 | 106 | copy_copyright: 107 | author: 著者 108 | link: リンク 109 | source: ソース 110 | info: 著作権は著者に帰属します。商業的利用の場合は著者に連絡して許可を得てください。非商業的利用の場合は出典を明記してください。 111 | 112 | Snackbar: 113 | chs_to_cht: 繁体字に切り替えました 114 | cht_to_chs: 簡体字に切り替えました 115 | day_to_night: ダークモードに切り替えました 116 | night_to_day: ライトモードに切り替えました 117 | 118 | loading: ローディング中... 119 | load_more: もっと見る 120 | 121 | error404: ページが見つかりません 122 | -------------------------------------------------------------------------------- /languages/ko.yml: -------------------------------------------------------------------------------- 1 | footer: 2 | framework: 프레임워크 3 | theme: 테마 4 | 5 | copy: 6 | success: 복사 성공 7 | error: 복사 실패 8 | noSupport: 브라우저가 지원되지 않음 9 | 10 | page: 11 | articles: 모든 글 12 | tag: 태그 13 | category: 카테고리 14 | archives: 아카이브 15 | 16 | card_post_count: 댓글 수 17 | 18 | no_title: 제목 없음 19 | 20 | post: 21 | created: 작성일 22 | updated: 수정일 23 | wordcount: 총 글자 수 24 | min2read: 읽기 시간 25 | min2read_unit: 분 26 | page_pv: 조회수 27 | comments: 댓글 28 | copyright: 29 | author: 작성자 30 | link: 링크 31 | copyright_notice: 저작권 고지 32 | copyright_content: '이 블로그의 모든 글은 %s 라이선스를 따르며, 별도로 명시되지 않는 한 모든 권리를 보유합니다. 재배포 시 출처를 명시해 주세요: %s.' 33 | recommend: 관련 글 34 | edit: 편집 35 | 36 | search: 37 | title: 검색 38 | load_data: 데이터베이스 로드 중 39 | input_placeholder: 글 검색 40 | algolia_search: 41 | hits_empty: '${query}에 대한 결과를 찾을 수 없습니다.' 42 | hits_stats: '${hits}개의 결과를 ${time}ms 만에 찾음' 43 | local_search: 44 | hits_empty: '${query}에 대한 결과를 찾을 수 없습니다.' 45 | hits_stats: '${hits}개의 글을 찾음' 46 | 47 | pagination: 48 | prev: 이전 49 | next: 다음 50 | 51 | comment: 댓글 52 | 53 | aside: 54 | articles: 글 55 | tags: 태그 56 | categories: 카테고리 57 | card_announcement: 공지 58 | card_categories: 카테고리 59 | card_tags: 태그 60 | card_archives: 아카이브 61 | card_recent_post: 최근 글 62 | card_webinfo: 63 | headline: 사이트 정보 64 | article_name: 글 수 65 | runtime: 66 | name: 운영 시간 67 | unit: 일 68 | last_push_date: 69 | name: 마지막 업데이트 70 | site_wordcount: 총 글자 수 71 | site_uv_name: 방문자 수 72 | site_pv_name: 총 조회수 73 | more_button: 더 보기 74 | card_newest_comments: 75 | headline: 최신 댓글 76 | loading_text: 로딩 중... 77 | error: 댓글을 가져올 수 없습니다. 설정을 확인해 주세요. 78 | zero: 댓글 없음 79 | image: 이미지 80 | link: 링크 81 | code: 코드 82 | card_toc: 목차 83 | card_post_series: 시리즈 글 84 | 85 | date_suffix: 86 | just: 방금 87 | min: 분 전 88 | hour: 시간 전 89 | day: 일 전 90 | month: 달 전 91 | 92 | donate: 후원 93 | share: 공유 94 | 95 | rightside: 96 | readmode_title: 읽기 모드 97 | translate_title: 번체와 간체 전환 98 | night_mode_title: 라이트/다크 모드 전환 99 | back_to_top: 맨 위로 100 | toc: 목차 101 | scroll_to_comment: 댓글로 이동 102 | setting: 설정 103 | aside: 단일/이중 열 전환 104 | chat: 채팅 105 | 106 | copy_copyright: 107 | author: 작성자 108 | link: 링크 109 | source: 출처 110 | info: 저작권은 작성자에게 있습니다. 상업적 사용을 위해서는 작성자의 허가를 받아야 하며, 비상업적 사용 시에는 출처를 명시해 주세요. 111 | 112 | Snackbar: 113 | chs_to_cht: 번체로 전환되었습니다. 114 | cht_to_chs: 간체로 전환되었습니다. 115 | day_to_night: 다크 모드로 전환되었습니다. 116 | night_to_day: 라이트 모드로 전환되었습니다. 117 | 118 | loading: 로딩 중... 119 | load_more: 더 보기 120 | 121 | error404: 페이지를 찾을 수 없습니다. 122 | -------------------------------------------------------------------------------- /languages/zh-CN.yml: -------------------------------------------------------------------------------- 1 | footer: 2 | framework: 框架 3 | theme: 主题 4 | 5 | copy: 6 | success: 复制成功 7 | error: 复制失败 8 | noSupport: 浏览器不支持 9 | 10 | page: 11 | articles: 全部文章 12 | tag: 标签 13 | category: 分类 14 | archives: 归档 15 | 16 | card_post_count: 条评论 17 | 18 | no_title: 无标题 19 | 20 | post: 21 | created: 发表于 22 | updated: 更新于 23 | wordcount: 总字数 24 | min2read: 阅读时长 25 | min2read_unit: 分钟 26 | page_pv: 浏览量 27 | comments: 评论数 28 | copyright: 29 | author: 文章作者 30 | link: 文章链接 31 | copyright_notice: 版权声明 32 | copyright_content: '本博客所有文章除特别声明外,均采用 33 | %s 许可协议。转载请注明来源 %s!' 34 | recommend: 相关推荐 35 | edit: 编辑 36 | 37 | search: 38 | title: 搜索 39 | load_data: 数据加载中 40 | input_placeholder: 搜索文章 41 | algolia_search: 42 | hits_empty: '未找到符合您查询的内容:${query}' 43 | hits_stats: '找到 ${hits} 条结果,耗时 ${time} 毫秒' 44 | local_search: 45 | hits_empty: '未找到符合您查询的内容:${query}' 46 | hits_stats: '共找到 ${hits} 篇文章' 47 | 48 | pagination: 49 | prev: 上一篇 50 | next: 下一篇 51 | 52 | comment: 评论 53 | 54 | aside: 55 | articles: 文章 56 | tags: 标签 57 | categories: 分类 58 | card_announcement: 公告 59 | card_categories: 分类 60 | card_tags: 标签 61 | card_archives: 归档 62 | card_recent_post: 最新文章 63 | card_webinfo: 64 | headline: 网站信息 65 | article_name: 文章数目 66 | runtime: 67 | name: 运行时间 68 | unit: 天 69 | last_push_date: 70 | name: 最后更新时间 71 | site_wordcount: 本站总字数 72 | site_uv_name: 本站访客数 73 | site_pv_name: 本站总浏览量 74 | more_button: 查看更多 75 | card_newest_comments: 76 | headline: 最新评论 77 | loading_text: 加载中... 78 | error: 无法获取评论,请确认相关配置是否正确 79 | zero: 暂无评论 80 | image: 图片 81 | link: 链接 82 | code: 代码 83 | card_toc: 目录 84 | card_post_series: 系列文章 85 | 86 | date_suffix: 87 | just: 刚刚 88 | min: 分钟前 89 | hour: 小时前 90 | day: 天前 91 | month: 个月前 92 | 93 | donate: 赞助 94 | share: 分享 95 | 96 | rightside: 97 | readmode_title: 阅读模式 98 | translate_title: 简繁转换 99 | night_mode_title: 日间和夜间模式切换 100 | back_to_top: 回到顶部 101 | toc: 目录 102 | scroll_to_comment: 前往评论 103 | setting: 设置 104 | aside: 单栏和双栏切换 105 | chat: 聊天 106 | 107 | copy_copyright: 108 | author: 作者 109 | link: 链接 110 | source: 来源 111 | info: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 112 | 113 | Snackbar: 114 | chs_to_cht: 已切换为繁体中文 115 | cht_to_chs: 已切换为简体中文 116 | day_to_night: 已切换为深色模式 117 | night_to_day: 已切换为浅色模式 118 | 119 | loading: 加载中... 120 | load_more: 加载更多 121 | 122 | error404: 页面未找到 123 | -------------------------------------------------------------------------------- /languages/zh-HK.yml: -------------------------------------------------------------------------------- 1 | footer: 2 | framework: 框架 3 | theme: 主題 4 | 5 | copy: 6 | success: 複製成功 7 | error: 複製失敗 8 | noSupport: 瀏覽器不支援 9 | 10 | page: 11 | articles: 全部文章 12 | tag: 標籤 13 | category: 分類 14 | archives: 歸檔 15 | 16 | card_post_count: 條評論 17 | 18 | no_title: 無標題 19 | 20 | post: 21 | created: 發表於 22 | updated: 更新於 23 | wordcount: 字數統計 24 | min2read: 閱讀時間 25 | min2read_unit: 分鐘 26 | page_pv: 瀏覽量 27 | comments: 評論數 28 | copyright: 29 | author: 文章作者 30 | link: 文章連結 31 | copyright_notice: 版權聲明 32 | copyright_content: '除特別聲明外,本博客所有文章均採用%s 授權協議。轉載請註明出處:%s。' 33 | recommend: 相關文章 34 | edit: 編輯 35 | 36 | search: 37 | title: 搜尋 38 | load_data: 正在加載數據庫 39 | input_placeholder: 搜尋文章 40 | algolia_search: 41 | hits_empty: '未找到相關內容:${query}' 42 | hits_stats: '找到 ${hits} 條結果,耗時 ${time} 毫秒' 43 | local_search: 44 | hits_empty: '未找到相關內容:${query}' 45 | hits_stats: '找到 ${hits} 篇文章' 46 | 47 | pagination: 48 | prev: 上一頁 49 | next: 下一頁 50 | 51 | comment: 評論 52 | 53 | aside: 54 | articles: 文章 55 | tags: 標籤 56 | categories: 分類 57 | card_announcement: 公告 58 | card_categories: 分類 59 | card_tags: 標籤 60 | card_archives: 歸檔 61 | card_recent_post: 最新文章 62 | card_webinfo: 63 | headline: 網站資訊 64 | article_name: 文章數目 65 | runtime: 66 | name: 運行時間 67 | unit: 天 68 | last_push_date: 69 | name: 最後更新時間 70 | site_wordcount: 總字數 71 | site_uv_name: 訪客數 72 | site_pv_name: 總瀏覽量 73 | more_button: 查看更多 74 | card_newest_comments: 75 | headline: 最新評論 76 | loading_text: 正在加載... 77 | error: 無法取得評論,請確認配置是否正確 78 | zero: 暫無評論 79 | image: 圖片 80 | link: 連結 81 | code: 代碼 82 | card_toc: 目錄 83 | card_post_series: 系列文章 84 | 85 | date_suffix: 86 | just: 剛剛 87 | min: 分鐘前 88 | hour: 小時前 89 | day: 天前 90 | month: 個月前 91 | 92 | donate: 贊助 93 | share: 分享 94 | 95 | rightside: 96 | readmode_title: 閱讀模式 97 | translate_title: 簡繁轉換 98 | night_mode_title: 切換日夜模式 99 | back_to_top: 回到頂部 100 | toc: 目錄 101 | scroll_to_comment: 前往評論 102 | setting: 設定 103 | aside: 單欄與雙欄切換 104 | chat: 聊天 105 | 106 | copy_copyright: 107 | author: 作者 108 | link: 連結 109 | source: 來源 110 | info: 版權屬於作者所有。商業用途請聯絡作者獲得授權,非商業用途請註明出處。 111 | 112 | Snackbar: 113 | chs_to_cht: 已切換為繁體中文 114 | cht_to_chs: 已切換為簡體中文 115 | day_to_night: 已切換為深色模式 116 | night_to_day: 已切換為淺色模式 117 | 118 | loading: 正在加載... 119 | load_more: 加載更多 120 | 121 | error404: 未找到頁面 122 | -------------------------------------------------------------------------------- /languages/zh-TW.yml: -------------------------------------------------------------------------------- 1 | footer: 2 | framework: 框架 3 | theme: 主題 4 | 5 | copy: 6 | success: 複製成功 7 | error: 複製失敗 8 | noSupport: 瀏覽器不支援 9 | 10 | page: 11 | articles: 所有文章 12 | tag: 標籤 13 | category: 分類 14 | archives: 歸檔 15 | 16 | card_post_count: 則評論 17 | 18 | no_title: 無標題 19 | 20 | post: 21 | created: 發表於 22 | updated: 更新於 23 | wordcount: 總字數 24 | min2read: 閱讀時間 25 | min2read_unit: 分鐘 26 | page_pv: 瀏覽量 27 | comments: 評論數 28 | copyright: 29 | author: 文章作者 30 | link: 文章連結 31 | copyright_notice: 版權聲明 32 | copyright_content: '本部落格所有文章除特別聲明外,均採用%s 授權協議。轉載請註明來源 %s!' 33 | recommend: 相關推薦 34 | edit: 編輯 35 | 36 | search: 37 | title: 搜尋 38 | load_data: 資料載入中 39 | input_placeholder: 搜尋文章 40 | algolia_search: 41 | hits_empty: '找不到符合您查詢的內容:${query}' 42 | hits_stats: '找到 ${hits} 筆結果,耗時 ${time} 毫秒' 43 | local_search: 44 | hits_empty: '找不到符合您查詢的內容:${query}' 45 | hits_stats: '共找到 ${hits} 篇文章' 46 | 47 | pagination: 48 | prev: 上一篇 49 | next: 下一篇 50 | 51 | comment: 評論 52 | 53 | aside: 54 | articles: 文章 55 | tags: 標籤 56 | categories: 分類 57 | card_announcement: 公告 58 | card_categories: 分類 59 | card_tags: 標籤 60 | card_archives: 歸檔 61 | card_recent_post: 最新文章 62 | card_webinfo: 63 | headline: 網站資訊 64 | article_name: 文章數量 65 | runtime: 66 | name: 運行時間 67 | unit: 天 68 | last_push_date: 69 | name: 最後更新時間 70 | site_wordcount: 總字數 71 | site_uv_name: 訪客數 72 | site_pv_name: 總瀏覽量 73 | more_button: 檢視更多 74 | card_newest_comments: 75 | headline: 最新評論 76 | loading_text: 載入中... 77 | error: 無法獲取評論,請確認相關配置是否正確 78 | zero: 尚無評論 79 | image: 圖片 80 | link: 連結 81 | code: 程式碼 82 | card_toc: 目錄 83 | card_post_series: 系列文章 84 | 85 | date_suffix: 86 | just: 剛剛 87 | min: 分鐘前 88 | hour: 小時前 89 | day: 天前 90 | month: 個月前 91 | 92 | donate: 贊助 93 | share: 分享 94 | 95 | rightside: 96 | readmode_title: 閱讀模式 97 | translate_title: 繁簡轉換 98 | night_mode_title: 日夜模式切換 99 | back_to_top: 回到頂端 100 | toc: 目錄 101 | scroll_to_comment: 前往評論 102 | setting: 設定 103 | aside: 單欄和雙欄切換 104 | chat: 聊天 105 | 106 | copy_copyright: 107 | author: 作者 108 | link: 連結 109 | source: 來源 110 | info: 著作權歸作者所有。如需商業轉載,請聯絡作者獲得授權,非商業轉載請註明出處。 111 | 112 | Snackbar: 113 | chs_to_cht: 已切換為繁體中文 114 | cht_to_chs: 已切換為簡體中文 115 | day_to_night: 已切換為深色模式 116 | night_to_day: 已切換為淺色模式 117 | 118 | loading: 載入中... 119 | load_more: 載入更多 120 | 121 | error404: 找不到頁面 122 | -------------------------------------------------------------------------------- /layout/archive.pug: -------------------------------------------------------------------------------- 1 | extends includes/layout.pug 2 | 3 | block content 4 | include ./includes/mixins/article-sort.pug 5 | #archive 6 | .article-sort-title= `${_p('page.articles')} - ${getArchiveLength()}` 7 | +articleSort(page.posts) 8 | include includes/pagination.pug -------------------------------------------------------------------------------- /layout/category.pug: -------------------------------------------------------------------------------- 1 | extends includes/layout.pug 2 | 3 | block content 4 | if theme.category_ui == 'index' 5 | include ./includes/mixins/indexPostUI.pug 6 | +indexPostUI 7 | else 8 | include ./includes/mixins/article-sort.pug 9 | #category 10 | .article-sort-title= _p('page.category') + ' - ' + page.category 11 | +articleSort(page.posts) 12 | include includes/pagination.pug -------------------------------------------------------------------------------- /layout/includes/additional-js.pug: -------------------------------------------------------------------------------- 1 | div 2 | script(src=url_for(theme.asset.utils)) 3 | script(src=url_for(theme.asset.main)) 4 | 5 | if theme.translate.enable 6 | script(src=url_for(theme.asset.translate)) 7 | 8 | if theme.lightbox 9 | script(src=url_for(theme.asset[theme.lightbox])) 10 | 11 | if theme.instantpage 12 | script(src=url_for(theme.asset.instantpage), type='module') 13 | 14 | if theme.lazyload.enable && !theme.lazyload.native 15 | script(src=url_for(theme.asset.lazyload)) 16 | 17 | if theme.snackbar.enable 18 | script(src=url_for(theme.asset.snackbar)) 19 | 20 | .js-pjax 21 | if needLoadCountJs 22 | != partial("includes/third-party/card-post-count/index", {}, { cache: true }) 23 | 24 | if loadSubJs 25 | include ./third-party/subtitle.pug 26 | 27 | include ./third-party/math/index.pug 28 | include ./third-party/abcjs/index.pug 29 | 30 | if commentsJsLoad 31 | include ./third-party/comments/js.pug 32 | 33 | != partial("includes/third-party/prismjs", {}, { cache: true }) 34 | 35 | if theme.aside.enable && theme.aside.card_newest_comments.enable 36 | if theme.pjax.enable || (globalPageType !== 'post' && page.aside !== false) 37 | != partial("includes/third-party/newest-comments/index", {}, { cache: true }) 38 | 39 | != fragment_cache('injectBottom', function(){return injectHtml(theme.inject.bottom)}) 40 | 41 | != partial("includes/third-party/effect", {}, { cache: true }) 42 | != partial("includes/third-party/chat/index", {}, { cache: true }) 43 | 44 | if theme.aplayerInject && theme.aplayerInject.enable 45 | if theme.pjax.enable || theme.aplayerInject.per_page || page.aplayer 46 | include ./third-party/aplayer.pug 47 | 48 | if theme.pjax.enable 49 | != partial("includes/third-party/pjax", {}, { cache: true }) 50 | 51 | if theme.umami_analytics.enable 52 | != partial("includes/third-party/umami_analytics", {}, { cache: true }) 53 | 54 | if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv 55 | script(async data-pjax src= theme.asset.busuanzi || '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js') 56 | 57 | != partial('includes/third-party/search/index', {}, { cache: true }) 58 | 59 | if (theme.google_tag_manager && theme.google_tag_manager.tag_id) 60 | noscript 61 | iframe(src=`${theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/ns.html?id=${theme.google_tag_manager.tag_id}` height="0" width="0" style="display:none;visibility:hidden") -------------------------------------------------------------------------------- /layout/includes/footer.pug: -------------------------------------------------------------------------------- 1 | - const { nav, owner, copyright, custom_text } = theme.footer 2 | 3 | if nav 4 | .footer-flex 5 | for block in nav 6 | .footer-flex-items(style=`${ block.width ? 'flex-grow:' + block.width : '' }`) 7 | for blockItem in block.content 8 | .footer-flex-item 9 | .footer-flex-title= blockItem.title 10 | .footer-flex-content 11 | for subitem in blockItem.item 12 | if subitem.html 13 | div!= subitem.html 14 | else if subitem.url 15 | a(href=url_for(subitem.url), target='_blank' title=subitem.title)= subitem.title 16 | else if subitem.title 17 | div!= subitem.title 18 | .footer-other 19 | .footer-copyright 20 | if owner.enable 21 | - const currentYear = new Date().getFullYear() 22 | - const sinceYear = owner.since 23 | span.copyright 24 | if sinceYear && sinceYear != currentYear 25 | != `©${sinceYear} - ${currentYear} By ${config.author}` 26 | else 27 | != `©${currentYear} By ${config.author}` 28 | if copyright.enable 29 | - const v = copyright.version ? getVersion() : false 30 | span.framework-info 31 | if owner.enable && nav 32 | span.footer-separator | 33 | span= _p('footer.framework') + ' ' 34 | a(href='https://hexo.io')= `Hexo${ v ? ' ' + v.hexo : '' }` 35 | span.footer-separator | 36 | span= _p('footer.theme') + ' ' 37 | a(href='https://github.com/jerryc127/hexo-theme-butterfly')= `Butterfly${ v ? ' ' + v.theme : '' }` 38 | if theme.footer.custom_text 39 | .footer_custom_text!= theme.footer.custom_text 40 | -------------------------------------------------------------------------------- /layout/includes/head/Open_Graph.pug: -------------------------------------------------------------------------------- 1 | if theme.Open_Graph_meta.enable 2 | - 3 | const coverVal = page.cover_type === 'img' ? page.cover : theme.avatar.img 4 | let ogOption = Object.assign({ 5 | type: globalPageType === 'post' ? 'article' : 'website', 6 | image: coverVal ? full_url_for(coverVal) : '', 7 | fb_admins: theme.facebook_comments.user_id || '', 8 | fb_app_id: theme.facebook_comments.app_id || '', 9 | }, theme.Open_Graph_meta.option) 10 | - 11 | != open_graph(ogOption) 12 | else 13 | - const description = page.description || page.content || page.title || config.description 14 | if description 15 | meta(name="description" content=truncate(description, 150)) 16 | 17 | -------------------------------------------------------------------------------- /layout/includes/head/analytics.pug: -------------------------------------------------------------------------------- 1 | if theme.baidu_analytics 2 | script. 3 | var _hmt = _hmt || []; 4 | (function() { 5 | var hm = document.createElement("script"); 6 | hm.src = "https://hm.baidu.com/hm.js?!{theme.baidu_analytics}"; 7 | var s = document.getElementsByTagName("script")[0]; 8 | s.parentNode.insertBefore(hm, s); 9 | })(); 10 | btf.addGlobalFn('pjaxComplete', () => { 11 | _hmt.push(['_trackPageview',window.location.pathname]) 12 | }, 'baidu_analytics') 13 | 14 | if theme.google_analytics 15 | script(async src=`https://www.googletagmanager.com/gtag/js?id=${theme.google_analytics}`) 16 | script. 17 | window.dataLayer = window.dataLayer || [] 18 | function gtag(){dataLayer.push(arguments)} 19 | gtag('js', new Date()) 20 | gtag('config', '!{theme.google_analytics}') 21 | btf.addGlobalFn('pjaxComplete', () => { 22 | gtag('config', '!{theme.google_analytics}', {'page_path': window.location.pathname}) 23 | }, 'google_analytics') 24 | 25 | if theme.cloudflare_analytics 26 | script(defer data-pjax src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon=`{"token": "${theme.cloudflare_analytics}"}`) 27 | 28 | if theme.microsoft_clarity 29 | script. 30 | (function(c,l,a,r,i,t,y){ 31 | c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; 32 | t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; 33 | y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); 34 | })(window, document, "clarity", "script", "!{theme.microsoft_clarity}"); 35 | 36 | if (theme.google_tag_manager && theme.google_tag_manager.tag_id) 37 | script. 38 | (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 39 | new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 40 | j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 41 | "!{theme.google_tag_manager.domain ? theme.google_tag_manager.domain : 'https://www.googletagmanager.com'}/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f); 42 | })(window,document,'script','dataLayer','!{theme.google_tag_manager.tag_id}'); 43 | btf.addGlobalFn('pjaxComplete', () => { 44 | dataLayer.push({'event': 'pjaxComplete', 'page_title': document.title, 'page_location': location.href, 'page_path': window.location.pathname}); 45 | }, 'google_tag_manager'); -------------------------------------------------------------------------------- /layout/includes/head/config_site.pug: -------------------------------------------------------------------------------- 1 | - 2 | const titleVal = pageTitle.replace(/'/ig,"\\'") 3 | 4 | let isHighlightShrink 5 | if (theme.code_blocks.shrink == 'none') isHighlightShrink = 'undefined' 6 | else if (typeof page.highlight_shrink == 'boolean') isHighlightShrink = page.highlight_shrink 7 | else isHighlightShrink = theme.code_blocks.shrink 8 | 9 | var showToc = false 10 | if (theme.aside.enable && page.aside !== false) { 11 | let tocEnable = false 12 | if (globalPageType === 'post' && theme.toc.post) tocEnable = true 13 | else if (globalPageType === 'page' && theme.toc.page) tocEnable = true 14 | const pageToc = typeof page.toc === 'boolean' ? page.toc : tocEnable 15 | showToc = pageToc && (toc(page.content) !== '' || page.encrypt === true) 16 | } 17 | - 18 | 19 | script#config-diff. 20 | var GLOBAL_CONFIG_SITE = { 21 | title: '!{titleVal}', 22 | isHighlightShrink: !{isHighlightShrink}, 23 | isToc: !{showToc}, 24 | pageType: '!{page.type == 'shuoshuo' ? 'shuoshuo' : globalPageType}' 25 | } 26 | -------------------------------------------------------------------------------- /layout/includes/head/google_adsense.pug: -------------------------------------------------------------------------------- 1 | if (theme.google_adsense && theme.google_adsense.enable) 2 | script(async src=theme.google_adsense.js) 3 | 4 | if theme.google_adsense.auto_ads 5 | script. 6 | (adsbygoogle = window.adsbygoogle || []).push({ 7 | google_ad_client: '!{theme.google_adsense.client}', 8 | enable_page_level_ads: '!{theme.google_adsense.enable_page_level_ads}' 9 | }); -------------------------------------------------------------------------------- /layout/includes/head/preconnect.pug: -------------------------------------------------------------------------------- 1 | - 2 | const { internal_provider, third_party_provider, custom_format } = theme.CDN 3 | const providers = { 4 | 'jsdelivr': '//cdn.jsdelivr.net', 5 | 'cdnjs': '//cdnjs.cloudflare.com', 6 | 'unpkg': '//unpkg.com', 7 | 'custom': custom_format && custom_format.match(/^((https?:)?(\/\/[^/]+)|([^/]+))(\/|$)/)[1] 8 | } 9 | - 10 | 11 | if internal_provider === third_party_provider && internal_provider !== 'local' 12 | link(rel="preconnect" href=providers[internal_provider]) 13 | else 14 | if internal_provider !== 'local' 15 | link(rel="preconnect" href=providers[internal_provider]) 16 | if third_party_provider !== 'local' 17 | link(rel="preconnect" href=providers[third_party_provider]) 18 | 19 | if theme.google_analytics 20 | link(rel="preconnect" href="//www.google-analytics.com" crossorigin='') 21 | 22 | if theme.baidu_analytics 23 | link(rel="preconnect" href="//hm.baidu.com") 24 | 25 | if theme.cloudflare_analytics 26 | link(rel="preconnect" href="//static.cloudflareinsights.com") 27 | 28 | if theme.microsoft_clarity 29 | link(rel="preconnect" href="//www.clarity.ms") 30 | 31 | if theme.blog_title_font && theme.blog_title_font.font_link && theme.blog_title_font.font_link.indexOf('//fonts.googleapis.com') != -1 32 | link(rel="preconnect" href="//fonts.googleapis.com" crossorigin='') 33 | 34 | if !theme.asset.busuanzi && (theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv) 35 | link(rel="preconnect" href="//busuanzi.ibruce.info") -------------------------------------------------------------------------------- /layout/includes/head/pwa.pug: -------------------------------------------------------------------------------- 1 | - const { manifest, theme_color, apple_touch_icon, favicon_32_32, favicon_16_16, mask_icon } = theme.pwa 2 | 3 | link(rel="manifest" href=url_for(manifest)) 4 | if theme_color 5 | meta(name="msapplication-TileColor" content=theme_color) 6 | if apple_touch_icon 7 | link(rel="apple-touch-icon" sizes="180x180" href=url_for(apple_touch_icon)) 8 | if favicon_32_32 9 | link(rel="icon" type="image/png" sizes="32x32" href=url_for(favicon_32_32)) 10 | if favicon_16_16 11 | link(rel="icon" type="image/png" sizes="16x16" href=url_for(favicon_16_16)) 12 | if mask_icon 13 | link(rel="mask-icon" href=url_for(mask_icon) color="#5bbad5") 14 | -------------------------------------------------------------------------------- /layout/includes/head/site_verification.pug: -------------------------------------------------------------------------------- 1 | if theme.site_verification 2 | each item in theme.site_verification 3 | meta(name=item.name content=item.content) -------------------------------------------------------------------------------- /layout/includes/head/structured_data.pug: -------------------------------------------------------------------------------- 1 | if theme.structured_data && page.layout === 'post' 2 | - 3 | // use json-ld to add structured data 4 | 5 | const title = page.title 6 | const url = page.permalink 7 | const imageVal = page.cover_type === 'img' ? page.cover : theme.avatar.img 8 | const image = imageVal ? full_url_for(imageVal) : '' 9 | const datePublished = page.date.toISOString() 10 | const dateModified = (page.updated || page.date).toISOString() 11 | const author = page.copyright_author || config.author 12 | const authorHrefVal = page.copyright_author_href || theme.post_copyright.author_href || site.url 13 | const authorHref = full_url_for(authorHrefVal) 14 | 15 | const jsonLd = { 16 | "@context": "https://schema.org", 17 | "@type": "BlogPosting", 18 | "headline": title, 19 | "url": url, 20 | "image": image, 21 | "datePublished": datePublished, 22 | "dateModified": dateModified, 23 | "author": [{ 24 | "@type": "Person", 25 | "name": author, 26 | "url": authorHref 27 | }] 28 | } 29 | 30 | jsonLdScript = JSON.stringify(jsonLd, null, 2) 31 | - 32 | 33 | script(type="application/ld+json"). 34 | !{jsonLdScript} 35 | -------------------------------------------------------------------------------- /layout/includes/header/index.pug: -------------------------------------------------------------------------------- 1 | - 2 | const returnTopImg = img => img !== false ? img || theme.default_top_img : false 3 | const isFixedClass = theme.nav.fixed ? ' fixed' : '' 4 | var top_img = false 5 | let headerClassName = 'not-top-img' 6 | var bg_img = '' 7 | 8 | if !theme.disable_top_img && page.top_img !== false 9 | case globalPageType 10 | when 'post' 11 | - top_img = page.top_img || page.cover || theme.default_top_img 12 | when 'page' 13 | - top_img = page.top_img || theme.default_top_img 14 | when 'tag' 15 | - top_img = theme.tag_per_img && theme.tag_per_img[page.tag] || returnTopImg(theme.tag_img) 16 | when 'category' 17 | - top_img = theme.category_per_img && theme.category_per_img[page.category] || returnTopImg(theme.category_img) 18 | when 'home' 19 | - top_img = returnTopImg(theme.index_img) 20 | when 'archive' 21 | - top_img = returnTopImg(theme.archive_img) 22 | default 23 | - top_img = page.top_img || theme.default_top_img 24 | 25 | if top_img !== false 26 | - bg_img = getBgPath(top_img) 27 | - headerClassName = globalPageType === 'home' ? 'full_page' : globalPageType === 'post' ? 'post-bg' : 'not-home-page' 28 | 29 | header#page-header(class=`${headerClassName + isFixedClass}` style=bg_img) 30 | include ./nav.pug 31 | if top_img !== false 32 | if globalPageType === 'post' 33 | include ./post-info.pug 34 | else if globalPageType === 'home' 35 | #site-info 36 | h1#site-title=config.title 37 | if theme.subtitle.enable 38 | - var loadSubJs = true 39 | #site-subtitle 40 | span#subtitle 41 | if theme.social 42 | #site_social_icons 43 | !=partial('includes/header/social', {}, {cache: true}) 44 | #scroll-down 45 | i.fas.fa-angle-down.scroll-down-effects 46 | else 47 | #page-site-info 48 | h1#site-title=page.title || page.tag || page.category 49 | else 50 | //- improve seo 51 | if globalPageType !== 'post' 52 | h1.title-seo=page.title || page.tag || page.category || config.title -------------------------------------------------------------------------------- /layout/includes/header/menu_item.pug: -------------------------------------------------------------------------------- 1 | if theme.menu 2 | .menus_items 3 | each value, label in theme.menu 4 | if typeof value !== 'object' 5 | .menus_item 6 | - const [link, icon] = value.split('||').map(part => trim(part)) 7 | a.site-page(href=url_for(link)) 8 | if icon 9 | i.fa-fw(class=icon) 10 | span= ' ' + label 11 | else 12 | .menus_item 13 | - const [groupLabel, groupIcon, groupClass] = label.split('||').map(part => trim(part)) 14 | - const hideClass = groupClass === 'hide' ? 'hide' : '' 15 | span.site-page.group(class=hideClass) 16 | if groupIcon 17 | i.fa-fw(class=groupIcon) 18 | span= ' ' + groupLabel 19 | i.fas.fa-chevron-down 20 | ul.menus_item_child 21 | each val, lab in value 22 | - const [childLink, childIcon] = val.split('||').map(part => trim(part)) 23 | li 24 | a.site-page.child(href=url_for(childLink)) 25 | if childIcon 26 | i.fa-fw(class=childIcon) 27 | span= ' ' + lab -------------------------------------------------------------------------------- /layout/includes/header/nav.pug: -------------------------------------------------------------------------------- 1 | nav#nav 2 | span#blog-info 3 | a.nav-site-title(href=url_for('/')) 4 | if theme.nav.logo 5 | img.site-icon(src=url_for(theme.nav.logo) alt='Logo') 6 | if theme.nav.display_title 7 | span.site-name=config.title 8 | if globalPageType === 'post' 9 | a.nav-page-title(href=url_for('/')) 10 | span.site-name=(page.title || config.title) 11 | #menus 12 | if theme.search.use 13 | #search-button 14 | span.site-page.social-icon.search 15 | i.fas.fa-search.fa-fw 16 | span= ' ' + _p('search.title') 17 | if theme.menu 18 | != partial('includes/header/menu_item', {}, {cache: true}) 19 | 20 | #toggle-menu 21 | span.site-page 22 | i.fas.fa-bars.fa-fw -------------------------------------------------------------------------------- /layout/includes/header/social.pug: -------------------------------------------------------------------------------- 1 | each url, icon in theme.social 2 | - 3 | const [link, title, color] = url.split('||').map(i => trim(i)) 4 | const href = url_for(link) 5 | const iconStyle = color ? `color: ${color.replace(/[\'\"]/g, '')};` : '' 6 | const iconTitle = title || '' 7 | a.social-icon(href=href target="_blank" title=iconTitle) 8 | i(class=icon style=iconStyle) -------------------------------------------------------------------------------- /layout/includes/layout.pug: -------------------------------------------------------------------------------- 1 | - var globalPageType = getPageType(page, is_home) 2 | - var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : '' 3 | - page.aside = globalPageType === 'archive' ? theme.aside.display.archive: globalPageType === 'category' ? theme.aside.display.category : globalPageType === 'tag' ? theme.aside.display.tag : page.aside 4 | - var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : '' 5 | - var pageType = globalPageType === 'post' ? 'post' : 'page' 6 | - pageType = page.type ? pageType + ' type-' + page.type : pageType 7 | 8 | doctype html 9 | html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside) 10 | head 11 | include ./head.pug 12 | body 13 | !=partial('includes/loading/index', {}, {cache: true}) 14 | 15 | if theme.background 16 | #web_bg(style=getBgPath(theme.background)) 17 | 18 | !=partial('includes/sidebar', {}, {cache: true}) 19 | 20 | #body-wrap(class=pageType) 21 | include ./header/index.pug 22 | 23 | main#content-inner.layout(class=hideAside) 24 | if body 25 | div!= body 26 | else 27 | block content 28 | if theme.aside.enable && page.aside !== false 29 | include widget/index.pug 30 | 31 | - const footerBg = theme.footer_img 32 | - const footer_bg = footerBg ? footerBg === true ? bg_img : getBgPath(footerBg) : '' 33 | footer#footer(style=footer_bg) 34 | !=partial('includes/footer', {}, {cache: true}) 35 | 36 | include ./rightside.pug 37 | include ./additional-js.pug -------------------------------------------------------------------------------- /layout/includes/loading/fullpage-loading.pug: -------------------------------------------------------------------------------- 1 | #loading-box 2 | .loading-left-bg 3 | .loading-right-bg 4 | .spinner-box 5 | .configure-border-1 6 | .configure-core 7 | .configure-border-2 8 | .configure-core 9 | .loading-word= _p('loading') 10 | 11 | script. 12 | (()=>{ 13 | const $loadingBox = document.getElementById('loading-box') 14 | const $body = document.body 15 | const preloader = { 16 | endLoading: () => { 17 | $body.style.overflow = '' 18 | $loadingBox.classList.add('loaded') 19 | }, 20 | initLoading: () => { 21 | $body.style.overflow = 'hidden' 22 | $loadingBox.classList.remove('loaded') 23 | } 24 | } 25 | 26 | preloader.initLoading() 27 | window.addEventListener('load', preloader.endLoading) 28 | 29 | if (!{theme.pjax && theme.pjax.enable}) { 30 | btf.addGlobalFn('pjaxSend', preloader.initLoading, 'preloader_init') 31 | btf.addGlobalFn('pjaxComplete', preloader.endLoading, 'preloader_end') 32 | } 33 | })() -------------------------------------------------------------------------------- /layout/includes/loading/index.pug: -------------------------------------------------------------------------------- 1 | if theme.preloader.enable 2 | if theme.preloader.source === 1 3 | include ./fullpage-loading.pug 4 | else 5 | include ./pace.pug -------------------------------------------------------------------------------- /layout/includes/loading/pace.pug: -------------------------------------------------------------------------------- 1 | script. 2 | window.paceOptions = { 3 | restartOnPushState: false 4 | } 5 | 6 | btf.addGlobalFn('pjaxSend', () => { 7 | Pace.restart() 8 | }, 'pace_restart') 9 | 10 | 11 | link(rel="stylesheet", href=url_for(theme.preloader.pace_css_url || theme.asset.pace_default_css)) 12 | script(src=url_for(theme.asset.pace_js)) -------------------------------------------------------------------------------- /layout/includes/mixins/article-sort.pug: -------------------------------------------------------------------------------- 1 | mixin articleSort(posts) 2 | .article-sort 3 | - let year 4 | - posts.forEach(article => { 5 | - const tempYear = date(article.date, 'YYYY') 6 | - const noCoverClass = article.cover === false || !theme.cover.archives_enable ? 'no-article-cover' : '' 7 | - const title = article.title || _p('no_title') 8 | if tempYear !== year 9 | - year = tempYear 10 | .article-sort-item.year= year 11 | .article-sort-item(class=noCoverClass) 12 | if article.cover && theme.cover.archives_enable 13 | a.article-sort-item-img(href=url_for(article.path) title=title) 14 | if article.cover_type === 'img' 15 | img(src=url_for(article.cover) alt=title onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'`) 16 | else 17 | div(style=`background: ${article.cover}`) 18 | .article-sort-item-info 19 | .article-sort-item-time 20 | i.far.fa-calendar-alt 21 | time.post-meta-date-created(datetime=date_xml(article.date) title=_p('post.created') + ' ' + full_date(article.date))= date(article.date, config.date_format) 22 | a.article-sort-item-title(href=url_for(article.path) title=title)= title 23 | - }) -------------------------------------------------------------------------------- /layout/includes/page/404.pug: -------------------------------------------------------------------------------- 1 | - var top_img_404 = theme.error_404.background || theme.default_top_img 2 | 3 | .error-content 4 | .error-img 5 | img(src=url_for(top_img_404) alt='Page not found') 6 | .error-info 7 | h1.error_title= '404' 8 | .error_subtitle= theme.error_404.subtitle || _p('error404') 9 | -------------------------------------------------------------------------------- /layout/includes/page/categories.pug: -------------------------------------------------------------------------------- 1 | .category-lists!= list_categories() -------------------------------------------------------------------------------- /layout/includes/page/default-page.pug: -------------------------------------------------------------------------------- 1 | #article-container.container 2 | != page.content -------------------------------------------------------------------------------- /layout/includes/page/tags.pug: -------------------------------------------------------------------------------- 1 | .tag-cloud-list.text-center 2 | !=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em'}) -------------------------------------------------------------------------------- /layout/includes/pagination.pug: -------------------------------------------------------------------------------- 1 | - 2 | var options = { 3 | prev_text: '', 4 | next_text: '', 5 | mid_size: 1, 6 | escape: false 7 | } 8 | 9 | if globalPageType === 'post' 10 | - let paginationOrder = theme.post_pagination === 2 ? { prev: page.prev, next: page.next } : { prev: page.next, next: page.prev } 11 | 12 | nav#pagination.pagination-post 13 | each direction, key in paginationOrder 14 | if direction 15 | - const getPostDesc = direction.postDesc || postDesc(direction) 16 | - let className = key === 'prev' ? (paginationOrder.next ? '' : 'full-width') : (paginationOrder.prev ? '' : 'full-width') 17 | - className = getPostDesc ? className : className + ' no-desc' 18 | 19 | a.pagination-related(class=className href=url_for(direction.path) title=direction.title) 20 | if direction.cover_type === 'img' 21 | img.cover(src=url_for(direction.cover) onerror=`onerror=null;src='${url_for(theme.error_img.post_page)}'` alt=`cover of ${key === 'prev' ? 'previous' : 'next'} post`) 22 | else 23 | .cover(style=`background: ${direction.cover || 'var(--default-bg-color)'}`) 24 | 25 | .info(class=key === 'prev' ? '' : 'text-right') 26 | .info-1 27 | .info-item-1=_p(`pagination.${key}`) 28 | .info-item-2!=direction.title 29 | if getPostDesc 30 | .info-2 31 | .info-item-1!=getPostDesc 32 | else 33 | nav#pagination 34 | .pagination 35 | if globalPageType === 'home' 36 | - options.format = 'page/%d/#content-inner' 37 | !=paginator(options) -------------------------------------------------------------------------------- /layout/includes/post/outdate-notice.pug: -------------------------------------------------------------------------------- 1 | - const { limit_day, message_prev, message_next, position} = theme.noticeOutdate 2 | - const notice_data = { limitDay: limit_day, messagePrev: message_prev, messageNext: message_next, postUpdate: full_date(page.updated)} 3 | if position === 'top' 4 | #post-outdate-notice(data=notice_data hidden) 5 | !=page.content 6 | else 7 | !=page.content 8 | #post-outdate-notice(data=notice_data hidden) -------------------------------------------------------------------------------- /layout/includes/post/post-copyright.pug: -------------------------------------------------------------------------------- 1 | if theme.post_copyright.enable && page.copyright !== false 2 | - const author = page.copyright_author || config.author 3 | - const authorHref = page.copyright_author_href || theme.post_copyright.author_href || config.url 4 | - const url = page.copyright_url || page.permalink 5 | - const info = page.copyright_info || _p('post.copyright.copyright_content', theme.post_copyright.license_url, theme.post_copyright.license, config.url, config.title) 6 | .post-copyright 7 | .post-copyright__author 8 | span.post-copyright-meta 9 | i.fas.fa-circle-user.fa-fw 10 | = _p('post.copyright.author') + ": " 11 | span.post-copyright-info 12 | a(href=authorHref)= author 13 | .post-copyright__type 14 | span.post-copyright-meta 15 | i.fas.fa-square-arrow-up-right.fa-fw 16 | = _p('post.copyright.link') + ": " 17 | span.post-copyright-info 18 | a(href=url_for(url))= theme.post_copyright.decode ? decodeURI(url) : url 19 | .post-copyright__notice 20 | span.post-copyright-meta 21 | i.fas.fa-circle-exclamation.fa-fw 22 | = _p('post.copyright.copyright_notice') + ": " 23 | span.post-copyright-info!= info -------------------------------------------------------------------------------- /layout/includes/post/reward.pug: -------------------------------------------------------------------------------- 1 | .post-reward 2 | .reward-button 3 | i.fas.fa-qrcode 4 | = theme.reward.text || _p('donate') 5 | .reward-main 6 | ul.reward-all 7 | each item in theme.reward.QR_code 8 | - const clickTo = item.link || item.img 9 | li.reward-item 10 | a(href=url_for(clickTo) target='_blank') 11 | img.post-qr-code-img(src=url_for(item.img) alt=item.text) 12 | .post-qr-code-desc=item.text -------------------------------------------------------------------------------- /layout/includes/rightside.pug: -------------------------------------------------------------------------------- 1 | - const { readmode, translate, darkmode, aside, chat } = theme 2 | 3 | mixin rightsideItem(array) 4 | each item in array 5 | case item 6 | when 'readmode' 7 | if globalPageType === 'post' && readmode 8 | button#readmode(type="button" title=_p('rightside.readmode_title')) 9 | i.fas.fa-book-open 10 | when 'translate' 11 | if translate.enable 12 | button#translateLink(type="button" title=_p('rightside.translate_title'))= translate.default 13 | when 'darkmode' 14 | if darkmode.enable && darkmode.button 15 | button#darkmode(type="button" title=_p('rightside.night_mode_title')) 16 | i.fas.fa-adjust 17 | when 'hideAside' 18 | if aside.enable && aside.button && page.aside !== false 19 | button#hide-aside-btn(type="button" title=_p('rightside.aside')) 20 | i.fas.fa-arrows-alt-h 21 | when 'toc' 22 | if showToc 23 | button#mobile-toc-button.close(type="button" title=_p("rightside.toc")) 24 | i.fas.fa-list-ul 25 | when 'chat' 26 | if chat.rightside_button && chat.use 27 | button#chat-btn(type="button" title=_p("rightside.chat") style="display:none") 28 | i.fas.fa-message 29 | when 'comment' 30 | if commentsJsLoad 31 | a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment")) 32 | i.fas.fa-comments 33 | 34 | - const { enable, hide, show } = theme.rightside_item_order 35 | - const hideArray = enable && hide ? hide.split(',') : ['readmode','translate','darkmode','hideAside'] 36 | - const showArray = enable && show ? show.split(',') : ['toc','chat','comment'] 37 | - const needCogBtn = (enable && hide) || (!enable && ((globalPageType === 'post' && (readmode || translate.enable || (darkmode.enable && darkmode.button))) || (translate.enable || (darkmode.enable && darkmode.button)))) 38 | 39 | #rightside 40 | #rightside-config-hide 41 | if hideArray.length 42 | +rightsideItem(hideArray) 43 | 44 | #rightside-config-show 45 | if needCogBtn 46 | button#rightside-config(type="button" title=_p("rightside.setting")) 47 | i.fas.fa-cog(class=theme.rightside_config_animation ? 'fa-spin' : '') 48 | 49 | if showArray.length 50 | +rightsideItem(showArray) 51 | 52 | button#go-up(type="button" title=_p("rightside.back_to_top")) 53 | span.scroll-percent 54 | i.fas.fa-arrow-up -------------------------------------------------------------------------------- /layout/includes/sidebar.pug: -------------------------------------------------------------------------------- 1 | if theme.menu 2 | #sidebar 3 | #menu-mask 4 | #sidebar-menus 5 | .avatar-img.text-center 6 | img(src=url_for(theme.avatar.img) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.flink)}'` alt="avatar") 7 | .site-data.text-center 8 | a(href=`${url_for(config.archive_dir)}/`) 9 | .headline= _p('aside.articles') 10 | .length-num= site.posts.length 11 | a(href=`${url_for(config.tag_dir)}/`) 12 | .headline= _p('aside.tags') 13 | .length-num= site.tags.length 14 | a(href=`${url_for(config.category_dir)}/`) 15 | .headline= _p('aside.categories') 16 | .length-num= site.categories.length 17 | 18 | != partial('includes/header/menu_item', {}, { cache: true }) -------------------------------------------------------------------------------- /layout/includes/third-party/abcjs/abcjs.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() = { 3 | const abcjsInit = () => { 4 | const abcjsFn = () => { 5 | setTimeout(() => { 6 | const sheets = document.querySelectorAll(".abc-music-sheet") 7 | for (let i = 0; i < sheets.length; i++) { 8 | const ele = sheets[i] 9 | if (ele.children.length > 0) continue 10 | 11 | // Parse parameters from data-params attribute 12 | let params = {} 13 | const dp = ele.getAttribute("data-params") 14 | if (dp) { 15 | try { 16 | params = JSON.parse(dp) 17 | } catch (e) { 18 | console.error("Failed to parse data-params:", e) 19 | } 20 | } 21 | 22 | // Merge parsed parameters with the responsive option 23 | // Ensures params content appears before responsive 24 | const options = { ...params, responsive: "resize" } 25 | 26 | // Render the music score using ABCJS.renderAbc 27 | ABCJS.renderAbc(ele, ele.innerHTML, options) 28 | } 29 | }, 100) 30 | } 31 | 32 | if (typeof ABCJS === "object") { 33 | abcjsFn() 34 | } else { 35 | btf.getScript("!{url_for(theme.asset.abcjs_basic_js)}").then(abcjsFn) 36 | } 37 | } 38 | 39 | if (window.pjax) { 40 | abcjsInit() 41 | } else { 42 | window.addEventListener("load", abcjsInit) 43 | } 44 | 45 | btf.addGlobalFn("encrypt", abcjsInit, "abcjs") 46 | })() 47 | -------------------------------------------------------------------------------- /layout/includes/third-party/abcjs/index.pug: -------------------------------------------------------------------------------- 1 | if theme.abcjs.enable 2 | if theme.abcjs.per_page && (['post','page'].includes(globalPageType)) || page.abcjs 3 | include ./abcjs.pug 4 | -------------------------------------------------------------------------------- /layout/includes/third-party/aplayer.pug: -------------------------------------------------------------------------------- 1 | link(rel='stylesheet' href=url_for(theme.asset.aplayer_css) media="print" onload="this.media='all'") 2 | script(src=url_for(theme.asset.aplayer_js)) 3 | script(src=url_for(theme.asset.meting_js)) 4 | if theme.pjax.enable 5 | script. 6 | (() => { 7 | const destroyAplayer = () => { 8 | if (window.aplayers) { 9 | for (let i = 0; i < window.aplayers.length; i++) { 10 | if (!window.aplayers[i].options.fixed) { 11 | window.aplayers[i].destroy() 12 | } 13 | } 14 | } 15 | } 16 | 17 | const runMetingJS = () => { 18 | typeof loadMeting === 'function' && document.getElementsByClassName('aplayer').length && loadMeting() 19 | } 20 | 21 | btf.addGlobalFn('pjaxSend', destroyAplayer, 'destroyAplayer') 22 | btf.addGlobalFn('pjaxComplete', loadMeting, 'runMetingJS') 23 | })() 24 | -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/artalk.pug: -------------------------------------------------------------------------------- 1 | - const { server, site } = theme.artalk 2 | 3 | script. 4 | (() => { 5 | const getArtalkCount = async() => { 6 | try { 7 | const eleGroup = document.querySelectorAll('#recent-posts .artalk-count') 8 | const keyArray = Array.from(eleGroup).map(i => i.getAttribute('data-page-key')) 9 | 10 | const headerList = { 11 | method: 'GET', 12 | } 13 | 14 | const searchParams = new URLSearchParams({ 15 | 'site_name': '!{site}', 16 | 'page_keys': keyArray 17 | }) 18 | 19 | const res = await fetch(`!{server}/api/v2/stats/page_comment?${searchParams}`, headerList) 20 | const result = await res.json() 21 | 22 | keyArray.forEach((key, index) => { 23 | eleGroup[index].textContent = result.data[key] || 0 24 | }) 25 | } catch (err) { 26 | console.error(err) 27 | } 28 | } 29 | 30 | window.pjax ? getArtalkCount() : window.addEventListener('load', getArtalkCount) 31 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/disqus.pug: -------------------------------------------------------------------------------- 1 | - const { shortname, apikey } = theme.disqus 2 | script. 3 | (() => { 4 | const getCount = async () => { 5 | try { 6 | const eleGroup = document.querySelectorAll('#recent-posts .disqus-count') 7 | const cleanedLinks = Array.from(eleGroup).map(i => `thread:link=${i.href.replace(/#post-comment$/, '')}`); 8 | 9 | const res = await fetch(`https://disqus.com/api/3.0/threads/set.json?forum=!{shortname}&api_key=!{apikey}&${cleanedLinks.join('&')}`,{ 10 | method: 'GET' 11 | }) 12 | const result = await res.json() 13 | 14 | eleGroup.forEach(i => { 15 | const cleanedLink = i.href.replace(/#post-comment$/, '') 16 | const urlData = result.response.find(data => data.link === cleanedLink) || { posts: 0 } 17 | i.textContent = urlData.posts 18 | }) 19 | } catch (err) { 20 | console.error(err) 21 | } 22 | } 23 | 24 | window.pjax ? getCount() : window.addEventListener('load', getCount) 25 | })() 26 | -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/fb.pug: -------------------------------------------------------------------------------- 1 | - const fbSDKVer = 'v20.0' 2 | - const fbSDK = `https://connect.facebook.net/${theme.facebook_comments.lang}/sdk.js#xfbml=1&version=${fbSDKVer}` 3 | 4 | script. 5 | (()=>{ 6 | function loadFBComment () { 7 | if (typeof FB === 'object') FB.XFBML.parse(document.getElementById('recent-posts')) 8 | else { 9 | let ele = document.createElement('script') 10 | ele.setAttribute('src','!{fbSDK}') 11 | ele.setAttribute('async', 'true') 12 | ele.setAttribute('defer', 'true') 13 | ele.setAttribute('crossorigin', 'anonymous') 14 | document.body.appendChild(ele) 15 | } 16 | } 17 | window.pjax ? loadFBComment() : window.addEventListener('load', loadFBComment) 18 | })() 19 | -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/index.pug: -------------------------------------------------------------------------------- 1 | case theme.comments.use[0] 2 | when 'Twikoo' 3 | include ./twikoo.pug 4 | when 'Disqus' 5 | when 'Disqusjs' 6 | include ./disqus.pug 7 | when 'Valine' 8 | include ./valine.pug 9 | when 'Waline' 10 | include ./waline.pug 11 | when 'Facebook Comments' 12 | include ./fb.pug 13 | when 'Remark42' 14 | include ./remark42.pug 15 | when 'Artalk' 16 | include ./artalk.pug -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/remark42.pug: -------------------------------------------------------------------------------- 1 | - const { host, siteId, option } = theme.remark42 2 | 3 | script. 4 | (()=>{ 5 | window.remark_config = Object.assign({ 6 | host: '!{host}', 7 | site_id: '!{siteId}', 8 | },!{JSON.stringify(option)}) 9 | 10 | function getCount () { 11 | const s = document.createElement('script') 12 | s.src = remark_config.host + '/web/counter.js' 13 | s.defer = true 14 | document.head.appendChild(s) 15 | } 16 | 17 | window.pjax ? getCount() : window.addEventListener('load', getCount) 18 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/twikoo.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() => { 3 | const getCommentUrl = () => { 4 | const eleGroup = document.querySelectorAll('#recent-posts .article-title') 5 | let urlArray = [] 6 | eleGroup.forEach(i=>{ 7 | urlArray.push(i.getAttribute('href')) 8 | }) 9 | return urlArray 10 | } 11 | 12 | const getCount = () => { 13 | const runTwikoo = () => { 14 | twikoo.getCommentsCount({ 15 | envId: '!{theme.twikoo.envId}', 16 | region: '!{theme.twikoo.region}', 17 | urls: getCommentUrl(), 18 | includeReply: false 19 | }).then(function (res) { 20 | document.querySelectorAll('#recent-posts .twikoo-count').forEach((item,index) => { 21 | item.textContent = res[index].count 22 | }) 23 | }).catch(function (err) { 24 | console.log(err) 25 | }) 26 | } 27 | 28 | if (typeof twikoo === 'object') { 29 | runTwikoo() 30 | } else { 31 | btf.getScript('!{url_for(theme.asset.twikoo)}').then(runTwikoo) 32 | } 33 | } 34 | 35 | window.pjax ? getCount() : window.addEventListener('load', getCount) 36 | 37 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/valine.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() => { 3 | function loadValine () { 4 | function initValine () { 5 | let initData = { 6 | el: '#vcomment', 7 | appId: '#{theme.valine.appId}', 8 | appKey: '#{theme.valine.appKey}', 9 | serverURLs: '#{theme.valine.serverURLs}' 10 | } 11 | 12 | const valine = new Valine(initData) 13 | } 14 | 15 | if (typeof Valine === 'function') initValine() 16 | else btf.getScript('!{url_for(theme.asset.valine)}').then(initValine) 17 | } 18 | 19 | window.pjax ? loadValine() : window.addEventListener('load', loadValine) 20 | })() 21 | -------------------------------------------------------------------------------- /layout/includes/third-party/card-post-count/waline.pug: -------------------------------------------------------------------------------- 1 | - const serverURL = theme.waline.serverURL.replace(/\/$/, '') 2 | script. 3 | (() => { 4 | async function loadWaline () { 5 | try { 6 | const eleGroup = document.querySelectorAll('#recent-posts .waline-comment-count') 7 | const keyArray = Array.from(eleGroup).map(i => i.getAttribute('data-path')) 8 | 9 | const res = await fetch(`!{serverURL}/api/comment?type=count&url=${keyArray}`, { method: 'GET' }) 10 | const result = await res.json() 11 | 12 | result.data.forEach((count, index) => { 13 | eleGroup[index].textContent = count 14 | }) 15 | } catch (err) { 16 | console.error(err) 17 | } 18 | } 19 | 20 | window.pjax ? loadWaline() : window.addEventListener('load', loadWaline) 21 | })() 22 | -------------------------------------------------------------------------------- /layout/includes/third-party/chat/chatra.pug: -------------------------------------------------------------------------------- 1 | //- https://chatra.io/help/api/ 2 | script. 3 | (() => { 4 | window.ChatraID = '#{theme.chatra.id}' 5 | window.Chatra = window.Chatra || function() { 6 | (window.Chatra.q = window.Chatra.q || []).push(arguments) 7 | } 8 | 9 | btf.getScript('https://call.chatra.io/chatra.js').then(() => { 10 | const isChatBtn = !{theme.chat.rightside_button} 11 | const isChatHideShow = !{theme.chat.button_hide_show} 12 | 13 | if (isChatBtn) { 14 | const close = () => { 15 | Chatra('minimizeWidget') 16 | Chatra('hide') 17 | } 18 | 19 | const open = () => { 20 | Chatra('openChat', true) 21 | Chatra('show') 22 | } 23 | 24 | window.ChatraSetup = { startHidden: true } 25 | 26 | window.chatBtnFn = () => document.getElementById('chatra').classList.contains('chatra--expanded') ? close() : open() 27 | 28 | document.getElementById('chat-btn').style.display = 'block' 29 | } else if (isChatHideShow) { 30 | window.chatBtn = { 31 | hide: () => Chatra('hide'), 32 | show: () => Chatra('show') 33 | } 34 | } 35 | }) 36 | })() 37 | 38 | 39 | -------------------------------------------------------------------------------- /layout/includes/third-party/chat/crisp.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() => { 3 | window.$crisp = ['safe', true] 4 | window.CRISP_WEBSITE_ID = "!{theme.crisp.website_id}" 5 | 6 | btf.getScript('https://client.crisp.chat/l.js').then(() => { 7 | const isChatBtn = !{theme.chat.rightside_button} 8 | const isChatHideShow = !{theme.chat.button_hide_show} 9 | 10 | if (isChatBtn) { 11 | const open = () => { 12 | $crisp.push(["do", "chat:show"]) 13 | $crisp.push(["do", "chat:open"]) 14 | } 15 | 16 | const close = () => $crisp.push(["do", "chat:hide"]) 17 | 18 | close() 19 | 20 | $crisp.push(["on", "chat:closed", close]) 21 | 22 | window.chatBtnFn = () => $crisp.is("chat:visible") ? close() : open() 23 | 24 | document.getElementById('chat-btn').style.display = 'block' 25 | } else if (isChatHideShow) { 26 | window.chatBtn = { 27 | hide: () => $crisp.push(["do", "chat:hide"]), 28 | show: () => $crisp.push(["do", "chat:show"]) 29 | } 30 | } 31 | }) 32 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/chat/index.pug: -------------------------------------------------------------------------------- 1 | case theme.chat.use 2 | when 'chatra' 3 | include ./chatra.pug 4 | when 'tidio' 5 | include ./tidio.pug 6 | when 'crisp' 7 | include ./crisp.pug -------------------------------------------------------------------------------- /layout/includes/third-party/chat/tidio.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() => { 3 | btf.getScript('//code.tidio.co/!{theme.tidio.public_key}.js').then(() => { 4 | const isChatBtn = !{theme.chat.rightside_button} 5 | const isChatHideShow = !{theme.chat.button_hide_show} 6 | 7 | if (isChatBtn) { 8 | let isShow = false 9 | const close = () => { 10 | window.tidioChatApi.hide() 11 | isShow = false 12 | } 13 | 14 | const open = () => { 15 | window.tidioChatApi.open() 16 | window.tidioChatApi.show() 17 | isShow = true 18 | } 19 | 20 | const onTidioChatApiReady = () => { 21 | window.tidioChatApi.hide() 22 | window.tidioChatApi.on("close", close) 23 | } 24 | if (window.tidioChatApi) { 25 | window.tidioChatApi.on("ready", onTidioChatApiReady) 26 | } else { 27 | document.addEventListener("tidioChat-ready", onTidioChatApiReady) 28 | } 29 | 30 | window.chatBtnFn = () => { 31 | if (!window.tidioChatApi) return 32 | isShow ? close() : open() 33 | } 34 | 35 | document.getElementById('chat-btn').style.display = 'block' 36 | 37 | } else if (isChatHideShow) { 38 | window.chatBtn = { 39 | hide: () => window.tidioChatApi && window.tidioChatApi.hide(), 40 | show: () => window.tidioChatApi && window.tidioChatApi.show() 41 | } 42 | } 43 | }) 44 | })() 45 | 46 | -------------------------------------------------------------------------------- /layout/includes/third-party/comments/artalk.pug: -------------------------------------------------------------------------------- 1 | - const { server, site, option } = theme.artalk 2 | - const { use, lazyload } = theme.comments 3 | 4 | script. 5 | (() => { 6 | let artalkItem = null 7 | const option = !{JSON.stringify(option)} 8 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 9 | 10 | const destroyArtalk = () => { 11 | if (artalkItem) { 12 | artalkItem.destroy() 13 | artalkItem = null 14 | } 15 | } 16 | 17 | const artalkChangeMode = theme => artalkItem && artalkItem.setDarkMode(theme === 'dark') 18 | 19 | const initArtalk = (el = document, pageKey = location.pathname) => { 20 | artalkItem = Artalk.init({ 21 | el: el.querySelector('#artalk-wrap'), 22 | server: '!{server}', 23 | site: '!{site}', 24 | darkMode: document.documentElement.getAttribute('data-theme') === 'dark', 25 | ...option, 26 | pageKey: isShuoshuo ? pageKey : (option && option.pageKey) || pageKey 27 | }) 28 | 29 | if (GLOBAL_CONFIG.lightbox === 'null') return 30 | artalkItem.on('list-loaded', () => { 31 | artalkItem.ctx.get('list').getCommentNodes().forEach(comment => { 32 | const $content = comment.getRender().$content 33 | btf.loadLightbox($content.querySelectorAll('img:not([atk-emoticon])')) 34 | }) 35 | }) 36 | 37 | if (isShuoshuo) { 38 | window.shuoshuoComment.destroyArtalk = () => { 39 | destroyArtalk() 40 | if (el.children.length) { 41 | el.innerHTML = '' 42 | el.classList.add('no-comment') 43 | } 44 | } 45 | } 46 | 47 | btf.addGlobalFn('pjaxSendOnce', destroyArtalk, 'destroyArtalk') 48 | btf.addGlobalFn('themeChange', artalkChangeMode, 'artalk') 49 | } 50 | 51 | const loadArtalk = async (el, pageKey) => { 52 | if (typeof Artalk === 'object') initArtalk(el, pageKey) 53 | else { 54 | await btf.getCSS('!{theme.asset.artalk_css}') 55 | await btf.getScript('!{theme.asset.artalk_js}') 56 | initArtalk(el, pageKey) 57 | } 58 | } 59 | 60 | if (isShuoshuo) { 61 | '!{use[0]}' === 'Artalk' 62 | ? window.shuoshuoComment = { loadComment: loadArtalk } 63 | : window.loadOtherComment = loadArtalk 64 | return 65 | } 66 | 67 | if ('!{use[0]}' === 'Artalk' || !!{lazyload}) { 68 | if (!{lazyload}) btf.loadComment(document.getElementById('artalk-wrap'), loadArtalk) 69 | else setTimeout(loadArtalk, 100) 70 | } else { 71 | window.loadOtherComment = loadArtalk 72 | } 73 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/facebook_comments.pug: -------------------------------------------------------------------------------- 1 | - const fbSDKVer = 'v20.0' 2 | - const fbSDK = `https://connect.facebook.net/${theme.facebook_comments.lang}/sdk.js#xfbml=1&version=${fbSDKVer}` 3 | 4 | script. 5 | (()=>{ 6 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo'== 'shuoshuo' 7 | 8 | const loadFBComment = (el = document, path) => { 9 | if (isShuoshuo) { 10 | window.shuoshuoComment.destroyFB = () => { 11 | if (el.children.length) { 12 | el.innerHTML = '' 13 | el.classList.add('no-comment') 14 | } 15 | } 16 | } 17 | 18 | document.getElementById('fb-root') ? '' : document.body.insertAdjacentHTML('afterend', '
') 19 | 20 | const themeNow = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light' 21 | const $fbComment = el.getElementsByClassName('fb-comments')[0] 22 | $fbComment.setAttribute('data-colorscheme',themeNow) 23 | $fbComment.setAttribute('data-href', isShuoshuo ? '!{urlNoIndex(page.permalink)}' + '#' + path : '!{urlNoIndex(page.permalink)}') 24 | 25 | if (typeof FB === 'object') { 26 | FB.XFBML.parse(document.getElementsByClassName('post-meta-commentcount')[0]) 27 | FB.XFBML.parse(el.querySelector('#post-comment')) 28 | } 29 | else { 30 | let ele = document.createElement('script') 31 | ele.setAttribute('src','!{fbSDK}') 32 | ele.setAttribute('async', 'true') 33 | ele.setAttribute('defer', 'true') 34 | ele.setAttribute('crossorigin', 'anonymous') 35 | ele.setAttribute('id', 'facebook-jssdk') 36 | document.getElementById('fb-root').insertAdjacentElement('afterbegin',ele) 37 | } 38 | } 39 | 40 | const fbModeChange = theme => { 41 | const $fbComment = document.getElementsByClassName('fb-comments')[0] 42 | if ($fbComment && typeof FB === 'object') { 43 | $fbComment.setAttribute('data-colorscheme',theme) 44 | FB.XFBML.parse(document.getElementById('post-comment')) 45 | } 46 | } 47 | 48 | btf.addGlobalFn('themeChange', fbModeChange, 'facebook_comments') 49 | 50 | if (isShuoshuo) { 51 | '!{theme.comments.use[0]}' === 'Facebook Comments' 52 | ? window.shuoshuoComment = { loadComment: loadFBComment } 53 | : window.loadOtherComment = loadFBComment 54 | return 55 | } 56 | 57 | if ('!{theme.comments.use[0]}' === 'Facebook Comments' || !!{theme.comments.lazyload}) { 58 | if (!{theme.comments.lazyload}) btf.loadComment(document.querySelector('#post-comment .fb-comments'), loadFBComment) 59 | else loadFBComment() 60 | } else { 61 | window.loadOtherComment = loadFBComment 62 | } 63 | })() 64 | 65 | -------------------------------------------------------------------------------- /layout/includes/third-party/comments/gitalk.pug: -------------------------------------------------------------------------------- 1 | - const { client_id, client_secret, repo, owner, admin, option } = theme.gitalk 2 | 3 | script. 4 | (() => { 5 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 6 | const option = !{JSON.stringify(option)} 7 | 8 | const commentCount = n => { 9 | const isCommentCount = document.querySelector('#post-meta .gitalk-comment-count') 10 | if (isCommentCount) { 11 | isCommentCount.textContent= n 12 | } 13 | } 14 | 15 | const initGitalk = (el, path) => { 16 | if (isShuoshuo) { 17 | window.shuoshuoComment.destroyGitalk = () => { 18 | if (el.children.length) { 19 | el.innerHTML = '' 20 | el.classList.add('no-comment') 21 | } 22 | } 23 | } 24 | 25 | const gitalk = new Gitalk({ 26 | clientID: '!{client_id}', 27 | clientSecret: '!{client_secret}', 28 | repo: '!{repo}', 29 | owner: '!{owner}', 30 | admin: ['!{admin}'], 31 | updateCountCallback: commentCount, 32 | ...option, 33 | id: isShuoshuo ? path : (option && option.id) || '!{md5(page.path)}' 34 | }) 35 | 36 | gitalk.render('gitalk-container') 37 | } 38 | 39 | const loadGitalk = async(el, path) => { 40 | if (typeof Gitalk === 'function') initGitalk(el, path) 41 | else { 42 | await btf.getCSS('!{url_for(theme.asset.gitalk_css)}') 43 | await btf.getScript('!{url_for(theme.asset.gitalk)}') 44 | initGitalk(el, path) 45 | } 46 | } 47 | 48 | if (isShuoshuo) { 49 | '!{theme.comments.use[0]}' === 'Gitalk' 50 | ? window.shuoshuoComment = { loadComment: loadGitalk } 51 | : window.loadOtherComment = loadGitalk 52 | return 53 | } 54 | 55 | if ('!{theme.comments.use[0]}' === 'Gitalk' || !!{theme.comments.lazyload}) { 56 | if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('gitalk-container'), loadGitalk) 57 | else loadGitalk() 58 | } else { 59 | window.loadOtherComment = loadGitalk 60 | } 61 | })() 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /layout/includes/third-party/comments/index.pug: -------------------------------------------------------------------------------- 1 | - let defaultComment = theme.comments.use[0] 2 | hr.custom-hr 3 | #post-comment 4 | .comment-head 5 | .comment-headline 6 | i.fas.fa-comments.fa-fw 7 | span= ' ' + _p('comment') 8 | 9 | if theme.comments.use.length > 1 10 | .comment-switch 11 | span.first-comment=defaultComment 12 | span#switch-btn 13 | span.second-comment=theme.comments.use[1] 14 | 15 | 16 | .comment-wrap 17 | each name in theme.comments.use 18 | div 19 | case name 20 | when 'Disqus' 21 | #disqus_thread 22 | when 'Valine' 23 | #vcomment.vcomment 24 | when 'Disqusjs' 25 | #disqusjs-wrap 26 | when 'Livere' 27 | #lv-container(data-id="city" data-uid=theme.livere.uid) 28 | when 'Gitalk' 29 | #gitalk-container 30 | when 'Utterances' 31 | #utterances-wrap 32 | when 'Twikoo' 33 | #twikoo-wrap 34 | when 'Waline' 35 | #waline-wrap 36 | when 'Giscus' 37 | #giscus-wrap 38 | when 'Facebook Comments' 39 | .fb-comments(data-colorscheme = theme.display_mode === 'dark' ? 'dark' : 'light' 40 | data-numposts= theme.facebook_comments.pageSize || 10 41 | data-order-by= theme.facebook_comments.order_by || 'social' 42 | data-width="100%") 43 | when 'Remark42' 44 | #remark42 45 | when 'Artalk' 46 | #artalk-wrap 47 | -------------------------------------------------------------------------------- /layout/includes/third-party/comments/js.pug: -------------------------------------------------------------------------------- 1 | each name in theme.comments.use 2 | case name 3 | when 'Valine' 4 | !=partial('includes/third-party/comments/valine', {}, {cache: true}) 5 | when 'Disqus' 6 | include ./disqus.pug 7 | when 'Disqusjs' 8 | include ./disqusjs.pug 9 | when 'Livere' 10 | !=partial('includes/third-party/comments/livere', {}, {cache: true}) 11 | when 'Gitalk' 12 | include ./gitalk.pug 13 | when 'Utterances' 14 | !=partial('includes/third-party/comments/utterances', {}, {cache: true}) 15 | when 'Twikoo' 16 | !=partial('includes/third-party/comments/twikoo', {}, {cache: true}) 17 | when 'Waline' 18 | !=partial('includes/third-party/comments/waline', {}, {cache: true}) 19 | when 'Giscus' 20 | !=partial('includes/third-party/comments/giscus', {}, {cache: true}) 21 | when 'Facebook Comments' 22 | include ./facebook_comments.pug 23 | when 'Remark42' 24 | !=partial('includes/third-party/comments/remark42', {}, {cache: true}) 25 | when 'Artalk' 26 | !=partial('includes/third-party/comments/artalk', {}, {cache: true}) -------------------------------------------------------------------------------- /layout/includes/third-party/comments/livere.pug: -------------------------------------------------------------------------------- 1 | - const { use, lazyload } = theme.comments 2 | 3 | script. 4 | (() => { 5 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 6 | 7 | const loadLivere = (el, path) => { 8 | window.livereOptions = { 9 | refer: path || location.pathname 10 | } 11 | 12 | if (isShuoshuo) { 13 | window.shuoshuoComment.destroyLivere = () => { 14 | if (el.children.length) { 15 | el.innerHTML = '' 16 | el.classList.add('no-comment') 17 | } 18 | } 19 | } 20 | 21 | if (typeof LivereTower === 'object') window.LivereTower.init() 22 | else { 23 | (function(d, s) { 24 | var j, e = d.getElementsByTagName(s)[0]; 25 | if (typeof LivereTower === 'function') { return; } 26 | j = d.createElement(s); 27 | j.src = 'https://cdn-city.livere.com/js/embed.dist.js'; 28 | j.async = true; 29 | e.parentNode.insertBefore(j, e); 30 | })(document, 'script'); 31 | } 32 | } 33 | 34 | if (isShuoshuo) { 35 | '!{use[0]}' === 'Livere' 36 | ? window.shuoshuoComment = { loadComment: loadLivere } 37 | : window.loadOtherComment = loadLivere 38 | return 39 | } 40 | 41 | if ('!{use[0]}' === 'Livere' || !!{lazyload}) { 42 | if (!{lazyload}) btf.loadComment(document.getElementById('lv-container'), loadLivere) 43 | else loadLivere() 44 | } else { 45 | window.loadOtherComment = loadLivere 46 | } 47 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/remark42.pug: -------------------------------------------------------------------------------- 1 | - const { host, siteId, option } = theme.remark42 2 | 3 | script. 4 | (() => { 5 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 6 | const option = !{JSON.stringify(option)} 7 | 8 | const loadScript = src => { 9 | const script = document.createElement('script') 10 | script.src = src 11 | script.defer = true 12 | document.head.appendChild(script) 13 | } 14 | 15 | const addRemark42 = () => loadScript('!{host}/web/embed.js') 16 | 17 | const getCount = () => document.querySelector('.remark42__counter') && loadScript('!{host}/web/count.js') 18 | 19 | const destroyRemark42 = () => window.remark42Instance && window.remark42Instance.destroy() 20 | 21 | const initRemark42 = remark_config => { 22 | if (window.REMARK42) { 23 | destroyRemark42() 24 | window.remark42Instance = window.REMARK42.createInstance({ 25 | ...remark_config 26 | }) 27 | } 28 | } 29 | 30 | const loadRemark42 = (el, path) => { 31 | if (isShuoshuo) { 32 | window.shuoshuoComment.destroyRemark42 = () => { 33 | destroyRemark42() 34 | if (el.children.length) { 35 | el.innerHTML = '' 36 | el.classList.add('no-comment') 37 | } 38 | } 39 | } 40 | 41 | window.remark_config = { 42 | host: '!{host}', 43 | site_id: '!{siteId}', 44 | theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light', 45 | ...option, 46 | url: isShuoshuo ? window.location.origin + path : (option && option.url) || window.location.origin + window.location.pathname 47 | } 48 | 49 | if (window.REMARK42) { 50 | initRemark42(remark_config) 51 | getCount() 52 | } else { 53 | addRemark42() 54 | window.addEventListener('REMARK42::ready', () => { 55 | initRemark42(remark_config) 56 | getCount() 57 | }) 58 | } 59 | } 60 | 61 | const remarkChangeMode = theme => window.REMARK42 && window.REMARK42.changeTheme(theme) 62 | 63 | btf.addGlobalFn('themeChange', remarkChangeMode, 'remark42') 64 | 65 | if (isShuoshuo) { 66 | '!{theme.comments.use[0]}' === 'Remark42' 67 | ? window.shuoshuoComment = { loadComment: loadRemark42 } 68 | : window.loadOtherComment = loadRemark42 69 | return 70 | } 71 | 72 | if ('!{theme.comments.use[0]}' === 'Remark42' || !!{theme.comments.lazyload}) { 73 | if (!{theme.comments.lazyload}) btf.loadComment(document.getElementById('remark42'), loadRemark42) 74 | else loadRemark42() 75 | } else { 76 | window.loadOtherComment = loadRemark42 77 | } 78 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/twikoo.pug: -------------------------------------------------------------------------------- 1 | - const { envId, region, option } = theme.twikoo 2 | - const { use, lazyload, count } = theme.comments 3 | 4 | script. 5 | (() => { 6 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 7 | const option = !{JSON.stringify(option)} 8 | 9 | const getCount = () => { 10 | const countELement = document.getElementById('twikoo-count') 11 | if(!countELement) return 12 | twikoo.getCommentsCount({ 13 | envId: '!{envId}', 14 | region: '!{region}', 15 | urls: [window.location.pathname], 16 | includeReply: false 17 | }).then(res => { 18 | countELement.textContent = res[0].count 19 | }).catch(err => { 20 | console.error(err) 21 | }) 22 | } 23 | 24 | const init = (el = document, path = location.pathname) => { 25 | twikoo.init({ 26 | el: el.querySelector('#twikoo-wrap'), 27 | envId: '!{envId}', 28 | region: '!{region}', 29 | onCommentLoaded: () => { 30 | btf.loadLightbox(document.querySelectorAll('#twikoo .tk-content img:not(.tk-owo-emotion)')) 31 | }, 32 | ...option, 33 | path: isShuoshuo ? path : (option && option.path) || path 34 | }) 35 | 36 | !{count ? `GLOBAL_CONFIG_SITE.pageType === 'post' && getCount()` : ''} 37 | 38 | isShuoshuo && (window.shuoshuoComment.destroyTwikoo = () => { 39 | if (el.children.length) { 40 | el.innerHTML = '' 41 | el.classList.add('no-comment') 42 | } 43 | }) 44 | } 45 | 46 | const loadTwikoo = (el, path) => { 47 | if (typeof twikoo === 'object') setTimeout(() => init(el, path), 0) 48 | else btf.getScript('!{url_for(theme.asset.twikoo)}').then(() => init(el, path)) 49 | } 50 | 51 | if (isShuoshuo) { 52 | '!{use[0]}' === 'Twikoo' 53 | ? window.shuoshuoComment = { loadComment: loadTwikoo } 54 | : window.loadOtherComment = loadTwikoo 55 | return 56 | } 57 | 58 | if ('!{use[0]}' === 'Twikoo' || !!{lazyload}) { 59 | if (!{lazyload}) btf.loadComment(document.getElementById('twikoo-wrap'), loadTwikoo) 60 | else loadTwikoo() 61 | } else { 62 | window.loadOtherComment = loadTwikoo 63 | } 64 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/utterances.pug: -------------------------------------------------------------------------------- 1 | - const { use, lazyload } = theme.comments 2 | - const { repo, issue_term, light_theme, dark_theme, js, option } = theme.utterances 3 | - const utterancesUrl = js || 'https://utteranc.es/client.js' 4 | - const utterancesOriginUrl = new URL(utterancesUrl).origin 5 | 6 | script. 7 | (() => { 8 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 9 | const option = !{JSON.stringify(option)} 10 | const getUtterancesTheme = theme => theme === 'dark' ? '#{dark_theme}' : '#{light_theme}' 11 | 12 | const loadUtterances = (el = document, key) => { 13 | if (isShuoshuo) { 14 | window.shuoshuoComment.destroyUtterances = () => { 15 | if (el.children.length) { 16 | el.innerHTML = '' 17 | el.classList.add('no-comment') 18 | } 19 | } 20 | } 21 | 22 | const config = { 23 | src: '!{utterancesUrl}', 24 | repo: '!{repo}', 25 | theme: getUtterancesTheme(document.documentElement.getAttribute('data-theme')), 26 | crossorigin: 'anonymous', 27 | async: true, 28 | ...option, 29 | 'issue-term': isShuoshuo ? key : (option && option['issue-term']) || '!{issue_term}' 30 | } 31 | 32 | const ele = document.createElement('script') 33 | Object.entries(config).forEach(([key, value]) => ele.setAttribute(key, value)) 34 | el.querySelector('#utterances-wrap').appendChild(ele) 35 | } 36 | 37 | const changeUtterancesTheme = theme => { 38 | const iframe = document.querySelector('#utterances-wrap iframe') 39 | if (iframe) { 40 | const message = { 41 | type: 'set-theme', 42 | theme: getUtterancesTheme(theme) 43 | }; 44 | iframe.contentWindow.postMessage(message, '!{utterancesOriginUrl}') 45 | } 46 | } 47 | 48 | btf.addGlobalFn('themeChange', changeUtterancesTheme, 'utterances') 49 | 50 | if (isShuoshuo) { 51 | '!{use[0]}' === 'Utterances' 52 | ? window.shuoshuoComment = { loadComment: loadUtterances } 53 | : window.loadOtherComment = loadUtterances 54 | return 55 | } 56 | 57 | if ('!{use[0]}' === 'Utterances' || !!{lazyload}) { 58 | if (!{lazyload}) btf.loadComment(document.getElementById('utterances-wrap'), loadUtterances) 59 | else loadUtterances() 60 | } else { 61 | window.loadOtherComment = loadUtterances 62 | } 63 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/valine.pug: -------------------------------------------------------------------------------- 1 | - const { use, lazyload } = theme.comments 2 | - const { appId, appKey, avatar, serverURLs, visitor, option } = theme.valine 3 | 4 | - let emojiMaps = '""' 5 | if site.data.valine 6 | - emojiMaps = JSON.stringify(site.data.valine) 7 | 8 | script. 9 | (() => { 10 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 11 | const option = !{JSON.stringify(option)} 12 | 13 | const initValine = (el, path) => { 14 | if (isShuoshuo) { 15 | window.shuoshuoComment.destroyValine = () => { 16 | if (el.children.length) { 17 | el.innerHTML = '' 18 | el.classList.add('no-comment') 19 | } 20 | } 21 | } 22 | 23 | const valineConfig = { 24 | el: '#vcomment', 25 | appId: '#{appId}', 26 | appKey: '#{appKey}', 27 | avatar: '#{avatar}', 28 | serverURLs: '#{serverURLs}', 29 | emojiMaps: !{emojiMaps}, 30 | visitor: #{visitor}, 31 | ...option, 32 | path: isShuoshuo ? path : (option && option.path) || window.location.pathname 33 | } 34 | 35 | new Valine(valineConfig) 36 | } 37 | 38 | const loadValine = async (el, path) => { 39 | if (typeof Valine === 'function') { 40 | initValine(el, path) 41 | } else { 42 | await btf.getScript('!{url_for(theme.asset.valine)}') 43 | initValine(el, path) 44 | } 45 | } 46 | 47 | if (isShuoshuo) { 48 | '!{use[0]}' === 'Valine' 49 | ? window.shuoshuoComment = { loadComment: loadValine } 50 | : window.loadOtherComment = loadValine 51 | return 52 | } 53 | 54 | if ('!{use[0]}' === 'Valine' || !!{lazyload}) { 55 | if (!{lazyload}) btf.loadComment(document.getElementById('vcomment'),loadValine) 56 | else setTimeout(loadValine, 0) 57 | } else { 58 | window.loadOtherComment = loadValine 59 | } 60 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/comments/waline.pug: -------------------------------------------------------------------------------- 1 | - const { serverURL, option, pageview } = theme.waline 2 | - const { lazyload, count, use } = theme.comments 3 | 4 | script. 5 | (() => { 6 | let initFn = window.walineFn || null 7 | const isShuoshuo = GLOBAL_CONFIG_SITE.pageType === 'shuoshuo' 8 | const option = !{JSON.stringify(option)} 9 | 10 | const destroyWaline = ele => ele.destroy() 11 | 12 | const initWaline = (Fn, el = document, path = window.location.pathname) => { 13 | const waline = Fn({ 14 | el: el.querySelector('#waline-wrap'), 15 | serverURL: '!{serverURL}', 16 | pageview: !{lazyload ? false : pageview}, 17 | dark: 'html[data-theme="dark"]', 18 | comment: !{lazyload ? false : count}, 19 | ...option, 20 | path: isShuoshuo ? path : (option && option.path) || path 21 | }) 22 | 23 | if (isShuoshuo) { 24 | window.shuoshuoComment.destroyWaline = () => { 25 | destroyWaline(waline) 26 | if (el.children.length) { 27 | el.innerHTML = '' 28 | el.classList.add('no-comment') 29 | } 30 | } 31 | } 32 | } 33 | 34 | const loadWaline = (el, path) => { 35 | if (initFn) initWaline(initFn, el, path) 36 | else { 37 | btf.getCSS('!{url_for(theme.asset.waline_css)}') 38 | .then(() => import('!{url_for(theme.asset.waline_js)}')) 39 | .then(({ init }) => { 40 | initFn = init || Waline.init 41 | initWaline(initFn, el, path) 42 | window.walineFn = initFn 43 | }) 44 | } 45 | } 46 | 47 | if (isShuoshuo) { 48 | '!{use[0]}' === 'Waline' 49 | ? window.shuoshuoComment = { loadComment: loadWaline } 50 | : window.loadOtherComment = loadWaline 51 | return 52 | } 53 | 54 | if ('!{use[0]}' === 'Waline' || !!{lazyload}) { 55 | if (!{lazyload}) btf.loadComment(document.getElementById('waline-wrap'),loadWaline) 56 | else setTimeout(loadWaline, 0) 57 | } else { 58 | window.loadOtherComment = loadWaline 59 | } 60 | })() 61 | 62 | -------------------------------------------------------------------------------- /layout/includes/third-party/effect.pug: -------------------------------------------------------------------------------- 1 | if theme.fireworks && theme.fireworks.enable 2 | canvas.fireworks(mobile=`${theme.fireworks.mobile}`) 3 | script(src=url_for(theme.asset.fireworks)) 4 | 5 | if (theme.canvas_ribbon && theme.canvas_ribbon.enable) 6 | script(defer id="ribbon" src=url_for(theme.asset.canvas_ribbon) size=theme.canvas_ribbon.size 7 | alpha=theme.canvas_ribbon.alpha zIndex=theme.canvas_ribbon.zIndex mobile=`${theme.canvas_ribbon.mobile}` data-click=`${theme.canvas_ribbon.click_to_change}`) 8 | 9 | if (theme.canvas_fluttering_ribbon && theme.canvas_fluttering_ribbon.enable) 10 | script(defer id="fluttering_ribbon" mobile=`${theme.canvas_fluttering_ribbon.mobile}` src=url_for(theme.asset.canvas_fluttering_ribbon)) 11 | 12 | if (theme.canvas_nest && theme.canvas_nest.enable) 13 | script#canvas_nest(defer color=theme.canvas_nest.color opacity=theme.canvas_nest.opacity zIndex=theme.canvas_nest.zIndex count=theme.canvas_nest.count mobile=`${theme.canvas_nest.mobile}` src=url_for(theme.asset.canvas_nest)) 14 | 15 | if theme.activate_power_mode.enable 16 | script(src=url_for(theme.asset.activate_power_mode)) 17 | script. 18 | POWERMODE.colorful = !{theme.activate_power_mode.colorful}; 19 | POWERMODE.shake = !{theme.activate_power_mode.shake}; 20 | POWERMODE.mobile = !{theme.activate_power_mode.mobile}; 21 | document.body.addEventListener('input', POWERMODE); 22 | 23 | //- 鼠標特效 24 | if theme.click_heart && theme.click_heart.enable 25 | script#click-heart(src=url_for(theme.asset.click_heart) async mobile=`${theme.click_heart.mobile}`) 26 | 27 | if theme.clickShowText && theme.clickShowText.enable 28 | script#click-show-text( 29 | src= url_for(theme.asset.clickShowText) 30 | data-mobile= `${theme.clickShowText.mobile}` 31 | data-text= theme.clickShowText.text.join(",") 32 | data-fontsize= theme.clickShowText.fontSize 33 | data-random= `${theme.clickShowText.random}` 34 | async 35 | ) -------------------------------------------------------------------------------- /layout/includes/third-party/math/index.pug: -------------------------------------------------------------------------------- 1 | case theme.math.use 2 | when 'mathjax' 3 | if (theme.math.per_page && (['post','page'].includes(globalPageType))) || page.mathjax 4 | include ./mathjax.pug 5 | 6 | when 'katex' 7 | if (theme.math.per_page && (['post','page'].includes(globalPageType))) || page.katex 8 | include ./katex.pug 9 | 10 | if theme.mermaid.enable 11 | include ./mermaid.pug 12 | 13 | if theme.chartjs.enable 14 | include ./chartjs.pug -------------------------------------------------------------------------------- /layout/includes/third-party/math/katex.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (async () => { 3 | const showKatex = () => { 4 | document.querySelectorAll('#article-container .katex').forEach(el => el.classList.add('katex-show')) 5 | } 6 | 7 | if (!window.katex_js_css) { 8 | window.katex_js_css = true 9 | await btf.getCSS('!{url_for(theme.asset.katex)}') 10 | if (!{theme.math.katex.copy_tex}) { 11 | await btf.getScript('!{url_for(theme.asset.katex_copytex)}') 12 | } 13 | } 14 | 15 | showKatex() 16 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/math/mathjax.pug: -------------------------------------------------------------------------------- 1 | //- Mathjax 3 2 | - const { tags, enableMenu } = theme.math.mathjax 3 | script. 4 | (() => { 5 | const loadMathjax = () => { 6 | if (!window.MathJax) { 7 | window.MathJax = { 8 | tex: { 9 | inlineMath: [['$', '$'], ['\\(', '\\)']], 10 | tags: '!{tags}', 11 | }, 12 | chtml: { 13 | scale: 1.1 14 | }, 15 | options: { 16 | enableMenu: !{enableMenu}, 17 | renderActions: { 18 | findScript: [10, doc => { 19 | for (const node of document.querySelectorAll('script[type^="math/tex"]')) { 20 | const display = !!node.type.match(/; *mode=display/) 21 | const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display) 22 | const text = document.createTextNode('') 23 | node.parentNode.replaceChild(text, node) 24 | math.start = {node: text, delim: '', n: 0} 25 | math.end = {node: text, delim: '', n: 0} 26 | doc.math.push(math) 27 | } 28 | }, ''] 29 | } 30 | } 31 | } 32 | 33 | const script = document.createElement('script') 34 | script.src = '!{url_for(theme.asset.mathjax)}' 35 | script.id = 'MathJax-script' 36 | script.async = true 37 | document.head.appendChild(script) 38 | } else { 39 | MathJax.startup.document.state(0) 40 | MathJax.texReset() 41 | MathJax.typesetPromise() 42 | } 43 | } 44 | 45 | btf.addGlobalFn('encrypt', loadMathjax, 'mathjax') 46 | window.pjax ? loadMathjax() : window.addEventListener('load', loadMathjax) 47 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/math/mermaid.pug: -------------------------------------------------------------------------------- 1 | script. 2 | (() => { 3 | const runMermaid = ele => { 4 | window.loadMermaid = true 5 | const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? '!{theme.mermaid.theme.dark}' : '!{theme.mermaid.theme.light}' 6 | 7 | ele.forEach((item, index) => { 8 | const mermaidSrc = item.firstElementChild 9 | const mermaidThemeConfig = `%%{init:{ 'theme':'${theme}'}}%%\n` 10 | const mermaidID = `mermaid-${index}` 11 | const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent 12 | 13 | const renderFn = mermaid.render(mermaidID, mermaidDefinition) 14 | const renderMermaid = svg => { 15 | mermaidSrc.insertAdjacentHTML('afterend', svg) 16 | } 17 | 18 | // mermaid v9 and v10 compatibility 19 | typeof renderFn === 'string' ? renderMermaid(renderFn) : renderFn.then(({ svg }) => renderMermaid(svg)) 20 | }) 21 | } 22 | 23 | const codeToMermaid = () => { 24 | const codeMermaidEle = document.querySelectorAll('pre > code.mermaid') 25 | if (codeMermaidEle.length === 0) return 26 | 27 | codeMermaidEle.forEach(ele => { 28 | const preEle = document.createElement('pre') 29 | preEle.className = 'mermaid-src' 30 | preEle.hidden = true 31 | preEle.textContent = ele.textContent 32 | const newEle = document.createElement('div') 33 | newEle.className = 'mermaid-wrap' 34 | newEle.appendChild(preEle) 35 | ele.parentNode.replaceWith(newEle) 36 | }) 37 | } 38 | 39 | const loadMermaid = () => { 40 | if (!{theme.mermaid.code_write}) codeToMermaid() 41 | const $mermaid = document.querySelectorAll('#article-container .mermaid-wrap') 42 | if ($mermaid.length === 0) return 43 | 44 | const runMermaidFn = () => runMermaid($mermaid) 45 | btf.addGlobalFn('themeChange', runMermaidFn, 'mermaid') 46 | window.loadMermaid ? runMermaidFn() : btf.getScript('!{url_for(theme.asset.mermaid)}').then(runMermaidFn) 47 | } 48 | 49 | btf.addGlobalFn('encrypt', loadMermaid, 'mermaid') 50 | window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid) 51 | })() -------------------------------------------------------------------------------- /layout/includes/third-party/newest-comments/common.pug: -------------------------------------------------------------------------------- 1 | script. 2 | window.newestComments = { 3 | changeContent: content => { 4 | if (content === '') return content 5 | 6 | content = content.replace(/.*?<\/pre>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code
9 | content = content.replace(/.*?<\/code>/gi, '[!{_p("aside.card_newest_comments.code")}]') // replace code
10 | content = content.replace(/<[^>]+>/g, "") // remove html tag
11 |
12 | if (content.length > 150) {
13 | content = content.substring(0, 150) + '...'
14 | }
15 | return content
16 | },
17 |
18 | generateHtml: (array, ele) => {
19 | let result = ''
20 |
21 | if (array.length) {
22 | for (let i = 0; i < array.length; i++) {
23 | result += ''
24 |
25 | if (!{theme.aside.card_newest_comments.avatar} && array[i].avatar) {
26 | const imgAttr = '!{theme.lazyload.enable && !theme.lazyload.native ? "data-lazy-src" : "src"}'
27 | const lazyloadNative = '!{theme.lazyload.enable && theme.lazyload.native ? "loading=\"lazy\"" : ""}'
28 | result += `
`
29 | }
30 |
31 | result += `
32 | ${array[i].content}
33 | ${array[i].nick} /
34 | `
35 | }
36 | } else {
37 | result += '!{_p("aside.card_newest_comments.zero")}'
38 | }
39 |
40 | ele.innerHTML = result
41 | window.lazyLoadInstance && window.lazyLoadInstance.update()
42 | window.pjax && window.pjax.refresh(ele)
43 | },
44 |
45 | newestCommentInit: (name, getComment) => {
46 | const $dom = document.querySelector('#card-newest-comments .aside-list')
47 | if ($dom) {
48 | const data = btf.saveToLocal.get(name)
49 | if (data) {
50 | newestComments.generateHtml(JSON.parse(data), $dom)
51 | } else {
52 | getComment($dom)
53 | }
54 | }
55 | },
56 |
57 | run: (name, getComment) => {
58 | newestComments.newestCommentInit(name, getComment)
59 | btf.addGlobalFn('pjaxComplete', () => newestComments.newestCommentInit(name, getComment), name)
60 | }
61 | }
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/disqus-comment.pug:
--------------------------------------------------------------------------------
1 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
2 |
3 | script.
4 | window.addEventListener('load', () => {
5 | const keyName = 'disqus-newest-comments'
6 | const { changeContent, generateHtml, run } = window.newestComments
7 |
8 | const getComment = ele => {
9 | fetch('https://disqus.com/api/3.0/forums/listPosts.json?forum=!{forum}&related=thread&limit=!{newestCommentsLimit}&api_key=!{apiKey}')
10 | .then(response => response.json())
11 | .then(data => {
12 | const disqusArray = data.response.map(item => {
13 | return {
14 | 'avatar': item.author.avatar.cache,
15 | 'content': changeContent(item.message),
16 | 'nick': item.author.name,
17 | 'url': item.url,
18 | 'date': item.createdAt
19 | }
20 | })
21 |
22 | btf.saveToLocal.set(keyName, JSON.stringify(disqusArray), !{theme.aside.card_newest_comments.storage}/(60*24))
23 | generateHtml(disqusArray, ele)
24 | }).catch(e => {
25 | console.error(e)
26 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
27 | })
28 | }
29 |
30 | run(keyName, getComment)
31 | })
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/github-issues.pug:
--------------------------------------------------------------------------------
1 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
2 |
3 | script.
4 | window.addEventListener('load', () => {
5 | const keyName = 'github-newest-comments'
6 | const { changeContent, generateHtml, run } = window.newestComments
7 |
8 | const findTrueUrl = (array, ele) => {
9 | Promise.all(array.map(item =>
10 | fetch(item.url).then(resp => resp.json()).then(data => {
11 | let urlArray = data.body ? data.body.match(/(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?/ig) : []
12 | if (!Array.isArray(urlArray) || urlArray.length === 0) {
13 | urlArray = [`${data.html_url}`]
14 | }
15 | if (data.user.login === 'utterances-bot') {
16 | return urlArray.pop()
17 | } else {
18 | return urlArray.shift()
19 | }
20 | })
21 | )).then(res => {
22 | array = array.map((i,index)=> {
23 | return {
24 | ...i,
25 | url: res[index]
26 | }
27 | })
28 |
29 | btf.saveToLocal.set(keyName, JSON.stringify(array), !{theme.aside.card_newest_comments.storage}/(60*24))
30 | generateHtml(array, ele)
31 | });
32 | }
33 |
34 | const getComment = ele => {
35 | fetch('https://api.github.com/repos/!{userRepo}/issues/comments?sort=updated&direction=desc&per_page=!{newestCommentsLimit}&page=1',{
36 | "headers": {
37 | Accept: 'application/vnd.github.v3.html+json'
38 | }
39 | })
40 | .then(response => response.json())
41 | .then(data => {
42 | const githubArray = data.map(item => {
43 | return {
44 | 'avatar': item.user.avatar_url,
45 | 'content': changeContent(item.body_html || item.body),
46 | 'nick': item.user.login,
47 | 'url': item.issue_url,
48 | 'date': item.updated_at
49 | }
50 | })
51 | findTrueUrl(githubArray, ele)
52 | }).catch(e => {
53 | console.error(e)
54 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
55 | })
56 | }
57 | run(keyName, getComment)
58 | })
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/index.pug:
--------------------------------------------------------------------------------
1 | - let { use } = theme.comments
2 |
3 | if use
4 | -
5 | let forum,apiKey,userRepo
6 | let { limit:newestCommentsLimit } = theme.aside.card_newest_comments
7 | if (newestCommentsLimit > 10 || newestCommentsLimit < 1) newestCommentsLimit = 6
8 |
9 | case use[0]
10 | when 'Valine'
11 | include ./valine.pug
12 | when 'Waline'
13 | include ./waline.pug
14 | when 'Twikoo'
15 | include ./twikoo-comment.pug
16 | when 'Disqus'
17 | - forum = theme.disqus.shortname
18 | - apiKey = theme.disqus.apikey
19 | include ./disqus-comment.pug
20 | when 'Disqusjs'
21 | - forum = theme.disqusjs.shortname
22 | - apiKey = theme.disqusjs.apikey
23 | include ./disqus-comment.pug
24 | when 'Gitalk'
25 | - let { repo,owner } = theme.gitalk
26 | - userRepo = owner + '/' + repo
27 | include ./github-issues.pug
28 | when 'Utterances'
29 | - userRepo = theme.utterances.repo
30 | include ./github-issues.pug
31 | when 'Remark42'
32 | include ./remark42.pug
33 | when 'Artalk'
34 | include ./artalk.pug
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/remark42.pug:
--------------------------------------------------------------------------------
1 | - const { host, siteId } = theme.remark42
2 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
3 |
4 | script.
5 | window.addEventListener('load', () => {
6 | const keyName = 'remark42-newest-comments'
7 | const { changeContent, generateHtml, run } = window.newestComments
8 |
9 | const getComment = ele => {
10 | fetch('!{host}/api/v1/last/!{newestCommentsLimit}?site=!{siteId}')
11 | .then(response => response.json())
12 | .then(data => {
13 | const remark42 = data.map(e => {
14 | return {
15 | 'avatar': e.user.picture,
16 | 'content': changeContent(e.text),
17 | 'nick': e.user.name,
18 | 'url': e.locator.url,
19 | 'date': e.time,
20 | }
21 | })
22 | btf.saveToLocal.set(keyName, JSON.stringify(remark42), !{theme.aside.card_newest_comments.storage}/(60*24))
23 | generateHtml(remark42, ele)
24 | }).catch(e => {
25 | console.error(e)
26 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
27 | })
28 | }
29 |
30 | run(keyName, getComment)
31 | })
32 |
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/twikoo-comment.pug:
--------------------------------------------------------------------------------
1 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
2 |
3 | script.
4 | window.addEventListener('load', () => {
5 | const keyName = 'twikoo-newest-comments'
6 | const { changeContent, generateHtml, run } = window.newestComments
7 |
8 | const getComment = ele => {
9 | const runTwikoo = () => {
10 | twikoo.getRecentComments({
11 | envId: '!{theme.twikoo.envId}',
12 | region: '!{theme.twikoo.region}',
13 | pageSize: !{newestCommentsLimit},
14 | includeReply: true
15 | }).then(res => {
16 | const twikooArray = res.map(e => {
17 | return {
18 | 'content': changeContent(e.comment),
19 | 'avatar': e.avatar,
20 | 'nick': e.nick,
21 | 'url': e.url + '#' + e.id,
22 | 'date': new Date(e.created).toISOString()
23 | }
24 | })
25 |
26 | btf.saveToLocal.set(keyName, JSON.stringify(twikooArray), !{theme.aside.card_newest_comments.storage}/(60*24))
27 | generateHtml(twikooArray, ele)
28 | }).catch(err => {
29 | console.error(err)
30 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
31 | })
32 | }
33 |
34 | if (typeof twikoo === 'object') {
35 | runTwikoo()
36 | } else {
37 | btf.getScript('!{url_for(theme.asset.twikoo)}').then(runTwikoo)
38 | }
39 | }
40 |
41 | run(keyName, getComment)
42 | })
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/valine.pug:
--------------------------------------------------------------------------------
1 | - let default_avatar = theme.valine.avatar
2 |
3 | script(src=url_for(theme.asset.blueimp_md5))
4 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
5 |
6 | script.
7 | window.addEventListener('load', () => {
8 | const keyName = 'valine-newest-comments'
9 | const { changeContent, generateHtml, run } = window.newestComments
10 |
11 | const getIcon = (icon, mail) => {
12 | if (icon) return icon
13 | let defaultIcon = '!{ default_avatar ? `?d=${default_avatar}` : ''}'
14 | let iconUrl = `https://gravatar.loli.net/avatar/${md5(mail.toLowerCase()) + defaultIcon}`
15 | return iconUrl
16 | }
17 |
18 | const getComment = ele => {
19 | const serverURL = '!{theme.valine.serverURLs || `https://${theme.valine.appId.substring(0,8)}.api.lncldglobal.com` }'
20 |
21 | var settings = {
22 | "method": "GET",
23 | "headers": {
24 | "X-LC-Id": '!{theme.valine.appId}',
25 | "X-LC-Key": '!{theme.valine.appKey}',
26 | "Content-Type": "application/json"
27 | },
28 | }
29 |
30 | fetch(`${serverURL}/1.1/classes/Comment?limit=!{newestCommentsLimit}&order=-createdAt`,settings)
31 | .then(response => response.json())
32 | .then(data => {
33 | const valineArray = data.results.map(e => {
34 | return {
35 | 'avatar': getIcon(e.QQAvatar, e.mail),
36 | 'content': changeContent(e.comment),
37 | 'nick': e.nick,
38 | 'url': e.url + '#' + e.objectId,
39 | 'date': e.updatedAt,
40 | }
41 | })
42 | btf.saveToLocal.set(keyName, JSON.stringify(valineArray), !{theme.aside.card_newest_comments.storage}/(60*24))
43 | generateHtml(valineArray, ele)
44 | }).catch(e => {
45 | console.error(e)
46 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
47 | })
48 | }
49 |
50 | run(keyName, getComment)
51 | })
52 |
--------------------------------------------------------------------------------
/layout/includes/third-party/newest-comments/waline.pug:
--------------------------------------------------------------------------------
1 | - const serverURL = theme.waline.serverURL.replace(/\/$/, '')
2 |
3 | != partial("includes/third-party/newest-comments/common.pug", {}, { cache: true })
4 |
5 | script.
6 | window.addEventListener('load', () => {
7 | const keyName = 'waline-newest-comments'
8 | const { changeContent, generateHtml, run } = window.newestComments
9 |
10 | const getComment = async (ele) => {
11 | try {
12 | const res = await fetch('!{serverURL}/api/comment?type=recent&count=!{newestCommentsLimit}')
13 | const result = await res.json()
14 | const walineArray = result.data.map(e => {
15 | return {
16 | 'content': changeContent(e.comment),
17 | 'avatar': e.avatar,
18 | 'nick': e.nick,
19 | 'url': e.url + '#' + e.objectId,
20 | 'date': e.time || e.insertedAt
21 | }
22 | })
23 | btf.saveToLocal.set(keyName, JSON.stringify(walineArray), !{theme.aside.card_newest_comments.storage}/(60*24))
24 | generateHtml(walineArray, ele)
25 | } catch (err) {
26 | console.error(err)
27 | ele.textContent= "!{_p('aside.card_newest_comments.error')}"
28 | }
29 | }
30 |
31 | run(keyName, getComment)
32 | })
33 |
--------------------------------------------------------------------------------
/layout/includes/third-party/pjax.pug:
--------------------------------------------------------------------------------
1 | - var pjaxExclude = 'a:not([target="_blank"])'
2 | if theme.pjax.exclude
3 | each val in theme.pjax.exclude
4 | - pjaxExclude += `:not([href="${val}"])`
5 |
6 | - let pjaxSelectors = ['head > title', '#config-diff', '#body-wrap', '#rightside-config-hide', '#rightside-config-show', '.js-pjax']
7 |
8 | - let choose = theme.comments.use
9 | if choose
10 | if choose.includes('Livere') || choose.includes('Utterances') || choose.includes('Giscus')
11 | - pjaxSelectors.unshift('link[rel="canonical"]')
12 | if theme.Open_Graph_meta.enable
13 | - pjaxSelectors.unshift('meta[property="og:image"]', 'meta[property="og:title"]', 'meta[property="og:url"]', 'meta[property="og:description"]')
14 | else
15 | - pjaxSelectors.unshift('meta[name="description"]')
16 |
17 | script(src=url_for(theme.asset.pjax))
18 | script.
19 | (() => {
20 | const pjaxSelectors = !{JSON.stringify(pjaxSelectors)}
21 |
22 | window.pjax = new Pjax({
23 | elements: '!{pjaxExclude}',
24 | selectors: pjaxSelectors,
25 | cacheBust: false,
26 | analytics: !{theme.google_analytics ? true : false},
27 | scrollRestoration: false
28 | })
29 |
30 | const triggerPjaxFn = (val) => {
31 | if (!val) return
32 | Object.values(val).forEach(fn => fn())
33 | }
34 |
35 | document.addEventListener('pjax:send', () => {
36 | // removeEventListener
37 | btf.removeGlobalFnEvent('pjaxSendOnce')
38 | btf.removeGlobalFnEvent('themeChange')
39 |
40 | // reset readmode
41 | const $bodyClassList = document.body.classList
42 | if ($bodyClassList.contains('read-mode')) $bodyClassList.remove('read-mode')
43 |
44 | triggerPjaxFn(window.globalFn.pjaxSend)
45 | })
46 |
47 | document.addEventListener('pjax:complete', () => {
48 | btf.removeGlobalFnEvent('pjaxCompleteOnce')
49 | document.querySelectorAll('script[data-pjax]').forEach(item => {
50 | const newScript = document.createElement('script')
51 | const content = item.text || item.textContent || item.innerHTML || ""
52 | Array.from(item.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value))
53 | newScript.appendChild(document.createTextNode(content))
54 | item.parentNode.replaceChild(newScript, item)
55 | })
56 |
57 | triggerPjaxFn(window.globalFn.pjaxComplete)
58 | })
59 |
60 | document.addEventListener('pjax:error', e => {
61 | if (e.request.status === 404) {
62 | const usePjax = !{theme.pjax && theme.pjax.enable}
63 | !{theme.error_404 && theme.error_404.enable}
64 | ? (usePjax ? pjax.loadUrl('!{url_for("/404.html")}') : window.location.href = '!{url_for("/404.html")}')
65 | : window.location.href = e.request.responseURL
66 | }
67 | })
68 | })()
--------------------------------------------------------------------------------
/layout/includes/third-party/prismjs.pug:
--------------------------------------------------------------------------------
1 | - const { prismjs_js, prismjs_autoloader, prismjs_lineNumber_js } = theme.asset
2 | - const { prismjs, syntax_highlighter } = config
3 | - const { enable, preprocess, line_number } = prismjs
4 |
5 | if (syntax_highlighter === 'prismjs' || enable) && !preprocess
6 | script.
7 | (() => {
8 | window.Prism = window.Prism || {}
9 | window.Prism.manual = true
10 |
11 | const highlightAll = () => {
12 | window.Prism.highlightAll()
13 | }
14 |
15 | window.addEventListener('load', highlightAll)
16 | btf.addGlobalFn('pjaxComplete', highlightAll, 'prismjs')
17 | btf.addGlobalFn('encrypt', highlightAll, 'prismjs')
18 | })()
19 |
20 | script(src=url_for(prismjs_js))
21 | script(src=url_for(prismjs_autoloader))
22 | if (line_number)
23 | script(src=url_for(prismjs_lineNumber_js))
--------------------------------------------------------------------------------
/layout/includes/third-party/search/algolia.pug:
--------------------------------------------------------------------------------
1 | #algolia-search
2 | .search-dialog
3 | nav.search-nav
4 | span.search-dialog-title= _p('search.title')
5 | button.search-close-button
6 | i.fas.fa-times
7 |
8 | .search-wrap
9 | #algolia-search-input
10 | hr
11 | #algolia-search-results
12 | #algolia-hits
13 | #algolia-pagination
14 | #algolia-info
15 | .algolia-stats
16 | .algolia-poweredBy
17 |
18 | #search-mask
19 |
20 | script(src=url_for(theme.asset.algolia_search))
21 | script(src=url_for(theme.asset.instantsearch))
22 | script(src=url_for(theme.asset.algolia_js))
--------------------------------------------------------------------------------
/layout/includes/third-party/search/docsearch.pug:
--------------------------------------------------------------------------------
1 | - const { placeholder, docsearch: { appId, apiKey, indexName, option } } = theme.search
2 |
3 | .docsearch-wrap
4 | #docsearch(style="display:none")
5 | link(rel="stylesheet" href=url_for(theme.asset.docsearch_css))
6 | script(src=url_for(theme.asset.docsearch_js))
7 | script.
8 | (() => {
9 | docsearch(Object.assign({
10 | appId: '!{appId}',
11 | apiKey: '!{apiKey}',
12 | indexName: '!{indexName}',
13 | container: '#docsearch',
14 | placeholder: '!{ placeholder || _p("search.input_placeholder")}',
15 | }, !{JSON.stringify(option)}))
16 |
17 | const handleClick = () => {
18 | document.querySelector('.DocSearch-Button').click()
19 | }
20 |
21 | const searchClickFn = () => {
22 | btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', handleClick)
23 | }
24 |
25 | searchClickFn()
26 | window.addEventListener('pjax:complete', searchClickFn)
27 | })()
28 |
29 |
30 |
--------------------------------------------------------------------------------
/layout/includes/third-party/search/index.pug:
--------------------------------------------------------------------------------
1 | case theme.search.use
2 | when 'algolia_search'
3 | include ./algolia.pug
4 | when 'local_search'
5 | include ./local-search.pug
6 | when 'docsearch'
7 | include ./docsearch.pug
--------------------------------------------------------------------------------
/layout/includes/third-party/search/local-search.pug:
--------------------------------------------------------------------------------
1 | #local-search
2 | .search-dialog
3 | nav.search-nav
4 | span.search-dialog-title= _p('search.title')
5 | span#loading-status
6 | button.search-close-button
7 | i.fas.fa-times
8 |
9 | #loading-database.text-center
10 | i.fas.fa-spinner.fa-pulse
11 | span= ' ' + _p("search.load_data")
12 |
13 | .search-wrap
14 | #local-search-input
15 | .local-search-box
16 | input(placeholder=theme.search.placeholder || _p("search.input_placeholder") type="text").local-search-box--input
17 | hr
18 | #local-search-results
19 | #local-search-stats-wrap
20 | #search-mask
21 |
22 | script(src=url_for(theme.asset.local_search))
--------------------------------------------------------------------------------
/layout/includes/third-party/share/addtoany.pug:
--------------------------------------------------------------------------------
1 | .addtoany
2 | .a2a_kit.a2a_kit_size_32.a2a_default_style
3 | - let addtoanyItem = theme.share.addtoany.item.split(',')
4 | each name in addtoanyItem
5 | a(class="a2a_button_" + name)
6 |
7 | a.a2a_dd(href="https://www.addtoany.com/share")
8 | script(async src='https://static.addtoany.com/menu/page.js')
9 |
10 |
11 |
--------------------------------------------------------------------------------
/layout/includes/third-party/share/index.pug:
--------------------------------------------------------------------------------
1 | - const { use } = theme.share
2 |
3 | if use
4 | .post-share
5 | case use
6 | when 'addtoany'
7 | !=partial('includes/third-party/share/addtoany', {}, {cache: true})
8 | when 'sharejs'
9 | include ./share-js.pug
--------------------------------------------------------------------------------
/layout/includes/third-party/share/share-js.pug:
--------------------------------------------------------------------------------
1 | - const coverVal = page.cover_type === 'img' ? page.cover : theme.avatar.img
2 | .social-share(data-image=url_for(coverVal) data-sites= theme.share.sharejs.sites)
3 | link(rel='stylesheet' href=url_for(theme.asset.sharejs_css) media="print" onload="this.media='all'")
4 | script(src=url_for(theme.asset.sharejs) defer)
--------------------------------------------------------------------------------
/layout/includes/third-party/umami_analytics.pug:
--------------------------------------------------------------------------------
1 | - let { serverURL, website_id, option, UV_PV } = theme.umami_analytics
2 | - const isServerURL = !!serverURL
3 | - const baseURL = serverURL ? serverURL.replace(/\/$/, '') : 'https://cloud.umami.is'
4 | - const apiUrl = serverURL ? serverURL.replace(/\/$/, '') + '/api' : 'https://api.umami.is/v1'
5 |
6 | script.
7 | (() => {
8 | const option = !{JSON.stringify(option)}
9 | const config = !{JSON.stringify(UV_PV)}
10 |
11 | const runTrack = () => {
12 | umami.track(props => ({ ...props, url: window.location.pathname, title: GLOBAL_CONFIG_SITE.title }))
13 | }
14 |
15 | const loadUmamiJS = () => {
16 | btf.getScript('!{baseURL}/script.js', {
17 | 'data-website-id': '!{website_id}',
18 | 'data-auto-track': 'false',
19 | ...option
20 | }).then(runTrack)
21 | }
22 |
23 | const getData = async (isPost) => {
24 | const now = Date.now()
25 | const keyUrl = isPost ? `&url=${window.location.pathname}` : ''
26 | const headerList = { 'Accept': 'application/json' }
27 | if (!{isServerURL}) headerList['Authorization'] = `Bearer ${config.token}`
28 | else headerList['x-umami-api-key'] = config.token
29 | const res = await fetch(`!{apiUrl}/websites/!{website_id}/stats?startAt=0000000000&endAt=${now}${keyUrl}`, {
30 | method: "GET",
31 | headers: headerList
32 | })
33 | return await res.json()
34 | }
35 |
36 | const insertData = async () => {
37 | try {
38 | if (GLOBAL_CONFIG_SITE.pageType === 'post' && config.page_pv) {
39 | const pagePV = document.getElementById('umamiPV')
40 | if (pagePV) {
41 | const data = await getData(true)
42 | pagePV.textContent = data.pageviews.value
43 | }
44 | } else {
45 | const data = (config.site_uv || config.site_pv) && await getData()
46 | if (config.site_uv) {
47 | const siteUV = document.getElementById('umami-site-uv')
48 | if (siteUV) siteUV.textContent = data.visitors.value
49 | }
50 | if (config.site_pv) {
51 | const sitePV = document.getElementById('umami-site-pv')
52 | if (sitePV) sitePV.textContent = data.pageviews.value
53 | }
54 | }
55 | } catch (e) {
56 | console.error('Failed to load Umami Analytics:', e)
57 | }
58 | }
59 |
60 | btf.addGlobalFn('pjaxComplete', runTrack, 'umami_analytics_run_track')
61 | btf.addGlobalFn('pjaxComplete', insertData, 'umami_analytics_insert')
62 |
63 | loadUmamiJS()
64 | insertData()
65 | })()
--------------------------------------------------------------------------------
/layout/includes/widget/card_ad.pug:
--------------------------------------------------------------------------------
1 | if theme.ad && theme.ad.aside
2 | .card-widget.ads-wrap
3 | != theme.ad.aside
4 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_announcement.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_announcement.enable
2 | .card-widget.card-announcement
3 | .item-headline
4 | i.fas.fa-bullhorn.fa-shake
5 | span= _p('aside.card_announcement')
6 | .announcement_content!= theme.aside.card_announcement.content
--------------------------------------------------------------------------------
/layout/includes/widget/card_archives.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_archives.enable
2 | .card-widget.card-archives
3 | - let type = theme.aside.card_archives.type || 'monthly'
4 | - let format = theme.aside.card_archives.format || 'MMMM YYYY'
5 | - let order = theme.aside.card_archives.order || -1
6 | - let limit = theme.aside.card_archives.limit === 0 ? 0 : theme.aside.card_archives.limit || 8
7 | != aside_archives({ type:type, format: format, order: order, limit: limit })
8 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_author.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_author.enable
2 | .card-widget.card-info.text-center
3 | .avatar-img
4 | img(src=url_for(theme.avatar.img) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.flink) + `'` alt="avatar")
5 | .author-info-name= config.author
6 | .author-info-description!= theme.aside.card_author.description || config.description
7 |
8 | .site-data
9 | a(href=url_for(config.archive_dir) + '/')
10 | .headline= _p('aside.articles')
11 | .length-num= site.posts.length
12 | a(href=url_for(config.tag_dir) + '/')
13 | .headline= _p('aside.tags')
14 | .length-num= site.tags.length
15 | a(href=url_for(config.category_dir) + '/')
16 | .headline= _p('aside.categories')
17 | .length-num= site.categories.length
18 |
19 | if theme.aside.card_author.button.enable
20 | a#card-info-btn(href=theme.aside.card_author.button.link)
21 | i(class=theme.aside.card_author.button.icon)
22 | span=theme.aside.card_author.button.text
23 |
24 | if(theme.social)
25 | .card-info-social-icons
26 | !=partial('includes/header/social', {}, {cache: true})
27 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_bottom_self.pug:
--------------------------------------------------------------------------------
1 | if site.data.widget && site.data.widget.bottom
2 | each item in site.data.widget.bottom
3 | .card-widget(class=item.class_name id=item.id_name style=item.order ? `order: ${item.order}` : '')
4 | .item-headline
5 | i(class=item.icon)
6 | span=item.name
7 | .item-content
8 | !=item.html
9 |
10 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_categories.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_categories.enable
2 | if site.categories.length
3 | .card-widget.card-categories
4 | !=aside_categories({ limit: theme.aside.card_categories.limit === 0 ? 0 : theme.aside.card_categories.limit || 8 , expand: theme.aside.card_categories.expand })
5 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_newest_comment.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_newest_comments.enable && theme.comments.use && !['Livere','Facebook Comments','Giscus'].includes(theme.comments.use[0])
2 | .card-widget#card-newest-comments
3 | .item-headline
4 | i.fas.fa-comment-dots
5 | span= _p('aside.card_newest_comments.headline')
6 | .aside-list
7 | span= _p('aside.card_newest_comments.loading_text')
8 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_post_series.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_post_series.enable
2 | - const array = fragment_cache('seriesArr', groupPosts)
3 | .card-widget.card-post-series
4 | .item-headline
5 | i.fa-solid.fa-layer-group
6 | span= theme.aside.card_post_series.series_title ? page.series : _p('aside.card_post_series')
7 | .aside-list
8 | each item in array[page.series]
9 | - const { path, title = _p('no_title'), cover, cover_type, date:dateA } = item
10 | - let link = url_for(path)
11 | - let no_cover = cover === false || !theme.cover.aside_enable ? 'no-cover' : ''
12 | .aside-list-item(class=no_cover)
13 | if cover && theme.cover.aside_enable
14 | a.thumbnail(href=link title=title)
15 | if cover_type === 'img'
16 | img(src=url_for(cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
17 | else
18 | div(style=`background: ${cover}`)
19 | .content
20 | a.title(href=link title=title)= title
21 | time(datetime=date_xml(dateA) title=_p('post.created') + ' ' + full_date(dateA)) #[=date(dateA, config.date_format)]
22 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_post_toc.pug:
--------------------------------------------------------------------------------
1 | - let tocNumber = typeof page.toc_number === 'boolean' ? page.toc_number : theme.toc.number
2 | - let tocExpand = typeof page.toc_expand === 'boolean' ? page.toc_expand : theme.toc.expand
3 | - let tocExpandClass = tocExpand ? 'is-expand' : ''
4 |
5 | #card-toc.card-widget
6 | .item-headline
7 | i.fas.fa-stream
8 | span= _p('aside.card_toc')
9 | span.toc-percentage
10 |
11 | if (page.encrypt == true)
12 | .toc-content.toc-div-class(class=tocExpandClass style="display:none")!=toc(page.origin, {list_number: tocNumber})
13 | else
14 | .toc-content(class=tocExpandClass)!=toc(page.content, {list_number: tocNumber})
15 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_recent_post.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_recent_post.enable
2 | .card-widget.card-recent-post
3 | .item-headline
4 | i.fas.fa-history
5 | span= _p('aside.card_recent_post')
6 | .aside-list
7 | - let postLimit = theme.aside.card_recent_post.limit === 0 ? site.posts.length : theme.aside.card_recent_post.limit || 5
8 | - let sort = theme.aside.card_recent_post.sort === 'updated' ? 'updated' : 'date'
9 | - site.posts.sort(sort, -1).limit(postLimit).each(function(article){
10 | - let link = article.link || article.path
11 | - let title = article.title || _p('no_title')
12 | - let no_cover = article.cover === false || !theme.cover.aside_enable ? 'no-cover' : ''
13 | - let post_cover = article.cover
14 | .aside-list-item(class=no_cover)
15 | if post_cover && theme.cover.aside_enable
16 | a.thumbnail(href=url_for(link) title=title)
17 | if article.cover_type === 'img'
18 | img(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
19 | else
20 | div(style=`background: ${post_cover}`)
21 | .content
22 | a.title(href=url_for(link) title=title)= title
23 | if theme.aside.card_recent_post.sort === 'updated'
24 | time(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated)) #[=date(article.updated, config.date_format)]
25 | else
26 | time(datetime=date_xml(article.date) title=_p('post.created') + ' ' + full_date(article.date)) #[=date(article.date, config.date_format)]
27 | - })
--------------------------------------------------------------------------------
/layout/includes/widget/card_tags.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_tags.enable
2 | if site.tags.length
3 | .card-widget.card-tags
4 | .item-headline
5 | i.fas.fa-tags
6 | span= _p('aside.card_tags')
7 |
8 | - let { limit, orderby, order } = theme.aside.card_tags
9 | - limit = limit === 0 ? 0 : limit || 40
10 |
11 | if theme.aside.card_tags.color
12 | .card-tag-cloud!= cloudTags({source: site.tags, orderby: orderby, order: order, minfontsize: 1.15, maxfontsize: 1.45, limit: limit, unit: 'em'})
13 | else
14 | .card-tag-cloud!= tagcloud({orderby: orderby, order: order, min_font: 1.1, max_font: 1.5, amount: limit , color: true, start_color: '#999', end_color: '#99a9bf', unit: 'em'})
15 |
--------------------------------------------------------------------------------
/layout/includes/widget/card_top_self.pug:
--------------------------------------------------------------------------------
1 | if site.data.widget && site.data.widget.top
2 | each item in site.data.widget.top
3 | .card-widget(class=item.class_name id=item.id_name)
4 | .item-headline
5 | i(class=item.icon)
6 | span=item.name
7 | .item-content
8 | !=item.html
--------------------------------------------------------------------------------
/layout/includes/widget/card_webinfo.pug:
--------------------------------------------------------------------------------
1 | if theme.aside.card_webinfo.enable
2 | .card-widget.card-webinfo
3 | .item-headline
4 | i.fas.fa-chart-line
5 | span= _p('aside.card_webinfo.headline')
6 | .webinfo
7 | if theme.aside.card_webinfo.post_count
8 | .webinfo-item
9 | .item-name= `${_p('aside.card_webinfo.article_name')} :`
10 | .item-count= site.posts.length
11 | if theme.aside.card_webinfo.runtime_date
12 | .webinfo-item
13 | .item-name= `${_p('aside.card_webinfo.runtime.name')} :`
14 | .item-count#runtimeshow(data-publishDate=date_xml(theme.aside.card_webinfo.runtime_date))
15 | i.fa-solid.fa-spinner.fa-spin
16 | if theme.wordcount.enable && theme.wordcount.total_wordcount
17 | .webinfo-item
18 | .item-name= `${_p('aside.card_webinfo.site_wordcount')} :`
19 | .item-count= totalcount(site)
20 | if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_uv
21 | .webinfo-item
22 | .item-name= `${_p('aside.card_webinfo.site_uv_name')} :`
23 | .item-count#umami-site-uv
24 | i.fa-solid.fa-spinner.fa-spin
25 | else if theme.busuanzi.site_uv
26 | .webinfo-item
27 | .item-name= `${_p('aside.card_webinfo.site_uv_name')} :`
28 | .item-count#busuanzi_value_site_uv
29 | i.fa-solid.fa-spinner.fa-spin
30 | if theme.umami_analytics.enable && theme.umami_analytics.UV_PV.site_pv
31 | .webinfo-item
32 | .item-name= `${_p('aside.card_webinfo.site_pv_name')} :`
33 | .item-count#umami-site-pv
34 | i.fa-solid.fa-spinner.fa-spin
35 | else if theme.busuanzi.site_pv
36 | .webinfo-item
37 | .item-name= `${_p('aside.card_webinfo.site_pv_name')} :`
38 | .item-count#busuanzi_value_site_pv
39 | i.fa-solid.fa-spinner.fa-spin
40 | if theme.aside.card_webinfo.last_push_date
41 | .webinfo-item
42 | .item-name= `${_p('aside.card_webinfo.last_push_date.name')} :`
43 | .item-count#last-push-date(data-lastPushDate=date_xml(Date.now()))
44 | i.fa-solid.fa-spinner.fa-spin
--------------------------------------------------------------------------------
/layout/includes/widget/index.pug:
--------------------------------------------------------------------------------
1 | #aside-content.aside-content
2 | //- post
3 | if globalPageType === 'post'
4 | - const tocStyle = page.toc_style_simple
5 | - const tocStyleVal = tocStyle === true || tocStyle === false ? tocStyle : theme.toc.style_simple
6 | if showToc && tocStyleVal
7 | .sticky_layout
8 | include ./card_post_toc.pug
9 | else
10 | !=partial('includes/widget/card_author', {}, {cache: true})
11 | !=partial('includes/widget/card_announcement', {}, {cache: true})
12 | !=partial('includes/widget/card_top_self', {}, {cache: true})
13 | .sticky_layout
14 | if showToc
15 | include ./card_post_toc.pug
16 | if page.series
17 | include ./card_post_series.pug
18 | !=partial('includes/widget/card_recent_post', {}, {cache: true})
19 | !=partial('includes/widget/card_ad', {}, {cache: true})
20 | else
21 | //- page
22 | !=partial('includes/widget/card_author', {}, {cache: true})
23 | !=partial('includes/widget/card_announcement', {}, {cache: true})
24 | !=partial('includes/widget/card_top_self', {}, {cache: true})
25 |
26 | .sticky_layout
27 | if showToc
28 | include ./card_post_toc.pug
29 | !=partial('includes/widget/card_recent_post', {}, {cache: true})
30 | !=partial('includes/widget/card_ad', {}, {cache: true})
31 | !=partial('includes/widget/card_newest_comment', {}, {cache: true})
32 | !=partial('includes/widget/card_categories', {}, {cache: true})
33 | !=partial('includes/widget/card_tags', {}, {cache: true})
34 | !=partial('includes/widget/card_archives', {}, {cache: true})
35 | !=partial('includes/widget/card_webinfo', {}, {cache: true})
36 | !=partial('includes/widget/card_bottom_self', {}, {cache: true})
--------------------------------------------------------------------------------
/layout/index.pug:
--------------------------------------------------------------------------------
1 | extends includes/layout.pug
2 |
3 | block content
4 | include ./includes/mixins/indexPostUI.pug
5 | +indexPostUI
--------------------------------------------------------------------------------
/layout/page.pug:
--------------------------------------------------------------------------------
1 | extends includes/layout.pug
2 |
3 | block content
4 | - const noCardLayout = ['shuoshuo', '404'].includes(page.type) ? 'nc' : ''
5 | - var commentsJsLoad = false
6 |
7 | mixin commentLoad
8 | if page.comments !== false && theme.comments.use
9 | - commentsJsLoad = true
10 | !=partial('includes/third-party/comments/index', {}, {cache: true})
11 |
12 | #page(class=noCardLayout)
13 | if top_img === false && page.title
14 | .page-title= page.title
15 |
16 | case page.type
17 | when 'tags'
18 | include includes/page/tags.pug
19 | +commentLoad
20 | when 'link'
21 | include includes/page/flink.pug
22 | +commentLoad
23 | when 'categories'
24 | include includes/page/categories.pug
25 | +commentLoad
26 | when '404'
27 | include includes/page/404.pug
28 | when 'shuoshuo'
29 | include includes/page/shuoshuo.pug
30 | default
31 | include includes/page/default-page.pug
32 | +commentLoad
--------------------------------------------------------------------------------
/layout/post.pug:
--------------------------------------------------------------------------------
1 | extends includes/layout.pug
2 |
3 | block content
4 | #post
5 | if top_img === false
6 | include includes/header/post-info.pug
7 |
8 | article#article-container.container.post-content
9 | if theme.noticeOutdate.enable && page.noticeOutdate !== false
10 | include includes/post/outdate-notice.pug
11 | else
12 | !=page.content
13 | include includes/post/post-copyright.pug
14 | .tag_share
15 | if (page.tags.length > 0 && theme.post_meta.post.tags)
16 | .post-meta__tag-list
17 | each item, index in page.tags.data
18 | a(href=url_for(item.path)).post-meta__tags #[=item.name]
19 | include includes/third-party/share/index.pug
20 |
21 | if theme.reward.enable && theme.reward.QR_code
22 | !=partial('includes/post/reward', {}, {cache: true})
23 |
24 | //- ad
25 | if theme.ad && theme.ad.post
26 | .ads-wrap!=theme.ad.post
27 |
28 | if theme.post_pagination
29 | include includes/pagination.pug
30 | if theme.related_post && theme.related_post.enable
31 | != related_posts(page,site.posts)
32 |
33 | if page.comments !== false && theme.comments.use
34 | - var commentsJsLoad = true
35 | !=partial('includes/third-party/comments/index', {}, {cache: true})
36 |
--------------------------------------------------------------------------------
/layout/tag.pug:
--------------------------------------------------------------------------------
1 | extends includes/layout.pug
2 |
3 | block content
4 | if theme.tag_ui == 'index'
5 | include ./includes/mixins/indexPostUI.pug
6 | +indexPostUI
7 | else
8 | include ./includes/mixins/article-sort.pug
9 | #tag
10 | .article-sort-title= _p('page.tag') + ' - ' + page.tag
11 | +articleSort(page.posts)
12 | include includes/pagination.pug
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hexo-theme-butterfly",
3 | "version": "5.4.0-b2",
4 | "description": "A Simple and Card UI Design theme for Hexo",
5 | "main": "package.json",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "hexo",
11 | "theme",
12 | "butterfly",
13 | "Card UI Design",
14 | "Jerry",
15 | "hexo-theme-butterfly"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/jerryc127/hexo-theme-butterfly.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/jerryc127/hexo-theme-butterfly/issues",
23 | "email": "my@crazywong.com"
24 | },
25 | "dependencies": {
26 | "hexo-renderer-pug": "^3.0.0",
27 | "hexo-renderer-stylus": "^3.0.1",
28 | "hexo-util": "^3.3.0",
29 | "moment-timezone": "^0.5.48"
30 | },
31 | "homepage": "https://butterfly.js.org/",
32 | "author": "Jerry ",
33 | "license": "Apache-2.0"
34 | }
35 |
--------------------------------------------------------------------------------
/scripts/common/postDesc.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { stripHTML, truncate } = require('hexo-util')
4 |
5 | // Truncates the given content to a specified length, removing HTML tags and replacing newlines with spaces.
6 | const truncateContent = (content, length, encrypt = false) => {
7 | if (!content || encrypt) return ''
8 | return truncate(stripHTML(content).replace(/\n/g, ' '), { length })
9 | }
10 |
11 | // Generates a post description based on the provided data and theme configuration.
12 | const postDesc = (data, hexo) => {
13 | const { description, content, postDesc, encrypt } = data
14 |
15 | if (postDesc) return postDesc
16 |
17 | const { length, method } = hexo.theme.config.index_post_content
18 |
19 | if (method === false) return
20 |
21 | let result
22 | switch (method) {
23 | case 1:
24 | result = description
25 | break
26 | case 2:
27 | result = description || truncateContent(content, length, encrypt)
28 | break
29 | default:
30 | result = truncateContent(content, length, encrypt)
31 | }
32 |
33 | data.postDesc = result
34 | return result
35 | }
36 |
37 | module.exports = { truncateContent, postDesc }
38 |
--------------------------------------------------------------------------------
/scripts/events/404.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * 404 error page
4 | */
5 |
6 | 'use strict'
7 |
8 | hexo.extend.generator.register('404', function (locals) {
9 | if (!hexo.theme.config.error_404.enable) return
10 | return {
11 | path: '404.html',
12 | layout: ['page'],
13 | data: {
14 | type: '404',
15 | top_img: false,
16 | comments: false,
17 | aside: false
18 | }
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/scripts/events/comment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Capitalize the first letter of comment name
3 | */
4 |
5 | hexo.extend.filter.register('before_generate', () => {
6 | const themeConfig = hexo.theme.config
7 | let { use } = themeConfig.comments
8 | if (!use) return
9 |
10 | // Make sure use is an array
11 | use = Array.isArray(use) ? use : use.split(',')
12 |
13 | // Capitalize the first letter of each comment name
14 | use = use.map(item =>
15 | item.trim().toLowerCase().replace(/\b[a-z]/g, s => s.toUpperCase())
16 | )
17 |
18 | // Disqus and Disqusjs conflict, only keep the first one
19 | if (use.includes('Disqus') && use.includes('Disqusjs')) {
20 | use = [use[0]]
21 | }
22 |
23 | themeConfig.comments.use = use
24 | })
25 |
--------------------------------------------------------------------------------
/scripts/events/init.js:
--------------------------------------------------------------------------------
1 | hexo.extend.filter.register('before_generate', () => {
2 | // Get first two digits of the Hexo version number
3 | const { version, log, locals } = hexo
4 | const hexoVer = version.replace(/(^.*\..*)\..*/, '$1')
5 |
6 | if (hexoVer < 5.3) {
7 | log.error('Please update Hexo to V5.3.0 or higher!')
8 | log.error('請把 Hexo 升級到 V5.3.0 或更高的版本!')
9 | process.exit(-1)
10 | }
11 |
12 | if (locals.get) {
13 | const data = locals.get('data')
14 | if (data && data.butterfly) {
15 | log.error("'butterfly.yml' is deprecated. Please use '_config.butterfly.yml'")
16 | log.error("'butterfly.yml' 已經棄用,請使用 '_config.butterfly.yml'")
17 | process.exit(-1)
18 | }
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/scripts/events/stylus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Stylus renderer
3 | */
4 |
5 | 'use strict'
6 |
7 | hexo.extend.filter.register('stylus:renderer', style => {
8 | const { syntax_highlighter: syntaxHighlighter, highlight, prismjs } = hexo.config
9 | let { enable: highlightEnable, line_number: highlightLineNumber } = highlight
10 | let { enable: prismjsEnable, line_number: prismjsLineNumber } = prismjs
11 |
12 | // for hexo > 7.0
13 | if (syntaxHighlighter) {
14 | highlightEnable = syntaxHighlighter === 'highlight.js'
15 | prismjsEnable = syntaxHighlighter === 'prismjs'
16 | }
17 |
18 | style.define('$highlight_enable', highlightEnable)
19 | .define('$highlight_line_number', highlightLineNumber)
20 | .define('$prismjs_enable', prismjsEnable)
21 | .define('$prismjs_line_number', prismjsLineNumber)
22 | .define('$language', hexo.config.language)
23 | // .import(`${this.source_dir.replace(/\\/g, '/')}_data/css/*`)
24 | })
25 |
--------------------------------------------------------------------------------
/scripts/events/welcome.js:
--------------------------------------------------------------------------------
1 | hexo.on('ready', () => {
2 | const { version } = require('../../package.json')
3 | hexo.log.info(`
4 | ===================================================================
5 | ##### # # ##### ##### ###### ##### ###### # # #
6 | # # # # # # # # # # # # #
7 | ##### # # # # ##### # # ##### # #
8 | # # # # # # # ##### # # #
9 | # # # # # # # # # # # #
10 | ##### #### # # ###### # # # ###### #
11 | ${version}
12 | ===================================================================`)
13 | })
14 |
--------------------------------------------------------------------------------
/scripts/filters/post_lazyload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * lazyload
4 | * replace src to data-lazy-src
5 | */
6 |
7 | 'use strict'
8 |
9 | const urlFor = require('hexo-util').url_for.bind(hexo)
10 |
11 | const lazyload = htmlContent => {
12 | if (hexo.theme.config.lazyload.native) {
13 | return htmlContent.replace(/()/ig, '$1 loading=\'lazy\'$2')
14 | }
15 |
16 | const bg = hexo.theme.config.lazyload.placeholder ? urlFor(hexo.theme.config.lazyload.placeholder) : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
17 | return htmlContent.replace(/( {
21 | const { enable, field } = hexo.theme.config.lazyload
22 | if (!enable || field !== 'site') return
23 | return lazyload(data)
24 | })
25 |
26 | hexo.extend.filter.register('after_post_render', data => {
27 | const { enable, field } = hexo.theme.config.lazyload
28 | if (!enable || field !== 'post') return
29 | data.content = lazyload(data.content)
30 | return data
31 | })
32 |
--------------------------------------------------------------------------------
/scripts/filters/random_cover.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Random cover for posts
3 | */
4 |
5 | 'use strict'
6 |
7 | hexo.extend.generator.register('post', locals => {
8 | const previousIndexes = []
9 |
10 | const getRandomCover = defaultCover => {
11 | if (!defaultCover) return false
12 | if (!Array.isArray(defaultCover)) return defaultCover
13 |
14 | const coverCount = defaultCover.length
15 |
16 | if (coverCount === 1) {
17 | return defaultCover[0]
18 | }
19 |
20 | const maxPreviousIndexes = coverCount === 2 ? 1 : (coverCount === 3 ? 2 : 3)
21 |
22 | let index
23 | do {
24 | index = Math.floor(Math.random() * coverCount)
25 | } while (previousIndexes.includes(index) && previousIndexes.length < coverCount)
26 |
27 | previousIndexes.push(index)
28 | if (previousIndexes.length > maxPreviousIndexes) {
29 | previousIndexes.shift()
30 | }
31 |
32 | return defaultCover[index]
33 | }
34 |
35 | const handleImg = data => {
36 | const imgTestReg = /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i
37 | let { cover: coverVal, top_img: topImg } = data
38 |
39 | // Add path to top_img and cover if post_asset_folder is enabled
40 | if (hexo.config.post_asset_folder) {
41 | if (topImg && topImg.indexOf('/') === -1 && imgTestReg.test(topImg)) {
42 | data.top_img = `${data.path}${topImg}`
43 | }
44 | if (coverVal && coverVal.indexOf('/') === -1 && imgTestReg.test(coverVal)) {
45 | data.cover = `${data.path}${coverVal}`
46 | }
47 | }
48 |
49 | if (coverVal === false) return data
50 |
51 | // If cover is not set, use random cover
52 | if (!coverVal) {
53 | const { cover: { default_cover: defaultCover } } = hexo.theme.config
54 | const randomCover = getRandomCover(defaultCover)
55 | data.cover = randomCover
56 | coverVal = randomCover // update coverVal
57 | }
58 |
59 | if (coverVal && (coverVal.indexOf('//') !== -1 || imgTestReg.test(coverVal))) {
60 | data.cover_type = 'img'
61 | }
62 |
63 | return data
64 | }
65 |
66 | // https://github.com/hexojs/hexo/blob/master/lib%2Fplugins%2Fgenerator%2Fpost.ts
67 | const posts = locals.posts.sort('date').toArray()
68 | const { length } = posts
69 |
70 | return posts.map((post, i) => {
71 | if (i) post.prev = posts[i - 1]
72 | if (i < length - 1) post.next = posts[i + 1]
73 |
74 | post.__post = true
75 |
76 | return {
77 | data: handleImg(post),
78 | layout: 'post',
79 | path: post.path
80 | }
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/scripts/helpers/getArchiveLength.js:
--------------------------------------------------------------------------------
1 | hexo.extend.helper.register('getArchiveLength', function () {
2 | const archiveGenerator = hexo.config.archive_generator
3 | const posts = this.site.posts
4 |
5 | const { yearly, monthly, daily } = archiveGenerator
6 | const { year, month, day } = this.page
7 |
8 | // Archives Page
9 | if (!year) return posts.length
10 |
11 | // Function to generate a unique key based on the granularity
12 | const getKey = (post, type) => {
13 | const date = post.date.clone()
14 | const y = date.year()
15 | const m = date.month() + 1
16 | const d = date.date()
17 | if (type === 'year') return `${y}`
18 | if (type === 'month') return `${y}-${m}`
19 | if (type === 'day') return `${y}-${m}-${d}`
20 | }
21 |
22 | // Create a map to count posts per period
23 | const mapData = this.fragment_cache('createArchiveObj', () => {
24 | const map = new Map()
25 | posts.forEach(post => {
26 | const keyYear = getKey(post, 'year')
27 | const keyMonth = getKey(post, 'month')
28 | const keyDay = getKey(post, 'day')
29 |
30 | if (yearly) map.set(keyYear, (map.get(keyYear) || 0) + 1)
31 | if (monthly) map.set(keyMonth, (map.get(keyMonth) || 0) + 1)
32 | if (daily) map.set(keyDay, (map.get(keyDay) || 0) + 1)
33 | })
34 | return map
35 | })
36 |
37 | // Determine the appropriate key to fetch based on current page context
38 | let key
39 | if (yearly && year) key = `${year}`
40 | if (monthly && month) key = `${year}-${month}`
41 | if (daily && day) key = `${year}-${month}-${day}`
42 |
43 | // Return the count for the current period or default to the total posts
44 | return mapData.get(key) || posts.length
45 | })
46 |
--------------------------------------------------------------------------------
/scripts/helpers/series.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | hexo.extend.helper.register('groupPosts', function () {
4 | const getGroupArray = array => {
5 | return array.reduce((groups, item) => {
6 | const key = item.series
7 | if (key) {
8 | groups[key] = groups[key] || []
9 | groups[key].push(item)
10 | }
11 | return groups
12 | }, {})
13 | }
14 |
15 | const sortPosts = posts => {
16 | const { orderBy = 'date', order = 1 } = this.theme.aside.card_post_series
17 | if (orderBy === 'title') return posts.sort('title', order)
18 | return posts.sort('date', order)
19 | }
20 |
21 | return getGroupArray(sortPosts(this.site.posts))
22 | })
23 |
--------------------------------------------------------------------------------
/scripts/tag/button.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Button
3 | * {% btn url text icon option %}
4 | * option: color outline center block larger
5 | * color : default/blue/pink/red/purple/orange/green
6 | */
7 |
8 | 'use strict'
9 |
10 | const urlFor = require('hexo-util').url_for.bind(hexo)
11 |
12 | const btn = args => {
13 | const [url = '', text = '', icon = '', option = ''] = args.join(' ').split(',').map(arg => arg.trim())
14 |
15 | const iconHTML = icon ? `` : ''
16 | const textHTML = text ? `${text}` : ''
17 |
18 | return `${iconHTML}${textHTML}`
19 | }
20 |
21 | hexo.extend.tag.register('btn', btn, { ends: false })
22 |
--------------------------------------------------------------------------------
/scripts/tag/chartjs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * chartjs
4 | * https://www.chartjs.org/
5 | * {% chartjs [width, abreast, chartId] %}
6 | *
7 | *
8 | *
9 | *
10 | * {% endchartjs %}
11 | */
12 |
13 | 'use strict'
14 |
15 | const { escapeHTML } = require('hexo-util')
16 |
17 | const chartjs = (args, content) => {
18 | if (!content) return
19 |
20 | const chartRegex = /\n([\w\W\s\S]*?)/
21 | const descRegex = /\n([\w\W\s\S]*?)/
22 | const selfConfig = args.join(' ').trim()
23 |
24 | const [width = '', layout = false, chartId = ''] = selfConfig.split(',').map(s => s.trim())
25 |
26 | const chartMatch = content.match(chartRegex)
27 | const descMatch = content.match(descRegex)
28 |
29 | if (!chartMatch) {
30 | hexo.log.warn('chartjs tag: chart content is required!')
31 | return
32 | }
33 |
34 | const chartConfig = chartMatch && chartMatch[1] ? chartMatch[1] : ''
35 | const descContent = descMatch && descMatch[1] ? descMatch[1] : ''
36 |
37 | const renderedDesc = descContent ? hexo.render.renderSync({ text: descContent, engine: 'markdown' }).trim() : ''
38 |
39 | const descDOM = renderedDesc ? `${renderedDesc}` : ''
40 | const abreastClass = layout ? ' chartjs-abreast' : ''
41 | const widthStyle = width ? `data-width="${width}%"` : ''
42 |
43 | return `
44 | ${escapeHTML(chartConfig)}
45 | ${descDOM}
46 | `
47 | }
48 |
49 | hexo.extend.tag.register('chartjs', chartjs, { ends: true })
50 |
--------------------------------------------------------------------------------
/scripts/tag/flink.js:
--------------------------------------------------------------------------------
1 | /**
2 | * flink
3 | */
4 |
5 | 'use strict'
6 |
7 | const urlFor = require('hexo-util').url_for.bind(hexo)
8 |
9 | const flinkFn = (args, content) => {
10 | const data = hexo.render.renderSync({ text: content, engine: 'yaml' })
11 | let result = ''
12 |
13 | data.forEach(item => {
14 | const className = item.class_name ? `${item.class_name}` : ''
15 | const classDesc = item.class_desc ? `${item.class_desc}` : ''
16 |
17 | const listResult = item.link_list.map(link => `
18 |
19 |
20 | ${link.name}
24 | ${link.descr}
25 |
26 |
23 | `).join('')
27 |
28 | result += `${className}${classDesc}${listResult}`
29 | })
30 |
31 | return `${result}`
32 | }
33 |
34 | hexo.extend.tag.register('flink', flinkFn, { ends: true })
35 |
--------------------------------------------------------------------------------
/scripts/tag/gallery.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * galleryGroup and gallery
4 | * {% galleryGroup [name] [descr] [url] [img] %}
5 | *
6 | * {% gallery [button],[limit],[firstLimit] %}
7 | * {% gallery url,[url],[button] %}
8 | */
9 |
10 | 'use strict'
11 |
12 | const urlFor = require('hexo-util').url_for.bind(hexo)
13 |
14 | const DEFAULT_LIMIT = 10
15 | const DEFAULT_FIRST_LIMIT = 10
16 | const IMAGE_REGEX = /!\[(.*?)\]\(([^\s]*)\s*(?:["'](.*?)["']?)?\s*\)/g
17 |
18 | // Helper functions
19 | const parseGalleryArgs = args => {
20 | const [type, ...rest] = args.join(' ').split(',').map(arg => arg.trim())
21 | return {
22 | isUrl: type === 'url',
23 | params: type === 'url' ? rest : [type, ...rest]
24 | }
25 | }
26 |
27 | const parseImageContent = content => {
28 | const images = []
29 | let match
30 |
31 | while ((match = IMAGE_REGEX.exec(content)) !== null) {
32 | images.push({
33 | url: match[2],
34 | alt: match[1] || '',
35 | title: match[3] || ''
36 | })
37 | }
38 |
39 | return images
40 | }
41 |
42 | const createGalleryHTML = (type, dataStr, button, limit, firstLimit) => {
43 | return `
44 | ${dataStr}
45 | `
46 | }
47 |
48 | const gallery = (args, content) => {
49 | const { isUrl, params } = parseGalleryArgs(args)
50 |
51 | if (isUrl) {
52 | const [dataStr, button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
53 | return createGalleryHTML('url', urlFor(dataStr), button, limit, firstLimit)
54 | }
55 |
56 | const [button = false, limit = DEFAULT_LIMIT, firstLimit = DEFAULT_FIRST_LIMIT] = params
57 | const images = parseImageContent(content)
58 | return createGalleryHTML('data', JSON.stringify(images), button, limit, firstLimit)
59 | }
60 |
61 | const galleryGroup = args => {
62 | const [name = '', descr = '', url = '', img = ''] = args.map(arg => arg.trim())
63 |
64 | return `
65 |
66 |
67 | ${name}
68 | ${descr}
69 |
70 |
71 | `
72 | }
73 |
74 | // Register tags
75 | hexo.extend.tag.register('gallery', gallery, { ends: true })
76 | hexo.extend.tag.register('galleryGroup', galleryGroup)
77 |
--------------------------------------------------------------------------------
/scripts/tag/hide.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * @example
4 | * hideInline
5 | * {% hideInline content,display,bg,color %}
6 | * content不能包含當引號,可用 '
7 | * hideBlock
8 | * {% hideBlock display,bg,color %}
9 | * content
10 | * {% endhideBlock %}
11 | * hideToggle
12 | * {% hideToggle display,bg,color %}
13 | * content
14 | * {% endhideToggle %}
15 | */
16 |
17 | 'use strict'
18 |
19 | const parseArgs = args => args.join(' ').split(',')
20 |
21 | const generateStyle = (bg, color) => {
22 | let style = 'style="'
23 | if (bg) style += `background-color: ${bg};`
24 | if (color) style += `color: ${color}`
25 | style += '"'
26 | return style
27 | }
28 |
29 | const hideInline = args => {
30 | const [content, display = 'Click', bg = false, color = false] = parseArgs(args)
31 | const style = generateStyle(bg, color)
32 | return ` `
33 | }
34 |
35 | const hideBlock = (args, content) => {
36 | const [display = 'Click', bg = false, color = false] = parseArgs(args)
37 | const style = generateStyle(bg, color)
38 | const renderedContent = hexo.render.renderSync({ text: content, engine: 'markdown' })
39 | return ` `
40 | }
41 |
42 | const hideToggle = (args, content) => {
43 | const [display, bg = false, color = false] = parseArgs(args)
44 | const style = generateStyle(bg, color)
45 | const border = bg ? `style="border: 1px solid ${bg}"` : ''
46 | const renderedContent = hexo.render.renderSync({ text: content, engine: 'markdown' })
47 | return `${display}
${renderedContent}`
48 | }
49 |
50 | hexo.extend.tag.register('hideInline', hideInline)
51 | hexo.extend.tag.register('hideBlock', hideBlock, { ends: true })
52 | hexo.extend.tag.register('hideToggle', hideToggle, { ends: true })
--------------------------------------------------------------------------------
/scripts/tag/inlineImg.js:
--------------------------------------------------------------------------------
1 | /**
2 | * inlineImg
3 | * @param {Array} args - Image name and height
4 | * @param {string} args[0] - Image name
5 | * @param {number} args[1] - Image height
6 | * @returns {string} - Image tag
7 | */
8 |
9 | 'use strict'
10 |
11 | const urlFor = require('hexo-util').url_for.bind(hexo)
12 |
13 | const inlineImg = ([img, height = '']) => {
14 | const heightStyle = height ? `style="height:${height}"` : ''
15 | const src = urlFor(img)
16 | return `
`
17 | }
18 |
19 | hexo.extend.tag.register('inlineImg', inlineImg, { ends: false })
20 |
--------------------------------------------------------------------------------
/scripts/tag/label.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * label
4 | * {% label text color %}
5 | */
6 |
7 | 'use strict'
8 |
9 | const addLabel = args => {
10 | const [text, className = 'default'] = args
11 | return `${text}`
12 | }
13 |
14 | hexo.extend.tag.register('label', addLabel, { ends: false })
15 |
--------------------------------------------------------------------------------
/scripts/tag/mermaid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Butterfly
3 | * mermaid
4 | * https://github.com/mermaid-js/mermaid
5 | */
6 |
7 | 'use strict'
8 |
9 | const { escapeHTML } = require('hexo-util')
10 |
11 | const mermaid = (args, content) => {
12 | return `
13 | ${escapeHTML(content)}
14 |
`
15 | }
16 |
17 | hexo.extend.tag.register('mermaid', mermaid, { ends: true })
18 |
--------------------------------------------------------------------------------
/scripts/tag/note.js:
--------------------------------------------------------------------------------
1 | /**
2 | * note.js
3 | * transplant from hexo-theme-next
4 | * Modify by Jerry
5 | */
6 |
7 | 'use strict'
8 |
9 | const postNote = (args, content) => {
10 | const styleConfig = hexo.theme.config.note.style
11 | const noteTag = ['flat', 'modern', 'simple', 'disabled']
12 | if (!noteTag.includes(args[args.length - 1])) {
13 | args.push(styleConfig)
14 | }
15 |
16 | let icon = ''
17 | const iconArray = args[args.length - 2]
18 | if (iconArray && iconArray.startsWith('fa')) {
19 | icon = ``
20 | args[args.length - 2] = 'icon-padding'
21 | }
22 |
23 | return `${icon + hexo.render.renderSync({ text: content, engine: 'markdown' })}`
24 | }
25 |
26 | hexo.extend.tag.register('note', postNote, { ends: true })
27 | hexo.extend.tag.register('subnote', postNote, { ends: true })
28 |
--------------------------------------------------------------------------------
/scripts/tag/score.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Music Score
3 | * {% score %}
4 | */
5 |
6 | 'use strict'
7 |
8 | const score = (args, content) => {
9 | // Escape HTML tags and some special characters, including curly braces
10 | const escapeHtmlTags = s => {
11 | const lookup = {
12 | '&': '&',
13 | '"': '"',
14 | "'": ''',
15 | '<': '<',
16 | '>': '>',
17 | '{': '{',
18 | '}': '}'
19 | }
20 | return s.replace(/[&"'<>{}]/g, c => lookup[c])
21 | }
22 |
23 | const trimmed = content.trim()
24 | // Split content using six dashes as a delimiter
25 | const parts = trimmed.split('------')
26 |
27 | if (parts.length < 2) {
28 | // If no delimiter is found, treat the entire content as the score
29 | return `${escapeHtmlTags(trimmed)}`
30 | }
31 |
32 | // First part is parameters (JSON string), the rest is the score content
33 | const paramPart = parts[0].trim()
34 | const scorePart = parts.slice(1).join('------').trim()
35 |
36 | let paramsObj = {}
37 | try {
38 | paramsObj = JSON.parse(paramPart)
39 | } catch (e) {
40 | console.error("Failed to parse JSON in score tag:", e)
41 | }
42 |
43 | // Use double quotes for data-params attribute value,
44 | // ensuring JSON internal double quotes are escaped
45 | return `
46 | ${escapeHtmlTags(scorePart)}
47 | `
48 | }
49 |
50 | hexo.extend.tag.register("score", score, { ends: true })
--------------------------------------------------------------------------------
/scripts/tag/series.js:
--------------------------------------------------------------------------------
1 | /**
2 | * series plugin
3 | * Syntax:
4 | * {% series [series name] %}
5 | * Usage:
6 | * {% series %}
7 | * {% series series1 %}
8 | */
9 |
10 | 'use strict'
11 |
12 | const urlFor = require('hexo-util').url_for.bind(hexo)
13 | const groups = {}
14 |
15 | hexo.extend.filter.register('before_post_render', data => {
16 | if (!hexo.theme.config.series.enable) return data
17 |
18 | const { layout, series } = data
19 | if (layout === 'post' && series) {
20 | if (!groups[series]) groups[series] = []
21 | groups[series].push({
22 | title: data.title,
23 | path: data.path,
24 | date: data.date.unix()
25 | })
26 | }
27 | return data
28 | })
29 |
30 | function series (args) {
31 | const { series } = hexo.theme.config
32 | if (!series.enable) {
33 | hexo.log.warn('Series plugin is disabled in the theme config')
34 | return ''
35 | }
36 |
37 | const seriesArr = args.length ? groups[args[0]] : groups[this.series]
38 |
39 | if (!seriesArr) {
40 | hexo.log.warn(`There is no series named "${args[0]}"`)
41 | return ''
42 | }
43 |
44 | const isAsc = (series.order || 1) === 1 // 1: asc, -1: desc
45 | const isSortByTitle = series.orderBy === 'title'
46 |
47 | const compareFn = (a, b) => {
48 | const itemA = isSortByTitle ? a.title.toUpperCase() : a.date
49 | const itemB = isSortByTitle ? b.title.toUpperCase() : b.date
50 |
51 | return itemA < itemB ? (isAsc ? -1 : 1) : itemA > itemB ? (isAsc ? 1 : -1) : 0
52 | }
53 |
54 | seriesArr.sort(compareFn)
55 |
56 | const listItems = seriesArr.map(ele =>
57 | `${ele.title} `
58 | ).join('')
59 |
60 | return series.number ? `${listItems}
` : `${listItems}
`
61 | }
62 |
63 | hexo.extend.tag.register('series', series, { ends: false })
64 |
--------------------------------------------------------------------------------
/scripts/tag/tabs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Tabs
3 | * transplant from hexo-theme-next
4 | * modify by Jerry
5 | */
6 |
7 | 'use strict'
8 |
9 | const postTabs = (args, content) => {
10 | const tabBlock = /\n([\w\W\s\S]*?)/g
11 | args = args.join(' ').split(',')
12 | const tabName = args[0] || 'tab'
13 | const tabActive = Number(args[1]) || 0
14 | const matches = []
15 | let match
16 | let tabId = 0
17 | let tabNav = ''
18 | let tabContent = ''
19 | let noDefault = true
20 |
21 | while ((match = tabBlock.exec(content)) !== null) {
22 | matches.push(match[1], match[2])
23 | }
24 |
25 | for (let i = 0; i < matches.length; i += 2) {
26 | const [tabCaption = '', tabIcon = ''] = matches[i].split('@')
27 | let postContent = matches[i + 1]
28 |
29 | postContent = hexo.render.renderSync({ text: postContent, engine: 'markdown' }).trim()
30 | tabId += 1
31 |
32 | const caption = (tabCaption || tabIcon) ? tabCaption : `${tabName} ${tabId}`
33 | const iconHtml = tabIcon ? `` : ''
34 | const isActive = (tabActive > 0 && tabActive === tabId) || (tabActive === 0 && tabId === 1) ? ' active' : ''
35 |
36 | if (isActive) noDefault = false
37 |
38 | tabNav += ``
39 | tabContent += `${postContent}`
40 | }
41 |
42 | const toTop = ''
43 | tabNav = ` `
44 | tabContent = `${tabContent}`
45 |
46 | return `${tabNav}${tabContent}${toTop}`
47 | }
48 |
49 | hexo.extend.tag.register('tabs', postTabs, { ends: true })
50 | hexo.extend.tag.register('subtabs', postTabs, { ends: true })
51 | hexo.extend.tag.register('subsubtabs', postTabs, { ends: true })
52 |
--------------------------------------------------------------------------------
/scripts/tag/timeline.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Timeline tag for Hexo
3 | * Syntax:
4 | * {% timeline [headline],[color] %}
5 | *
6 | * [content]
7 | *
8 | *
9 | * [content]
10 | *
11 | * {% endtimeline %}
12 | */
13 |
14 | 'use strict'
15 |
16 | const timeLineFn = (args, content) => {
17 | // Use named capture groups for better readability
18 | const tlBlock = /\n(?[\s\S]*?)/g
19 |
20 | // Pre-compile markdown render function
21 | const renderMd = text => hexo.render.renderSync({ text, engine: 'markdown' })
22 |
23 | // Parse arguments more efficiently
24 | const [text, color = ''] = args.length ? args.join(' ').split(',') : []
25 |
26 | // Build initial headline if text exists
27 | const headline = text
28 | ? `
29 |
30 | ${renderMd(text)}
31 |
32 | `
33 | : ''
34 |
35 | // Match all timeline blocks in one pass and transform
36 | const items = Array.from(content.matchAll(tlBlock))
37 | .map(({ groups: { title, content } }) =>
38 | `
39 |
40 | ${renderMd(title)}
41 |
42 | ${renderMd(content)}
43 | `
44 | )
45 | .join('')
46 |
47 | return `${headline}${items}`
48 | }
49 |
50 | hexo.extend.tag.register('timeline', timeLineFn, { ends: true })
51 |
--------------------------------------------------------------------------------
/source/css/_highlight/highlight/diff.styl:
--------------------------------------------------------------------------------
1 | figure.highlight
2 | table
3 | // scrollbar - firefox
4 | @-moz-document url-prefix()
5 | scrollbar-color: var(--hlscrollbar-bg) transparent
6 |
7 | &::-webkit-scrollbar-thumb
8 | background: var(--hlscrollbar-bg)
9 |
10 | pre .deletion
11 | color: $highlight-deletion
12 |
13 | pre .addition
14 | color: $highlight-addition
15 |
16 | pre .meta
17 | color: $highlight-purple
18 |
19 | pre
20 | .comment
21 | color: $highlight-comment
22 |
23 | .variable,
24 | .attribute,
25 | .regexp,
26 | .ruby .constant,
27 | .xml .tag .title,
28 | .xml .pi,
29 | .xml .doctype,
30 | .html .doctype,
31 | .css .id,
32 | .tag .name,
33 | .css .class,
34 | .css .pseudo
35 | color: $highlight-red
36 |
37 | .tag
38 | color: $highlight-aqua
39 |
40 | .number,
41 | .preprocessor,
42 | .literal,
43 | .params,
44 | .constant,
45 | .command
46 | color: $highlight-orange
47 |
48 | .built_in
49 | color: $highlight-yellow
50 |
51 | .ruby .class .title,
52 | .css .rules .attribute,
53 | .string,
54 | .value,
55 | .inheritance,
56 | .header,
57 | .ruby .symbol,
58 | .xml .cdata,
59 | .special,
60 | .number,
61 | .formula
62 | color: $highlight-green
63 |
64 | .keyword,
65 | .title,
66 | .css .hexcolor
67 | color: $highlight-aqua
68 |
69 | .function,
70 | .python .decorator,
71 | .python .title,
72 | .ruby .function .title,
73 | .ruby .title .keyword,
74 | .perl .sub,
75 | .javascript .title,
76 | .coffeescript .title
77 | color: $highlight-blue
78 |
79 | .tag .attr,
80 | .javascript .function
81 | color: $highlight-purple
82 |
--------------------------------------------------------------------------------
/source/css/_highlight/highlight/index.styl:
--------------------------------------------------------------------------------
1 | if $highlight_theme != false
2 | @require 'diff'
3 |
4 | .container
5 | figure.highlight
6 | .line
7 | if wordWrap
8 | &:before
9 | display: inline-block
10 | padding: 0 6px 0 0
11 | min-width: 30px
12 | color: var(--hlnumber-color)
13 | content: counter(line)
14 | counter-increment: line
15 | text-align: left
16 |
17 | &.marked
18 | background-color: $highlight-selection
19 |
20 | table
21 | display: block
22 | overflow: auto
23 | border: none
24 |
25 | td
26 | padding: 0
27 | border: none
28 |
29 | .gutter pre
30 | padding-right: 10px
31 | padding-left: 10px
32 | background-color: var(--hlnumber-bg)
33 | color: var(--hlnumber-color)
34 | text-align: right
35 |
36 | .code pre
37 | padding-right: 10px
38 | padding-left: 10px
39 | width: 100%
40 |
--------------------------------------------------------------------------------
/source/css/_highlight/prismjs/index.styl:
--------------------------------------------------------------------------------
1 | if $prismjs_line_number
2 | @require 'line-number'
3 |
4 | if $highlight_theme != false
5 | @require 'diff'
6 |
7 | .container
8 | pre[class*='language-']
9 | // scrollbar - firefox
10 | @-moz-document url-prefix()
11 | scrollbar-color: var(--hlscrollbar-bg) transparent
12 |
13 | &::-webkit-scrollbar-thumb
14 | background: var(--hlscrollbar-bg)
15 |
16 | &:not(.line-numbers)
17 | padding: 10px 20px
18 |
19 | .caption
20 | margin-left: -3.8em
21 | padding: 4px 16px !important
22 |
23 | a
24 | padding: 0 !important
25 |
--------------------------------------------------------------------------------
/source/css/_highlight/prismjs/line-number.styl:
--------------------------------------------------------------------------------
1 | .container
2 | pre[class*='language-']
3 | &.line-numbers
4 | position: relative
5 | padding-left: 3.8em
6 | counter-reset: linenumber
7 | line-height: $line-height-code-block
8 |
9 | > code
10 | position: relative
11 | line-height: $line-height-code-block
12 |
13 | if hexo-config('code_blocks.word_wrap')
14 | white-space: pre-wrap
15 | else
16 | white-space: inherit
17 | word-wrap: normal
18 | word-break: normal
19 | overflow-wrap: normal
20 |
21 | .line-numbers-rows
22 | position: absolute
23 | top: 0
24 | left: -3.8em
25 | width: 3em
26 | letter-spacing: -1px
27 | font-size: 100%
28 | pointer-events: none
29 | user-select: none
30 | -webkit-user-select: none
31 |
32 | & > span
33 | display: block
34 | counter-increment: linenumber
35 | pointer-events: none
36 |
37 | &:before
38 | display: block
39 | padding-right: .8em
40 | color: var(--hlnumber-color)
41 | content: counter(linenumber)
42 | text-align: right
43 |
--------------------------------------------------------------------------------
/source/css/_layout/chat.styl:
--------------------------------------------------------------------------------
1 | // chat
2 | if hexo-config('chat.rightside_button') == true
3 | if hexo-config('chat.use') == 'chatra'
4 | #chatra:not(.chatra--expanded)
5 | visibility: hidden !important
6 | width: 1px !important
7 | height: 1px !important
8 | opacity: 0 !important
9 | pointer-events: none
--------------------------------------------------------------------------------
/source/css/_layout/comments.styl:
--------------------------------------------------------------------------------
1 | #post-comment
2 | .comment-head
3 | margin-bottom: 20px
4 |
5 | &:after
6 | display: block
7 | clear: both
8 | content: ''
9 |
10 | .comment-headline
11 | display: inline-block
12 | vertical-align: middle
13 | font-weight: 700
14 | font-size: 1.43em
15 |
16 | .comment-switch
17 | display: inline-block
18 |
19 | if hexo-config('comments.text')
20 | float: right
21 | margin: 2px auto 0
22 | padding: 4px 16px
23 | width: max-content
24 | border-radius: 8px
25 | background: $comments-switch-bg
26 | else
27 | vertical-align: middle
28 |
29 | > span
30 | display: none
31 |
32 | .first-comment
33 | color: $comments-switch-first-text
34 |
35 | .second-comment
36 | color: $comments-switch-second-text
37 |
38 | #switch-btn
39 | position: relative
40 | display: inline-block
41 | margin: -4px 8px 0
42 | width: 42px
43 | height: 22px
44 | border-radius: 34px
45 | background-color: $comments-switch-first-text
46 | vertical-align: middle
47 | cursor: pointer
48 | transition: .4s
49 |
50 | &:before
51 | position: absolute
52 | bottom: 4px
53 | left: 4px
54 | width: 14px
55 | height: 14px
56 | border-radius: 50%
57 | background-color: $comments-switch-round
58 | content: ''
59 | transition: .4s
60 |
61 | .comment-wrap
62 | > div
63 | animation: tabshow .5s
64 |
65 | &:nth-child(2)
66 | display: none
67 |
68 | &.move
69 | #switch-btn
70 | background-color: $comments-switch-second-text
71 |
72 | &:before
73 | transform: translateX(20px)
74 |
75 | .comment-wrap
76 | > div
77 | &:first-child
78 | display: none
79 |
80 | &:last-child
81 | display: block
--------------------------------------------------------------------------------
/source/css/_layout/footer.styl:
--------------------------------------------------------------------------------
1 | #footer
2 | position: relative
3 | background-color: $light-blue
4 | background-attachment: scroll
5 | background-position: bottom
6 | background-size: cover
7 |
8 | if hexo-config('footer_img') != false && hexo-config('mask.footer')
9 | &:before
10 | position: absolute
11 | width: 100%
12 | height: 100%
13 | background-color: var(--mark-bg)
14 | content: ''
15 |
16 | & > *
17 | position: relative
18 | color: var(--light-grey)
19 |
20 | a
21 | color: var(--light-grey)
22 | transition: all .3s ease-in-out
23 |
24 | &:hover
25 | color: $light-blue
26 |
27 | .footer-separator
28 | margin: 0 4px
29 |
30 | .icp-icon
31 | padding: 0 4px
32 | max-height: 1.4em
33 | width: auto
34 | vertical-align: text-bottom
35 |
36 | .footer-flex
37 | display: flex
38 | flex-direction: row
39 | flex-wrap: wrap
40 | justify-content: space-between
41 | margin: 0 auto
42 | padding: 40px 60px
43 | max-width: 1200px
44 | width: 100%
45 | text-align: left
46 | gap: 13px
47 |
48 | +maxWidth768()
49 | padding: 30px
50 | gap: 10px
51 |
52 | .footer-flex-items
53 | flex-shrink: 0
54 | min-width: 100px
55 | text-align: left
56 | white-space: nowrap
57 |
58 | .footer-flex-title
59 | margin-bottom: 5px
60 | white-space: nowrap
61 | font-weight: 600
62 | font-size: 1.4em
63 |
64 | .footer-flex-item
65 | margin: 10px 0
66 | white-space: nowrap
67 |
68 | a
69 | display: block
70 | white-space: nowrap
71 |
72 | .footer-other
73 | padding: 40px 20px
74 | width: 100%
75 | text-align: center
76 |
77 | if hexo-config('footer.nav')
78 | padding: 10px 8px
79 | background-color: rgba(0, 0, 0, .1)
80 |
81 | .copyright,
82 | .framework-info,
83 | .footer_custom_text
84 | font-size: .9em
85 | else
86 | .framework-info
87 | display: block
--------------------------------------------------------------------------------
/source/css/_layout/loading.styl:
--------------------------------------------------------------------------------
1 | if hexo-config('preloader.enable') && hexo-config('preloader.source') == 1
2 | .loading-bg
3 | position: fixed
4 | z-index: 1000
5 | width: 50%
6 | height: 100%
7 | background-color: var(--preloader-bg)
8 |
9 | #loading-box
10 | .loading-left-bg
11 | @extend .loading-bg
12 |
13 | .loading-right-bg
14 | @extend .loading-bg
15 | right: 0
16 |
17 | .spinner-box
18 | position: fixed
19 | z-index: 1001
20 | display: flex
21 | justify-content: center
22 | align-items: center
23 | width: 100%
24 | height: 100vh
25 |
26 | .configure-border-1
27 | position: absolute
28 | padding: 3px
29 | width: 115px
30 | height: 115px
31 | background: #ffab91
32 | animation: configure-clockwise 3s ease-in-out 0s infinite alternate
33 |
34 | .configure-border-2
35 | left: -115px
36 | padding: 3px
37 | width: 115px
38 | height: 115px
39 | background: rgb(63, 249, 220)
40 | transform: rotate(45deg)
41 | animation: configure-xclockwise 3s ease-in-out 0s infinite alternate
42 |
43 | .loading-word
44 | position: absolute
45 | color: var(--preloader-color)
46 | font-size: 16px
47 |
48 | .configure-core
49 | width: 100%
50 | height: 100%
51 | background-color: var(--preloader-bg)
52 |
53 | &.loaded
54 | .loading-left-bg
55 | transition: all .5s
56 | transform: translate(-100%, 0)
57 |
58 | .loading-right-bg
59 | transition: all .5s
60 | transform: translate(100%, 0)
61 |
62 | .spinner-box
63 | display: none
64 |
65 | @keyframes configure-clockwise
66 | 0%
67 | transform: rotate(0)
68 |
69 | 25%
70 | transform: rotate(90deg)
71 |
72 | 50%
73 | transform: rotate(180deg)
74 |
75 | 75%
76 | transform: rotate(270deg)
77 |
78 | 100%
79 | transform: rotate(360deg)
80 |
81 | @keyframes configure-xclockwise
82 | 0%
83 | transform: rotate(45deg)
84 |
85 | 25%
86 | transform: rotate(-45deg)
87 |
88 | 50%
89 | transform: rotate(-135deg)
90 |
91 | 75%
92 | transform: rotate(-225deg)
93 |
94 | 100%
95 | transform: rotate(-315deg)
96 |
--------------------------------------------------------------------------------
/source/css/_layout/pagination.styl:
--------------------------------------------------------------------------------
1 | #pagination
2 | .pagination
3 | margin-top: 20px
4 | text-align: center
5 |
6 | .page-number
7 | &.current
8 | background: $theme-paginator-color
9 | color: var(--white)
10 |
11 | .full-width
12 | width: 100% !important
13 |
14 | .pagination-related
15 | height: 150px
16 |
17 | +minWidth768()
18 | flex: 1
19 |
20 | .info-1
21 | .info-item-2
22 | -webkit-line-clamp: 1
23 |
24 | .info-2
25 | .info-item-1
26 | -webkit-line-clamp: 2
27 |
28 | &.pagination-post
29 | overflow: hidden
30 | margin-top: 40px
31 | width: 100%
32 | addBorderRadius()
33 | display: flex
34 |
35 | +maxWidth768()
36 | flex-direction: column
37 |
38 | .layout
39 | .pagination
40 | & > *
41 | display: inline-block
42 | margin: 0 6px
43 | width: w = 2.5em
44 | height: w
45 | line-height: w
46 |
47 | & > *:not(.space)
48 | @extend .cardHover
49 |
50 | &:hover
51 | background: var(--btn-hover-color)
52 | color: var(--btn-color)
53 |
54 | #archive
55 | .pagination
56 | margin-top: 30px
57 |
58 | & > *:not(.space)
59 | box-shadow: none
60 |
61 | .pagination-related
62 | position: relative
63 | display: inline-block
64 | overflow: hidden
65 | background: $dark-black
66 | vertical-align: bottom
67 | @extend .postImgHover
68 |
69 | &.next-post
70 | .info
71 | text-align: right
72 |
73 | .info
74 | .info-1,
75 | .info-2
76 | @extend .verticalCenter
77 | padding: 20px 40px
78 | color: var(--white)
79 | transition: transform .3s, opacity .3s
80 |
81 | .info-1
82 | .info-item-1
83 | color: var(--light-grey)
84 | text-transform: uppercase
85 | font-size: 90%
86 |
87 | .info-item-2
88 | @extend .limit-more-line
89 | color: var(--white)
90 | font-weight: 500
91 |
92 | .info-2
93 | opacity: 0
94 | transform: translate(0, 0)
95 |
96 | .info-item-1
97 | @extend .limit-more-line
98 |
99 | &:not(.no-desc):hover
100 | .info-1
101 | opacity: 0
102 | transform: translate(0, -100%)
103 |
104 | .info-2
105 | opacity: 1
106 | transform: translate(0, -50%)
--------------------------------------------------------------------------------
/source/css/_layout/relatedposts.styl:
--------------------------------------------------------------------------------
1 | .relatedPosts
2 | margin-top: 40px
3 |
4 | & > .headline
5 | margin-bottom: 5px
6 | font-weight: 700
7 | font-size: 1.43em
8 |
9 | & > .relatedPosts-list
10 | & > a
11 | margin: 3px
12 | width: calc(33.333% - 6px)
13 | height: 200px
14 | addBorderRadius()
15 |
16 | +maxWidth768()
17 | margin: 2px
18 | width: calc(50% - 4px)
19 | height: 150px
20 |
21 | +maxWidth600()
22 | width: calc(100% - 4px)
23 |
24 | .info
25 | .info-1
26 | .info-item-2
27 | -webkit-line-clamp: 2
28 |
29 | .info-2
30 | .info-item-1
31 | -webkit-line-clamp: 3
--------------------------------------------------------------------------------
/source/css/_layout/reward.styl:
--------------------------------------------------------------------------------
1 | .post-reward
2 | position: relative
3 | margin-top: 80px
4 | width: 100%
5 | text-align: center
6 | pointer-events: none
7 |
8 | & > *
9 | pointer-events: auto
10 |
11 | .reward-button
12 | display: inline-block
13 | padding: 4px 24px
14 | background: var(--btn-bg)
15 | color: var(--btn-color)
16 | cursor: pointer
17 | addBorderRadius()
18 |
19 | i
20 | margin-right: 5px
21 |
22 | &:hover
23 | .reward-button
24 | background: var(--btn-hover-color)
25 |
26 | & > .reward-main
27 | display: block
28 |
29 | .reward-main
30 | position: absolute
31 | bottom: 40px
32 | left: 0
33 | z-index: 100
34 | display: none
35 | padding: 0 0 15px
36 | width: 100%
37 | addBorderRadius()
38 |
39 | .reward-all
40 | display: inline-block
41 | margin: 0
42 | padding: 20px 10px
43 | background: var(--reward-pop)
44 |
45 | &:before
46 | position: absolute
47 | bottom: -10px
48 | left: 0
49 | width: 100%
50 | height: 20px
51 | content: ''
52 |
53 | &:after
54 | position: absolute
55 | right: 0
56 | bottom: 2px
57 | left: 0
58 | margin: 0 auto
59 | width: 0
60 | height: 0
61 | border-top: 13px solid var(--reward-pop)
62 | border-right: 13px solid transparent
63 | border-left: 13px solid transparent
64 | content: ''
65 |
66 | .reward-item
67 | display: inline-block
68 | padding: 0 8px
69 | list-style-type: none
70 | vertical-align: top
71 |
72 | img
73 | width: 130px
74 | height: 130px
75 |
76 | .post-qr-code-desc
77 | width: 130px
78 | color: $reward-pop-up-color
79 |
--------------------------------------------------------------------------------
/source/css/_layout/rightside.styl:
--------------------------------------------------------------------------------
1 | #rightside
2 | position: fixed
3 | right: -48px
4 | bottom: $rightside-bottom
5 | z-index: 100
6 | opacity: 0
7 | transition: all .5s
8 |
9 | &.rightside-show
10 | opacity: .8
11 | transform: translate(-58px, 0)
12 |
13 | #rightside-config-hide
14 | height: 0
15 | opacity: 0
16 | transition: transform .4s
17 | transform: translate(45px, 0)
18 |
19 | &.show
20 | height: auto
21 | opacity: 1
22 | transform: translate(0, 0)
23 |
24 | &.status
25 | height: auto
26 | opacity: 1
27 |
28 | & > div
29 | & > button,
30 | & > a
31 | display: block
32 | margin-bottom: 5px
33 | width: w = 35px
34 | height: w
35 | background-color: var(--btn-bg)
36 | color: var(--btn-color)
37 | text-align: center
38 | font-size: 16px
39 | line-height: w
40 | addBorderRadius(5)
41 |
42 | &:hover
43 | background-color: var(--btn-hover-color)
44 |
45 | #mobile-toc-button
46 | display: none
47 |
48 | +maxWidth900()
49 | display: block
50 |
51 | +maxWidth900()
52 | #hide-aside-btn
53 | display: none
54 |
55 | if hexo-config('rightside_scroll_percent')
56 | #go-up
57 | .scroll-percent
58 | display: none
59 |
60 | &.show-percent
61 | .scroll-percent
62 | display: block
63 |
64 | & + i
65 | display: none
66 |
67 | &:hover
68 | .scroll-percent
69 | display: none
70 |
71 | & + i
72 | display: block
73 |
--------------------------------------------------------------------------------
/source/css/_layout/sidebar.styl:
--------------------------------------------------------------------------------
1 | #sidebar
2 | #menu-mask
3 | position: fixed
4 | z-index: 102
5 | display: none
6 | width: 100%
7 | height: 100%
8 | background: alpha($dark-black, .8)
9 |
10 | #sidebar-menus
11 | position: fixed
12 | top: 0
13 | right: -($sidebar-width)
14 | z-index: 103
15 | overflow-x: hidden
16 | overflow-y: scroll
17 | padding-left: 5px
18 | width: $sidebar-width
19 | height: 100%
20 | background: var(--sidebar-bg)
21 | transition: all .5s
22 |
23 | &.open
24 | transform: translate3d(-100%, 0, 0)
25 |
26 | & > .avatar-img
27 | margin: 20px auto
28 |
29 | .site-data
30 | padding: 0 10px
31 |
32 | hr
33 | margin: 20px auto
34 |
35 | .menus_items
36 | margin: 20px
37 | padding: 15px
38 | background: var(--sidebar-menu-bg)
39 | box-shadow: 0 0 1px 1px rgba(7, 17, 27, .05)
40 | addBorderRadius(10)
41 |
42 | .site-page
43 | @extend .limit-one-line
44 | position: relative
45 | display: block
46 | margin: 4px 0
47 | padding: 2px 23px 2px 15px
48 | color: var(--font-color)
49 | font-size: 1.15em
50 | cursor: pointer
51 | addBorderRadius(6)
52 |
53 | &:hover
54 | background: var(--text-bg-hover)
55 | color: var(--white)
56 |
57 | i:first-child
58 | width: 15%
59 | text-align: left
60 |
61 | &.group
62 | & > i:last-child
63 | position: absolute
64 | top: .6em
65 | right: 10px
66 | transition: transform .3s
67 |
68 | &.hide
69 | & > i:last-child
70 | transform: rotate(90deg)
71 |
72 | & + .menus_item_child
73 | display: none
74 |
75 | .menus_item_child
76 | margin: 0
77 | padding-left: 25px
78 | list-style: none
--------------------------------------------------------------------------------
/source/css/_page/404.styl:
--------------------------------------------------------------------------------
1 | if hexo-config('error_404.enable')
2 | .type-404
3 | .error-content
4 | @extend .cardHover
5 | overflow: hidden
6 | margin: 0 20px
7 | height: 360px
8 |
9 | +maxWidth768()
10 | margin: 0
11 | height: 500px
12 |
13 | .error-img
14 | display: inline-block
15 | overflow: hidden
16 | width: 50%
17 | height: 100%
18 |
19 | +maxWidth768()
20 | width: 100%
21 | height: 45%
22 |
23 | img
24 | @extend .imgHover
25 | background-color: $theme-color
26 |
27 | .error-info
28 | display: inline-flex
29 | flex-direction: column
30 | justify-content: center
31 | align-content: center
32 | width: 50%
33 | height: 100%
34 | vertical-align: top
35 | text-align: center
36 |
37 | if $site-name-font
38 | font-family: $site-name-font
39 |
40 | +maxWidth768()
41 | width: 100%
42 | height: 55%
43 |
44 | .error_title
45 | margin-top: -.6em
46 | font-size: 9em
47 |
48 | +maxWidth768()
49 | font-size: 8em
50 |
51 | .error_subtitle
52 | @extend .limit-more-line
53 | margin-top: -3em
54 | word-break: break-word
55 | font-size: 1.6em
56 | -webkit-line-clamp: 2
57 |
58 | .nc
59 | margin-top: 5%
60 | padding: 0 20px
61 |
62 | #footer
63 | display: none
64 |
65 | & + #rightside
66 | display: none
--------------------------------------------------------------------------------
/source/css/_page/archives.styl:
--------------------------------------------------------------------------------
1 | .article-sort
2 | margin-left: 10px
3 | padding-left: 20px
4 | border-left: 2px solid lighten($light-blue, 20)
5 |
6 | &-title
7 | position: relative
8 | margin-left: 10px
9 | padding-bottom: 20px
10 | padding-left: 20px
11 | font-size: 1.72em
12 |
13 | &:hover
14 | &:before
15 | border-color: var(--pseudo-hover)
16 |
17 | &:before
18 | position: absolute
19 | top: calc(((100% - 36px) / 2))
20 | left: -9px
21 | z-index: 1
22 | width: w = 10px
23 | height: h = w
24 | border: .5 * w solid $light-blue
25 | border-radius: w
26 | background: var(--card-bg)
27 | content: ''
28 | line-height: h
29 | transition: all .2s ease-in-out
30 |
31 | &:after
32 | position: absolute
33 | bottom: 0
34 | left: 0
35 | z-index: 0
36 | width: 2px
37 | height: 1.5em
38 | background: lighten($light-blue, 20)
39 | content: ''
40 |
41 | &-item
42 | position: relative
43 | display: flex
44 | align-items: center
45 | margin: 0 0 20px 10px
46 | transition: all .2s ease-in-out
47 |
48 | &:hover
49 | &:before
50 | border-color: var(--pseudo-hover)
51 |
52 | &:before
53 | $w = 6px
54 | position: absolute
55 | left: calc(-20px - 17px)
56 | width: w = $w
57 | height: h = w
58 | border: .5 * w solid $light-blue
59 | border-radius: w
60 | background: var(--card-bg)
61 | content: ''
62 | transition: all .2s ease-in-out
63 |
64 | &.no-article-cover
65 | height: 80px
66 |
67 | .article-sort-item-info
68 | padding: 0
69 |
70 | &.year
71 | font-size: 1.43em
72 | margin-bottom: 10px
73 |
74 | &:hover
75 | &:before
76 | border-color: $light-blue
77 |
78 | &:before
79 | border-color: var(--pseudo-hover)
80 |
81 | &-time
82 | color: var(--card-meta)
83 | font-size: .85em
84 |
85 | time
86 | padding-left: 6px
87 | cursor: default
88 |
89 | &-title
90 | @extend .limit-more-line
91 | color: var(--font-color)
92 | font-size: 1.05em
93 | transition: all .3s
94 | -webkit-line-clamp: 2
95 |
96 | &:hover
97 | color: $text-hover
98 | transform: translateX(10px)
99 |
100 | &-img
101 | overflow: hidden
102 | width: 100px
103 | height: 70px
104 | addBorderRadius()
105 |
106 | +maxWidth768()
107 | width: 70px
108 | height: 70px
109 |
110 | :first-child
111 | @extend .imgHover
112 |
113 | &-info
114 | flex: 1
115 | padding: 0 16px
116 |
--------------------------------------------------------------------------------
/source/css/_page/categories.styl:
--------------------------------------------------------------------------------
1 | .category-lists
2 | .category-title
3 | font-size: 2.57em
4 |
5 | +maxWidth768()
6 | font-size: 2em
7 |
8 | .category-list
9 | margin-bottom: 0
10 |
11 | a
12 | color: var(--font-color)
13 |
14 | &:hover
15 | color: $text-hover
16 |
17 | .category-list-count
18 | margin-left: 8px
19 | color: var(--card-meta)
20 |
21 | &:before
22 | content: '('
23 |
24 | &:after
25 | content: ')'
26 |
27 | ul
28 | padding: 0 0 0 20px
29 | @extend .list-beauty
30 |
31 | ul
32 | padding-left: 4px
33 |
34 | li
35 | position: relative
36 | margin: 6px 0
37 | padding: .12em .4em .12em 1.4em
38 |
--------------------------------------------------------------------------------
/source/css/_page/common.styl:
--------------------------------------------------------------------------------
1 | #body-wrap
2 | display: flex
3 | flex-direction: column
4 | min-height: 100vh
5 |
6 | .layout
7 | display: flex
8 | flex: 1 auto
9 | margin: 0 auto
10 | padding: 40px 15px
11 | max-width: 1200px
12 | width: 100%
13 |
14 | +maxWidth900()
15 | flex-direction: column
16 |
17 | +maxWidth768()
18 | padding: 20px 5px
19 |
20 | +minWidth2000()
21 | max-width: 70%
22 |
23 | & > div:first-child:not(.nc)
24 | @extend .cardHover
25 | align-self: flex-start
26 | padding: 50px 40px
27 |
28 | +maxWidth768()
29 | padding: 36px 14px
30 |
31 | & > div:first-child
32 | width: 74%
33 | transition: all .3s
34 |
35 | +maxWidth900()
36 | width: 100% !important
37 |
38 | if hexo-config('aside.position') == 'left'
39 | +minWidth900()
40 | order: 2
41 |
42 | // 隱藏aside
43 | &.hide-aside
44 | max-width: 1000px
45 |
46 | +minWidth2000()
47 | max-width: 1300px
48 |
49 | & > div
50 | width: 100% !important
51 |
52 | // for apple device
53 | .apple
54 | #page-header.full_page
55 | background-attachment: scroll !important
56 |
57 | .recent-post-item,
58 | .avatar-img,
59 | .flink-item-icon
60 | transform: translateZ(0)
61 |
--------------------------------------------------------------------------------
/source/css/_page/flink.styl:
--------------------------------------------------------------------------------
1 | .container
2 | .flink
3 | margin-bottom: 20px
4 |
5 | .flink-list
6 | overflow: auto
7 | padding: 10px 10px 0
8 | text-align: center
9 |
10 | & > .flink-list-item
11 | position: relative
12 | float: left
13 | overflow: hidden
14 | margin: 15px 7px
15 | width: calc(100% / 3 - 15px)
16 | height: 90px
17 | line-height: 17px
18 | -webkit-transform: translateZ(0)
19 | addBorderRadius(8)
20 |
21 | +maxWidth1024()
22 | width: calc(50% - 15px) !important
23 |
24 | +maxWidth600()
25 | width: calc(100% - 15px) !important
26 |
27 | &:hover
28 | .flink-item-icon
29 | margin-left: -10px
30 | width: 0
31 |
32 | &:before
33 | position: absolute
34 | top: 0
35 | right: 0
36 | bottom: 0
37 | left: 0
38 | z-index: -1
39 | background: var(--text-bg-hover)
40 | content: ''
41 | transition: transform .3s ease-out
42 | transform: scale(0)
43 |
44 | &:hover:before,
45 | &:focus:before,
46 | &:active:before
47 | transform: scale(1)
48 |
49 | a
50 | color: var(--font-color)
51 | text-decoration: none
52 |
53 | .flink-item-icon
54 | float: left
55 | overflow: hidden
56 | margin: 15px 10px
57 | width: 60px
58 | height: 60px
59 | border-radius: 7px
60 | transition: width .3s ease-out
61 |
62 | img
63 | width: 100%
64 | height: 100%
65 | transition: filter 375ms ease-in .2s, transform .3s
66 | object-fit: cover
67 |
68 | .img-alt
69 | display: none
70 |
71 | .flink-item-name
72 | @extend .limit-one-line
73 | padding: 16px 10px 0 0
74 | height: 40px
75 | font-weight: bold
76 | font-size: 1.43em
77 |
78 | .flink-item-desc
79 | @extend .limit-one-line
80 | padding: 16px 10px 16px 0
81 | height: 50px
82 | font-size: .93em
83 |
84 | .flink-name
85 | margin-bottom: 5px
86 | font-weight: bold
87 | font-size: 1.5em
--------------------------------------------------------------------------------
/source/css/_page/shuoshuo.styl:
--------------------------------------------------------------------------------
1 | #article-container
2 | .shuoshuo-item
3 | @extend .cardHover
4 | margin-bottom: 20px
5 | padding: 35px 30px 30px
6 |
7 | +maxWidth768()
8 | padding: 25px 20px 20px
9 |
10 | .shuoshuo-item-header
11 | display: flex
12 | align-items: center
13 | cursor: default
14 |
15 | .shuoshuo-avatar
16 | overflow: hidden
17 | width: 40px
18 | height: 40px
19 | border-radius: 40px
20 |
21 | img
22 | margin: 0
23 | width: 100%
24 | height: 100%
25 |
26 | .shuoshuo-info
27 | margin-left: 10px
28 | line-height: 1.5
29 |
30 | .shuoshuo-date
31 | color: #858585
32 | font-size: .8em
33 |
34 | .shuoshuo-content
35 | padding: 15px 0 10px
36 |
37 | & > *:last-child
38 | margin-bottom: 0
39 |
40 | .shuoshuo-footer
41 | display: flex
42 | align-items: center
43 |
44 | &.flex-between
45 | justify-content: space-between
46 |
47 | &.flex-end
48 | justify-content: flex-end
49 |
50 | .shuoshuo-tag
51 | display: inline-block
52 | margin-right: 8px
53 | padding: 0 8px
54 | width: fit-content
55 | border: 1px solid $light-blue
56 | border-radius: 12px
57 | color: $light-blue
58 | font-size: .85em
59 | cursor: default
60 | transition: all .2s ease-in-out
61 |
62 | &:hover
63 | background: $light-blue
64 | color: var(--white)
65 |
66 | .shuoshuo-comment-btn
67 | padding: 2px
68 | color: #90a4ae
69 | cursor: pointer
70 |
71 | &:hover
72 | color: $light-blue
73 |
74 | .shuoshuo-comment
75 | padding-top: 10px
76 |
77 | &.no-comment
78 | display: none
79 |
--------------------------------------------------------------------------------
/source/css/_page/tags.styl:
--------------------------------------------------------------------------------
1 | .tag-cloud
2 | &-list
3 | a
4 | display: inline-block
5 | margin: 2px
6 | padding: 2px 7px
7 | line-height: 1.7
8 | transition: all .3s
9 | addBorderRadius(5)
10 |
11 | &:hover
12 | background: var(--btn-bg) !important
13 | box-shadow: 2px 2px 6px rgba(0, 0, 0, .2)
14 | color: var(--btn-color) !important
15 |
16 | +maxWidth768()
17 | zoom: .85
18 |
19 | &-title
20 | font-size: 2.57em
21 |
22 | +maxWidth768()
23 | font-size: 2em
24 |
25 | .page-title
26 | & + .tag-cloud-list
27 | text-align: left
28 |
--------------------------------------------------------------------------------
/source/css/_search/algolia.styl:
--------------------------------------------------------------------------------
1 | #algolia-search
2 | .search-dialog
3 | .ais-SearchBox
4 | input
5 | padding: 5px 14px
6 | width: 100%
7 | outline: none
8 | border: 2px solid $search-color
9 | border-radius: 40px
10 | background: var(--search-bg)
11 | color: var(--search-input-color)
12 |
13 | .ais-SearchBox-loadingIndicator
14 | position: absolute
15 | top: 18px
16 | left: 67px
17 |
18 | .ais-Hits-list
19 | margin: 0
20 | padding: 0
21 | @extend .list-beauty
22 |
23 | a
24 | color: var(--search-a-color)
25 |
26 | &:hover
27 | color: $search-color
28 |
29 | mark
30 | background: transparent
31 | color: $search-keyword-highlight
32 | font-weight: bold
33 |
34 | .algolia-hits-item-title
35 | font-weight: 600
36 |
37 | .algolia-hit-item-content
38 | margin: 0 0 8px
39 | word-break: break-word
40 |
41 | .ais-Pagination
42 | margin: 15px 0 0
43 | padding: 0
44 | text-align: center
45 |
46 | .ais-Pagination-list
47 | margin: 0
48 | padding: 0
49 | list-style: none
50 |
51 | .ais-Pagination-item
52 | display: inline
53 | margin: 0 4px
54 | padding: 0
55 |
56 | .ais-Pagination-link
57 | display: inline-block
58 | min-width: 24px
59 | height: 24px
60 | text-align: center
61 | line-height: 24px
62 | addBorderRadius()
63 |
64 | .ais-Pagination-item--selected
65 | a
66 | background: $theme-paginator-color
67 | color: #eee
68 | cursor: default
69 |
70 | .ais-Pagination-item--disabled
71 | visibility: hidden
72 |
73 | #algolia-hits
74 | > div
75 | overflow-y: overlay
76 | margin: 0 -20px
77 | padding: 0 22px
78 | max-height: calc(80vh - 220px)
79 |
80 | +maxWidth768()
81 | max-height: none
82 | height: calc(var(--search-height) - 235px)
83 |
84 | #algolia-info
85 | div
86 | display: inline
87 |
88 | .algolia-poweredBy
89 | float: right
90 | vertical-align: text-top
91 |
92 | svg
93 | height: 1.1em
--------------------------------------------------------------------------------
/source/css/_search/index.styl:
--------------------------------------------------------------------------------
1 | .search-dialog
2 | position: fixed
3 | top: 10%
4 | left: 50%
5 | z-index: 1001
6 | display: none
7 | margin-left: -300px
8 | padding: 20px
9 | width: 600px
10 | background: var(--search-bg)
11 | --search-height: 100vh
12 | addBorderRadius(8)
13 |
14 | +maxWidth768()
15 | top: 0
16 | left: 0
17 | margin: 0
18 | width: 100%
19 | height: 100%
20 | border-radius: 0
21 |
22 | .search-nav
23 | margin: 0 0 14px
24 | color: $search-color
25 | font-size: 1.4em
26 | line-height: 1
27 |
28 | .search-dialog-title
29 | margin-right: 10px
30 |
31 | .search-close-button
32 | float: right
33 | color: $grey
34 | transition: color .2s ease-in-out
35 |
36 | &:hover
37 | color: $search-color
38 |
39 | hr
40 | margin: 15px auto
41 | @extend .custom-hr
42 |
43 | #search-mask
44 | position: fixed
45 | top: 0
46 | right: 0
47 | bottom: 0
48 | left: 0
49 | z-index: 1000
50 | display: none
51 | background: rgba($dark-black, .6)
52 |
53 | if hexo-config('search.use') == 'algolia_search'
54 | @require 'algolia'
55 | else if hexo-config('search.use') == 'local_search'
56 | @require 'local-search'
--------------------------------------------------------------------------------
/source/css/_search/local-search.styl:
--------------------------------------------------------------------------------
1 | #local-search
2 | .search-dialog
3 | .local-search-box
4 | margin: 0 auto
5 | max-width: 100%
6 | width: 100%
7 |
8 | input
9 | padding: 5px 14px
10 | width: 100%
11 | outline: none
12 | border: 2px solid $search-color
13 | border-radius: 40px
14 | background: var(--search-bg)
15 | color: var(--search-input-color)
16 | -webkit-appearance: none
17 |
18 | .search-wrap
19 | display: none
20 |
21 | .local-search-hit-item
22 | margin-left: 24px
23 | padding-left: 3px
24 | line-height: 1.8
25 |
26 | &::marker
27 | color: $search-color
28 | font-weight: bold
29 | font-style: italic
30 |
31 | a
32 | color: var(--search-a-color)
33 |
34 | &:hover
35 | color: $search-color
36 |
37 | .search-result-title
38 | font-weight: 600
39 |
40 | .search-result
41 | margin: 0 0 8px
42 | word-break: break-all
43 | font-size: .9em
44 |
45 | .search-result-list
46 | overflow-y: overlay
47 | margin: 0 -20px
48 | padding: 0 22px
49 | max-height: calc(80vh - 180px)
50 |
51 | +maxWidth768()
52 | max-height: calc(var(--search-height) - 190px) !important
53 |
54 | .search-keyword
55 | background: transparent
56 | color: $search-keyword-highlight
57 | font-weight: 600
--------------------------------------------------------------------------------
/source/css/_tags/button.styl:
--------------------------------------------------------------------------------
1 | .container
2 | .btn-center
3 | margin: 0 0 20px
4 | text-align: center
5 |
6 | .btn-beautify
7 | display: inline-block
8 | margin: 0 4px 6px
9 | padding: 0 15px
10 | background-color: var(--btn-beautify-color, $btn-default-color)
11 | color: $btn-color
12 | line-height: 2
13 | addBorderRadius()
14 |
15 | for $type in $color-types
16 | &.{$type}
17 | --btn-beautify-color: lookup('$tagsP-' + $type + '-color')
18 |
19 | &:hover
20 | background-color: var(--btn-hover-color)
21 |
22 | i + span
23 | margin-left: 6px
24 |
25 | &:not(.block) + .btn-beautify:not(.block)
26 | margin: 0 4px 20px
27 |
28 | &.block
29 | display: block
30 | margin: 0 0 20px
31 | width: fit-content
32 | width: -moz-fit-content
33 |
34 | &.center
35 | margin: 0 auto 20px
36 |
37 | &.right
38 | margin: 0 0 20px auto
39 |
40 | &.larger
41 | padding: 6px 15px
42 |
43 | &:hover
44 | text-decoration: none
45 |
46 | &.outline
47 | border: 1px solid transparent
48 | border-color: var(--btn-beautify-color, $btn-default-color)
49 | background-color: transparent
50 | color: var(--btn-beautify-color, $btn-default-color)
51 |
52 | &:hover
53 | background-color: var(--btn-beautify-color, $btn-default-color)
54 |
55 | &:hover
56 | color: white !important
57 |
--------------------------------------------------------------------------------
/source/css/_tags/hexo.styl:
--------------------------------------------------------------------------------
1 | // pullquote
2 | blockquote
3 | &.pullquote
4 | position: relative
5 | max-width: 45%
6 | font-size: 110%
7 |
8 | &.left
9 | float: left
10 | margin: 1em .5em 0 0
11 |
12 | &.right
13 | float: right
14 | margin: 1em 0 0 .5em
15 |
16 | // hexo tag video
17 | .video-container
18 | position: relative
19 | overflow: hidden
20 | margin-bottom: 16px
21 | padding-top: 56.25%
22 | height: 0
23 |
24 | iframe
25 | position: absolute
26 | top: 0
27 | left: 0
28 | margin-top: 0
29 | width: 100%
30 | height: 100%
31 |
--------------------------------------------------------------------------------
/source/css/_tags/hide.styl:
--------------------------------------------------------------------------------
1 | // tag-hide
2 | .hide-inline,
3 | .hide-block
4 | & > .hide-button
5 | display: inline-block
6 | padding: 5px 18px
7 | background: $tag-hide-bg
8 | color: var(--white)
9 | addBorderRadius()
10 |
11 | &:hover
12 | background-color: var(--btn-hover-color)
13 |
14 | &.open
15 | display: none
16 |
17 | & + div
18 | display: block
19 |
20 | & + span
21 | display: inline
22 |
23 | & > .hide-content
24 | display: none
25 |
26 | .hide-inline
27 | & > .hide-button
28 | margin: 0 6px
29 |
30 | & > .hide-content
31 | margin: 0 6px
32 |
33 | .hide-block
34 | margin: 0 0 16px
35 |
36 | .toggle
37 | margin-bottom: 20px
38 | border: 1px solid $tag-hide-toggle-bg
39 | addBorderRadius(5, true)
40 |
41 | & > .toggle-button
42 | padding: 6px 15px
43 | background: $tag-hide-toggle-bg
44 | color: #1F2D3D
45 | cursor: pointer
46 |
47 | & > .toggle-content
48 | margin: 30px 24px
49 |
--------------------------------------------------------------------------------
/source/css/_tags/inlineImg.styl:
--------------------------------------------------------------------------------
1 | .container
2 | .inline-img
3 | display: inline
4 | margin: 0 3px
5 | height: 1.1em
6 | vertical-align: text-bottom
--------------------------------------------------------------------------------
/source/css/_tags/label.styl:
--------------------------------------------------------------------------------
1 | .hl-label
2 | padding: 2px 4px
3 | color: $btn-color
4 | addBorderRadius(3)
5 |
6 | &.default
7 | background-color: $btn-default-color
8 |
9 | for $type in $color-types
10 | &.{$type}
11 | background-color: lookup('$tagsP-' + $type + '-color')
12 |
--------------------------------------------------------------------------------
/source/css/_tags/series.styl:
--------------------------------------------------------------------------------
1 | .container
2 | .series-items
3 | a
4 | &:hover
5 | color: var(--pseudo-hover)
--------------------------------------------------------------------------------
/source/css/_tags/tabs.styl:
--------------------------------------------------------------------------------
1 |
2 | .container
3 | .tabs
4 | position: relative
5 | margin: 0 0 20px
6 | border-right: 1px solid var(--tab-border-color)
7 | border-bottom: 1px solid var(--tab-border-color)
8 | border-left: 1px solid var(--tab-border-color)
9 | addBorderRadius()
10 | overflow: hidden
11 |
12 | > .nav-tabs
13 | display: flex
14 | flex-wrap: wrap
15 | margin: 0
16 | padding: 0
17 | background: var(--tab-button-bg)
18 |
19 | > .tab
20 | flex-grow: 1
21 | padding: 8px 18px
22 | border-top: 2px solid var(--tab-border-color)
23 | background: var(--tab-button-bg)
24 | color: var(--tab-button-color)
25 | line-height: 2
26 | transition: all .4s
27 |
28 | i
29 | width: 1.5em
30 |
31 | &.active
32 | border-top: 2px solid $tab-active-border-color
33 | background: var(--tab-button-active-bg)
34 | cursor: default
35 |
36 | &:not(.active)
37 | &:hover
38 | border-top: 2px solid var(--tab-button-hover-bg)
39 | background: var(--tab-button-hover-bg)
40 |
41 | &.no-default
42 | & ~ .tab-to-top
43 | display: none
44 |
45 | > .tab-contents
46 | .tab-item-content
47 | position: relative
48 | display: none
49 | padding: 36px 24px 10px
50 |
51 | +maxWidth768()
52 | padding: 24px 14px
53 |
54 | &.active
55 | display: block
56 | animation: tabshow .5s
57 |
58 | > :last-child
59 | margin-bottom: 0
60 |
61 | > .tab-to-top
62 | padding: 0 16px 10px 0
63 | width: 100%
64 | text-align: right
65 |
66 | button
67 | color: $tab-to-top-color
68 |
69 | &:hover
70 | color: $tab-to-top-hover-color
71 |
72 | @keyframes tabshow
73 | 0%
74 | transform: translateY(15px)
75 |
76 | 100%
77 | transform: translateY(0)
78 |
--------------------------------------------------------------------------------
/source/css/_tags/timeline.styl:
--------------------------------------------------------------------------------
1 | .container
2 | .timeline
3 | margin: 0 10px 20px
4 | padding: 14px 0 5px 20px
5 | border-left: 2px solid var(--timeline-color, $theme-color)
6 |
7 | for $type in $color-types
8 | &.{$type}
9 | --timeline-color: lookup('$tagsP-' + $type + '-color')
10 | --timeline-bg: s('rgba(%s,%s,%s, 0.2)', red(lookup('$tagsP-' + $type + '-color')), green(lookup('$tagsP-' + $type + '-color')), blue(lookup('$tagsP-' + $type + '-color')))
11 |
12 | .timeline-item
13 | margin: 0 0 15px
14 |
15 | &:hover
16 | .item-circle
17 | &:before
18 | border-color: var(--timeline-color, $theme-color)
19 |
20 | &.headline
21 | .timeline-item-title
22 | .item-circle
23 | > p
24 | font-weight: 600
25 | font-size: 1.2em
26 |
27 | &:before
28 | left: -28px
29 | border: 4px solid var(--timeline-color, $theme-color)
30 |
31 | &:hover
32 | .item-circle
33 | &:before
34 | border-color: var(--pseudo-hover)
35 |
36 | .timeline-item-title
37 | position: relative
38 |
39 | .item-circle
40 | &:before
41 | position: absolute
42 | top: 50%
43 | left: -27px
44 | width: 6px
45 | height: 6px
46 | border: 3px solid var(--pseudo-hover)
47 | border-radius: 50%
48 | background: var(--card-bg)
49 | content: ''
50 | transition: all .3s
51 | transform: translate(0, -50%)
52 |
53 | > p
54 | margin: 0 0 8px
55 | font-weight: 500
56 |
57 | .timeline-item-content
58 | position: relative
59 | padding: 12px 15px
60 | border-radius: 8px
61 | background: var(--timeline-bg, lighten($theme-color, 85%))
62 | font-size: .93em
63 |
64 | & > :last-child
65 | margin-bottom: 0
66 |
67 | & + .timeline
68 | margin-top: -20px
--------------------------------------------------------------------------------
/source/css/index.styl:
--------------------------------------------------------------------------------
1 | if hexo-config('css_prefix')
2 | @import 'nib'
3 |
4 | @import '_third-party/normalize.min.css'
5 | // project
6 | @import 'var'
7 | @import '_global/*'
8 | @import '_highlight/highlight'
9 | @import '_page/*'
10 | @import '_layout/*'
11 | @import '_tags/*'
12 | @import '_mode/*'
13 |
14 | // search
15 | @import '_search/index'
--------------------------------------------------------------------------------
/source/img/404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/73de62a6e1c5cb7b633d07bdeb9921a8740e2d64/source/img/404.jpg
--------------------------------------------------------------------------------
/source/img/butterfly-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/73de62a6e1c5cb7b633d07bdeb9921a8740e2d64/source/img/butterfly-icon.png
--------------------------------------------------------------------------------
/source/img/error-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/73de62a6e1c5cb7b633d07bdeb9921a8740e2d64/source/img/error-page.png
--------------------------------------------------------------------------------
/source/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/73de62a6e1c5cb7b633d07bdeb9921a8740e2d64/source/img/favicon.ico
--------------------------------------------------------------------------------
/source/img/friend_404.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/73de62a6e1c5cb7b633d07bdeb9921a8740e2d64/source/img/friend_404.gif
--------------------------------------------------------------------------------