Repository: hexojs/hexo Branch: master Commit: bc395f7fa1aa Files: 297 Total size: 912.2 KB Directory structure: gitextract_47fk84wq/ ├── .editorconfig ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature-request-improvement.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── benchmark.yml │ ├── commenter.yml │ ├── dependencies-review.yml │ ├── linter.yml │ └── tester.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .lintstagedrc.json ├── .mocharc.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin/ │ └── hexo ├── eslint.config.js ├── lib/ │ ├── box/ │ │ ├── file.ts │ │ └── index.ts │ ├── extend/ │ │ ├── console.ts │ │ ├── deployer.ts │ │ ├── filter.ts │ │ ├── generator.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── injector.ts │ │ ├── migrator.ts │ │ ├── processor.ts │ │ ├── renderer.ts │ │ ├── syntax_highlight.ts │ │ └── tag.ts │ ├── hexo/ │ │ ├── default_config.ts │ │ ├── index.ts │ │ ├── load_config.ts │ │ ├── load_database.ts │ │ ├── load_plugins.ts │ │ ├── load_theme_config.ts │ │ ├── locals.ts │ │ ├── multi_config_path.ts │ │ ├── post.ts │ │ ├── register_models.ts │ │ ├── render.ts │ │ ├── router.ts │ │ ├── scaffold.ts │ │ ├── source.ts │ │ ├── update_package.ts │ │ └── validate_config.ts │ ├── models/ │ │ ├── asset.ts │ │ ├── binary_relation_index.ts │ │ ├── cache.ts │ │ ├── category.ts │ │ ├── data.ts │ │ ├── index.ts │ │ ├── page.ts │ │ ├── post.ts │ │ ├── post_asset.ts │ │ ├── post_category.ts │ │ ├── post_tag.ts │ │ ├── tag.ts │ │ └── types/ │ │ └── moment.ts │ ├── plugins/ │ │ ├── console/ │ │ │ ├── clean.ts │ │ │ ├── config.ts │ │ │ ├── deploy.ts │ │ │ ├── generate.ts │ │ │ ├── index.ts │ │ │ ├── list/ │ │ │ │ ├── category.ts │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ ├── page.ts │ │ │ │ ├── post.ts │ │ │ │ ├── route.ts │ │ │ │ └── tag.ts │ │ │ ├── migrate.ts │ │ │ ├── new.ts │ │ │ ├── publish.ts │ │ │ └── render.ts │ │ ├── filter/ │ │ │ ├── after_post_render/ │ │ │ │ ├── excerpt.ts │ │ │ │ ├── external_link.ts │ │ │ │ └── index.ts │ │ │ ├── after_render/ │ │ │ │ ├── external_link.ts │ │ │ │ ├── index.ts │ │ │ │ └── meta_generator.ts │ │ │ ├── before_exit/ │ │ │ │ ├── index.ts │ │ │ │ └── save_database.ts │ │ │ ├── before_generate/ │ │ │ │ ├── index.ts │ │ │ │ └── render_post.ts │ │ │ ├── before_post_render/ │ │ │ │ ├── backtick_code_block.ts │ │ │ │ ├── index.ts │ │ │ │ └── titlecase.ts │ │ │ ├── index.ts │ │ │ ├── new_post_path.ts │ │ │ ├── post_permalink.ts │ │ │ └── template_locals/ │ │ │ ├── i18n.ts │ │ │ └── index.ts │ │ ├── generator/ │ │ │ ├── asset.ts │ │ │ ├── index.ts │ │ │ ├── page.ts │ │ │ └── post.ts │ │ ├── helper/ │ │ │ ├── css.ts │ │ │ ├── date.ts │ │ │ ├── debug.ts │ │ │ ├── favicon_tag.ts │ │ │ ├── feed_tag.ts │ │ │ ├── format.ts │ │ │ ├── fragment_cache.ts │ │ │ ├── full_url_for.ts │ │ │ ├── gravatar.ts │ │ │ ├── image_tag.ts │ │ │ ├── index.ts │ │ │ ├── is.ts │ │ │ ├── js.ts │ │ │ ├── link_to.ts │ │ │ ├── list_archives.ts │ │ │ ├── list_categories.ts │ │ │ ├── list_posts.ts │ │ │ ├── list_tags.ts │ │ │ ├── mail_to.ts │ │ │ ├── markdown.ts │ │ │ ├── meta_generator.ts │ │ │ ├── number_format.ts │ │ │ ├── open_graph.ts │ │ │ ├── paginator.ts │ │ │ ├── partial.ts │ │ │ ├── relative_url.ts │ │ │ ├── render.ts │ │ │ ├── search_form.ts │ │ │ ├── tagcloud.ts │ │ │ ├── toc.ts │ │ │ └── url_for.ts │ │ ├── highlight/ │ │ │ ├── highlight.ts │ │ │ ├── index.ts │ │ │ └── prism.ts │ │ ├── injector/ │ │ │ └── index.ts │ │ ├── processor/ │ │ │ ├── asset.ts │ │ │ ├── common.ts │ │ │ ├── data.ts │ │ │ ├── index.ts │ │ │ └── post.ts │ │ ├── renderer/ │ │ │ ├── index.ts │ │ │ ├── json.ts │ │ │ ├── nunjucks.ts │ │ │ ├── plain.ts │ │ │ └── yaml.ts │ │ └── tag/ │ │ ├── asset_img.ts │ │ ├── asset_link.ts │ │ ├── asset_path.ts │ │ ├── blockquote.ts │ │ ├── code.ts │ │ ├── full_url_for.ts │ │ ├── iframe.ts │ │ ├── img.ts │ │ ├── include_code.ts │ │ ├── index.ts │ │ ├── link.ts │ │ ├── post_link.ts │ │ ├── post_path.ts │ │ ├── pullquote.ts │ │ └── url_for.ts │ ├── theme/ │ │ ├── index.ts │ │ ├── processors/ │ │ │ ├── config.ts │ │ │ ├── i18n.ts │ │ │ ├── source.ts │ │ │ └── view.ts │ │ └── view.ts │ └── types.ts ├── package.json ├── test/ │ ├── benchmark.js │ ├── fixtures/ │ │ ├── _config.json │ │ ├── hello.njk │ │ └── post_render.ts │ ├── scripts/ │ │ ├── box/ │ │ │ ├── box.ts │ │ │ └── file.ts │ │ ├── console/ │ │ │ ├── clean.ts │ │ │ ├── config.ts │ │ │ ├── deploy.ts │ │ │ ├── generate.ts │ │ │ ├── list.ts │ │ │ ├── list_categories.ts │ │ │ ├── list_page.ts │ │ │ ├── list_post.ts │ │ │ ├── list_route.ts │ │ │ ├── list_tags.ts │ │ │ ├── migrate.ts │ │ │ ├── new.ts │ │ │ ├── publish.ts │ │ │ └── render.ts │ │ ├── extend/ │ │ │ ├── console.ts │ │ │ ├── deployer.ts │ │ │ ├── filter.ts │ │ │ ├── generator.ts │ │ │ ├── helper.ts │ │ │ ├── injector.ts │ │ │ ├── migrator.ts │ │ │ ├── processor.ts │ │ │ ├── renderer.ts │ │ │ ├── tag.ts │ │ │ └── tag_errors.ts │ │ ├── filters/ │ │ │ ├── backtick_code_block.ts │ │ │ ├── excerpt.ts │ │ │ ├── external_link.ts │ │ │ ├── i18n_locals.ts │ │ │ ├── meta_generator.ts │ │ │ ├── new_post_path.ts │ │ │ ├── post_permalink.ts │ │ │ ├── render_post.ts │ │ │ ├── save_database.ts │ │ │ └── titlecase.ts │ │ ├── generators/ │ │ │ ├── asset.ts │ │ │ ├── page.ts │ │ │ └── post.ts │ │ ├── helpers/ │ │ │ ├── css.ts │ │ │ ├── date.ts │ │ │ ├── debug.ts │ │ │ ├── escape_html.ts │ │ │ ├── favicon_tag.ts │ │ │ ├── feed_tag.ts │ │ │ ├── fragment_cache.ts │ │ │ ├── full_url_for.ts │ │ │ ├── gravatar.ts │ │ │ ├── image_tag.ts │ │ │ ├── is.ts │ │ │ ├── js.ts │ │ │ ├── link_to.ts │ │ │ ├── list_archives.ts │ │ │ ├── list_categories.ts │ │ │ ├── list_posts.ts │ │ │ ├── list_tags.ts │ │ │ ├── mail_to.ts │ │ │ ├── markdown.ts │ │ │ ├── meta_generator.ts │ │ │ ├── number_format.ts │ │ │ ├── open_graph.ts │ │ │ ├── paginator.ts │ │ │ ├── partial.ts │ │ │ ├── relative_url.ts │ │ │ ├── render.ts │ │ │ ├── search_form.ts │ │ │ ├── tagcloud.ts │ │ │ ├── toc.ts │ │ │ └── url_for.ts │ │ ├── hexo/ │ │ │ ├── hexo.ts │ │ │ ├── load_config.ts │ │ │ ├── load_database.ts │ │ │ ├── load_plugins.ts │ │ │ ├── load_theme_config.ts │ │ │ ├── locals.ts │ │ │ ├── multi_config_path.ts │ │ │ ├── post.ts │ │ │ ├── render.ts │ │ │ ├── router.ts │ │ │ ├── scaffold.ts │ │ │ ├── update_package.ts │ │ │ └── validate_config.ts │ │ ├── models/ │ │ │ ├── asset.ts │ │ │ ├── cache.ts │ │ │ ├── category.ts │ │ │ ├── moment.ts │ │ │ ├── page.ts │ │ │ ├── post.ts │ │ │ ├── post_asset.ts │ │ │ └── tag.ts │ │ ├── processors/ │ │ │ ├── asset.ts │ │ │ ├── common.ts │ │ │ ├── data.ts │ │ │ └── post.ts │ │ ├── renderers/ │ │ │ ├── json.ts │ │ │ ├── nunjucks.ts │ │ │ ├── plain.ts │ │ │ └── yaml.ts │ │ ├── tags/ │ │ │ ├── asset_img.ts │ │ │ ├── asset_link.ts │ │ │ ├── asset_path.ts │ │ │ ├── blockquote.ts │ │ │ ├── code.ts │ │ │ ├── full_url_for.ts │ │ │ ├── iframe.ts │ │ │ ├── img.ts │ │ │ ├── include_code.ts │ │ │ ├── link.ts │ │ │ ├── post_link.ts │ │ │ ├── post_path.ts │ │ │ ├── pullquote.ts │ │ │ └── url_for.ts │ │ ├── theme/ │ │ │ ├── theme.ts │ │ │ └── view.ts │ │ └── theme_processors/ │ │ ├── config.ts │ │ ├── i18n.ts │ │ ├── source.ts │ │ └── view.ts │ └── util/ │ ├── index.ts │ └── stream.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true [vcbuild.bat] end_of_line = crlf [*.{md,markdown}] trim_trailing_whitespace = false [{lib,src,test}/**.js] indent_style = space indent_size = 2 [src/**.{h,cc}] indent_style = space indent_size = 2 [test/*.py] indent_style = space indent_size = 2 [configure] indent_style = space indent_size = 2 [Makefile] indent_style = tab indent_size = 8 [{deps,tools}/**] indent_style = ignore indent_size = ignore end_of_line = ignore trim_trailing_whitespace = ignore charset = ignore ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing ## Style Guide We use [ESLint] to maintain the code style. You can install linter plugins on your editor or check the status with the following commands: ``` bash $ npm run eslint # You can append `--fix` option to these commands to fix the code style automatically $ npm run eslint -- --fix ``` ## Pull Requests 1. Fork [hexojs/hexo](https://github.com/hexojs/hexo). 2. Clone the repository to your computer and install dependencies. ``` bash $ git clone https://github.com//hexo.git $ cd hexo $ npm install ``` 3. Create a feature branch. ``` bash $ git checkout -b new_feature ``` 4. Start hacking. 5. Push the branch. ``` bash $ git push origin new_feature ``` 6. Create a pull request and describe the change. ## Testing Before you submitting the pull request. Please make sure your code is coveraged and passes the tests. Otherwise your pull request won't be merged. ``` bash $ npm test ``` ## Updating Documentation The Hexo documentation is open source and you can find the source code on [hexojs/site]. ### Workflow 1. Fork [hexojs/site](https://github.com/hexojs/site). 2. Clone the repository to your computer and install dependencies. ``` bash $ git clone https://github.com//site.git $ cd site $ npm install ``` 3. Start editing the documentation. You can start the server for live previewing. ``` bash $ hexo server ``` 4. Push the branch. 5. Create a pull request and describe the change. ### Translating 1. Add a new language folder in `source` folder. (all in lower case) 2. Copy Markdown and template files in `source` folder to the new language folder. 3. Add the new language to `source/_data/language.yml`. 4. Copy `en.yml` in `themes/navy/languages` and rename to the language name (all in lower case). ## Reporting Issues When you encounter some problems when using Hexo, you can find the solutions in [Troubleshooting](https://hexo.io/docs/troubleshooting.html) or ask me on [GitHub](https://github.com/hexojs/hexo/issues) or [Google Group](https://groups.google.com/group/hexo). If you can't find the answer, please report it on GitHub. 1. Represent the problem in [debug mode](https://hexo.io/docs/commands.html#Debug_mode). 2. Run `hexo version` and check the version info. 3. Post both debug message and version info on GitHub. [ESLint]: https://eslint.org/ ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: hexo ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Something isn't working as expected. # title: "" # labels: [] # assignees: [] body: - type: markdown attributes: value: | ## Tips - 给简体中文用户的提示: - 在提交 issue 时请按照下面的模板提供相关信息,这将有助于我们发现问题。 - 请尽量使用英语描述你遇到的问题,这可以让更多的人帮助到你。 - A good bug report should have your configuration and build environment information, which are essential for us to investigate the problem. We've provided the following steps on how to attach the necessary information. - If you find that markdown files are not rendered as expected, please go to https://marked.js.org/demo/ to see if it can be reproduced there. If it can be reproduced, please file a bug to https://github.com/markedjs/marked. - If you want help on your bug, please also send us the git repository (GitHub, GitLab, Bitbucket, etc.) where your hexo code is stored. It would greatly help. If you prefer not to have your hexo code out in public, please upload to a private GitHub repository and grant read-only access to `hexojs/core`. - Please take extra precaution not to attach any secret or personal information. (likes personal privacy, password, GitHub Personal Access Token, etc.) ------ - type: checkboxes validations: required: true attributes: label: Check List description: Please check followings before submitting a new issue. options: - label: I have already read [Docs page](https://hexo.io/docs/) & [Troubleshooting page](https://hexo.io/docs/troubleshooting). - label: I have already searched existing issues and they are not help to me. - label: I examined error or warning messages and it's difficult to solve. - label: I am using the [latest](https://github.com/hexojs/hexo/releases) version of Hexo. (run `hexo version` to check) - label: My Node.js is matched [the required version](https://hexo.io/docs/#Required-Node-js-version). - type: textarea validations: required: true attributes: label: Expected behavior # description: placeholder: Descripe what you expected to happen. # value: # render: - type: textarea validations: required: true attributes: label: Actual behavior # description: placeholder: Descripe what actually happen. # value: # render: - type: textarea validations: required: true attributes: label: How to reproduce? description: How do you trigger this bug? Please walk us through it step by step. placeholder: | 1. Step1 2. Step2 3. etc. ... # value: # render: - type: input validations: required: true attributes: label: Is the problem still there under `Safe mode`? description: | https://hexo.io/docs/commands#Safe-mode "Safe mode" will disable all the plugins and scripts. If your problem disappear under "Safe mode" means the problem is probably at your newly installed plugins, not at hexo. # placeholder: # value: | # render: - type: markdown attributes: value: | ------ ## Environment & Settings - type: textarea validations: required: false attributes: label: Your Node.js & npm version description: | Please run `node -v && npm -v` and paste the output here. placeholder: node -v && npm -v # value: | render: text - type: textarea validations: required: false attributes: label: Your Hexo and Plugin version description: | Please run `npm ls --depth 0` and paste the output here. placeholder: npm ls --depth 0 # value: render: text - type: textarea validations: required: false attributes: label: Your `package.json` description: Please paste the content of `package.json` here. placeholder: package.json # value: render: json - type: textarea validations: required: false attributes: label: Your site's `_config.yml` (Optional) description: Please paste the content of your `_config.yml` here. placeholder: _config.yml # value: | render: yaml - type: textarea validations: required: false attributes: label: Others description: If you have other information. Please write here. # placeholder: # value: # render: ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a Question, Help, Discuss url: https://github.com/hexojs/hexo/discussions about: I have a question, help for hexo (e.g. Customize) ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request-improvement.yml ================================================ name: Feature request / Improvement description: I have a feature request, suggestion, improvement etc... # title: "" # labels: [] # assignees: [] body: - type: checkboxes validations: required: true attributes: label: Check List description: Please check followings before submitting a new feature request. options: - label: I have already read [Docs page](https://hexo.io/docs/). - label: I have already searched existing issues. - type: textarea validations: required: true attributes: label: Feature Request description: Descripe the feature and why it is needed. # placeholder: # value: # render: - type: textarea validations: required: false attributes: label: Others description: If you have other information. Please write here. # placeholder: # value: # render: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## What does it do? ## Screenshots ## Pull request tasks - [ ] Add test cases for the changes. - [ ] Passed the CI test. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily - package-ecosystem: github-actions directory: "/" schedule: interval: daily ================================================ FILE: .github/workflows/benchmark.yml ================================================ name: Benchmark on: workflow_dispatch: push: branches: - "master" paths: - "lib/**" - ".github/workflows/benchmark.yml" pull_request: branches: - "master" paths: - "lib/**" - ".github/workflows/benchmark.yml" jobs: benchmark: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] node-version: ["20", "22", "24"] fail-fast: false steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install --silent - name: Running benchmark run: node test/benchmark.js --benchmark profiling: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] node-version: ["20", "22", "24"] fail-fast: false env: comment_file: ".tmp-comment-flamegraph-node${{ matrix.node-version }}.md" steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install --silent - name: Running profiling run: node test/benchmark.js --profiling - name: Publish flamegraph to https://${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh/flamegraph.html uses: dswistowski/surge-sh-action@v1 with: domain: ${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh project: ./.tmp-hexo-theme-unit-test/0x/ login: ${{ secrets.SURGE_LOGIN }} token: ${{ secrets.SURGE_TOKEN }} - name: save comment to file if: ${{github.event_name == 'pull_request' }} run: | echo "https://${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh/flamegraph.html" > ${{env.comment_file}} - uses: actions/upload-artifact@v6 if: ${{github.event_name == 'pull_request' }} with: retention-days: 1 name: comment-node${{ matrix.node-version }} path: ${{env.comment_file}} number: runs-on: ubuntu-latest if: ${{github.event_name == 'pull_request' }} env: pr_number_file: .tmp-comment-pr_number steps: - name: save PR number to file run: | echo -n "${{ github.event.number }}" > ${{env.pr_number_file}} - uses: actions/upload-artifact@v6 with: retention-days: 1 name: comment-pr_number path: ${{env.pr_number_file}} ================================================ FILE: .github/workflows/commenter.yml ================================================ name: Commenter on: pull_request_target: workflow_run: workflows: ["Benchmark"] types: - completed permissions: contents: read jobs: comment-test: name: How to test permissions: pull-requests: write # for marocchino/sticky-pull-request-comment to create or update PR comment runs-on: ubuntu-latest if: ${{github.event_name == 'pull_request_target'}} steps: - name: Comment PR - How to test uses: marocchino/sticky-pull-request-comment@v2 with: header: How to test message: | ## How to test ```sh git clone -b ${{ github.head_ref }} https://github.com/${{ github.event.pull_request.head.repo.full_name }}.git cd hexo npm install npm test ``` comment-flamegraph: name: Flamegraph permissions: pull-requests: write # for marocchino/sticky-pull-request-comment to create or update PR comment actions: read # get artifact runs-on: ubuntu-latest if: ${{github.event_name == 'workflow_run' && github.event.workflow_run.conclusion=='success'}} env: comment_result: ".tmp-comment-flamegraph.md" steps: - name: download artifact uses: actions/download-artifact@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} run-id: ${{toJSON(github.event.workflow_run.id)}} pattern: "comment-*" merge-multiple: true - name: get PR number run: | echo "pr_number=$(cat .tmp-comment-pr_number)" >> "$GITHUB_ENV" - name: combime comment if: ${{env.pr_number!=''}} run: | echo "## Flamegraph" > ${{env.comment_result}} echo "" >> ${{env.comment_result}} cat .tmp-comment-flamegraph-*.md >> ${{env.comment_result}} - name: Comment PR - flamegraph if: ${{env.pr_number!=''}} uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} number: ${{env.pr_number}} header: Flamegraph path: ${{env.comment_result}} ================================================ FILE: .github/workflows/dependencies-review.yml ================================================ name: 'Dependencies Review' on: pull_request: paths: - 'package.json' - 'package-lock.json' workflow_dispatch: permissions: contents: read pull-requests: write jobs: dependency-review: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #6.0.1 - name: 'Dependencies Review' uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 #4.8.3 with: vulnerability-check: true fail-on-severity: high comment-summary-in-pr: always ================================================ FILE: .github/workflows/linter.yml ================================================ name: Linter on: push: branches: - "master" paths: - "lib/**" - "test/**" - ".github/workflows/linter.yml" pull_request: paths: - "lib/**" - "test/**" - ".github/workflows/linter.yml" permissions: contents: read jobs: linter: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 22 uses: actions/setup-node@v6 with: node-version: "22" - name: Install Dependencies run: npm install - name: Lint run: | npm run eslint env: CI: true ================================================ FILE: .github/workflows/tester.yml ================================================ name: Tester on: push: branches: - "master" paths: - "lib/**" - "test/**" - "package.json" - "tsconfig.json" - ".github/workflows/tester.yml" pull_request: paths: - "lib/**" - "test/**" - "package.json" - "tsconfig.json" - ".github/workflows/tester.yml" permissions: contents: read jobs: tester: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: ["20", "22", "24"] fail-fast: false steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install Dependencies run: npm install - name: Test run: npm test -- --no-parallel env: CI: true coverage: permissions: checks: write # for coverallsapp/github-action to create new checks contents: read # for actions/checkout to fetch code runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] node-version: ["22"] steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install Dependencies run: npm install - name: Coverage run: npm run test-cov env: CI: true - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ tmp/ *.log .idea/ yarn.lock package-lock.json pnpm-lock.yaml .nyc_output/ coverage/ .tmp* .vscode dist/ ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .lintstagedrc.json ================================================ { "*.js": "eslint --fix", "*.ts": "eslint --fix" } ================================================ FILE: .mocharc.yml ================================================ color: true reporter: spec ui: bdd full-trace: true exit: true ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at report@hexo.io. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the project community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ Copyright (c) 2012-present Tommy Chen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Hexo logo # Hexo > A fast, simple & powerful blog framework, powered by [Node.js](https://nodejs.org). [Website](https://hexo.io) | [Documentation](https://hexo.io/docs/) | [Installation Guide](https://hexo.io/docs/#Installation) | [Contribution Guide](https://hexo.io/docs/contributing) | [Code of Conduct](CODE_OF_CONDUCT.md) | [API](https://hexo.io/api/) | [Twitter](https://twitter.com/hexojs) [![NPM version](https://badge.fury.io/js/hexo.svg)](https://www.npmjs.com/package/hexo) ![Required Node version](https://img.shields.io/node/v/hexo) [![Build Status](https://github.com/hexojs/hexo/workflows/Tester/badge.svg)](https://github.com/hexojs/hexo/actions?query=workflow%3ATester) [![dependencies Status](https://img.shields.io/librariesio/release/npm/hexo)](https://libraries.io/npm/hexo) [![Coverage Status](https://coveralls.io/repos/hexojs/hexo/badge.svg?branch=master)](https://coveralls.io/r/hexojs/hexo?branch=master) [![Gitter](https://badges.gitter.im/hexojs/hexo.svg)](https://gitter.im/hexojs/hexo) [![Discord Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/teM2Anj) [![Telegram Chat](https://img.shields.io/badge/chat-on%20telegram-32afed.svg)](https://t.me/hexojs) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhexojs%2Fhexo.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhexojs%2Fhexo?ref=badge_shield) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) ## Features - Blazing fast generating - Support for GitHub Flavored Markdown and most Octopress plugins - One-command deploy to GitHub Pages, Heroku, etc. - Powerful API for limitless extensibility - Hundreds of [themes](https://hexo.io/themes/) & [plugins](https://hexo.io/plugins/) ## Quick Start **Install Hexo** ``` bash $ npm install hexo-cli -g ``` Install with [brew](https://brew.sh/) on macOS and Linux: ```bash $ brew install hexo ``` **Setup your blog** ``` bash $ hexo init blog $ cd blog ``` **Start the server** ``` bash $ hexo server ``` **Create a new post** ``` bash $ hexo new "Hello Hexo" ``` **Generate static files** ``` bash $ hexo generate ``` ## More Information - Read the [documentation](https://hexo.io/) - Visit the [Awesome Hexo](https://github.com/hexojs/awesome-hexo) list - Find solutions in [troubleshooting](https://hexo.io/docs/troubleshooting.html) - Join discussion on [Google Group](https://groups.google.com/group/hexo), [Discord](https://discord.gg/teM2Anj), [Gitter](https://gitter.im/hexojs/hexo) or [Telegram](https://t.me/hexojs) - See the [plugin list](https://hexo.io/plugins/) and the [theme list](https://hexo.io/themes/) on wiki - Follow [@hexojs](https://twitter.com/hexojs) for latest news ## Contributing We welcome you to join the development of Hexo. Please see [contributing document](https://hexo.io/docs/contributing). 🤗 Also, we welcome PR or issue to [official-plugins](https://github.com/hexojs). ## Contributors [![](https://opencollective.com/Hexo/contributors.svg?width=890)](https://github.com/hexojs/hexo/graphs/contributors) ## Backers [![Backers](https://opencollective.com/hexo/tiers/backers.svg?avatarHeight=36&width=600)](https://opencollective.com/hexo) ## Sponsors [![Sponsors](https://opencollective.com/hexo/tiers/sponsors.svg?width=600)](https://opencollective.com/hexo) ## License [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhexojs%2Fhexo.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhexojs%2Fhexo?ref=badge_large) ================================================ FILE: bin/hexo ================================================ #!/usr/bin/env node 'use strict'; require('hexo-cli')(); ================================================ FILE: eslint.config.js ================================================ const config = require('eslint-config-hexo/ts'); const testConfig = require('eslint-config-hexo/test'); module.exports = [ // Configurations applied globally ...config, { rules: { '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-var-requires': 0, '@typescript-eslint/no-require-imports': 0, 'n/no-missing-require': 0, 'n/no-missing-import': 0, '@typescript-eslint/no-unused-vars': [ 'error', { 'argsIgnorePattern': '^_' } ] } }, // Configurations applied only to test files { files: [ 'test/**/*.ts' ], languageOptions: { ...testConfig.languageOptions }, rules: { ...testConfig.rules, '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/no-unused-expressions': 0, '@typescript-eslint/no-unused-vars': [ 'error', { 'varsIgnorePattern': '^_', 'argsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' } ] } } ]; ================================================ FILE: lib/box/file.ts ================================================ import type Promise from 'bluebird'; import { readFile, readFileSync, stat, statSync, type ReadFileOptions } from 'hexo-fs'; import type fs from 'fs'; class File { /** * Full path of the file */ public source: string; /** * Relative path to the box of the file */ public path: string; /** * The information from path matching. */ public params: any; /** * File type. The value can be create, update, skip, delete. */ // eslint-disable-next-line no-use-before-define public type: typeof File.TYPE_CREATE | typeof File.TYPE_UPDATE | typeof File.TYPE_SKIP | typeof File.TYPE_DELETE; static TYPE_CREATE: 'create'; static TYPE_UPDATE: 'update'; static TYPE_SKIP: 'skip'; static TYPE_DELETE: 'delete'; constructor({ source, path, params, type }: { source: string; path: string; params: any; type: typeof File.TYPE_CREATE | typeof File.TYPE_UPDATE | typeof File.TYPE_SKIP | typeof File.TYPE_DELETE; }) { this.source = source; this.path = path; this.params = params; this.type = type; } read(options?: ReadFileOptions): Promise { return readFile(this.source, options) as Promise; } readSync(options?: ReadFileOptions): string { return readFileSync(this.source, options) as string; } stat(): Promise { return stat(this.source); } statSync(): fs.Stats { return statSync(this.source); } } File.TYPE_CREATE = 'create'; File.TYPE_UPDATE = 'update'; File.TYPE_SKIP = 'skip'; File.TYPE_DELETE = 'delete'; export = File; ================================================ FILE: lib/box/index.ts ================================================ import { join, sep } from 'path'; import BlueBirdPromise from 'bluebird'; import File from './file'; import { Pattern, createSha1Hash } from 'hexo-util'; import { createReadStream, readdir, stat, watch } from 'hexo-fs'; import { magenta } from 'picocolors'; import { EventEmitter } from 'events'; import { isMatch, makeRe } from 'micromatch'; import type Hexo from '../hexo'; import type { NodeJSLikeCallback } from '../types'; import type fs from 'fs'; const defaultPattern = new Pattern(() => ({})); interface Processor { pattern: Pattern; process: (file?: File) => any; } interface BoxOptions { persistent: boolean; awaitWriteFinish: { stabilityThreshold: number }; ignored: RegExp[]; [key: string]: any; } class Box extends EventEmitter { public options: BoxOptions; public context: Hexo; public base: string; public processors: Processor[]; public _processingFiles: Record; public watcher: Awaited> | null; public Cache: any; // TODO: replace runtime class _File public File: any; public ignore: string[]; constructor(ctx: Hexo, base: string, options?: any) { super(); this.options = Object.assign({ persistent: true, awaitWriteFinish: { stabilityThreshold: 200 } }, options); if (!base.endsWith(sep)) { base += sep; } this.context = ctx; this.base = base; this.processors = []; this._processingFiles = {}; this.watcher = null; this.Cache = ctx.model('Cache'); this.File = this._createFileClass(); let targets = this.options.ignored as unknown as string[] || []; if (ctx.config.ignore && ctx.config.ignore.length) { targets = targets.concat(ctx.config.ignore); } this.ignore = targets; this.options.ignored = targets.map(s => toRegExp(ctx, s)).filter(x => x); } _createFileClass() { const ctx = this.context; class _File extends File { public box: Box; render(options?: any) { return ctx.render.render({ path: this.source }, options); } renderSync(options?: any) { return ctx.render.renderSync({ path: this.source }, options); } } _File.prototype.box = this; return _File; } addProcessor(pattern: (...args: any[]) => any): void; addProcessor(pattern: string | RegExp | Pattern | ((str: string) => any), fn: (...args: any[]) => any): void; addProcessor(pattern: string | RegExp | Pattern | ((str: string) => any), fn?: (...args: any[]) => any): void { if (!fn && typeof pattern === 'function') { fn = pattern; pattern = defaultPattern; } if (typeof fn !== 'function') throw new TypeError('fn must be a function'); if (!(pattern instanceof Pattern)) pattern = new Pattern(pattern); this.processors.push({ pattern, process: fn }); } _readDir(base: string, prefix = ''): BlueBirdPromise { const { context: ctx } = this; const results: string[] = []; return readDirWalker(ctx, base, results, this.ignore, prefix) .return(results) .map(path => this._checkFileStatus(path)) .map(file => this._processFile(file.type, file.path).return(file.path)); } _checkFileStatus(path: string): { type: string; path: string } { const { Cache, context: ctx } = this; const src = join(this.base, path); return Cache.compareFile( escapeBackslash(src.substring(ctx.base_dir.length)), () => getHash(src), () => stat(src) ).then(result => ({ type: result.type, path })); } process(callback?: NodeJSLikeCallback): BlueBirdPromise { const { base, Cache, context: ctx } = this; return stat(base).then(stats => { if (!stats.isDirectory()) return; // Check existing files in cache const relativeBase = escapeBackslash(base.substring(ctx.base_dir.length)); const cacheFiles: string[] = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length)); // Handle deleted files return this._readDir(base) .then(files => cacheFiles.filter(path => !files.includes(path))) .map(path => this._processFile(File.TYPE_DELETE, path)); }).catch(err => { if (err && err.code !== 'ENOENT') throw err; }).asCallback(callback); } _processFile(type: string, path: string): BlueBirdPromise { if (this._processingFiles[path]) { return BlueBirdPromise.resolve(); } this._processingFiles[path] = true; const { base, File, context: ctx } = this; this.emit('processBefore', { type, path }); return BlueBirdPromise.reduce(this.processors, (count, processor) => { const params = processor.pattern.match(path); if (!params) return count; const file: File = new File({ // source is used for filesystem path, keep backslashes on Windows source: join(base, path), // path is used for URL path, replace backslashes on Windows path: escapeBackslash(path), params, type }); return Reflect.apply(BlueBirdPromise.method(processor.process), ctx, [file]) .thenReturn(count + 1); }, 0).then(count => { if (count) { ctx.log.debug('Processed: %s', magenta(path)); } this.emit('processAfter', { type, path }); }).catch(err => { ctx.log.error({ err }, 'Process failed: %s', magenta(path)); }).finally(() => { this._processingFiles[path] = false; }).thenReturn(path); } watch(callback?: NodeJSLikeCallback): BlueBirdPromise { if (this.isWatching()) { return BlueBirdPromise.reject(new Error('Watcher has already started.')).asCallback(callback); } const { base } = this; function getPath(path) { return escapeBackslash(path.substring(base.length)); } return this.process().then(() => watch(base, this.options)).then(watcher => { this.watcher = watcher; watcher.on('add', path => { this._processFile(File.TYPE_CREATE, getPath(path)); }); watcher.on('change', path => { this._processFile(File.TYPE_UPDATE, getPath(path)); }); watcher.on('unlink', path => { this._processFile(File.TYPE_DELETE, getPath(path)); }); watcher.on('addDir', path => { let prefix = getPath(path); if (prefix) prefix += '/'; this._readDir(path, prefix); }); }).asCallback(callback); } unwatch(): void { if (!this.isWatching()) return; this.watcher.close(); this.watcher = null; } isWatching(): boolean { return Boolean(this.watcher); } } function escapeBackslash(path: string): string { // Replace backslashes on Windows return path.replace(/\\/g, '/'); } function getHash(path: string): BlueBirdPromise { const src = createReadStream(path); const hasher = createSha1Hash(); const finishedPromise = new BlueBirdPromise((resolve, reject) => { src.once('error', reject); src.once('end', resolve); }); src.on('data', chunk => { hasher.update(chunk); }); return finishedPromise.then(() => hasher.digest('hex')); } function toRegExp(ctx: Hexo, arg: string): RegExp | null { if (!arg) return null; if (typeof arg !== 'string') { ctx.log.warn('A value of "ignore:" section in "_config.yml" is not invalid (not a string)'); return null; } const result = makeRe(arg); if (!result) { ctx.log.warn('A value of "ignore:" section in "_config.yml" can not be converted to RegExp:' + arg); return null; } return result; } function isIgnoreMatch(path: string, ignore: string | string[]): boolean { return path && ignore && ignore.length && isMatch(path, ignore); } function readDirWalker(ctx: Hexo, base: string, results: string[], ignore: string | string[], prefix: string): BlueBirdPromise { if (isIgnoreMatch(base, ignore)) return BlueBirdPromise.resolve(); return BlueBirdPromise.map(readdir(base).catch(err => { ctx.log.error({ err }, 'Failed to read directory: %s', base); if (err && err.code === 'ENOENT') return []; throw err; }), async (path: string) => { const fullPath = join(base, path); const stats: fs.Stats | null = await stat(fullPath).catch(err => { ctx.log.error({ err }, 'Failed to stat file: %s', fullPath); if (err && err.code === 'ENOENT') return null; throw err; }); const prefixPath = `${prefix}${path}`; if (stats) { if (stats.isDirectory()) { return readDirWalker(ctx, fullPath, results, ignore, `${prefixPath}/`); } if (!isIgnoreMatch(fullPath, ignore)) { results.push(prefixPath); } } }); } export interface _File extends File { box: Box; render(options?: any): any; renderSync(options?: any): any; } export default Box; ================================================ FILE: lib/extend/console.ts ================================================ import Promise from 'bluebird'; import abbrev from 'abbrev'; import type { NodeJSLikeCallback } from '../types'; import type Hexo from '../hexo'; type Option = Partial<{ usage: string; desc: string; init: boolean; arguments: { name: string; desc: string; }[]; options: { name: string; desc: string; }[]; }> interface Args { _: string[]; [key: string]: string | boolean | string[]; } type AnyFn = (this: Hexo, args: Args, callback?: NodeJSLikeCallback) => any; interface StoreFunction { (this: Hexo, args: Args): Promise; desc?: string; options?: Option; } interface Store { [key: string]: StoreFunction } interface Alias { [abbreviation: string]: string } /** * The console forms the bridge between Hexo and its users. It registers and describes the available console commands. */ class Console { public store: Store; public alias: Alias; constructor() { this.store = {}; this.alias = {}; } /** * Get a console plugin function by name * @param {String} name - The name of the console plugin * @returns {StoreFunction} - The console plugin function */ get(name: string): StoreFunction { name = name.toLowerCase(); return this.store[this.alias[name]]; } list(): Store { return this.store; } /** * Register a console plugin * @param {String} name - The name of console plugin to be registered * @param {String} desc - More detailed information about a console command * @param {Option} options - The description of each option of a console command * @param {AnyFn} fn - The console plugin to be registered */ register(name: string, fn: AnyFn): void register(name: string, desc: string, fn: AnyFn): void register(name: string, options: Option, fn: AnyFn): void register(name: string, desc: string, options: Option, fn: AnyFn): void register(name: string, desc: string | Option | AnyFn, options?: Option | AnyFn, fn?: AnyFn): void { if (!name) throw new TypeError('name is required'); if (!fn) { if (options) { if (typeof options === 'function') { fn = options; if (typeof desc === 'object') { // name, options, fn options = desc; desc = ''; } else { // name, desc, fn options = {}; } } else { throw new TypeError('fn must be a function'); } } else { // name, fn if (typeof desc === 'function') { fn = desc; options = {}; desc = ''; } else { throw new TypeError('fn must be a function'); } } } if (fn.length > 1) { fn = Promise.promisify(fn); } else { fn = Promise.method(fn); } const c = fn as StoreFunction; this.store[name.toLowerCase()] = c; c.options = options as Option; c.desc = desc as string; this.alias = abbrev(Object.keys(this.store)); } } export = Console; ================================================ FILE: lib/extend/deployer.ts ================================================ import Promise from 'bluebird'; import type { NodeJSLikeCallback } from '../types'; import type Hexo from '../hexo'; interface StoreFunction { (this: Hexo, deployArg: { type: string; [key: string]: any }): Promise; } interface Store { [key: string]: StoreFunction; } /** * A deployer helps users quickly deploy their site to a remote server without complicated commands. */ class Deployer { public store: Store; constructor() { this.store = {}; } list(): Store { return this.store; } get(name: string): StoreFunction { return this.store[name]; } register( name: string, fn: ( this: Hexo, deployArg: { type: string; [key: string]: any; }, callback?: NodeJSLikeCallback ) => any ): void { if (!name) throw new TypeError('name is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); if (fn.length > 1) { fn = Promise.promisify(fn); } else { fn = Promise.method(fn); } this.store[name] = fn; } } export = Deployer; ================================================ FILE: lib/extend/filter.ts ================================================ import Promise from 'bluebird'; import { FilterOptions } from '../types'; const typeAlias = { pre: 'before_post_render', post: 'after_post_render', 'after_render:html': '_after_html_render' }; interface StoreFunction { (data?: any, ...args: any[]): any; priority?: number; } interface Store { [key: string]: StoreFunction[] } /** * A filter is used to modify some specified data. Hexo passes data to filters in sequence and the filters then modify the data one after the other. * This concept was borrowed from WordPress. */ class Filter { public store: Store; constructor() { this.store = {}; } list(): Store; list(type: string): StoreFunction[]; list(type?: string) { if (!type) return this.store; return this.store[type] || []; } register(fn: StoreFunction): void register(fn: StoreFunction, priority: number): void register(type: string, fn: StoreFunction): void register(type: string, fn: StoreFunction, priority: number): void register(type: string | StoreFunction, fn?: StoreFunction | number, priority?: number): void { if (!priority) { if (typeof type === 'function') { priority = fn as number; fn = type; type = 'after_post_render'; } } if (typeof fn !== 'function') throw new TypeError('fn must be a function'); type = typeAlias[type as string] || type; priority = priority == null ? 10 : priority; const store = this.store[type as string] || []; this.store[type as string] = store; fn.priority = priority; store.push(fn); store.sort((a, b) => a.priority - b.priority); } unregister(type: string, fn: StoreFunction): void { if (!type) throw new TypeError('type is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); type = typeAlias[type] || type; const list = this.list(type); if (!list || !list.length) return; const index = list.indexOf(fn); if (index !== -1) list.splice(index, 1); } exec(type: string, data: any, options: FilterOptions = {}): Promise { const filters = this.list(type); if (filters.length === 0) return Promise.resolve(data); const ctx = options.context; const args = options.args || []; args.unshift(data); return Promise.each(filters, filter => Reflect.apply(Promise.method(filter), ctx, args).then(result => { args[0] = result == null ? args[0] : result; return args[0]; })).then(() => args[0]); } execSync(type: string, data: any, options: FilterOptions = {}) { const filters = this.list(type); const filtersLen = filters.length; if (filtersLen === 0) return data; const ctx = options.context; const args = options.args || []; args.unshift(data); for (let i = 0, len = filtersLen; i < len; i++) { const result = Reflect.apply(filters[i], ctx, args); args[0] = result == null ? args[0] : result; } return args[0]; } } export = Filter; ================================================ FILE: lib/extend/generator.ts ================================================ import Promise from 'bluebird'; import type { BaseGeneratorReturn, NodeJSLikeCallback, SiteLocals } from '../types'; type ReturnType = BaseGeneratorReturn | BaseGeneratorReturn[]; type GeneratorReturnType = ReturnType | Promise; interface GeneratorFunction { (locals: SiteLocals, callback?: NodeJSLikeCallback): GeneratorReturnType; } type StoreFunctionReturn = Promise; interface StoreFunction { (locals: SiteLocals): StoreFunctionReturn; } interface Store { [key: string]: StoreFunction } /** * A generator builds routes based on processed files. */ class Generator { public id: number; public store: Store; constructor() { this.id = 0; this.store = {}; } list(): Store { return this.store; } get(name: string): StoreFunction { return this.store[name]; } register(fn: GeneratorFunction): void register(name: string, fn: GeneratorFunction): void register(name: string | GeneratorFunction, fn?: GeneratorFunction): void { if (!fn) { if (typeof name === 'function') { // fn fn = name; name = `generator-${this.id++}`; } else { throw new TypeError('fn must be a function'); } } if (fn.length > 1) fn = Promise.promisify(fn); this.store[name as string] = Promise.method(fn); } } export = Generator; ================================================ FILE: lib/extend/helper.ts ================================================ import Hexo from '../hexo'; import { PageSchema } from '../types'; import * as hutil from 'hexo-util'; interface HexoContext extends Hexo { // get current page information // https://github.com/dimaslanjaka/hexo-renderers/blob/147340f6d03a8d3103e9589ddf86778ed7f9019b/src/helper/related-posts.ts#L106-L113 page?: PageSchema; // hexo-util shims url_for: typeof hutil.url_for; full_url_for: typeof hutil.full_url_for; relative_url: typeof hutil.relative_url; slugize: typeof hutil.slugize; escapeDiacritic: typeof hutil.escapeDiacritic; escapeHTML: typeof hutil.escapeHTML; unescapeHTML: typeof hutil.unescapeHTML; encodeURL: typeof hutil.encodeURL; decodeURL: typeof hutil.decodeURL; escapeRegExp: typeof hutil.escapeRegExp; stripHTML: typeof hutil.stripHTML; stripIndent: typeof hutil.stripIndent; hash: typeof hutil.hash; createSha1Hash: typeof hutil.createSha1Hash; highlight: typeof hutil.highlight; prismHighlight: typeof hutil.prismHighlight; tocObj: typeof hutil.tocObj; wordWrap: typeof hutil.wordWrap; prettyUrls: typeof hutil.prettyUrls; isExternalLink: typeof hutil.isExternalLink; gravatar: typeof hutil.gravatar; htmlTag: typeof hutil.htmlTag; truncate: typeof hutil.truncate; spawn: typeof hutil.spawn; camelCaseKeys: typeof hutil.camelCaseKeys; deepMerge: typeof hutil.deepMerge; } interface StoreFunction { (this: HexoContext, ...args: any[]): any; } interface Store { [key: string]: StoreFunction; } /** * A helper makes it easy to quickly add snippets to your templates. We recommend using helpers instead of templates when you’re dealing with more complicated code. */ class Helper { public store: Store; constructor() { this.store = {}; } /** * @returns {Store} - The plugin store */ list(): Store { return this.store; } /** * Get helper plugin function by name * @param {String} name - The name of the helper plugin * @returns {StoreFunction} */ get(name: string): StoreFunction { return this.store[name]; } /** * Register a helper plugin * @param {String} name - The name of the helper plugin * @param {StoreFunction} fn - The helper plugin function */ register(name: string, fn: StoreFunction): void { if (!name) throw new TypeError('name is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); this.store[name] = fn; } } export = Helper; ================================================ FILE: lib/extend/index.ts ================================================ export { default as Console } from './console'; export { default as Deployer } from './deployer'; export { default as Filter } from './filter'; export { default as Generator } from './generator'; export { default as Helper } from './helper'; export { default as Highlight } from './syntax_highlight'; export { default as Injector } from './injector'; export { default as Migrator } from './migrator'; export { default as Processor } from './processor'; export { default as Renderer } from './renderer'; export { default as Tag } from './tag'; ================================================ FILE: lib/extend/injector.ts ================================================ import { Cache } from 'hexo-util'; type Entry = 'head_begin' | 'head_end' | 'body_begin' | 'body_end'; type Store = { [key in Entry]: { [key: string]: Set; }; }; /** * An injector is used to add static code snippet to the `` or/and `` of generated HTML files. * Hexo run injector before `after_render:html` filter is executed. */ class Injector { public store: Store; public cache: InstanceType; public page: any; constructor() { this.store = { head_begin: {}, head_end: {}, body_begin: {}, body_end: {} }; this.cache = new Cache(); } list(): Store { return this.store; } get(entry: Entry, to = 'default'): any[] { return Array.from(this.store[entry][to] || []); } getText(entry: Entry, to = 'default'): string { const arr = this.get(entry, to); if (!arr || !arr.length) return ''; return arr.join(''); } getSize(entry: Entry): number { return this.cache.apply(`${entry}-size`, () => Object.keys(this.store[entry]).length) as number; } register(entry: Entry, value: string | (() => string), to = 'default'): void { if (!entry) throw new TypeError('entry is required'); if (typeof value === 'function') value = value(); const entryMap = this.store[entry] || this.store.head_end; const valueSet = entryMap[to] || new Set(); valueSet.add(value); entryMap[to] = valueSet; } _getPageType(pageLocals): string { let currentType = 'default'; if (pageLocals.__index) currentType = 'home'; if (pageLocals.__post) currentType = 'post'; if (pageLocals.__page) currentType = 'page'; if (pageLocals.archive) currentType = 'archive'; if (pageLocals.category) currentType = 'category'; if (pageLocals.tag) currentType = 'tag'; if (pageLocals.layout) currentType = pageLocals.layout; return currentType; } _injector(input: string, pattern: string | RegExp, flag: Entry, isBegin = true, currentType: string): string { if (input.includes(`' + content + ''; }) as string; // avoid unnecessary replace() for better performance if (!code.length) return input; return input.replace(pattern, str => { return isBegin ? str + code : code + str; }); } exec(data: string, locals = { page: {} }): string { const { page } = locals; const currentType = this._getPageType(page); if (this.getSize('head_begin') !== 0) { // Inject head_begin data = this._injector(data, //, 'head_begin', true, currentType); } if (this.getSize('head_end') !== 0) { // Inject head_end data = this._injector(data, '', 'head_end', false, currentType); } if (this.getSize('body_begin') !== 0) { // Inject body_begin data = this._injector(data, //, 'body_begin', true, currentType); } if (this.getSize('body_end') !== 0) { // Inject body_end data = this._injector(data, '', 'body_end', false, currentType); } return data; } } export = Injector; ================================================ FILE: lib/extend/migrator.ts ================================================ import Promise from 'bluebird'; import type { NodeJSLikeCallback } from '../types'; import type Hexo from '../hexo'; interface StoreFunction { (this: Hexo, args: any): Promise; } interface Store { [key: string]: StoreFunction } /** * A migrator helps users migrate from other systems to Hexo. */ class Migrator { public store: Store; constructor() { this.store = {}; } list(): Store { return this.store; } get(name: string): StoreFunction { return this.store[name]; } register(name: string, fn: (this: Hexo, args: any, callback?: NodeJSLikeCallback) => any): void { if (!name) throw new TypeError('name is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); if (fn.length > 1) { fn = Promise.promisify(fn); } else { fn = Promise.method(fn); } this.store[name] = fn; } } export = Migrator; ================================================ FILE: lib/extend/processor.ts ================================================ import Promise from 'bluebird'; import { Pattern } from 'hexo-util'; import type File from '../box/file'; interface StoreFunction { (file: File | string): any; } type Store = { pattern: Pattern; process: StoreFunction; }[]; type patternType = Exclude[0], (str: string) => string>; /** * A processor is used to process source files in the `source` folder. */ class Processor { public store: Store; constructor() { this.store = []; } list(): Store { return this.store; } register(fn: StoreFunction): void; register(pattern: patternType, fn: StoreFunction): void; register(pattern: patternType | StoreFunction, fn?: StoreFunction): void { if (!fn) { if (typeof pattern === 'function') { fn = pattern; pattern = /(.*)/; } else { throw new TypeError('fn must be a function'); } } if (fn.length > 1) { fn = Promise.promisify(fn); } else { fn = Promise.method(fn); } this.store.push({ pattern: new Pattern(pattern as patternType), process: fn }); } } export = Processor; ================================================ FILE: lib/extend/renderer.ts ================================================ import { extname } from 'path'; import Promise from 'bluebird'; import type { NodeJSLikeCallback } from '../types'; const getExtname = (str: string) => { if (typeof str !== 'string') return ''; const ext = extname(str) || str; return ext.startsWith('.') ? ext.slice(1) : ext; }; export interface StoreFunctionData { path?: any; text?: string; engine?: string; toString?: any; onRenderEnd?: (data: string) => any; } export interface StoreSyncFunction { ( data: StoreFunctionData, options?: object ): any; output?: string; compile?: (data: StoreFunctionData) => (local: any) => any; disableNunjucks?: boolean; [key: string]: any; } export interface StoreFunction { ( data: StoreFunctionData, options?: object ): Promise; output?: string; compile?: (data: StoreFunctionData) => (local: any) => any; disableNunjucks?: boolean; [key: string]: any; } interface StoreFunctionWithCallback { ( data: StoreFunctionData, options: object, callback?: NodeJSLikeCallback ): Promise; output?: string; compile?: (data: StoreFunctionData) => (local: any) => any; disableNunjucks?: boolean; [key: string]: any; } interface SyncStore { [key: string]: StoreSyncFunction; } interface Store { [key: string]: StoreFunction; } /** * A renderer is used to render content. */ class Renderer { public store: Store; public storeSync: SyncStore; constructor() { this.store = {}; this.storeSync = {}; } list(sync = false): Store | SyncStore { return sync ? this.storeSync : this.store; } get(name: string, sync?: boolean): StoreSyncFunction | StoreFunction { const store = this[sync ? 'storeSync' : 'store']; return store[getExtname(name)] || store[name]; } isRenderable(path: string): boolean { return Boolean(this.get(path)); } isRenderableSync(path: string): boolean { return Boolean(this.get(path, true)); } getOutput(path: string): string { const renderer = this.get(path); return renderer ? renderer.output : ''; } register(name: string, output: string, fn: StoreFunctionWithCallback): void; register(name: string, output: string, fn: StoreFunctionWithCallback, sync: false): void; register(name: string, output: string, fn: StoreSyncFunction, sync: true): void; register(name: string, output: string, fn: StoreFunctionWithCallback | StoreSyncFunction, sync: boolean): void; register(name: string, output: string, fn: StoreFunctionWithCallback | StoreSyncFunction, sync?: boolean) { if (!name) throw new TypeError('name is required'); if (!output) throw new TypeError('output is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); name = getExtname(name); output = getExtname(output); if (sync) { this.storeSync[name] = fn; this.storeSync[name].output = output; this.store[name] = Promise.method(fn); this.store[name].disableNunjucks = (fn as StoreFunction).disableNunjucks; } else { if (fn.length > 2) fn = Promise.promisify(fn); this.store[name] = fn; } this.store[name].output = output; this.store[name].compile = fn.compile; } } export default Renderer; ================================================ FILE: lib/extend/syntax_highlight.ts ================================================ import type Hexo from '../hexo'; export interface HighlightOptions { lang: string | undefined, caption: string | undefined, lines_length?: number | undefined, // plugins/filter/before_post_render/backtick_code_block firstLineNumber?: string | number // plugins/tag/code.ts language_attr?: boolean | undefined; firstLine?: string | number; line_number?: boolean | undefined; line_threshold?: number | undefined; mark?: number[] | string; wrap?: boolean | undefined; } interface HighlightExecArgs { context?: Hexo; args?: [string, HighlightOptions]; } interface StoreFunction { (content: string, options: HighlightOptions): string; priority?: number; } interface Store { [key: string]: StoreFunction } class SyntaxHighlight { public store: Store; constructor() { this.store = {}; } register(name: string, fn: StoreFunction): void { if (typeof fn !== 'function') throw new TypeError('fn must be a function'); this.store[name] = fn; } query(name: string): StoreFunction { return name && this.store[name]; } exec(name: string, options: HighlightExecArgs): string { const fn = this.store[name]; if (!fn) throw new TypeError(`syntax highlighter ${name} is not registered`); const ctx = options.context; const args = options.args || []; return Reflect.apply(fn, ctx, args); } } export default SyntaxHighlight; ================================================ FILE: lib/extend/tag.ts ================================================ import { stripIndent } from 'hexo-util'; import { cyan, magenta, red, bold } from 'picocolors'; import { Environment } from 'nunjucks'; import Promise from 'bluebird'; import type { NodeJSLikeCallback } from '../types'; const rSwigRawFullBlock = /{% *raw *%}/; const rCodeTag = /]*>[\s\S]+?<\/code>/g; const escapeSwigTag = (str: string) => str.replace(/{/g, '{').replace(/}/g, '}'); interface TagFunction { (args: any[], content: string, callback?: NodeJSLikeCallback): string | PromiseLike; } interface AsyncTagFunction { (args: any[], content: string): Promise; } class NunjucksTag { public tags: string[]; public fn: TagFunction | AsyncTagFunction; constructor(name: string, fn: TagFunction | AsyncTagFunction) { this.tags = [name]; this.fn = fn; } parse(parser, nodes, lexer) { const node = this._parseArgs(parser, nodes, lexer); return new nodes.CallExtension(this, 'run', node, []); } _parseArgs(parser, nodes, lexer) { const tag = parser.nextToken(); const node = new nodes.NodeList(tag.lineno, tag.colno); const argarray = new nodes.Array(tag.lineno, tag.colno); let token; let argitem = ''; while ((token = parser.nextToken(true))) { if (token.type === lexer.TOKEN_WHITESPACE || token.type === lexer.TOKEN_BLOCK_END) { if (argitem !== '') { const argnode = new nodes.Literal(tag.lineno, tag.colno, argitem.trim()); argarray.addChild(argnode); argitem = ''; } if (token.type === lexer.TOKEN_BLOCK_END) { break; } } else { argitem += token.value; } } node.addChild(argarray); return node; } run(context, args, _body, _callback) { return this._run(context, args, ''); } _run(context, args, body): any { return Reflect.apply(this.fn, context.ctx, [args, body]); } } const trimBody = (body: () => any) => { return stripIndent(body()).replace(/^\n?|\n?$/g, ''); }; class NunjucksBlock extends NunjucksTag { parse(parser, nodes, lexer) { const node = this._parseArgs(parser, nodes, lexer); const body = this._parseBody(parser, nodes, lexer); return new nodes.CallExtension(this, 'run', node, [body]); } _parseBody(parser, _nodes, _lexer) { const body = parser.parseUntilBlocks(`end${this.tags[0]}`); parser.advanceAfterBlockEnd(); return body; } run(context, args, body, _callback) { return this._run(context, args, trimBody(body)); } } class NunjucksAsyncTag extends NunjucksTag { parse(parser, nodes, lexer) { const node = this._parseArgs(parser, nodes, lexer); return new nodes.CallExtensionAsync(this, 'run', node, []); } run(context, args, callback) { return this._run(context, args, '').then(result => { callback(null, result); }, callback); } } class NunjucksAsyncBlock extends NunjucksBlock { parse(parser, nodes, lexer) { const node = this._parseArgs(parser, nodes, lexer); const body = this._parseBody(parser, nodes, lexer); return new nodes.CallExtensionAsync(this, 'run', node, [body]); } run(context, args, body, callback) { // enable async tag nesting body((err, result) => { // wrapper for trimBody expecting // body to be a function body = () => result || ''; this._run(context, args, trimBody(body)).then(result => { callback(err, result); }); }); } } const getContextLineNums = (min: number, max: number, center: number, amplitude: number) => { const result = []; let lbound = Math.max(min, center - amplitude); const hbound = Math.min(max, center + amplitude); while (lbound <= hbound) result.push(lbound++); return result; }; const LINES_OF_CONTEXT = 5; const getContext = (lines: string[], errLine: number, location: string, type: string) => { const message = [ location + ' ' + red(type), cyan(' ===== Context Dump ====='), cyan(' === (line number probably different from source) ===') ]; message.push( // get LINES_OF_CONTEXT lines surrounding `errLine` ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT) .map(lnNum => { const line = ' ' + lnNum + ' | ' + lines[lnNum - 1]; if (lnNum === errLine) { return cyan(bold(line)); } return cyan(line); }) ); message.push(cyan( ' ===== Context Dump Ends =====')); return message; }; class NunjucksError extends Error { line?: number; location?: string; type?: string; } /** * Provide context for Nunjucks error * @param {Error} err Nunjucks error * @param {string} str string input for Nunjucks * @return {Error} New error object with embedded context */ const formatNunjucksError = (err: Error, input: string, source = ''): Error => { err.message = err.message.replace('(unknown path)', source ? magenta(source) : ''); const match = err.message.match(/Line (\d+), Column \d+/); if (!match) return err; const errLine = parseInt(match[1], 10); if (isNaN(errLine)) return err; // trim useless info from Nunjucks Error const splitted = err.message.split('\n'); const e = new NunjucksError(); e.name = 'Nunjucks Error'; e.line = errLine; e.location = splitted[0]; e.type = splitted[1].trim(); e.message = getContext(input.split(/\r?\n/), errLine, e.location, e.type).join('\n'); return e; }; type RegisterOptions = { async?: boolean; ends?: boolean; } /** * A tag allows users to quickly and easily insert snippets into their posts. */ class Tag { public env: Environment; public source: string; constructor() { this.env = new Environment(null, { autoescape: false }); } register(name: string, fn: TagFunction): void register(name: string, fn: TagFunction, ends: boolean): void register(name: string, fn: TagFunction, options: RegisterOptions): void register(name: string, fn: TagFunction, options?: RegisterOptions | boolean):void { if (!name) throw new TypeError('name is required'); if (typeof fn !== 'function') throw new TypeError('fn must be a function'); if (options == null || typeof options === 'boolean') { options = { ends: options as boolean }; } let tag: NunjucksTag; if (options.async) { let asyncFn: AsyncTagFunction; if (fn.length > 2) { asyncFn = Promise.promisify(fn); } else { asyncFn = Promise.method(fn); } if (options.ends) { tag = new NunjucksAsyncBlock(name, asyncFn); } else { tag = new NunjucksAsyncTag(name, asyncFn); } } else if (options.ends) { tag = new NunjucksBlock(name, fn); } else { tag = new NunjucksTag(name, fn); } this.env.addExtension(name, tag); } unregister(name: string): void { if (!name) throw new TypeError('name is required'); const { env } = this; if (env.hasExtension(name)) env.removeExtension(name); } render(str: string): Promise; render(str: string, callback: NodeJSLikeCallback): Promise; render(str: string, options: { source?: string, [key: string]: any }, callback?: NodeJSLikeCallback): Promise; render(str: string, options: { source?: string, [key: string]: any } | NodeJSLikeCallback = {}, callback?: NodeJSLikeCallback): Promise { if (!callback && typeof options === 'function') { callback = options; options = {}; } // Get path of post from source const { source = '' } = options as { source?: string }; return Promise.fromCallback(cb => { this.env.renderString( str.replace(rCodeTag, s => { // https://hexo.io/docs/tag-plugins#Raw // https://mozilla.github.io/nunjucks/templating.html#raw // Only escape code block when there is no raw tag included return s.match(rSwigRawFullBlock) ? s : escapeSwigTag(s); }), options, cb ); }).catch(err => { return Promise.reject(formatNunjucksError(err, str, source)); }) .asCallback(callback); } } export = Tag; ================================================ FILE: lib/hexo/default_config.ts ================================================ export = { // Site title: 'Hexo', subtitle: '', description: '', author: 'John Doe', language: 'en', timezone: '', // URL url: 'http://example.com', root: '/', permalink: ':year/:month/:day/:title/', permalink_defaults: {} as Record, pretty_urls: { trailing_index: true, trailing_html: true }, // Directory source_dir: 'source', public_dir: 'public', tag_dir: 'tags', archive_dir: 'archives', category_dir: 'categories', code_dir: 'downloads/code', i18n_dir: ':lang', skip_render: [] as string[], // Writing new_post_name: ':title.md', default_layout: 'post', titlecase: false, external_link: { enable: true, field: 'site', exclude: '' }, filename_case: 0, render_drafts: false, post_asset_folder: false, relative_link: false, future: true, syntax_highlighter: 'highlight.js', highlight: { auto_detect: false, line_number: true, tab_replace: '', wrap: true, exclude_languages: [] as string[], language_attr: false, hljs: false, line_threshold: 0, first_line_number: 'always1', strip_indent: true }, prismjs: { preprocess: true, line_number: true, tab_replace: '', exclude_languages: [] as string[], strip_indent: true }, use_filename_as_post_title: false, // Category & Tag default_category: 'uncategorized', category_map: {} as Record, tag_map: {} as Record, // Date / Time format date_format: 'YYYY-MM-DD', time_format: 'HH:mm:ss', updated_option: 'mtime', // * mtime: file modification date (default) // * empty: no more update // Pagination per_page: 10, pagination_dir: 'page', // Extensions theme: 'landscape', server: { cache: false }, // Deployment deploy: {} as { type: string; [keys: string]: any } | { type: string; [keys: string]: any }[], // ignore files from processing ignore: [] as string[], // Category & Tag meta_generator: true }; ================================================ FILE: lib/hexo/index.ts ================================================ import Promise from 'bluebird'; import { sep, join, dirname } from 'path'; import tildify from 'tildify'; import Database from 'warehouse'; import { magenta, underline } from 'picocolors'; import { EventEmitter } from 'events'; import { readFile } from 'hexo-fs'; import Module from 'module'; import { runInThisContext } from 'vm'; const { version } = require('../../package.json'); import logger from 'hexo-log'; import { Console, Deployer, Filter, Generator, Helper, Highlight, Injector, Migrator, Processor, Renderer, Tag } from '../extend'; import Render from './render'; import registerModels from './register_models'; import Post from './post'; import Scaffold from './scaffold'; import Source from './source'; import Router from './router'; import Theme from '../theme'; import Locals from './locals'; import defaultConfig from './default_config'; import loadDatabase from './load_database'; import multiConfigPath from './multi_config_path'; import { deepMerge, full_url_for } from 'hexo-util'; import type Box from '../box'; import type { BaseGeneratorReturn, FilterOptions, LocalsType, NodeJSLikeCallback, SiteLocals } from '../types'; import type { AddSchemaTypeOptions } from 'warehouse/dist/types'; import type Schema from 'warehouse/dist/schema'; import BinaryRelationIndex from '../models/binary_relation_index'; const libDir = dirname(__dirname); const dbVersion = 1; const stopWatcher = (box: Box) => { if (box.isWatching()) box.unwatch(); }; const routeCache = new WeakMap(); const castArray = (obj: any) => { return Array.isArray(obj) ? obj : [obj]; }; // eslint-disable-next-line no-use-before-define const mergeCtxThemeConfig = (ctx: Hexo) => { // Merge hexo.config.theme_config into hexo.theme.config before post rendering & generating // config.theme_config has "_config.[theme].yml" merged in load_theme_config.js if (ctx.config.theme_config) { ctx.theme.config = deepMerge(ctx.theme.config, ctx.config.theme_config); } }; // eslint-disable-next-line no-use-before-define const createLoadThemeRoute = function(generatorResult: BaseGeneratorReturn, locals: LocalsType, ctx: Hexo) { const { log, theme } = ctx; const { path, cache: useCache } = locals; const layout = [...new Set(castArray(generatorResult.layout))]; const layoutLength = layout.length; // always use cache in fragment_cache locals.cache = true; return () => { if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult); for (let i = 0; i < layoutLength; i++) { const name = layout[i]; const view = theme.getView(name); if (view) { log.debug(`Rendering HTML ${name}: ${magenta(path)}`); return view.render(locals) .then(result => ctx.extend.injector.exec(result, locals)) .then(result => ctx.execFilter('_after_html_render', result, { context: ctx, args: [locals] })) .tap(result => { if (useCache) { routeCache.set(generatorResult, result); } }).tapCatch(err => { log.error({ err }, `Render HTML failed: ${magenta(path)}`); }); } } log.warn(`No layout: ${magenta(path)}`); }; }; function debounce(func: () => void, wait: number): () => void { let timeout: NodeJS.Timeout; return function() { clearTimeout(timeout); timeout = setTimeout(() => { func.apply(this); }, wait); }; } interface Args { /** * Enable debug mode. Display debug messages in the terminal and save debug.log in the root directory. */ debug?: boolean; /** * Enable safe mode. Don’t load any plugins. */ safe?: boolean; /** * Enable silent mode. Don’t display any messages in the terminal. */ silent?: boolean; /** * Enable to add drafts to the posts list. */ draft?: boolean; /** * Enable to add drafts to the posts list. */ drafts?: boolean; _?: string[]; output?: string; /** * Specify the path of the configuration file. */ config?: string; [key: string]: any; } interface Query { date?: any; published?: boolean; } interface Extend { console: Console, deployer: Deployer, filter: Filter, generator: Generator, helper: Helper, highlight: Highlight, injector: Injector, migrator: Migrator, processor: Processor, renderer: Renderer, tag: Tag } interface Env { args: Args; debug: boolean; safe: boolean; silent: boolean; env: string; version: string; cmd: string; init: boolean; } type DefaultConfigType = typeof defaultConfig; interface Config extends DefaultConfigType { [key: string]: any; } // Node.js internal APIs declare module 'module' { function _nodeModulePaths(path: string): string[]; function _resolveFilename(request: string, parent: Module, isMain?: any, options?: any): string; const _extensions: NodeJS.RequireExtensions, _cache: any; } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging interface Hexo { /** * Emitted before deployment begins. * @param event * @param listener * @link https://hexo.io/api/events.html#deployBefore */ on(event: 'deployBefore', listener: (...args: any[]) => any): this; /** * Emitted after deployment begins. * @param event * @param listener * @link https://hexo.io/api/events.html#deployAfter */ on(event: 'deployAfter', listener: (...args: any[]) => any): this; /** * Emitted before Hexo exits. * @param event * @param listener * @link https://hexo.io/api/events.html#exit */ on(event: 'exit', listener: (...args: any[]) => any): this; /** * Emitted before generation begins. * @param event * @param listener * @link https://hexo.io/api/events.html#generateBefore */ on(event: 'generateBefore', listener: (...args: any[]) => any): this; /** * Emitted after generation finishes. * @param event * @param listener * @link https://hexo.io/api/events.html#generateAfter */ on(event: 'generateAfter', listener: (...args: any[]) => any): this; /** * Emitted after a new post has been created. This event returns the post data: * @param event * @param listener * @link https://hexo.io/api/events.html#new */ on(event: 'new', listener: (post: { path: string; content: string; }) => any): this; /** * Emitted before processing begins. This event returns a path representing the root directory of the box. * @param event * @param listener * @link https://hexo.io/api/events.html#processBefore */ on(event: 'processBefore', listener: (...args: any[]) => any): this; /** * Emitted after processing finishes. This event returns a path representing the root directory of the box. * @param event * @param listener * @link https://hexo.io/api/events.html#processAfter */ on(event: 'processAfter', listener: (...args: any[]) => any): this; /** * Emitted after initialization finishes. * @param event * @param listener */ on(event: 'ready', listener: (...args: any[]) => any): this; /** * undescripted on emit * @param event * @param listener */ on(event: string, listener: (...args: any[]) => any): any; emit(event: string, ...args: any[]): any; } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging class Hexo extends EventEmitter { public base_dir: string; public public_dir: string; public source_dir: string; public plugin_dir: string; public script_dir: string; public scaffold_dir: string; public theme_dir: string; public theme_script_dir: string; public env: Env; public extend: Extend; public config: Config; public log: ReturnType; public render: Render; public route: Router; public post: Post; public scaffold: Scaffold; public _dbLoaded: boolean; public _isGenerating: boolean; public database: Database; public config_path: string; public source: Source; public theme: Theme; public locals: Locals; public version: string; public _watchBox: () => void; public lib_dir: string; public core_dir: string; static lib_dir: string; static core_dir: string; static version: string; public _binaryRelationIndex: { post_tag: BinaryRelationIndex<'post_id', 'tag_id'>; post_category: BinaryRelationIndex<'post_id', 'category_id'>; }; constructor(base = process.cwd(), args: Args = {}) { super(); this.base_dir = base + sep; this.public_dir = join(base, 'public') + sep; this.source_dir = join(base, 'source') + sep; this.plugin_dir = join(base, 'node_modules') + sep; this.script_dir = join(base, 'scripts') + sep; this.scaffold_dir = join(base, 'scaffolds') + sep; this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep; this.theme_script_dir = join(this.theme_dir, 'scripts') + sep; this.env = { args, debug: Boolean(args.debug), safe: Boolean(args.safe), silent: Boolean(args.silent), env: process.env.NODE_ENV || 'development', version, cmd: args._ ? args._[0] : '', init: false }; this.extend = { console: new Console(), deployer: new Deployer(), filter: new Filter(), generator: new Generator(), helper: new Helper(), highlight: new Highlight(), injector: new Injector(), migrator: new Migrator(), processor: new Processor(), renderer: new Renderer(), tag: new Tag() }; this.config = { ...defaultConfig }; this.log = logger(this.env); this.render = new Render(this); this.route = new Router(); this.post = new Post(this); this.scaffold = new Scaffold(this); this._dbLoaded = false; this._isGenerating = false; // If `output` is provided, use that as the // root for saving the db. Otherwise default to `base`. const dbPath = args.output || base; if (/^(init|new|g|publish|s|deploy|render|migrate)/.test(this.env.cmd)) { this.log.d(`Writing database to ${join(dbPath, 'db.json')}`); } this.database = new Database({ version: dbVersion, path: join(dbPath, 'db.json') }); const mcp = multiConfigPath(this); this.config_path = args.config ? mcp(base, args.config, args.output) : join(base, '_config.yml'); registerModels(this); this.source = new Source(this); this.theme = new Theme(this); this.locals = new Locals(); this._bindLocals(); this._binaryRelationIndex = { post_tag: new BinaryRelationIndex<'post_id', 'tag_id'>('post_id', 'tag_id', 'PostTag', this), post_category: new BinaryRelationIndex<'post_id', 'category_id'>('post_id', 'category_id', 'PostCategory', this) }; } _bindLocals(): void { const db = this.database; const { locals } = this; locals.set('posts', () => { const query: Query = {}; if (!this.config.future) { query.date = { $lte: Date.now() }; } if (!this._showDrafts()) { query.published = true; } return db.model('Post').find(query); }); locals.set('pages', () => { const query: Query = {}; if (!this.config.future) { query.date = { $lte: Date.now() }; } return db.model('Page').find(query); }); locals.set('categories', () => { // Ignore categories with zero posts return db.model('Category').filter(category => category.length); }); locals.set('tags', () => { // Ignore tags with zero posts return db.model('Tag').filter(tag => tag.length); }); locals.set('data', () => { const obj = {}; db.model('Data').forEach(data => { obj[data._id] = data.data; }); return obj; }); } /** * Load configuration and plugins. * @returns {Promise} * @link https://hexo.io/api#Initialize */ init(): Promise { this.log.debug('Hexo version: %s', magenta(this.version)); this.log.debug('Working directory: %s', magenta(tildify(this.base_dir))); // Load internal plugins require('../plugins/console')(this); require('../plugins/filter')(this); require('../plugins/generator')(this); require('../plugins/helper')(this); require('../plugins/highlight')(this); require('../plugins/injector')(this); require('../plugins/processor')(this); require('../plugins/renderer')(this); require('../plugins/tag').default(this); // Load config return Promise.each([ 'update_package', // Update package.json 'load_config', // Load config 'load_theme_config', // Load alternate theme config 'load_plugins' // Load external plugins & scripts ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, { context: this })).then(() => { // Ready to go! this.emit('ready'); }); } /** * Call any console command explicitly. * @param name * @param args * @param callback * @returns {Promise} * @link https://hexo.io/api#Execute-Commands */ call(name: string, callback?: NodeJSLikeCallback): Promise; call(name: string, args: object, callback?: NodeJSLikeCallback): Promise; call(name: string, args?: object | NodeJSLikeCallback, callback?: NodeJSLikeCallback): Promise { if (!callback && typeof args === 'function') { callback = args as NodeJSLikeCallback; args = {}; } const c = this.extend.console.get(name); if (c) return (Reflect.apply(c, this, [args]) as any).asCallback(callback); return Promise.reject(new Error(`Console \`${name}\` has not been registered yet!`)); } model(name: string, schema?: Schema | Record) { return this.database.model(name, schema); } resolvePlugin(name: string, basedir: string): string { try { // Try to resolve the plugin with the Node.js's built-in require.resolve. return require.resolve(name, { paths: [basedir] }); } catch { // There was an error (likely the node_modules is corrupt or from early version of npm), // so return a possibly non-existing path that a later part of the resolution process will check. return join(basedir, 'node_modules', name); } } loadPlugin(path: string, callback?: NodeJSLikeCallback): Promise { return readFile(path).then(script => { // Based on: https://github.com/nodejs/node-v0.x-archive/blob/v0.10.33/src/node.js#L516 const module = new Module(path); module.filename = path; module.paths = Module._nodeModulePaths(path); function req(path: string) { return module.require(path); } req.resolve = (request: string) => Module._resolveFilename(request, module); req.main = require.main; req.extensions = Module._extensions; req.cache = Module._cache; script = `(async function(exports, require, module, __filename, __dirname, hexo){${script}\n});`; const fn = runInThisContext(script, path); return fn(module.exports, req, module, path, dirname(path), this); }).asCallback(callback); } _showDrafts(): boolean { const { args } = this.env; return args.draft || args.drafts || this.config.render_drafts; } /** * Load all files in the source folder as well as the theme data. * @param callback * @returns {Promise} * @link https://hexo.io/api#Load-Files */ load(callback?: NodeJSLikeCallback): Promise { return loadDatabase(this).then(() => { this._binaryRelationIndex.post_tag.load(); this._binaryRelationIndex.post_category.load(); this.log.info('Start processing'); return Promise.all([ this.source.process(), this.theme.process() ]); }).then(() => { mergeCtxThemeConfig(this); return this._generate({ cache: false }); }).asCallback(callback); } /** * Load all files in the source folder as well as the theme data. * Start watching for file changes continuously. * @param callback * @returns {Promise} * @link https://hexo.io/api#Load-Files */ watch(callback?: NodeJSLikeCallback): Promise { let useCache = false; const { cache } = Object.assign({ cache: false }, this.config.server); const { alias } = this.extend.console; if (alias[this.env.cmd] === 'server' && cache) { // enable cache when run hexo server useCache = true; } this._watchBox = debounce(() => this._generate({ cache: useCache }), 100); return loadDatabase(this).then(() => { this._binaryRelationIndex.post_tag.load(); this._binaryRelationIndex.post_category.load(); this.log.info('Start processing'); return Promise.all([ this.source.watch(), this.theme.watch() ]); }).then(() => { mergeCtxThemeConfig(this); this.source.on('processAfter', this._watchBox); this.theme.on('processAfter', () => { this._watchBox(); mergeCtxThemeConfig(this); }); return this._generate({ cache: useCache }); }).asCallback(callback); } unwatch(): void { if (this._watchBox != null) { this.source.removeListener('processAfter', this._watchBox); this.theme.removeListener('processAfter', this._watchBox); this._watchBox = null; } stopWatcher(this.source); stopWatcher(this.theme); } _generateLocals() { const { config, env, theme, theme_dir } = this; const ctx = { config: { url: this.config.url } }; const localsObj = this.locals.toObject() as SiteLocals; class Locals { page: any; path: string; url: string; config: Config; theme: any; layout: string; env: Env; view_dir: string; site: SiteLocals; cache?: boolean; constructor(path: string, locals: any) { this.page = { ...locals }; if (this.page.path == null) this.page.path = path; this.path = path; this.url = full_url_for.call(ctx, path); this.config = config; this.theme = theme.config; this.layout = 'layout'; this.env = env; this.view_dir = join(theme_dir, 'layout') + sep; this.site = localsObj; } } return Locals; } _runGenerators(): Promise { this.locals.invalidate(); const siteLocals = this.locals.toObject() as SiteLocals; const generators = this.extend.generator.list(); const { log } = this; // Run generators return Promise.map(Object.keys(generators), key => { const generator = generators[key]; log.debug('Generator: %s', magenta(key)); return Reflect.apply(generator, this, [siteLocals]); }).reduce((result, data) => { return data ? result.concat(data) : result; }, []); } _routerRefresh(runningGenerators: Promise, useCache: boolean): Promise { const { route } = this; const routeList = route.list(); const Locals = this._generateLocals(); Locals.prototype.cache = useCache; return runningGenerators.map(generatorResult => { if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined; // add Route const path = route.format(generatorResult.path); const { data, layout } = generatorResult; if (!layout) { route.set(path, data); return path; } return this.execFilter('template_locals', new Locals(path, data), { context: this }) .then((locals: LocalsType) => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); }) .thenReturn(path); }).then(newRouteList => { // Remove old routes for (let i = 0, len = routeList.length; i < len; i++) { const item = routeList[i]; if (!newRouteList.includes(item)) { route.remove(item); } } }); } _generate(options: { cache?: boolean } = {}): Promise { if (this._isGenerating) return; const useCache = options.cache; this._isGenerating = true; this.emit('generateBefore'); // Run before_generate filters // https://github.com/hexojs/hexo/issues/5287 // locals should be invalidated before before_generate filters because tags may use locals this.locals.invalidate(); return this.execFilter('before_generate', null, { context: this }) .then(() => this._routerRefresh(this._runGenerators(), useCache)).then(() => { this.emit('generateAfter'); // Run after_generate filters return this.execFilter('after_generate', null, { context: this }); }).finally(() => { this._isGenerating = false; }); } /** * Exit gracefully and finish up important things such as saving the database. * @param err * @returns {Promise} * @link https://hexo.io/api/#Exit */ exit(err?: any): Promise { if (err) { this.log.fatal( { err }, 'Something\'s wrong. Maybe you can find the solution here: %s', underline('https://hexo.io/docs/troubleshooting.html') ); } return this.execFilter('before_exit', null, { context: this }).then(() => { this.emit('exit', err); }); } execFilter(type: string, data: any, options?: FilterOptions) { return this.extend.filter.exec(type, data, options); } execFilterSync(type: string, data: any, options?: FilterOptions) { return this.extend.filter.execSync(type, data, options); } } Hexo.lib_dir = libDir + sep; Hexo.prototype.lib_dir = Hexo.lib_dir; Hexo.core_dir = dirname(libDir) + sep; Hexo.prototype.core_dir = Hexo.core_dir; Hexo.version = version; Hexo.prototype.version = Hexo.version; // define global variable // this useful for plugin written in typescript declare global { // eslint-disable-next-line one-var const hexo: Hexo; } export = Hexo; ================================================ FILE: lib/hexo/load_config.ts ================================================ import { sep, resolve, join, parse, basename, extname } from 'path'; import tildify from 'tildify'; import Theme from '../theme'; import Source from './source'; import { exists, readdir } from 'hexo-fs'; import { magenta } from 'picocolors'; import { deepMerge } from 'hexo-util'; import validateConfig from './validate_config'; import type Hexo from './index'; export = async (ctx: Hexo): Promise => { if (!ctx.env.init) return; const baseDir = ctx.base_dir; let configPath = ctx.config_path; const path = await exists(configPath) ? configPath : await findConfigPath(configPath); if (!path) return; configPath = path; let config = await ctx.render.render({ path }); if (!config || typeof config !== 'object') return; ctx.log.debug('Config loaded: %s', magenta(tildify(configPath))); ctx.config = deepMerge(ctx.config, config); // If root is not exist, create it by config.url if (!config.root) { let { pathname } = new URL(ctx.config.url); if (!pathname.endsWith('/')) pathname += '/'; ctx.config.root = pathname; } config = ctx.config; validateConfig(ctx); ctx.config_path = configPath; // Trim multiple trailing '/' config.root = config.root.replace(/\/*$/, '/'); // Remove any trailing '/' config.url = config.url.replace(/\/+$/, ''); ctx.public_dir = resolve(baseDir, config.public_dir) + sep; ctx.source_dir = resolve(baseDir, config.source_dir) + sep; ctx.source = new Source(ctx); if (!config.theme) return; const theme = config.theme.toString(); config.theme = theme; const themeDirFromThemes = join(baseDir, 'themes', theme) + sep; // base_dir/themes/[config.theme]/ const themeDirFromNodeModules = join(ctx.plugin_dir, 'hexo-theme-' + theme) + sep; // base_dir/node_modules/hexo-theme-[config.theme]/ // themeDirFromThemes has higher priority than themeDirFromNodeModules let ignored: string[] = []; if (await exists(themeDirFromThemes)) { ctx.theme_dir = themeDirFromThemes; ignored = ['**/themes/*/node_modules/**', '**/themes/*/.git/**']; } else if (await exists(themeDirFromNodeModules)) { ctx.theme_dir = themeDirFromNodeModules; ignored = ['**/node_modules/hexo-theme-*/node_modules/**', '**/node_modules/hexo-theme-*/.git/**']; } ctx.theme_script_dir = join(ctx.theme_dir, 'scripts') + sep; ctx.theme = new Theme(ctx, { ignored }); }; async function findConfigPath(path: string): Promise { const { dir, name } = parse(path); const files = await readdir(dir); const item = files.find(item => basename(item, extname(item)) === name); if (item != null) return join(dir, item); } ================================================ FILE: lib/hexo/load_database.ts ================================================ import { exists, unlink } from 'hexo-fs'; import Promise from 'bluebird'; import type Hexo from './index'; export = (ctx: Hexo): Promise => { if (ctx._dbLoaded) return Promise.resolve(); const db = ctx.database; const { path } = db.options; const { log } = ctx; return exists(path).then(exist => { if (!exist) return; log.debug('Loading database.'); return db.load(); }).then(() => { ctx._dbLoaded = true; }).catch(() => { log.error('Database load failed. Deleting database.'); return unlink(path); }); }; ================================================ FILE: lib/hexo/load_plugins.ts ================================================ import { join } from 'path'; import { exists, readFile, listDir } from 'hexo-fs'; import Promise from 'bluebird'; import { magenta } from 'picocolors'; import type Hexo from './index'; export = (ctx: Hexo): Promise => { if (!ctx.env.init || ctx.env.safe) return; return loadModules(ctx).then(() => loadScripts(ctx)); }; function loadModuleList(ctx: Hexo, basedir: string): Promise> { const packagePath = join(basedir, 'package.json'); // Make sure package.json exists return exists(packagePath).then(exist => { if (!exist) return []; // Read package.json and find dependencies return readFile(packagePath).then(content => { const json = JSON.parse(content); const deps = Object.keys(json.dependencies || {}); const devDeps = Object.keys(json.devDependencies || {}); return basedir === ctx.base_dir ? deps.concat(devDeps) : deps; }); }).filter((name: string) => { // Ignore plugins whose name is not started with "hexo-" if (!/^hexo-|^@[^/]+\/hexo-/.test(name)) return false; // Ignore plugin whose name is started with "hexo-theme" if (/^hexo-theme-|^@[^/]+\/hexo-theme-/.test(name)) return false; // Ignore typescript definition file that is started with "@types/" if (name.startsWith('@types/')) return false; // Make sure the plugin exists const path = ctx.resolvePlugin(name, basedir); return exists(path); }).then((modules: string[]) => { return Object.fromEntries(modules.map(name => [name, ctx.resolvePlugin(name, basedir)])); }); } function loadModules(ctx: Hexo): Promise { return Promise.map([ctx.base_dir, ctx.theme_dir], basedir => loadModuleList(ctx, basedir)) .then(([hexoModuleList, themeModuleList]) => { return Object.entries(Object.assign(themeModuleList, hexoModuleList)); }) .map(([name, path]) => { // Load plugins return ctx.loadPlugin(path as string).then(() => { ctx.log.debug('Plugin loaded: %s', magenta(name)); }).catch(err => { ctx.log.error({err}, 'Plugin load failed: %s', magenta(name)); }); }); } function loadScripts(ctx: Hexo): Promise { const baseDirLength = ctx.base_dir.length; return Promise.filter([ ctx.theme_script_dir, ctx.script_dir ], scriptDir => { // Ignore the directory if it does not exist return scriptDir ? exists(scriptDir) : false; }).map(scriptDir => listDir(scriptDir).map(name => { const path = join(scriptDir, name); return ctx.loadPlugin(path).then(() => { ctx.log.debug('Script loaded: %s', displayPath(path, baseDirLength)); }).catch(err => { ctx.log.error({err}, 'Script load failed: %s', displayPath(path, baseDirLength)); }); })); } function displayPath(path: string, baseDirLength: number): string { return magenta(path.substring(baseDirLength)); } ================================================ FILE: lib/hexo/load_theme_config.ts ================================================ import { join, parse, basename, extname } from 'path'; import tildify from 'tildify'; import { exists, readdir } from 'hexo-fs'; import { magenta } from 'picocolors'; import { deepMerge } from 'hexo-util'; import type Hexo from './index'; import type Promise from 'bluebird'; export = (ctx: Hexo): Promise => { if (!ctx.env.init) return; if (!ctx.config.theme) return; let configPath = join(ctx.base_dir, `_config.${String(ctx.config.theme)}.yml`); return exists(configPath).then(exist => { return exist ? configPath : findConfigPath(configPath); }).then(path => { if (!path) return; configPath = path; return ctx.render.render({ path }); }).then(config => { if (!config || typeof config !== 'object') return; ctx.log.debug('Second Theme Config loaded: %s', magenta(tildify(configPath))); // ctx.config.theme_config should have highest priority // If ctx.config.theme_config exists, then merge it with _config.[theme].yml // If ctx.config.theme_config doesn't exist, set it to _config.[theme].yml ctx.config.theme_config = ctx.config.theme_config ? deepMerge(config, ctx.config.theme_config) : config; }); }; function findConfigPath(path: string): Promise { const { dir, name } = parse(path); return readdir(dir).then(files => { const item = files.find(item => basename(item, extname(item)) === name); if (item != null) return join(dir, item); }); } ================================================ FILE: lib/hexo/locals.ts ================================================ import { Cache } from 'hexo-util'; class Locals { public cache: InstanceType; public getters: Record any>; constructor() { this.cache = new Cache(); this.getters = {}; } get(name: string): any { if (typeof name !== 'string') throw new TypeError('name must be a string!'); return this.cache.apply(name, () => { const getter = this.getters[name]; if (!getter) return; return getter(); }); } set(name: string, value: any): this { if (typeof name !== 'string') throw new TypeError('name must be a string!'); if (value == null) throw new TypeError('value is required!'); const getter = typeof value === 'function' ? value : () => value; this.getters[name] = getter; this.cache.del(name); return this; } remove(name: string): this { if (typeof name !== 'string') throw new TypeError('name must be a string!'); this.getters[name] = null; this.cache.del(name); return this; } invalidate(): this { this.cache.flush(); return this; } toObject(): Record { const result = {}; const keys = Object.keys(this.getters); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; const item = this.get(key); if (item != null) result[key] = item; } return result; } } export = Locals; ================================================ FILE: lib/hexo/multi_config_path.ts ================================================ import { isAbsolute, resolve, join, extname } from 'path'; import { existsSync, readFileSync, writeFileSync } from 'hexo-fs'; import yml from 'js-yaml'; import { deepMerge } from 'hexo-util'; import type Hexo from './index'; export = (ctx: Hexo) => function multiConfigPath(base: string, configPaths?: string, outputDir?: string): string { const { log } = ctx; const defaultPath = join(base, '_config.yml'); if (!configPaths) { log.w('No config file entered.'); return join(base, '_config.yml'); } let paths: string[]; // determine if comma or space separated if (configPaths.includes(',')) { paths = configPaths.replace(' ', '').split(','); } else { // only one config let configPath = isAbsolute(configPaths) ? configPaths : resolve(base, configPaths); if (!existsSync(configPath)) { log.w(`Config file ${configPaths} not found, using default.`); configPath = defaultPath; } return configPath; } const numPaths = paths.length; // combine files let combinedConfig = {}; let count = 0; for (let i = 0; i < numPaths; i++) { const configPath = isAbsolute(paths[i]) ? paths[i] : join(base, paths[i]); if (!existsSync(configPath)) { log.w(`Config file ${paths[i]} not found.`); continue; } // files read synchronously to ensure proper overwrite order const file = readFileSync(configPath); const ext = extname(paths[i]).toLowerCase(); if (ext === '.yml') { combinedConfig = deepMerge(combinedConfig, yml.load(file)); count++; } else if (ext === '.json') { combinedConfig = deepMerge(combinedConfig, yml.load(file, {json: true})); count++; } else { log.w(`Config file ${paths[i]} not supported type.`); } } if (count === 0) { log.e('No config files found. Using _config.yml.'); return defaultPath; } log.i('Config based on', count.toString(), 'files'); const multiconfigRoot = outputDir || base; const outputPath = join(multiconfigRoot, '_multiconfig.yml'); log.d(`Writing _multiconfig.yml to ${outputPath}`); writeFileSync(outputPath, yml.dump(combinedConfig)); // write file and return path return outputPath; }; ================================================ FILE: lib/hexo/post.ts ================================================ import assert from 'assert'; import moment from 'moment'; import Promise from 'bluebird'; import { join, extname, basename } from 'path'; import { magenta } from 'picocolors'; import { load } from 'js-yaml'; import { slugize, escapeRegExp, deepMerge} from 'hexo-util'; import { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import { parse as yfmParse, split as yfmSplit, stringify as yfmStringify } from 'hexo-front-matter'; import type Hexo from './index'; import type { NodeJSLikeCallback, RenderData } from '../types'; const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content']; const rHexoPostRenderEscape = /([\s\S]+?)<\/hexoPostRenderCodeBlock>/g; const rCommentEscape = /()/g; const rSwigTag = /(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s; const rSwigPlaceHolder = /(?:<|<)!--swig\uFFFC(\d+)--(?:>|>)/g; const rCodeBlockPlaceHolder = /(?:<|<)!--code\uFFFC(\d+)--(?:>|>)/g; const rCommentHolder = /(?:<|<)!--comment\uFFFC(\d+)--(?:>|>)/g; const STATE_PLAINTEXT = 0; const STATE_SWIG_VAR = 1; const STATE_SWIG_COMMENT = 2; const STATE_SWIG_TAG = 3; const STATE_SWIG_FULL_TAG = 4; const STATE_PLAINTEXT_COMMENT = 5; const isNonWhiteSpaceChar = (char: string) => char !== '\r' && char !== '\n' && char !== '\t' && char !== '\f' && char !== '\v' && char !== ' '; class PostRenderEscape { public stored: string[]; public length: number; constructor() { this.stored = []; } static escapeContent(cache: string[], flag: string, str: string) { return ``; } static restoreContent(cache: string[]) { return (_: string, index: number) => { assert(cache[index]); const value = cache[index]; cache[index] = null; return value; }; } restoreAllSwigTags(str: string) { const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored)); return restored; } restoreCodeBlocks(str: string) { return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored)); } restoreComments(str: string) { return str.replace(rCommentHolder, PostRenderEscape.restoreContent(this.stored)); } escapeComments(str: string) { return str.replace(rCommentEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'comment', content)); } escapeCodeBlocks(str: string) { return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content)); } /** * @param {string} str * @returns string */ escapeAllSwigTags(str: string) { let state = STATE_PLAINTEXT; let buffer_start = -1; let plaintext_comment_start = -1; let plain_text_start = 0; let output = ''; let swig_tag_name_begin = false; let swig_tag_name_end = false; let swig_tag_name = ''; let swig_full_tag_start_start = -1; let swig_full_tag_start_end = -1; // current we just consider one level of string quote let swig_string_quote = ''; const { length } = str; let idx = 0; // for backtracking const swig_start_idx = [0, 0, 0, 0, 0]; const flushPlainText = (end: number) => { if (plain_text_start !== -1 && end > plain_text_start) { output += str.slice(plain_text_start, end); } plain_text_start = -1; }; const ensurePlainTextStart = (position: number) => { if (plain_text_start === -1) { plain_text_start = position; } }; const pushAndReset = (value: string) => { output += value; plain_text_start = -1; }; while (idx < length) { while (idx < length) { const char = str[idx]; const next_char = str[idx + 1]; if (state === STATE_PLAINTEXT) { // From plain text to swig ensurePlainTextStart(idx); if (char === '{') { // check if it is a complete tag {{ }} if (next_char === '{') { flushPlainText(idx); state = STATE_SWIG_VAR; idx++; buffer_start = idx + 1; swig_start_idx[state] = idx; } else if (next_char === '#') { flushPlainText(idx); state = STATE_SWIG_COMMENT; idx++; buffer_start = idx + 1; swig_start_idx[state] = idx; } else if (next_char === '%') { flushPlainText(idx); state = STATE_SWIG_TAG; idx++; buffer_start = idx + 1; swig_full_tag_start_start = idx + 1; swig_full_tag_start_end = idx + 1; swig_tag_name = ''; swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag swig_tag_name_end = false; swig_start_idx[state] = idx; } } if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') { flushPlainText(idx); state = STATE_PLAINTEXT_COMMENT; plaintext_comment_start = idx; idx += 3; } } else if (state === STATE_SWIG_TAG) { if (char === '"' || char === '\'') { if (swig_string_quote === '') { swig_string_quote = char; } else if (swig_string_quote === char) { swig_string_quote = ''; } } if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text idx++; if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) { state = STATE_SWIG_FULL_TAG; buffer_start = idx + 1; // since we have already move idx to next char of '}', so here is idx -1 swig_full_tag_start_end = idx - 1; swig_start_idx[state] = idx; } else { swig_tag_name = ''; state = STATE_PLAINTEXT; // since we have already move idx to next char of '}', so here is idx -1 pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(buffer_start, idx - 1)}%}`)); } } else { if (isNonWhiteSpaceChar(char)) { if (!swig_tag_name_begin && !swig_tag_name_end) { swig_tag_name_begin = true; } if (swig_tag_name_begin) { swig_tag_name += char; } } else { if (swig_tag_name_begin === true) { swig_tag_name_begin = false; swig_tag_name_end = true; } } } } else if (state === STATE_SWIG_VAR) { if (char === '"' || char === '\'') { if (swig_string_quote === '') { swig_string_quote = char; } else if (swig_string_quote === char) { swig_string_quote = ''; } } // {{ } if (char === '}' && next_char !== '}' && swig_string_quote === '') { // From swig back to plain text state = STATE_PLAINTEXT; pushAndReset(`{{${str.slice(buffer_start, idx)}${char}`); } else if (char === '}' && next_char === '}' && swig_string_quote === '') { pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{{${str.slice(buffer_start, idx)}}}`)); idx++; state = STATE_PLAINTEXT; } } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text if (char === '#' && next_char === '}') { idx++; state = STATE_PLAINTEXT; plain_text_start = -1; } } else if (state === STATE_SWIG_FULL_TAG) { if (char === '{' && next_char === '%') { let swig_full_tag_end_buffer = ''; let swig_full_tag_found = false; let _idx = idx + 2; for (; _idx < length; _idx++) { const _char = str[_idx]; const _next_char = str[_idx + 1]; if (_char === '%' && _next_char === '}') { _idx++; swig_full_tag_found = true; break; } swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char; } if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) { state = STATE_PLAINTEXT; pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%}${str.slice(buffer_start, idx)}{%${swig_full_tag_end_buffer}%}`)); idx = _idx; swig_full_tag_end_buffer = ''; } } } else if (state === STATE_PLAINTEXT_COMMENT) { if (char === '-' && next_char === '-' && str[idx + 2] === '>') { state = STATE_PLAINTEXT; const comment = str.slice(plaintext_comment_start, idx + 3); pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment)); idx += 2; } } idx++; } if (state === STATE_PLAINTEXT) { break; } if (state === STATE_PLAINTEXT_COMMENT) { // Unterminated comment, just push the rest as comment const comment = str.slice(plaintext_comment_start, length); pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment)); break; } // If the swig tag is not closed, then it is a plain text, we need to backtrack if (state === STATE_SWIG_FULL_TAG) { pushAndReset(`{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%`); } else { pushAndReset('{'); } idx = swig_start_idx[state]; swig_string_quote = ''; state = STATE_PLAINTEXT; } if (plain_text_start !== -1 && plain_text_start < length) { output += str.slice(plain_text_start); } return output; } } const prepareFrontMatter = (data: any, jsonMode: boolean): Record => { for (const [key, item] of Object.entries(data)) { if (moment.isMoment(item)) { data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss'); } else if (moment.isDate(item)) { data[key] = moment.utc(item).format('YYYY-MM-DD HH:mm:ss'); } else if (typeof item === 'string') { if (jsonMode || item.includes(':') || item.startsWith('#') || item.startsWith('!!') || item.includes('{') || item.includes('}') || item.includes('[') || item.includes(']') || item.includes('\'') || item.includes('"')) data[key] = `"${item.replace(/"/g, '\\"')}"`; } } return data; }; const removeExtname = (str: string) => { return str.substring(0, str.length - extname(str).length); }; const createAssetFolder = (path: string, assetFolder: boolean) => { if (!assetFolder) return Promise.resolve(); const target = removeExtname(path); if (basename(target) === 'index') return Promise.resolve(); return exists(target).then(exist => { if (!exist) return mkdirs(target); }); }; interface Result { path: string; content: string; } interface PostData { title?: string | number; layout?: string; slug?: string | number; path?: string; date?: moment.Moment; [prop: string]: any; } class Post { public context: Hexo; constructor(context: Hexo) { this.context = context; } create(data: PostData, callback?: NodeJSLikeCallback): Promise; create(data: PostData, replace: boolean, callback?: NodeJSLikeCallback): Promise; create(data: PostData, replace: boolean | (NodeJSLikeCallback), callback?: NodeJSLikeCallback): Promise { if (!callback && typeof replace === 'function') { callback = replace; replace = false; } const ctx = this.context; const { config } = ctx; data.slug = slugize((data.slug || data.title).toString(), { transform: config.filename_case }); data.layout = (data.layout || config.default_layout).toLowerCase(); data.date = data.date ? moment(data.date) : moment(); return Promise.all([ // Get the post path ctx.execFilter('new_post_path', data, { args: [replace], context: ctx }), this._renderScaffold(data) ]).spread((path: string, content: string) => { const result = { path, content }; return Promise.all([ // Write content to file writeFile(path, content), // Create asset folder createAssetFolder(path, config.post_asset_folder) ]).then(() => { ctx.emit('new', result); return result; }); }).asCallback(callback); } _getScaffold(layout: string) { const ctx = this.context; return ctx.scaffold.get(layout).then(result => { if (result != null) return result; return ctx.scaffold.get('normal'); }); } _renderScaffold(data: PostData) { const { tag } = this.context.extend; let splitted: ReturnType; return this._getScaffold(data.layout).then(scaffold => { splitted = yfmSplit(scaffold); const jsonMode = splitted.separator.startsWith(';'); const frontMatter = prepareFrontMatter({ ...data }, jsonMode); return tag.render(splitted.data, frontMatter); }).then(frontMatter => { const { separator } = splitted; const jsonMode = separator.startsWith(';'); // Parse front-matter let obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter); obj = deepMerge(obj, Object.fromEntries(Object.entries(data).filter(([key, value]) => !preservedKeys.includes(key) && value != null))); let content = ''; // Prepend the separator if (splitted.prefixSeparator) content += `${separator}\n`; content += yfmStringify(obj, { mode: jsonMode ? 'json' : '' }); // Concat content content += splitted.content; if (data.content) { content += `\n${data.content}`; } return content; }); } publish(data: PostData, replace?: boolean): Promise; publish(data: PostData, callback?: NodeJSLikeCallback): Promise; publish(data: PostData, replace: boolean, callback?: NodeJSLikeCallback): Promise; publish(data: PostData, replace?: boolean | NodeJSLikeCallback, callback?: NodeJSLikeCallback): Promise { if (!callback && typeof replace === 'function') { callback = replace; replace = false; } if (data.layout === 'draft') data.layout = 'post'; const ctx = this.context; const { config } = ctx; const draftDir = join(ctx.source_dir, '_drafts'); const slug = slugize(data.slug.toString(), { transform: config.filename_case }); data.slug = slug; const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`); let src = ''; const result: Result = {} as any; data.layout = (data.layout || config.default_layout).toLowerCase(); // Find the draft return listDir(draftDir).then(list => { const item = list.find(item => regex.test(item)); if (!item) throw new Error(`Draft "${slug}" does not exist.`); // Read the content src = join(draftDir, item); return readFile(src); }).then(content => { // Create post Object.assign(data, yfmParse(content)); data.content = data._content; data._content = undefined; return this.create(data, replace as boolean); }).then(post => { result.path = post.path; result.content = post.content; return unlink(src); }).then(() => { // Remove the original draft file if (!config.post_asset_folder) return; // Copy assets const assetSrc = removeExtname(src); const assetDest = removeExtname(result.path); return exists(assetSrc).then(exist => { if (!exist) return; return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc)); }); }).thenReturn(result).asCallback(callback); } render(source: string, data: RenderData = {}, callback?: NodeJSLikeCallback) { const ctx = this.context; const { config } = ctx; const { tag } = ctx.extend; const ext = data.engine || (source ? extname(source) : ''); let promise; if (data.content != null) { promise = Promise.resolve(data.content); } else if (source) { // Read content from files promise = readFile(source); } else { return Promise.reject(new Error('No input file or string!')).asCallback(callback); } // Files like js and css are also processed by this function, but they do not require preprocessing like markdown // data.source does not exist when tag plugins call the markdown renderer const isPost = !data.source || ['html', 'htm'].includes(ctx.render.getOutput(data.source)); if (!isPost) { return promise.then(content => { data.content = content; ctx.log.debug('Rendering file: %s', magenta(source)); return ctx.render.render({ text: data.content, path: source, engine: data.engine, toString: true }); }).then(content => { data.content = content; return data; }).asCallback(callback); } // disable Nunjucks when the renderer specify that. let disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks; // front-matter overrides renderer's option if (typeof data.disableNunjucks === 'boolean') disableNunjucks = data.disableNunjucks; const cacheObj = new PostRenderEscape(); return promise.then(content => { data.content = content; // Run "before_post_render" filters return ctx.execFilter('before_post_render', data, { context: ctx }); }).then(() => { // Escape all comments to avoid conflict with Nunjucks and code block data.content = cacheObj.escapeCodeBlocks(data.content); // Escape all Nunjucks/Swig tags let hasSwigTag = true; if (disableNunjucks === false) { hasSwigTag = rSwigTag.test(data.content); if (hasSwigTag) { data.content = cacheObj.escapeAllSwigTags(data.content); } } const options: { highlight?: boolean; } = data.markdown || {}; if (!config.syntax_highlighter) options.highlight = null; ctx.log.debug('Rendering post: %s', magenta(source)); // Render with markdown or other renderer return ctx.render.render({ text: data.content, path: source, engine: data.engine, toString: true, onRenderEnd(content) { // Replace cache data with real contents data.content = cacheObj.restoreAllSwigTags(content); // Return content after replace the placeholders if (disableNunjucks || !hasSwigTag) return data.content; // Render with Nunjucks if there are Swig tags return tag.render(data.content, data); } }, options); }).then(content => { data.content = cacheObj.restoreComments(content); data.content = cacheObj.restoreCodeBlocks(data.content); // Run "after_post_render" filters return ctx.execFilter('after_post_render', data, { context: ctx }); }).asCallback(callback); } } export = Post; ================================================ FILE: lib/hexo/register_models.ts ================================================ import * as models from '../models'; import type Hexo from './index'; export = (ctx: Hexo): void => { const db = ctx.database; const keys = Object.keys(models); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; db.model(key, models[key](ctx)); } }; ================================================ FILE: lib/hexo/render.ts ================================================ import { extname } from 'path'; import Promise from 'bluebird'; import { readFile, readFileSync } from 'hexo-fs'; import type Hexo from './index'; import type { Renderer } from '../extend'; import type { StoreFunction, StoreFunctionData, StoreSyncFunction } from '../extend/renderer'; import { NodeJSLikeCallback } from '../types'; const getExtname = (str: string): string => { if (typeof str !== 'string') return ''; const ext = extname(str); return ext.startsWith('.') ? ext.slice(1) : ext; }; const toString = (result: any, options: StoreFunctionData): string => { if (!Object.prototype.hasOwnProperty.call(options, 'toString') || typeof result === 'string') return result; if (typeof options.toString === 'function') { return options.toString(result); } else if (typeof result === 'object') { return JSON.stringify(result); } else if (result.toString) { return result.toString(); } return result; }; class Render { public context: Hexo; public renderer: Renderer; constructor(ctx: Hexo) { this.context = ctx; this.renderer = ctx.extend.renderer; } isRenderable(path: string): boolean { return this.renderer.isRenderable(path); } isRenderableSync(path: string): boolean { return this.renderer.isRenderableSync(path); } getOutput(path: string): string { return this.renderer.getOutput(path); } getRenderer(ext: string, sync?: boolean): StoreSyncFunction | StoreFunction { return this.renderer.get(ext, sync); } getRendererSync(ext: string): StoreSyncFunction | StoreFunction { return this.getRenderer(ext, true); } render(data: StoreFunctionData, callback?: NodeJSLikeCallback): Promise; render(data: StoreFunctionData, options: any, callback?: NodeJSLikeCallback): Promise; render(data: StoreFunctionData, options?: any | NodeJSLikeCallback, callback?: NodeJSLikeCallback): Promise { if (!callback && typeof options === 'function') { callback = options; options = {}; } const ctx = this.context; let ext = ''; let promise: Promise; if (!data) return Promise.reject(new TypeError('No input file or string!')); if (data.text != null) { promise = Promise.resolve(data.text); } else if (!data.path) { return Promise.reject(new TypeError('No input file or string!')); } else { promise = readFile(data.path); } return promise.then(text => { data.text = text; ext = data.engine || getExtname(data.path); if (!ext || !this.isRenderable(ext)) return text; const renderer = this.getRenderer(ext); return Reflect.apply(renderer, ctx, [data, options]); }).then(result => { result = toString(result, data); if (data.onRenderEnd) { return data.onRenderEnd(result); } return result; }).then(result => { const output = this.getOutput(ext) || ext; return ctx.execFilter(`after_render:${output}`, result, { context: ctx, args: [data] }); }).asCallback(callback); } renderSync(data: StoreFunctionData, options = {}): any { if (!data) throw new TypeError('No input file or string!'); const ctx = this.context; if (data.text == null) { if (!data.path) throw new TypeError('No input file or string!'); data.text = readFileSync(data.path); } if (data.text == null) throw new TypeError('No input file or string!'); const ext = data.engine || getExtname(data.path); let result; if (ext && this.isRenderableSync(ext)) { const renderer = this.getRendererSync(ext); result = Reflect.apply(renderer, ctx, [data, options]); } else { result = data.text; } const output = this.getOutput(ext) || ext; result = toString(result, data); if (data.onRenderEnd) { result = data.onRenderEnd(result); } return ctx.execFilterSync(`after_render:${output}`, result, { context: ctx, args: [data] }); } } export = Render; ================================================ FILE: lib/hexo/router.ts ================================================ import { EventEmitter } from 'events'; import Promise from 'bluebird'; import Stream from 'stream'; const { Readable } = Stream; interface Data { data: any; modified: boolean; } class RouteStream extends Readable { public _data: any; public _ended: boolean; public modified: boolean; constructor(data: Data) { super({ objectMode: true }); this._data = data.data; this._ended = false; this.modified = data.modified; } // Assume we only accept Buffer, plain object, or string _toBuffer(data: Buffer | object | string): Buffer | null { if (data instanceof Buffer) { return data; } if (typeof data === 'object') { data = JSON.stringify(data); } if (typeof data === 'string') { return Buffer.from(data); // Assume string is UTF-8 encoded string } return null; } _read(): boolean { const data = this._data; if (typeof data !== 'function') { const bufferData = this._toBuffer(data); if (bufferData) { this.push(bufferData); } this.push(null); return; } // Don't read it twice! if (this._ended) return false; this._ended = true; data().then(data => { if (data instanceof Stream && (data as Stream.Readable).readable) { data.on('data', d => { this.push(d); }); data.on('end', () => { this.push(null); }); data.on('error', err => { this.emit('error', err); }); } else { const bufferData = this._toBuffer(data); if (bufferData) { this.push(bufferData); } this.push(null); } }).catch(err => { this.emit('error', err); this.push(null); }); } } const _format = (path?: string): string => { path = path || ''; if (typeof path !== 'string') throw new TypeError('path must be a string!'); path = path .replace(/^\/+/, '') // Remove prefixed slashes .replace(/\\/g, '/') // Replaces all backslashes .replace(/\?.*$/, ''); // Remove query string // Appends `index.html` to the path with trailing slash if (!path || path.endsWith('/')) { path += 'index.html'; } return path; }; class Router extends EventEmitter { public routes: { [key: string]: Data | null; }; constructor() { super(); this.routes = {}; } list(): string[] { const { routes } = this; return Object.keys(routes).filter(key => routes[key]); } format(path?: string): string { return _format(path); } get(path: string): RouteStream { if (typeof path !== 'string') throw new TypeError('path must be a string!'); const data = this.routes[this.format(path)]; if (data == null) return; return new RouteStream(data); } isModified(path: string): boolean { if (typeof path !== 'string') throw new TypeError('path must be a string!'); const data = this.routes[this.format(path)]; return data ? data.modified : false; } set(path: string, data: any): this { if (typeof path !== 'string') throw new TypeError('path must be a string!'); if (data == null) throw new TypeError('data is required!'); let obj: Data; if (typeof data === 'object' && data.data != null) { obj = data; } else { obj = { data, modified: true }; } if (typeof obj.data === 'function') { if (obj.data.length) { obj.data = Promise.promisify(obj.data); } else { obj.data = Promise.method(obj.data); } } path = this.format(path); this.routes[path] = { data: obj.data, modified: obj.modified == null ? true : obj.modified }; this.emit('update', path); return this; } remove(path: string): this { if (typeof path !== 'string') throw new TypeError('path must be a string!'); path = this.format(path); this.routes[path] = null; this.emit('remove', path); return this; } } export = Router; ================================================ FILE: lib/hexo/scaffold.ts ================================================ import { extname, join } from 'path'; import { exists, listDir, readFile, unlink, writeFile } from 'hexo-fs'; import type Hexo from './index'; import type { NodeJSLikeCallback } from '../types'; import type Promise from 'bluebird'; class Scaffold { public context: Hexo; public scaffoldDir: string; public defaults: { normal: string }; constructor(context: Hexo) { this.context = context; this.scaffoldDir = context.scaffold_dir; this.defaults = { normal: [ '---', 'layout: {{ layout }}', 'title: {{ title }}', 'date: {{ date }}', 'tags:', '---' ].join('\n') }; } _listDir(): Promise<{ name: string; path: string; }[]> { const { scaffoldDir } = this; return exists(scaffoldDir).then(exist => { if (!exist) return []; return listDir(scaffoldDir, { ignorePattern: /^_|\/_/ }); }).map(item => ({ name: item.substring(0, item.length - extname(item).length), path: join(scaffoldDir, item) })); } _getScaffold(name: string): Promise<{ name: string; path: string; }> { return this._listDir().then(list => list.find(item => item.name === name)); } get(name: string, callback?: NodeJSLikeCallback): Promise { return this._getScaffold(name).then(item => { if (item) { return readFile(item.path); } return this.defaults[name]; }).asCallback(callback); } set(name: string, content: any, callback?: NodeJSLikeCallback): Promise { const { scaffoldDir } = this; return this._getScaffold(name).then(item => { let path = item ? item.path : join(scaffoldDir, name); if (!extname(path)) path += '.md'; return writeFile(path, content); }).asCallback(callback); } remove(name: string, callback?: NodeJSLikeCallback): Promise { return this._getScaffold(name).then(item => { if (!item) return; return unlink(item.path); }).asCallback(callback); } } export = Scaffold; ================================================ FILE: lib/hexo/source.ts ================================================ import Box from '../box'; import type Hexo from './index'; class Source extends Box { constructor(ctx: Hexo) { super(ctx, ctx.source_dir); this.processors = ctx.extend.processor.list(); } } export = Source; ================================================ FILE: lib/hexo/update_package.ts ================================================ import { join } from 'path'; import { writeFile, exists, readFile } from 'hexo-fs'; import type Hexo from './index'; import type Promise from 'bluebird'; export = (ctx: Hexo): Promise => { const pkgPath = join(ctx.base_dir, 'package.json'); return readPkg(pkgPath).then(pkg => { if (!pkg) return; ctx.env.init = true; if (pkg.hexo.version === ctx.version) return; pkg.hexo.version = ctx.version; ctx.log.debug('Updating package.json'); return writeFile(pkgPath, JSON.stringify(pkg, null, ' ')); }); }; function readPkg(path: string): Promise { return exists(path).then(exist => { if (!exist) return; return readFile(path).then(content => { const pkg = JSON.parse(content); if (typeof pkg.hexo !== 'object') return; return pkg; }); }); } ================================================ FILE: lib/hexo/validate_config.ts ================================================ import assert from 'assert'; import type Hexo from './index'; export = (ctx: Hexo): void => { const { config, log } = ctx; log.info('Validating config'); // Validation for config.url && config.root if (typeof config.url !== 'string') { throw new TypeError(`Invalid config detected: "url" should be string, not ${typeof config.url}!`); } try { // eslint-disable-next-line no-new new URL(config.url); assert(new URL(config.url).protocol.startsWith('http')); } catch { throw new TypeError('Invalid config detected: "url" should be a valid URL!'); } if (typeof config.root !== 'string') { throw new TypeError(`Invalid config detected: "root" should be string, not ${typeof config.root}!`); } if (config.root.trim().length <= 0) { throw new TypeError('Invalid config detected: "root" should not be empty!'); } }; ================================================ FILE: lib/models/asset.ts ================================================ import warehouse from 'warehouse'; import { join } from 'path'; import type Hexo from '../hexo'; import type { AssetSchema } from '../types'; export = (ctx: Hexo) => { const Asset = new warehouse.Schema({ _id: {type: String, required: true}, path: {type: String, required: true}, modified: {type: Boolean, default: true}, renderable: {type: Boolean, default: true} }); Asset.virtual('source').get(function() { return join(ctx.base_dir, this._id); }); return Asset; }; ================================================ FILE: lib/models/binary_relation_index.ts ================================================ import type Hexo from '../hexo'; type BinaryRelationType = { [key in K]: PropertyKey; } & { [key in V]: PropertyKey; }; class BinaryRelationIndex { keyIndex: Map> = new Map(); valueIndex: Map> = new Map(); key: K; value: V; ctx: Hexo; schemaName: string; constructor(key: K, value: V, schemaName: string, ctx: Hexo) { this.key = key; this.value = value; this.schemaName = schemaName; this.ctx = ctx; } load() { this.keyIndex.clear(); this.valueIndex.clear(); const raw = this.ctx.model(this.schemaName).data; for (const _id in raw) { this.saveHook(raw[_id]); } } saveHook(data: BinaryRelationType & { _id: PropertyKey }) { if (!data) return; const _id = data._id; const key = data[this.key]; const value = data[this.value]; if (!this.keyIndex.has(key)) { this.keyIndex.set(key, new Set()); } this.keyIndex.get(key).add(_id); if (!this.valueIndex.has(value)) { this.valueIndex.set(value, new Set()); } this.valueIndex.get(value).add(_id); } removeHook(data: BinaryRelationType & { _id: PropertyKey }) { const _id = data._id; const key = data[this.key]; const value = data[this.value]; this.keyIndex.get(key)?.delete(_id); if (this.keyIndex.get(key)?.size === 0) { this.keyIndex.delete(key); } this.valueIndex.get(value)?.delete(_id); if (this.valueIndex.get(value)?.size === 0) { this.valueIndex.delete(value); } } findById(_id: PropertyKey) { const raw = this.ctx.model(this.schemaName).findById(_id, { lean: true }); if (!raw) return; return { ...raw }; } find(query: Partial>) { const key = query[this.key]; const value = query[this.value]; if (key && value) { const ids = this.keyIndex.get(key); if (!ids) return []; return Array.from(ids) .map(_id => this.findById(_id)) .filter(record => record?.[this.value] === value); } if (key) { const ids = this.keyIndex.get(key); if (!ids) return []; return Array.from(ids).map(_id => this.findById(_id)); } if (value) { const ids = this.valueIndex.get(value); if (!ids) return []; return Array.from(ids).map(_id => this.findById(_id)); } return []; } findOne(query: Partial>) { return this.find(query)[0]; } } export default BinaryRelationIndex; ================================================ FILE: lib/models/cache.ts ================================================ import warehouse from 'warehouse'; import Promise from 'bluebird'; import type Hexo from '../hexo'; import type fs from 'fs'; import type Document from 'warehouse/dist/document'; import type { CacheSchema } from '../types'; export = (_ctx: Hexo) => { const Cache = new warehouse.Schema({ _id: {type: String, required: true}, hash: {type: String, default: ''}, modified: {type: Number, default: Date.now() } // UnixTime }); Cache.static('compareFile', function(id: string, hashFn: (id: string) => Promise, statFn: (id: string) => Promise): Promise<{ type: string }> { const cache = this.findById(id) as Document; // If cache does not exist, then it must be a new file. We have to get both // file hash and stats. if (!cache) { return Promise.all([hashFn(id), statFn(id)]).spread((hash: string, stats: fs.Stats) => this.insert({ _id: id, hash, modified: stats.mtime.getTime() })).thenReturn({ type: 'create' }); } let mtime: number; // Get file stats return statFn(id).then(stats => { mtime = stats.mtime.getTime(); // Skip the file if the modified time is unchanged if (cache.modified === mtime) { return { type: 'skip' }; } // Get file hash return hashFn(id); }).then((result: string | { type: string }) => { // If the result is an object, skip the following steps because it's an // unchanged file if (typeof result === 'object') return result; const hash = result; // Skip the file if the hash is unchanged if (cache.hash === hash) { return { type: 'skip' }; } // Update cache info cache.hash = hash; cache.modified = mtime; return cache.save().thenReturn({ type: 'update' }); }); }); return Cache; }; ================================================ FILE: lib/models/category.ts ================================================ import warehouse from 'warehouse'; import { slugize, full_url_for } from 'hexo-util'; import type Hexo from '../hexo'; import type { CategorySchema } from '../types'; export = (ctx: Hexo) => { const Category = new warehouse.Schema({ name: {type: String, required: true}, parent: { type: warehouse.Schema.Types.CUID, ref: 'Category'} }); Category.virtual('slug').get(function() { let name = this.name; if (!name) return; let str = ''; if (this.parent) { const parent = ctx.model('Category').findById(this.parent); str += `${parent.slug}/`; } const map = ctx.config.category_map || {}; name = map[name] || name; str += slugize(name, {transform: ctx.config.filename_case}); return str; }); Category.virtual('path').get(function() { let catDir = ctx.config.category_dir; if (catDir === '/') catDir = ''; if (!catDir.endsWith('/')) catDir += '/'; return `${catDir + this.slug}/`; }); Category.virtual('permalink').get(function() { return full_url_for.call(ctx, this.path); }); Category.virtual('posts').get(function() { const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; const ids = ReadOnlyPostCategory.find({category_id: this._id}).map(item => item.post_id); return ctx.locals.get('posts').find({ _id: {$in: ids} }); }); Category.virtual('length').get(function() { const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; return ReadOnlyPostCategory.find({category_id: this._id}).length; }); // Check whether a category exists Category.pre('save', (data: CategorySchema) => { const { name, parent } = data; if (!name) return; const Category = ctx.model('Category'); const cat = Category.findOne({ name, parent: parent || {$exists: false} }, {lean: true}); if (cat) { throw new Error(`Category \`${name}\` has already existed!`); } }); // Remove PostCategory references Category.pre('remove', (data: CategorySchema) => { const PostCategory = ctx.model('PostCategory'); return PostCategory.remove({category_id: data._id}); }); return Category; }; ================================================ FILE: lib/models/data.ts ================================================ import warehouse from 'warehouse'; import type Hexo from '../hexo'; import { DataSchema } from '../types'; export = (_ctx: Hexo) => { const Data = new warehouse.Schema({ _id: {type: String, required: true}, data: Object }); return Data; }; ================================================ FILE: lib/models/index.ts ================================================ export { default as Asset } from './asset'; export { default as Cache } from './cache'; export { default as Category } from './category'; export { default as Data } from './data'; export { default as Page } from './page'; export { default as Post } from './post'; export { default as PostAsset } from './post_asset'; export { default as PostCategory } from './post_category'; export { default as PostTag } from './post_tag'; export { default as Tag } from './tag'; ================================================ FILE: lib/models/page.ts ================================================ import warehouse from 'warehouse'; import { join } from 'path'; import Moment from './types/moment'; import moment from 'moment'; import { full_url_for } from 'hexo-util'; import type Hexo from '../hexo'; import type { PageSchema } from '../types'; export = (ctx: Hexo) => { const Page = new warehouse.Schema({ title: {type: String, default: ''}, date: { type: Moment, default: moment }, updated: { type: Moment }, comments: {type: Boolean, default: true}, layout: {type: String, default: 'page'}, _content: {type: String, default: ''}, source: {type: String, required: true}, path: {type: String, required: true}, raw: {type: String, default: ''}, content: {type: String}, excerpt: {type: String}, more: {type: String} }); Page.virtual('permalink').get(function() { return full_url_for.call(ctx, this.path); }); Page.virtual('full_source').get(function() { return join(ctx.source_dir, this.source || ''); }); return Page; }; ================================================ FILE: lib/models/post.ts ================================================ import warehouse from 'warehouse'; import moment from 'moment'; import { extname, join, sep } from 'path'; import Promise from 'bluebird'; import Moment from './types/moment'; import { full_url_for, Cache } from 'hexo-util'; import type Hexo from '../hexo'; import type { CategorySchema, PostCategorySchema, PostSchema } from '../types'; function pickID(data: PostSchema | PostCategorySchema) { return data._id; } function removeEmptyTag(tags: string[]) { return tags.filter(tag => tag != null && tag !== '').map(tag => `${tag}`); } const tagsGetterCache = new Cache(); export = (ctx: Hexo) => { const Post = new warehouse.Schema({ id: String, title: {type: String, default: ''}, date: { type: Moment, default: moment }, updated: { type: Moment }, comments: {type: Boolean, default: true}, layout: {type: String, default: 'post'}, _content: {type: String, default: ''}, source: {type: String, required: true}, slug: {type: String, required: true}, photos: [String], raw: {type: String, default: ''}, published: {type: Boolean, default: true}, content: {type: String}, excerpt: {type: String}, more: {type: String} }); Post.virtual('path').get(function() { const path = ctx.execFilterSync('post_permalink', this, {context: ctx}); return typeof path === 'string' ? path : ''; }); Post.virtual('permalink').get(function() { return full_url_for.call(ctx, this.path); }); Post.virtual('full_source').get(function() { return join(ctx.source_dir, this.source || ''); }); Post.virtual('asset_dir').get(function() { const src = this.full_source; return src.substring(0, src.length - extname(src).length) + sep; }); Post.virtual('tags').get(function() { return tagsGetterCache.apply(this._id, () => { const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; const Tag = ctx.model('Tag'); const ids = ReadOnlyPostTag.find({post_id: this._id}).map(item => item.tag_id); return Tag.find({_id: {$in: ids}}); }); }); Post.method('notPublished', function() { // The same condition as ctx._bindLocals return (!ctx.config.future && this.date.valueOf() > Date.now()) || (!ctx._showDrafts() && this.published === false); }); Post.method('setTags', function(tags: string[]) { if (this.notPublished()) { // Ignore tags of draft posts // If the post is unpublished then the tag needs to be removed, thus the function cannot be returned early here tags = []; } tagsGetterCache.flush(); tags = removeEmptyTag(tags); const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; const PostTag = ctx.model('PostTag'); const Tag = ctx.model('Tag'); const id = this._id; const existed = ReadOnlyPostTag.find({post_id: id}).map(pickID); return Promise.map(tags, tag => { // Find the tag by name const data = Tag.findOne({name: tag}, {lean: true}); if (data) return data; // Insert the tag if not exist return Tag.insert({name: tag}).catch(err => { // Try to find the tag again. Throw the error if not found const data = Tag.findOne({name: tag}, {lean: true}); if (data) return data; throw err; }); }).map(tag => { // Find the reference const ref = ReadOnlyPostTag.findOne({post_id: id, tag_id: tag._id}); if (ref) return ref; // Insert the reference if not exist return PostTag.insert({ post_id: id, tag_id: tag._id }); }).then(tags => { // Remove old tags const deleted = existed.filter(item => !tags.map(pickID).includes(item)); return deleted; }).map(tag => PostTag.removeById(tag)); }); Post.virtual('categories').get(function() { const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; const Category = ctx.model('Category'); const ids = ReadOnlyPostCategory.find({post_id: this._id}).map(item => item.category_id); return Category.find({_id: {$in: ids}}); }); Post.method('setCategories', function(cats: (string | string[])[]) { if (this.notPublished()) { cats = []; } // Remove empty categories, preserving hierarchies cats = cats.filter(cat => { return Array.isArray(cat) || (cat != null && cat !== ''); }).map(cat => { return Array.isArray(cat) ? removeEmptyTag(cat) : `${cat}`; }); const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; const PostCategory = ctx.model('PostCategory'); const Category = ctx.model('Category'); const id = this._id; const allIds: string[] = []; const existed = ReadOnlyPostCategory.find({post_id: id}).map(pickID); const hasHierarchy = cats.filter(Array.isArray).length > 0; // Add a hierarchy of categories const addHierarchy = (catHierarchy: string | string[]) => { const parentIds = []; if (!Array.isArray(catHierarchy)) catHierarchy = [catHierarchy]; // Don't use "Promise.map". It doesn't run in series. // MUST USE "Promise.each". return Promise.each(catHierarchy, (cat, i) => { // Find the category by name const data: CategorySchema = Category.findOne({ name: cat, parent: i ? parentIds[i - 1] : {$exists: false} }, {lean: true}); if (data) { allIds.push(data._id); parentIds.push(data._id); return data; } // Insert the category if not exist const obj: {name: string, parent?: string} = {name: cat}; if (i) obj.parent = parentIds[i - 1]; return Category.insert(obj).catch(err => { // Try to find the category again. Throw the error if not found const data: CategorySchema = Category.findOne({ name: cat, parent: i ? parentIds[i - 1] : {$exists: false} }, {lean: true}); if (data) return data; throw err; }).then((data: CategorySchema) => { allIds.push(data._id); parentIds.push(data._id); return data; }); }); }; return (hasHierarchy ? Promise.each(cats, addHierarchy) : Promise.resolve(addHierarchy(cats as string[])) ).then(() => allIds).map(catId => { // Find the reference const ref: PostCategorySchema = ReadOnlyPostCategory.findOne({post_id: id, category_id: catId}); if (ref) return ref; // Insert the reference if not exist return PostCategory.insert({ post_id: id, category_id: catId }); }).then((postCats: PostCategorySchema[]) => // Remove old categories existed.filter(item => !postCats.map(pickID).includes(item))).map(cat => PostCategory.removeById(cat)); }); // Remove PostTag references Post.pre('remove', (data: PostSchema) => { const PostTag = ctx.model('PostTag'); return PostTag.remove({post_id: data._id}); }); // Remove PostCategory references Post.pre('remove', (data: PostSchema) => { const PostCategory = ctx.model('PostCategory'); return PostCategory.remove({post_id: data._id}); }); // Remove assets Post.pre('remove', (data: PostSchema) => { const PostAsset = ctx.model('PostAsset'); return PostAsset.remove({post: data._id}); }); return Post; }; ================================================ FILE: lib/models/post_asset.ts ================================================ import warehouse from 'warehouse'; import { join, posix } from 'path'; import type Hexo from '../hexo'; import type { PostAssetSchema } from '../types'; export = (ctx: Hexo) => { const PostAsset = new warehouse.Schema({ _id: {type: String, required: true}, slug: {type: String, required: true}, modified: {type: Boolean, default: true}, post: {type: warehouse.Schema.Types.CUID, ref: 'Post'}, renderable: {type: Boolean, default: true} }); PostAsset.virtual('path').get(function() { const Post = ctx.model('Post'); const post = Post.findById(this.post); if (!post) return; // PostAsset.path is file path relative to `public_dir` // no need to urlescape, #1562 // strip /\.html?$/ extensions on permalink, #2134 // Use path.posix.join to avoid path.join introducing unwanted backslashes on Windows. return posix.join(post.path.replace(/\.html?$/, ''), this.slug); }); PostAsset.virtual('source').get(function() { return join(ctx.base_dir, this._id); }); return PostAsset; }; ================================================ FILE: lib/models/post_category.ts ================================================ import warehouse from 'warehouse'; import type Hexo from '../hexo'; import { PostCategorySchema } from '../types'; export = (ctx: Hexo) => { const PostCategory = new warehouse.Schema({ post_id: {type: warehouse.Schema.Types.CUID, ref: 'Post'}, category_id: {type: warehouse.Schema.Types.CUID, ref: 'Category'} }); PostCategory.pre('save', data => { ctx._binaryRelationIndex.post_category.removeHook(data); return data; }); PostCategory.post('save', data => { ctx._binaryRelationIndex.post_category.saveHook(data); return data; }); PostCategory.pre('remove', data => { ctx._binaryRelationIndex.post_category.removeHook(data); return data; }); return PostCategory; }; ================================================ FILE: lib/models/post_tag.ts ================================================ import warehouse from 'warehouse'; import type Hexo from '../hexo'; import { PostTagSchema } from '../types'; export = (ctx: Hexo) => { const PostTag = new warehouse.Schema({ post_id: {type: warehouse.Schema.Types.CUID, ref: 'Post'}, tag_id: {type: warehouse.Schema.Types.CUID, ref: 'Tag'} }); PostTag.pre('save', data => { ctx._binaryRelationIndex.post_tag.removeHook(data); return data; }); PostTag.post('save', data => { ctx._binaryRelationIndex.post_tag.saveHook(data); return data; }); PostTag.pre('remove', data => { ctx._binaryRelationIndex.post_tag.removeHook(data); return data; }); return PostTag; }; ================================================ FILE: lib/models/tag.ts ================================================ import warehouse from 'warehouse'; import { slugize, full_url_for } from 'hexo-util'; const { hasOwnProperty: hasOwn } = Object.prototype; import type Hexo from '../hexo'; import type { TagSchema } from '../types'; export = (ctx: Hexo) => { const Tag = new warehouse.Schema({ name: {type: String, required: true} }); Tag.virtual('slug').get(function() { const map = ctx.config.tag_map || {}; let name = this.name; if (!name) return; if (Reflect.apply(hasOwn, map, [name])) { name = map[name] || name; } return slugize(name, {transform: ctx.config.filename_case}); }); Tag.virtual('path').get(function() { let tagDir = ctx.config.tag_dir; if (!tagDir.endsWith('/')) tagDir += '/'; return `${tagDir + this.slug}/`; }); Tag.virtual('permalink').get(function() { return full_url_for.call(ctx, this.path); }); Tag.virtual('posts').get(function() { const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; const ids = ReadOnlyPostTag.find({tag_id: this._id}).map(item => item.post_id); return ctx.locals.get('posts').find({ _id: {$in: ids} }); }); Tag.virtual('length').get(function() { // Note: this.posts.length is also working // But it's slow because `find` has to iterate over all posts const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; return ReadOnlyPostTag.find({tag_id: this._id}).length; }); // Check whether a tag exists Tag.pre('save', (data: TagSchema) => { const { name } = data; if (!name) return; const Tag = ctx.model('Tag'); const tag = Tag.findOne({name}, {lean: true}); if (tag) { throw new Error(`Tag \`${name}\` has already existed!`); } }); // Remove PostTag references Tag.pre('remove', (data: TagSchema) => { const PostTag = ctx.model('PostTag'); return PostTag.remove({tag_id: data._id}); }); return Tag; }; ================================================ FILE: lib/models/types/moment.ts ================================================ import warehouse from 'warehouse'; import { moment } from '../../plugins/helper/date'; // It'll pollute the moment module. // declare module 'moment' { // export default interface Moment extends moment.Moment { // _d: Date; // // eslint-disable-next-line semi // } // } class SchemaTypeMoment extends warehouse.SchemaType { public options: any; constructor(name, options = {}) { super(name, options); } cast(value?, data?) { value = super.cast(value, data); if (value == null) return value; return toMoment(value); } validate(value, data?) { value = super.validate(value, data); if (value == null) return value; value = toMoment(value); if (!value.isValid()) { throw new Error('`' + value + '` is not a valid date!'); } return value; } match(value, query, _data?) { return value ? value.valueOf() === query.valueOf() : false; } compare(a?, b?) { if (a) { if (b) return a - b; return 1; } if (b) return -1; return 0; } parse(value?) { if (value) return toMoment(value); } value(value?, _data?) { // FIXME: Same as above. Also a dirty hack. return value ? value._d.toISOString() : value; } q$day(value, query, _data?) { return value ? value.date() === query : false; } q$month(value, query, _data?) { return value ? value.month() === query : false; } q$year(value, query, _data?) { return value ? value.year() === query : false; } u$inc(value, update, _data?) { if (!value) return value; return value.add(update); } u$dec(value, update, _data?) { if (!value) return value; return value.subtract(update); } } function toMoment(value) { // FIXME: Something is wrong when using a moment instance. I try to get the // original date object and create a new moment object again. if (moment.isMoment(value)) return moment((value as any)._d); return moment(value); } export = SchemaTypeMoment; ================================================ FILE: lib/plugins/console/clean.ts ================================================ import Promise from 'bluebird'; import { exists, unlink, rmdir } from 'hexo-fs'; import type Hexo from '../../hexo'; function cleanConsole(this: Hexo): Promise<[void, void, any]> { return Promise.all([ deleteDatabase(this), deletePublicDir(this), this.execFilter('after_clean', null, {context: this}) ]); } function deleteDatabase(ctx: Hexo): Promise { const dbPath = ctx.database.options.path; return exists(dbPath).then(exist => { if (!exist) return; return unlink(dbPath).then(() => { ctx.log.info('Deleted database.'); }); }); } function deletePublicDir(ctx: Hexo): Promise { const publicDir = ctx.public_dir; return exists(publicDir).then(exist => { if (!exist) return; return rmdir(publicDir).then(() => { ctx.log.info('Deleted public folder.'); }); }); } export = cleanConsole; ================================================ FILE: lib/plugins/console/config.ts ================================================ import yaml from 'js-yaml'; import { exists, writeFile } from 'hexo-fs'; import { extname } from 'path'; import Promise from 'bluebird'; import type Hexo from '../../hexo'; interface ConfigArgs { _: string[] [key: string]: any } function configConsole(this: Hexo, args: ConfigArgs): Promise { const key = args._[0]; let value = args._[1]; if (!key) { console.log(this.config); return Promise.resolve(); } if (!value) { value = getProperty(this.config, key); if (value) console.log(value); return Promise.resolve(); } const configPath = this.config_path; const ext = extname(configPath); return exists(configPath).then(exist => { if (!exist) return {}; return this.render.render({path: configPath}); }).then(config => { if (!config) config = {}; setProperty(config, key, castValue(value)); const result = ext === '.json' ? JSON.stringify(config) : yaml.dump(config); return writeFile(configPath, result); }); } function getProperty(obj: object, key: string): any { const split = key.split('.'); let result = obj[split[0]]; for (let i = 1, len = split.length; i < len; i++) { result = result[split[i]]; } return result; } function setProperty(obj: object, key: string, value: any): void { const split = key.split('.'); let cursor = obj; const lastKey = split.pop(); for (let i = 0, len = split.length; i < len; i++) { const name = split[i]; cursor[name] = cursor[name] || {}; cursor = cursor[name]; } cursor[lastKey] = value; } function castValue(value: string): any { switch (value) { case 'true': return true; case 'false': return false; case 'null': return null; case 'undefined': return undefined; } const num = Number(value); if (!isNaN(num)) return num; return value; } export = configConsole; ================================================ FILE: lib/plugins/console/deploy.ts ================================================ import { exists } from 'hexo-fs'; import { underline, magenta } from 'picocolors'; import type Hexo from '../../hexo'; import type Promise from 'bluebird'; interface DeployArgs { _?: string[] g?: boolean generate?: boolean [key: string]: any } function deployConsole(this: Hexo, args: DeployArgs): Promise { let config = this.config.deploy; const deployers = this.extend.deployer.list(); if (!config) { let help = ''; help += 'You should configure deployment settings in _config.yml first!\n\n'; help += 'Available deployer plugins:\n'; help += ` ${Object.keys(deployers).join(', ')}\n\n`; help += `For more help, you can check the online docs: ${underline('https://hexo.io/')}`; console.log(help); return; } let promise: Promise; if (args.g || args.generate) { promise = this.call('generate', args); } else { promise = exists(this.public_dir).then(exist => { if (!exist) return this.call('generate', args); }); } return promise.then(() => { this.emit('deployBefore'); if (!Array.isArray(config)) config = [config]; return config; }).each(item => { if (!item.type) return; const { type } = item; if (!deployers[type]) { this.log.error('Deployer not found: %s', magenta(type)); return; } this.log.info('Deploying: %s', magenta(type)); return (Reflect.apply(deployers[type], this, [{ ...item, ...args }]) as any).then(() => { this.log.info('Deploy done: %s', magenta(type)); }); }).then(() => { this.emit('deployAfter'); }); } export = deployConsole; ================================================ FILE: lib/plugins/console/generate.ts ================================================ import { exists, writeFile, unlink, stat, mkdirs } from 'hexo-fs'; import { join } from 'path'; import Promise from 'bluebird'; import prettyHrtime from 'pretty-hrtime'; import { cyan, magenta } from 'picocolors'; import tildify from 'tildify'; import { PassThrough, type Readable } from 'stream'; import { createSha1Hash } from 'hexo-util'; import type Hexo from '../../hexo'; import type Router from '../../hexo/router'; interface GenerateArgs { f?: boolean force?: boolean b?: boolean bail?: boolean c?: string concurrency?: string w?: boolean watch?: boolean d?: boolean deploy?: boolean [key: string]: any } class Generator { public context: Hexo; public force: boolean; public bail: boolean; public concurrency: string; public watch: boolean; public deploy: boolean; public generatingFiles: Set; public start: [number, number]; public args: GenerateArgs; constructor(ctx: Hexo, args: GenerateArgs) { this.context = ctx; this.force = args.f || args.force; this.bail = args.b || args.bail; this.concurrency = args.c || args.concurrency; this.watch = args.w || args.watch; this.deploy = args.d || args.deploy; this.generatingFiles = new Set(); this.start = process.hrtime(); this.args = args; } generateFile(path: string): Promise { const publicDir = this.context.public_dir; const { generatingFiles } = this; const { route } = this.context; // Skip if the file is generating if (generatingFiles.has(path)) return Promise.resolve(); // Lock the file generatingFiles.add(path); let promise: Promise; if (this.force) { promise = this.writeFile(path, true); } else { const dest = join(publicDir, path); promise = exists(dest).then(exist => { if (!exist) return this.writeFile(path, true); if (route.isModified(path)) return this.writeFile(path); }); } return promise.finally(() => { // Unlock the file generatingFiles.delete(path); }); } writeFile(path: string, force?: boolean): Promise { const { route, log } = this.context; const publicDir = this.context.public_dir; const Cache = this.context.model('Cache'); const dataStream = this.wrapDataStream(route.get(path)); const buffers = []; const hasher = createSha1Hash(); const finishedPromise = new Promise((resolve, reject) => { dataStream.once('error', reject); dataStream.once('end', resolve); }); // Get data => Cache data => Calculate hash dataStream.on('data', chunk => { buffers.push(chunk); hasher.update(chunk); }); return finishedPromise.then(() => { const dest = join(publicDir, path); const cacheId = `public/${path}`; const cache = Cache.findById(cacheId); const hash = hasher.digest('hex'); // Skip generating if hash is unchanged if (!force && cache && cache.hash === hash) { return; } // Save new hash to cache return Cache.save({ _id: cacheId, hash }).then(() => // Write cache data to public folder writeFile(dest, Buffer.concat(buffers))).then(() => { log.info('Generated: %s', magenta(path)); return true; }); }); } deleteFile(path: string): Promise { const { log } = this.context; const publicDir = this.context.public_dir; const dest = join(publicDir, path); return unlink(dest).then(() => { log.info('Deleted: %s', magenta(path)); }, err => { // Skip ENOENT errors (file was deleted) if (err && err.code === 'ENOENT') return; throw err; }); } wrapDataStream(dataStream: ReturnType): Readable { const { log } = this.context; // Pass original stream with all data and errors if (this.bail) { return dataStream; } // Pass all data, but don't populate errors dataStream.on('error', err => { log.error(err); }); return dataStream.pipe(new PassThrough()); } firstGenerate(): Promise { const { concurrency } = this; const { route, log } = this.context; const publicDir = this.context.public_dir; const Cache = this.context.model('Cache'); // Show the loading time const interval = prettyHrtime(process.hrtime(this.start)); log.info('Files loaded in %s', cyan(interval)); // Reset the timer for later usage this.start = process.hrtime(); // Check the public folder return stat(publicDir).then(stats => { if (!stats.isDirectory()) { throw new Error(`${magenta(tildify(publicDir))} is not a directory`); } }).catch(err => { // Create public folder if not exists if (err && err.code === 'ENOENT') { return mkdirs(publicDir); } throw err; }).then(() => { const task = (fn, path) => () => fn.call(this, path); const doTask = fn => fn(); const routeList = route.list(); const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7)); const tasks = publicFiles.filter(path => !routeList.includes(path)) // Clean files .map(path => task(this.deleteFile, path)) // Generate files .concat(routeList.map(path => task(this.generateFile, path))); return Promise.all(Promise.map(tasks, doTask, { concurrency: parseFloat(concurrency || 'Infinity') })); }).then(result => { const interval = prettyHrtime(process.hrtime(this.start)); const count = result.filter(Boolean).length; log.info('%d files generated in %s', count.toString(), cyan(interval)); }); } execWatch(): Promise { const { route, log } = this.context; return this.context.watch().then(() => this.firstGenerate()).then(() => { log.info('Hexo is watching for file changes. Press Ctrl+C to exit.'); // Watch changes of the route route.on('update', path => { const modified = route.isModified(path); if (!modified) return; this.generateFile(path); }).on('remove', path => { this.deleteFile(path); }); }); } execDeploy() { return this.context.call('deploy', this.args); } } function generateConsole(this: Hexo, args: GenerateArgs = {}): Promise { const generator = new Generator(this, args); if (generator.watch) { return generator.execWatch(); } return this.load().then(() => generator.firstGenerate()).then(() => { if (generator.deploy) { return generator.execDeploy(); } }); } export = generateConsole; ================================================ FILE: lib/plugins/console/index.ts ================================================ import type Hexo from '../../hexo'; export = function(ctx: Hexo) { const { console } = ctx.extend; console.register('clean', 'Remove generated files and cache.', require('./clean')); console.register('config', 'Get or set configurations.', { usage: '[name] [value]', arguments: [ {name: 'name', desc: 'Setting name. Leave it blank if you want to show all configurations.'}, {name: 'value', desc: 'New value of a setting. Leave it blank if you just want to show a single configuration.'} ] }, require('./config')); console.register('deploy', 'Deploy your website.', { options: [ {name: '--setup', desc: 'Setup without deployment'}, {name: '-g, --generate', desc: 'Generate before deployment'} ] }, require('./deploy')); console.register('generate', 'Generate static files.', { options: [ {name: '-d, --deploy', desc: 'Deploy after generated'}, {name: '-f, --force', desc: 'Force regenerate'}, {name: '-w, --watch', desc: 'Watch file changes'}, {name: '-b, --bail', desc: 'Raise an error if any unhandled exception is thrown during generation'}, {name: '-c, --concurrency', desc: 'Maximum number of files to be generated in parallel. Default is infinity'} ] }, require('./generate')); console.register('list', 'List the information of the site', { desc: 'List the information of the site.', usage: '', arguments: [ {name: 'type', desc: 'Available types: page, post, route, tag, category'} ] }, require('./list')); console.register('migrate', 'Migrate your site from other system to Hexo.', { init: true, usage: '', arguments: [ {name: 'type', desc: 'Migrator type.'} ] }, require('./migrate')); console.register('new', 'Create a new post.', { usage: '[layout] ', arguments: [ {name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'}, {name: 'title', desc: 'Post title. Wrap it with quotations to escape.'} ], options: [ {name: '-r, --replace', desc: 'Replace the current post if existed.'}, {name: '-s, --slug', desc: 'Post slug. Customize the URL of the post.'}, {name: '-p, --path', desc: 'Post path. Customize the path of the post.'} ] }, require('./new')); console.register('publish', 'Moves a draft post from _drafts to _posts folder.', { usage: '[layout] <filename>', arguments: [ {name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'}, {name: 'filename', desc: 'Draft filename. "hello-world" for example.'} ] }, require('./publish')); console.register('render', 'Render files with renderer plugins.', { init: true, desc: 'Render files with renderer plugins (e.g. Markdown) and save them at the specified path.', usage: '<file1> [file2] ...', options: [ {name: '--output', desc: 'Output destination. Result will be printed in the terminal if the output destination is not set.'}, {name: '--engine', desc: 'Specify render engine'}, {name: '--pretty', desc: 'Prettify JSON output'} ] }, require('./render')); } ================================================ FILE: lib/plugins/console/list/category.ts ================================================ import { underline } from 'picocolors'; import table from 'fast-text-table'; import { stringLength } from './common'; import type Hexo from '../../../hexo'; import type { CategorySchema } from '../../../types'; import type Model from 'warehouse/dist/model'; import type Document from 'warehouse/dist/document'; function listCategory(this: Hexo): void { const categories: Model<CategorySchema> = this.model('Category'); const data = categories.sort({name: 1}).map((cate: Document<CategorySchema> & CategorySchema) => [cate.name, String(cate.length)]); // Table header const header = ['Name', 'Posts'].map(str => underline(str)); data.unshift(header); const t = table(data, { align: ['l', 'r'], stringLength }); console.log(t); if (data.length === 1) console.log('No categories.'); } export = listCategory; ================================================ FILE: lib/plugins/console/list/common.ts ================================================ import strip from 'strip-ansi'; export function stringLength(str: string): number { str = strip(str); const len = str.length; let result = len; // Detect double-byte characters for (let i = 0; i < len; i++) { if (str.charCodeAt(i) > 255) { result++; } } return result; } ================================================ FILE: lib/plugins/console/list/index.ts ================================================ import abbrev from 'abbrev'; import page from './page'; import post from './post'; import route from './route'; import tag from './tag'; import category from './category'; import type Hexo from '../../../hexo'; import type Promise from 'bluebird'; interface ListArgs { _: string[] } const store = { page, post, route, tag, category }; const alias = abbrev(Object.keys(store)); function listConsole(this: Hexo, args: ListArgs): Promise<void> { const type = args._.shift(); // Display help message if user didn't input any arguments if (!type || !alias[type]) { return this.call('help', {_: ['list']}); } return this.load().then(() => Reflect.apply(store[alias[type]], this, [args])); } export = listConsole; ================================================ FILE: lib/plugins/console/list/page.ts ================================================ import { magenta, underline, gray } from 'picocolors'; import table from 'fast-text-table'; import { stringLength } from './common'; import type Hexo from '../../../hexo'; import type { PageSchema } from '../../../types'; import type Model from 'warehouse/dist/model'; import type Document from 'warehouse/dist/document'; function listPage(this: Hexo): void { const Page: Model<PageSchema> = this.model('Page'); const data = Page.sort({date: 1}).map((page: Document<PageSchema> & PageSchema) => { const date = page.date.format('YYYY-MM-DD'); return [gray(date), page.title, magenta(page.source)]; }); // Table header const header = ['Date', 'Title', 'Path'].map(str => underline(str)); data.unshift(header); const t = table(data, { stringLength }); console.log(t); if (data.length === 1) console.log('No pages.'); } export = listPage; ================================================ FILE: lib/plugins/console/list/post.ts ================================================ import { gray, magenta, underline } from 'picocolors'; import table from 'fast-text-table'; import { stringLength } from './common'; import type Hexo from '../../../hexo'; import type { PostSchema } from '../../../types'; import type Model from 'warehouse/dist/model'; import type Document from 'warehouse/dist/document'; function mapName(item: any): string { return item.name; } function listPost(this: Hexo): void { const Post: Model<PostSchema> = this.model('Post'); const data = Post.sort({published: -1, date: 1}).map((post: Document<PostSchema> & PostSchema) => { const date = post.published ? post.date.format('YYYY-MM-DD') : 'Draft'; const tags = post.tags.map(mapName); const categories = post.categories.map(mapName); return [ gray(date), post.title, magenta(post.source), categories.join(', '), tags.join(', ') ]; }); // Table header const header = ['Date', 'Title', 'Path', 'Category', 'Tags'].map(str => underline(str)); data.unshift(header); const t = table(data, { stringLength }); console.log(t); if (data.length === 1) console.log('No posts.'); } export = listPost; ================================================ FILE: lib/plugins/console/list/route.ts ================================================ import archy from 'fast-archy'; import type Hexo from '../../../hexo'; function listRoute(this: Hexo): void { const routes = this.route.list().sort(); const tree = buildTree(routes); const nodes = buildNodes(tree); const s = archy({ label: `Total: ${routes.length}`, nodes }); console.log(s); } function buildTree(routes: string[]) { const obj: Record<string, any> = {}; let cursor: typeof obj; for (let i = 0, len = routes.length; i < len; i++) { const item = routes[i].split('/'); cursor = obj; for (let j = 0, lenj = item.length; j < lenj; j++) { const seg = item[j]; cursor[seg] = cursor[seg] || {}; cursor = cursor[seg]; } } return obj; } function buildNodes(tree: Record<string, any>) { const nodes = []; for (const [key, item] of Object.entries(tree)) { if (Object.keys(item).length) { nodes.push({ label: key, nodes: buildNodes(item) }); } else { nodes.push(key); } } return nodes; } export = listRoute; ================================================ FILE: lib/plugins/console/list/tag.ts ================================================ import { magenta, underline } from 'picocolors'; import table from 'fast-text-table'; import { stringLength } from './common'; import type Hexo from '../../../hexo'; import type { TagSchema } from '../../../types'; import type Model from 'warehouse/dist/model'; import type Document from 'warehouse/dist/document'; function listTag(this: Hexo): void { const Tag: Model<TagSchema> = this.model('Tag'); const data = Tag.sort({name: 1}).map((tag: Document<TagSchema> & TagSchema) => [tag.name, String(tag.length), magenta(tag.path)]); // Table header const header = ['Name', 'Posts', 'Path'].map(str => underline(str)); data.unshift(header); const t = table(data, { align: ['l', 'r', 'l'], stringLength }); console.log(t); if (data.length === 1) console.log('No tags.'); } export = listTag; ================================================ FILE: lib/plugins/console/migrate.ts ================================================ import { underline, magenta } from 'picocolors'; import type Hexo from '../../hexo'; interface MigrateArgs { _: string[] [key: string]: any } function migrateConsole(this: Hexo, args: MigrateArgs): Promise<any> { // Display help message if user didn't input any arguments if (!args._.length) { return this.call('help', {_: ['migrate']}); } const type = args._.shift(); const migrators = this.extend.migrator.list(); if (!migrators[type]) { let help = ''; help += `${magenta(type)} migrator plugin is not installed.\n\n`; help += 'Installed migrator plugins:\n'; help += ` ${Object.keys(migrators).join(', ')}\n\n`; help += `For more help, you can check the online docs: ${underline('https://hexo.io/')}`; console.log(help); return; } return Reflect.apply(migrators[type], this, [args]); } export = migrateConsole; ================================================ FILE: lib/plugins/console/new.ts ================================================ import tildify from 'tildify'; import { magenta } from 'picocolors'; import { basename } from 'path'; import Hexo from '../../hexo'; import type Promise from 'bluebird'; const reservedKeys = { _: true, title: true, layout: true, slug: true, s: true, path: true, p: true, replace: true, r: true, // Global options config: true, debug: true, safe: true, silent: true }; interface NewArgs { _?: string[] p?: string path?: string s?: string slug?: string r?: boolean replace?: boolean [key: string]: any } function newConsole(this: Hexo, args: NewArgs): Promise<void> { const path = args.p || args.path; let title: string; if (args._.length) { title = args._.pop(); } else if (path) { // Default title title = basename(path); } else { // Display help message if user didn't input any arguments return this.call('help', { _: ['new'] }); } const data = { title, layout: args._.length ? args._[0] : this.config.default_layout, slug: args.s || args.slug, path }; const keys = Object.keys(args); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!reservedKeys[key]) data[key] = args[key]; } return this.post.create(data, args.r || args.replace).then(post => { this.log.info('Created: %s', magenta(tildify(post.path))); }); } export = newConsole; ================================================ FILE: lib/plugins/console/publish.ts ================================================ import tildify from 'tildify'; import { magenta } from 'picocolors'; import type Hexo from '../../hexo'; import type Promise from 'bluebird'; interface PublishArgs { _: string[] r?: boolean replace?: boolean [key: string]: any } function publishConsole(this: Hexo, args: PublishArgs): Promise<void> { // Display help message if user didn't input any arguments if (!args._.length) { return this.call('help', {_: ['publish']}); } return this.post.publish({ slug: args._.pop(), layout: args._.length ? args._[0] : this.config.default_layout }, args.r || args.replace).then(post => { this.log.info('Published: %s', magenta(tildify(post.path))); }); } export = publishConsole; ================================================ FILE: lib/plugins/console/render.ts ================================================ import { resolve } from 'path'; import tildify from 'tildify'; import prettyHrtime from 'pretty-hrtime'; import { writeFile } from 'hexo-fs'; import { cyan, magenta } from 'picocolors'; import type Hexo from '../../hexo'; import type Promise from 'bluebird'; interface RenderArgs { _: string[] o?: string output?: string pretty?: boolean engine?: string [key: string]: any } function renderConsole(this: Hexo, args: RenderArgs): Promise<void> { // Display help message if user didn't input any arguments if (!args._.length) { return this.call('help', {_: 'render'}); } const baseDir = this.base_dir; const src = resolve(baseDir, args._[0]); const output = args.o || args.output; const start = process.hrtime(); const { log } = this; return this.render.render({ path: src, engine: args.engine }).then(result => { if (typeof result === 'object') { if (args.pretty) { result = JSON.stringify(result, null, ' '); } else { result = JSON.stringify(result); } } if (!output) return console.log(result); const dest = resolve(baseDir, output); const interval = prettyHrtime(process.hrtime(start)); log.info('Rendered in %s: %s -> %s', cyan(interval), magenta(tildify(src)), magenta(tildify(dest))); return writeFile(dest, result); }); } export = renderConsole; ================================================ FILE: lib/plugins/filter/after_post_render/excerpt.ts ================================================ import type { RenderData } from '../../../types'; const rExcerpt = /<!-- ?more ?-->/i; function excerptFilter(data: RenderData): void { const { content } = data; if (typeof data.excerpt !== 'undefined') { data.more = content; } else if (rExcerpt.test(content)) { data.content = content.replace(rExcerpt, (match, index) => { data.excerpt = content.substring(0, index).trim(); data.more = content.substring(index + match.length).trim(); return '<span id="more"></span>'; }); } else { data.excerpt = ''; data.more = content; } } export = excerptFilter; ================================================ FILE: lib/plugins/filter/after_post_render/external_link.ts ================================================ import { isExternalLink } from 'hexo-util'; import type Hexo from '../../../hexo'; import type { RenderData } from '../../../types'; let EXTERNAL_LINK_POST_ENABLED = true; const rATag = /<a(?:\s+?|\s+?[^<>]+?\s+?)href=["']((?:https?:|\/\/)[^<>"']+)["'][^<>]*>/gi; const rTargetAttr = /target=/i; const rRelAttr = /rel=/i; const rRelStrAttr = /rel=["']([^<>"']*)["']/i; function externalLinkFilter(this: Hexo, data: RenderData): void { if (!EXTERNAL_LINK_POST_ENABLED) return; const { external_link, url } = this.config; if (!external_link.enable || external_link.field !== 'post') { EXTERNAL_LINK_POST_ENABLED = false; return; } data.content = data.content.replace(rATag, (str, href) => { if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) return str; if (rRelAttr.test(str)) { str = str.replace(rRelStrAttr, (relStr, rel) => { return rel.includes('noopener') ? relStr : `rel="${rel} noopener"`; }); return str.replace('href=', 'target="_blank" href='); } return str.replace('href=', 'target="_blank" rel="noopener" href='); }); } export = externalLinkFilter; ================================================ FILE: lib/plugins/filter/after_post_render/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('after_post_render', require('./external_link')); filter.register('after_post_render', require('./excerpt')); }; ================================================ FILE: lib/plugins/filter/after_render/external_link.ts ================================================ import { isExternalLink } from 'hexo-util'; import type Hexo from '../../../hexo'; let EXTERNAL_LINK_SITE_ENABLED = true; const rATag = /<a(?:\s+?|\s+?[^<>]+?\s+?)href=["']((?:https?:|\/\/)[^<>"']+)["'][^<>]*>/gi; const rTargetAttr = /target=/i; const rRelAttr = /rel=/i; const rRelStrAttr = /rel=["']([^<>"']*)["']/i; const addNoopener = (relStr: string, rel: string) => { return rel.includes('noopener') ? relStr : `rel="${rel} noopener"`; }; function externalLinkFilter(this: Hexo, data: string): string { if (!EXTERNAL_LINK_SITE_ENABLED) return; const { external_link, url } = this.config; if (!external_link.enable || external_link.field !== 'site') { EXTERNAL_LINK_SITE_ENABLED = false; return; } let result = ''; let lastIndex = 0; let match; while ((match = rATag.exec(data)) !== null) { result += data.slice(lastIndex, match.index); const str = match[0]; const href = match[1]; if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) { result += str; } else { if (rRelAttr.test(str)) { result += str.replace(rRelStrAttr, addNoopener).replace('href=', 'target="_blank" href='); } else { result += str.replace('href=', 'target="_blank" rel="noopener" href='); } } lastIndex = rATag.lastIndex; } result += data.slice(lastIndex); return result; } export = externalLinkFilter; ================================================ FILE: lib/plugins/filter/after_render/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('after_render:html', require('./external_link')); filter.register('after_render:html', require('./meta_generator')); }; ================================================ FILE: lib/plugins/filter/after_render/meta_generator.ts ================================================ import type Hexo from '../../../hexo'; let NEED_INJECT = true; let HAS_CHECKED = false; let META_GENERATOR_TAG; function hexoMetaGeneratorInject(this: Hexo, data: string): string { if (!NEED_INJECT) return; if (!HAS_CHECKED) { HAS_CHECKED = true; if (!this.config.meta_generator || data.match(/<meta\s+(?:[^<>/]+\s)?name=['"]generator['"]/i)) { NEED_INJECT = false; return; } } META_GENERATOR_TAG = META_GENERATOR_TAG || `<meta name="generator" content="Hexo ${this.version}">`; return data.replace('</head>', `${META_GENERATOR_TAG}</head>`); } export = hexoMetaGeneratorInject; ================================================ FILE: lib/plugins/filter/before_exit/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('before_exit', require('./save_database')); }; ================================================ FILE: lib/plugins/filter/before_exit/save_database.ts ================================================ import type Hexo from '../../../hexo'; function saveDatabaseFilter(this: Hexo): Promise<void> { if (!this.env.init || !this._dbLoaded) return; return this.database.save().then(() => { this.log.debug('Database saved'); }); } export = saveDatabaseFilter; ================================================ FILE: lib/plugins/filter/before_generate/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('before_generate', require('./render_post')); }; ================================================ FILE: lib/plugins/filter/before_generate/render_post.ts ================================================ import Promise from 'bluebird'; import type Hexo from '../../../hexo'; import type Model from 'warehouse/dist/model'; function renderPostFilter(this: Hexo): Promise<[any[], any[]]> { const renderPosts = (model: Model<any>) => { const posts = model.toArray().filter(post => post.content == null); return Promise.map(posts, (post: any) => { post.content = post._content; return this.post.render(post.full_source, post).then(() => post.save()); }); }; return Promise.all([ renderPosts(this.model('Post')), renderPosts(this.model('Page')) ]); } export = renderPostFilter; ================================================ FILE: lib/plugins/filter/before_post_render/backtick_code_block.ts ================================================ import type { HighlightOptions } from '../../../extend/syntax_highlight'; import type Hexo from '../../../hexo'; import type { RenderData } from '../../../types'; const rBacktick = /^((?:(?:[^\S\r\n]*>){0,3}|[-*+]|[0-9]+\.)[^\S\r\n]*)(`{3,}|~{3,})[^\S\r\n]*((?:.*?[^`\s])?)[^\S\r\n]*\n((?:[\s\S]*?\n)?)(?:(?:[^\S\r\n]*>){0,3}[^\S\r\n]*)\2[^\S\r\n]?(\n+|$)/gm; const rAllOptions = /([^\s]+)\s+(.+?)\s+(https?:\/\/\S+|\/\S+)\s*(.+)?/; const rLangCaption = /([^\s]+)\s*(.+)?/; const rCommentEscape = /(<!--[\s\S]*?-->)/g; const rAdditionalOptions = /\s((?:line_number|line_threshold|first_line|wrap|mark|language_attr|highlight):\S+)/g; const escapeSwigTag = (str: string) => str.replace(/{/g, '{').replace(/}/g, '}'); function parseArgs(args: string) { const matches = []; let match: RegExpExecArray | null, language_attr: boolean, line_number: boolean, line_threshold: number, wrap: boolean; let enableHighlight = true; while ((match = rAdditionalOptions.exec(args)) !== null) { matches.push(match[1]); } const len = matches.length; const mark: number[] = []; let firstLine = 1; for (let i = 0; i < len; i++) { const [key, value] = matches[i].split(':'); switch (key) { case 'highlight': enableHighlight = value === 'true'; break; case 'line_number': line_number = value === 'true'; break; case 'line_threshold': if (!isNaN(Number(value))) line_threshold = +value; break; case 'first_line': if (!isNaN(Number(value))) firstLine = +value; break; case 'wrap': wrap = value === 'true'; break; case 'mark': { for (const cur of value.split(',')) { const hyphen = cur.indexOf('-'); if (hyphen !== -1) { let a = +cur.slice(0, hyphen); let b = +cur.slice(hyphen + 1); if (Number.isNaN(a) || Number.isNaN(b)) continue; if (b < a) { // switch a & b [a, b] = [b, a]; } for (; a <= b; a++) { mark.push(a); } } if (!isNaN(Number(cur))) mark.push(+cur); } break; } case 'language_attr': { language_attr = value === 'true'; break; } } } return { options: { language_attr, firstLine, line_number, line_threshold, mark, wrap }, enableHighlight, _args: args.replace(rAdditionalOptions, '') }; } export = (ctx: Hexo): (data: RenderData) => void => { return function backtickCodeBlock(data: RenderData): void { const dataContent = data.content; if ((!dataContent.includes('```') && !dataContent.includes('~~~')) || !ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return; // get all comment starts and ends const commentStarts = []; const commentEnds = []; let match: RegExpExecArray | null; rCommentEscape.lastIndex = 0; while ((match = rCommentEscape.exec(dataContent)) !== null) { commentStarts.push(match.index); commentEnds.push(match.index + match[0].length); } // notice that commentStarts and commentEnds are sorted, and commentStarts[i] < commentEnds[i], commentEnds[i] <= commentStarts[i+1] let commentIndex = 0; data.content = data.content.replace(rBacktick, ($0, start, $2, _args, _content, end, matchIndex) => { // get the start and end of the code block const codeBlockStart = matchIndex; const codeBlockEnd = matchIndex + $0.length; // check if the code block is nested in a comment while (commentIndex < commentStarts.length && commentEnds[commentIndex] <= codeBlockStart) { commentIndex++; } if (commentIndex < commentStarts.length && commentStarts[commentIndex] < codeBlockStart && commentEnds[commentIndex] > codeBlockEnd) { // the code block is nested in a comment, return escaped content directly return escapeSwigTag($0); } let content = _content.replace(/\n$/, ''); // neither highlight or prismjs is enabled, return escaped content directly. if (!ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return escapeSwigTag($0); const parsedArgs = parseArgs(_args); if (!parsedArgs.enableHighlight) return escapeSwigTag($0); _args = parsedArgs._args; // Extract language and caption of code blocks const args = _args.split('=').shift(); let lang: string, caption: string; if (args) { const match = rAllOptions.exec(args) || rLangCaption.exec(args); if (match) { lang = match[1]; if (match[2]) { caption = `<span>${match[2]}</span>`; if (match[3]) { caption += `<a href="${match[3]}">${match[4] ? match[4] : 'link'}</a>`; } } } } // PR #3765 if (start.includes('>')) { // heading of last line is already removed by the top RegExp "rBacktick" const depth = start.split('>').length - 1; const regexp = new RegExp(`^([^\\S\\r\\n]*>){0,${depth}}([^\\S\\r\\n]|$)`, 'mg'); content = content.replace(regexp, ''); } const options: HighlightOptions = { lang, caption, lines_length: content.split('\n').length, ...parsedArgs.options }; // setup line number by inline _args = _args.replace('=+', '='); // setup firstLineNumber; if (_args.includes('=')) { options.firstLineNumber = _args.split('=')[1] || 1; } content = ctx.extend.highlight.exec(ctx.config.syntax_highlighter, { context: ctx, args: [content, options] }); return start + '<hexoPostRenderCodeBlock>' + escapeSwigTag(content) + '</hexoPostRenderCodeBlock>' + end; }); }; }; ================================================ FILE: lib/plugins/filter/before_post_render/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('before_post_render', require('./backtick_code_block')(ctx)); filter.register('before_post_render', require('./titlecase')); }; ================================================ FILE: lib/plugins/filter/before_post_render/titlecase.ts ================================================ import type { RenderData } from '../../../types'; let titlecase; function titlecaseFilter(data: RenderData): void { if (!(typeof data.titlecase !== 'undefined' ? data.titlecase : this.config.titlecase) || !data.title) return; if (!titlecase) titlecase = require('titlecase'); data.title = titlecase(data.title); } export = titlecaseFilter; ================================================ FILE: lib/plugins/filter/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; require('./after_render')(ctx); require('./after_post_render')(ctx); require('./before_post_render')(ctx); require('./before_exit')(ctx); require('./before_generate')(ctx); require('./template_locals')(ctx); filter.register('new_post_path', require('./new_post_path')); filter.register('post_permalink', require('./post_permalink')); }; ================================================ FILE: lib/plugins/filter/new_post_path.ts ================================================ import { join, extname } from 'path'; import moment from 'moment'; import Promise from 'bluebird'; import { createSha1Hash, Permalink } from 'hexo-util'; import { ensurePath } from 'hexo-fs'; import type Hexo from '../../hexo'; import type { PostSchema } from '../../types'; let permalink: Permalink; const reservedKeys = { year: true, month: true, i_month: true, day: true, i_day: true, title: true, hash: true }; function newPostPathFilter(this: Hexo, data: Partial<PostSchema> = {}, replace?: boolean): Promise<string> { const sourceDir = this.source_dir; const draftDir = join(sourceDir, '_drafts'); const postDir = join(sourceDir, '_posts'); const { config } = this; const newPostName = config.new_post_name; const permalinkDefaults = config.permalink_defaults; const { path, layout, slug } = data; if (!permalink || permalink.rule !== newPostName) { permalink = new Permalink(newPostName, {}); } let target = ''; if (path) { switch (layout) { case 'page': target = join(sourceDir, path); break; case 'draft': target = join(draftDir, path); break; default: target = join(postDir, path); } } else if (slug) { switch (layout) { case 'page': target = join(sourceDir, slug, 'index'); break; case 'draft': target = join(draftDir, slug); break; default: { const date = moment(data.date || Date.now()); const keys = Object.keys(data); const hash = createSha1Hash().update(slug + date.unix().toString()) .digest('hex').slice(0, 12); const filenameData = { year: date.format('YYYY'), month: date.format('MM'), i_month: date.format('M'), day: date.format('DD'), i_day: date.format('D'), title: slug, hash }; for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!reservedKeys[key]) filenameData[key] = data[key]; } target = join(postDir, permalink.stringify({ ...permalinkDefaults, ...filenameData })); } } } else { return Promise.reject(new TypeError('Either data.path or data.slug is required!')); } if (!extname(target)) { target += extname(newPostName) || '.md'; } if (replace) { return Promise.resolve(target); } return ensurePath(target); } export = newPostPathFilter; ================================================ FILE: lib/plugins/filter/post_permalink.ts ================================================ import { createSha1Hash, Permalink, slugize } from 'hexo-util'; import { basename } from 'path'; import type Hexo from '../../hexo'; import type { PostSchema } from '../../types'; let permalink: Permalink; function postPermalinkFilter(this: Hexo, data: PostSchema): string { const { config } = this; const { id, _id, slug, title, date } = data; let { __permalink } = data; const { post_asset_folder } = config; if (__permalink) { if (post_asset_folder && !__permalink.endsWith('/') && !__permalink.endsWith('.html')) { __permalink += '/'; } if (!__permalink.startsWith('/')) return `/${__permalink}`; return __permalink; } const hash = slug && date ? createSha1Hash().update(slug + date.unix().toString()).digest('hex').slice(0, 12) : null; const meta = { id: id || _id, title: slug, name: typeof slug === 'string' ? basename(slug) : '', post_title: slugize(title, {transform: 1}), year: date.format('YYYY'), month: date.format('MM'), day: date.format('DD'), hour: date.format('HH'), minute: date.format('mm'), second: date.format('ss'), i_month: date.format('M'), i_day: date.format('D'), timestamp: date.format('X'), hash, category: config.default_category }; if (!permalink || permalink.rule !== config.permalink) { permalink = new Permalink(config.permalink, {}); } const { categories } = data; if (categories.length) { meta.category = categories.last().slug; } const keys = Object.keys(data); for (const key of keys) { if (Object.prototype.hasOwnProperty.call(meta, key)) continue; // Use Object.getOwnPropertyDescriptor to copy getters to avoid "Maximum call // stack size exceeded" error Object.defineProperty(meta, key, Object.getOwnPropertyDescriptor(data, key)); } if (config.permalink_defaults) { const keys2 = Object.keys(config.permalink_defaults); for (const key of keys2) { if (Object.prototype.hasOwnProperty.call(meta, key)) continue; meta[key] = config.permalink_defaults[key]; } } const permalink_stringify = permalink.stringify(meta); if (post_asset_folder && !permalink_stringify.endsWith('/') && !permalink_stringify.endsWith('.html')) { return `${permalink_stringify}/`; } return permalink_stringify; } export = postPermalinkFilter; ================================================ FILE: lib/plugins/filter/template_locals/i18n.ts ================================================ import { Pattern } from 'hexo-util'; import type Hexo from '../../../hexo'; import type { LocalsType } from '../../../types'; function i18nLocalsFilter(this: Hexo, locals: LocalsType): void { const { i18n } = this.theme; const { config } = this; const i18nDir = config.i18n_dir; const { page } = locals; let lang = page.lang || page.language; const i18nLanguages = i18n.list(); const i18nConfigLanguages = i18n.languages; if (!lang) { const pattern = new Pattern(`${i18nDir}/*path`); const data = pattern.match(locals.path); if (data && 'lang' in data && i18nLanguages.includes(data.lang)) { lang = data.lang; page.canonical_path = data.path; } else { // i18n.languages is always an array with at least one argument ('default') lang = i18nConfigLanguages[0]; } } page.lang = lang; page.canonical_path = page.canonical_path || locals.path; const languages = [...new Set<string>([].concat(lang, i18nConfigLanguages, i18nLanguages).filter(Boolean))]; locals.__ = i18n.__(languages); locals._p = i18n._p(languages); } export = i18nLocalsFilter; ================================================ FILE: lib/plugins/filter/template_locals/index.ts ================================================ import type Hexo from '../../../hexo'; export = (ctx: Hexo) => { const { filter } = ctx.extend; filter.register('template_locals', require('./i18n')); }; ================================================ FILE: lib/plugins/generator/asset.ts ================================================ import { exists, createReadStream } from 'hexo-fs'; import Promise from 'bluebird'; import { extname } from 'path'; import { magenta } from 'picocolors'; import type Hexo from '../../hexo'; import type { AssetSchema, BaseGeneratorReturn } from '../../types'; import type Document from 'warehouse/dist/document'; interface AssetData { modified: boolean; data?: () => any; } interface AssetGenerator extends BaseGeneratorReturn { data: { modified: boolean; data?: () => any; } } const process = (name: string, ctx: Hexo) => { return Promise.filter(ctx.model(name).toArray(), (asset: Document<AssetSchema>) => exists(asset.source).tap(exist => { if (!exist) return asset.remove(); })).map((asset: Document<AssetSchema>) => { const { source } = asset; let { path } = asset; const data: AssetData = { modified: asset.modified }; if (asset.renderable && ctx.render.isRenderable(path)) { // Replace extension name if the asset is renderable const filename = path.substring(0, path.length - extname(path).length); path = `${filename}.${ctx.render.getOutput(path)}`; data.data = () => ctx.render.render({ path: source, toString: true }).catch((err: Error) => { ctx.log.error({err}, 'Asset render failed: %s', magenta(path)); }); } else { data.data = () => createReadStream(source); } return { path, data }; }); }; function assetGenerator(this: Hexo): Promise<AssetGenerator[]> { return Promise.all([ process('Asset', this), process('PostAsset', this) ]).then(data => [].concat(...data)); } export = assetGenerator; ================================================ FILE: lib/plugins/generator/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { generator } = ctx.extend; generator.register('asset', require('./asset')); generator.register('page', require('./page')); generator.register('post', require('./post')); }; ================================================ FILE: lib/plugins/generator/page.ts ================================================ import type { BaseGeneratorReturn, PageSchema, SiteLocals } from '../../types'; import type Document from 'warehouse/dist/document'; type SimplePageGenerator = Omit<BaseGeneratorReturn, 'layout'> & { data: string }; interface NormalPageGenerator extends BaseGeneratorReturn { layout: string[]; data: PageSchema; } type PageGenerator = SimplePageGenerator | NormalPageGenerator; function pageGenerator(locals: SiteLocals): PageGenerator[] { return locals.pages.map((page: Document<PageSchema> & PageSchema) => { const { path, layout } = page; if (!layout || layout === 'false' || layout === 'off') { return { path, data: page.content }; } const layouts = ['page', 'post', 'index']; if (layout !== 'page') layouts.unshift(layout); page.__page = true; return { path, layout: layouts, data: page }; }); } export = pageGenerator; ================================================ FILE: lib/plugins/generator/post.ts ================================================ import type { BaseGeneratorReturn, PostSchema, SiteLocals } from '../../types'; import type Document from 'warehouse/dist/document'; type SimplePostGenerator = Omit<BaseGeneratorReturn, 'layout'> & { data: string }; interface NormalPostGenerator extends BaseGeneratorReturn { data: PostSchema | Document<PostSchema>; layout: string[]; } type PostGenerator = SimplePostGenerator | NormalPostGenerator; function postGenerator(locals: SiteLocals): PostGenerator[] { const posts = locals.posts.sort('-date').toArray(); const { length } = posts; return posts.map((post: Document<PostSchema>, i: number) => { const { path, layout } = post; if (!layout || layout === 'false') { return { path, data: post.content }; } if (i) post.prev = posts[i - 1]; if (i < length - 1) post.next = posts[i + 1]; const layouts = ['post', 'page', 'index']; if (layout !== 'post') layouts.unshift(layout); post.__post = true; return { path, layout: layouts, data: post }; }); } export = postGenerator; ================================================ FILE: lib/plugins/helper/css.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import moize from 'moize'; import type { LocalsType } from '../../types'; let relative_link = true; function cssHelper(this: LocalsType, ...args: any[]) { let result = '\n'; relative_link = this.config.relative_link; args.flat(Infinity).forEach(item => { if (typeof item === 'string' || item instanceof String) { let path = item; if (!path.endsWith('.css')) { path += '.css'; } result += `<link rel="stylesheet" href="${url_for.call(this, path)}">\n`; } else { const newItem = { rel: 'stylesheet', ...item }; // Custom attributes newItem.href = url_for.call(this, newItem.href); if (!newItem.href.endsWith('.css')) newItem.href += '.css'; result += htmlTag('link', newItem) + '\n'; } }); return result; } export = moize(cssHelper, { maxSize: 10, isDeepEqual: true, updateCacheForKey() { return relative_link; } }); ================================================ FILE: lib/plugins/helper/date.ts ================================================ import moment from 'moment-timezone'; const { isMoment } = moment; import moize from 'moize'; import type { LocalsType } from '../../types'; const isDate = (value: moment.MomentInput | moment.Moment): boolean => typeof value === 'object' && value instanceof Date && !isNaN(value.getTime()); function getMoment(date: moment.MomentInput | moment.Moment, lang: string, timezone: string): moment.Moment { if (date == null) date = moment(); if (!isMoment(date)) date = moment(isDate(date) ? <Date>date : new Date(<string | number>date)); lang = _toMomentLocale(lang); if (lang) date = date.locale(lang); if (timezone) date = date.tz(timezone); return date; } function toISOString(date?: string | number | Date | moment.Moment) { if (date == null) { return new Date().toISOString(); } if (date instanceof Date || isMoment(date)) { return date.toISOString(); } return new Date(date as (string | number)).toISOString(); } function dateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) { const { config } = this; const moment = getMoment(date, getLanguage(this), config.timezone); return moment.format(format || config.date_format); } function timeHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) { const { config } = this; const moment = getMoment(date, getLanguage(this), config.timezone); return moment.format(format || config.time_format); } function fullDateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) { if (format) { const moment = getMoment(date, getLanguage(this), this.config.timezone); return moment.format(format); } return `${this.date(date)} ${this.time(date)}`; } function relativeDateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput) { const { config } = this; const moment = getMoment(date, getLanguage(this), config.timezone); return moment.fromNow(); } function timeTagHelper(this: LocalsType, date?: string | number | Date | moment.Moment, format?: string) { return `<time datetime="${toISOString(date)}">${this.date(date, format)}</time>`; } function getLanguage(ctx: LocalsType) { return ctx.page.lang || ctx.page.language || ctx.config.language; } /** * Convert Hexo language code to Moment locale code. * examples: * default => en * zh-CN => zh-cn * * Moment defined locales: https://github.com/moment/moment/tree/master/locale */ function _toMomentLocale(lang?: string) { if (lang === undefined) { return undefined; } // moment.locale('') equals moment.locale('en') // moment.locale(null) equals moment.locale('en') if (!lang || lang === 'en' || lang === 'default') { return 'en'; } return lang.toLowerCase().replace('_', '-'); } export {dateHelper as date}; export {toISOString as date_xml}; export {timeHelper as time}; export {fullDateHelper as full_date}; export {relativeDateHelper as relative_date}; export {timeTagHelper as time_tag}; export {moment}; export const toMomentLocale = moize.shallow(_toMomentLocale); ================================================ FILE: lib/plugins/helper/debug.ts ================================================ import { inspect } from 'util'; // this format object as string, resolves circular reference function inspectObject(object: any, options?: any) { return inspect(object, options); } // wrapper to log to console function log(...args: any[]) { return Reflect.apply(console.log, null, args); } export {inspectObject}; export {log}; ================================================ FILE: lib/plugins/helper/favicon_tag.ts ================================================ import { url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; function faviconTagHelper(this: LocalsType, path: string) { return `<link rel="shortcut icon" href="${url_for.call(this, path)}">`; } export = faviconTagHelper; ================================================ FILE: lib/plugins/helper/feed_tag.ts ================================================ import { url_for } from 'hexo-util'; import moize from 'moize'; import type { LocalsType } from '../../types'; const feedFn = (str = '') => { if (str) return str.replace(/2$/, ''); return str; }; interface Options { title?: string; type?: string | null; } function makeFeedTag(this: LocalsType, path?: string, options: Options = {}, configFeed?: any, configTitle?: string) { const title = options.title || configTitle; if (path) { if (typeof path !== 'string') throw new TypeError('path must be a string!'); let type = feedFn(options.type); if (!type) { if (path.includes('atom')) type = 'atom'; else if (path.includes('rss')) type = 'rss'; } const typeAttr = type ? `type="application/${type}+xml"` : ''; return `<link rel="alternate" href="${url_for.call(this, path)}" title="${title}" ${typeAttr}>`; } if (configFeed) { if (configFeed.type && configFeed.path) { if (typeof configFeed.type === 'string') { return `<link rel="alternate" href="${url_for.call(this, configFeed.path)}" title="${title}" type="application/${feedFn(configFeed.type)}+xml">`; } let result = ''; for (const i in configFeed.type) { result += `<link rel="alternate" href="${url_for.call(this, configFeed.path[i])}" title="${title}" type="application/${feedFn(configFeed.type[i])}+xml">`; } return result; } } return ''; } function feedTagHelper(this: LocalsType, path?: string, options: Options = {}) { const { config } = this; return moize.deep(makeFeedTag.bind(this))(path, options, (config as any).feed, config.title); } export = feedTagHelper; ================================================ FILE: lib/plugins/helper/format.ts ================================================ import { stripHTML, wordWrap, truncate, escapeHTML } from 'hexo-util'; import titlecase from 'titlecase'; export {stripHTML as strip_html}; export {stripHTML}; export function trim(str: string) { return str.trim(); } export {titlecase}; export {wordWrap as word_wrap}; export {wordWrap}; export {truncate}; export {escapeHTML as escape_html}; export {escapeHTML}; ================================================ FILE: lib/plugins/helper/fragment_cache.ts ================================================ import { Cache } from 'hexo-util'; import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const cache = new Cache(); // reset cache for watch mode ctx.on('generateBefore', () => { cache.flush(); }); return function fragmentCache(id: string, fn: () => any) { if (this.cache) return cache.apply(id, fn); const result = fn(); cache.set(id, result); return result; }; }; ================================================ FILE: lib/plugins/helper/full_url_for.ts ================================================ import { full_url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; export = function(this: LocalsType, path?: string) { return full_url_for.call(this, path); } ================================================ FILE: lib/plugins/helper/gravatar.ts ================================================ import { gravatar } from 'hexo-util'; export = gravatar; ================================================ FILE: lib/plugins/helper/image_tag.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; interface Options { src?: string; alt?: string; class?: string | string[]; } interface Attrs { src?: string; class?: string; [key: string]: string | undefined; } function imageTagHelper(this: LocalsType, path: string, options: Options = {}) { const attrs = Object.assign({ src: url_for.call(this, path) as string }, options); if (attrs.class && Array.isArray(attrs.class)) { attrs.class = attrs.class.join(' '); } return htmlTag('img', attrs as Attrs); } export = imageTagHelper; ================================================ FILE: lib/plugins/helper/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { helper } = ctx.extend; const date = require('./date'); helper.register('date', date.date); helper.register('date_xml', date.date_xml); helper.register('time', date.time); helper.register('full_date', date.full_date); helper.register('relative_date', date.relative_date); helper.register('time_tag', date.time_tag); helper.register('moment', date.moment); helper.register('search_form', require('./search_form')); const { strip_html, trim, titlecase, word_wrap, truncate, escape_html } = require('./format'); helper.register('strip_html', strip_html); helper.register('trim', trim); helper.register('titlecase', titlecase); helper.register('word_wrap', word_wrap); helper.register('truncate', truncate); helper.register('escape_html', escape_html); helper.register('fragment_cache', require('./fragment_cache')(ctx)); helper.register('gravatar', require('./gravatar')); const is = require('./is'); helper.register('is_current', is.current); helper.register('is_home', is.home); helper.register('is_home_first_page', is.home_first_page); helper.register('is_post', is.post); helper.register('is_page', is.page); helper.register('is_archive', is.archive); helper.register('is_year', is.year); helper.register('is_month', is.month); helper.register('is_category', is.category); helper.register('is_tag', is.tag); helper.register('list_archives', require('./list_archives')); helper.register('list_categories', require('./list_categories')); helper.register('list_tags', require('./list_tags')); helper.register('list_posts', require('./list_posts')); helper.register('meta_generator', require('./meta_generator')); helper.register('open_graph', require('./open_graph')); helper.register('number_format', require('./number_format')); helper.register('paginator', require('./paginator')); helper.register('partial', require('./partial')(ctx)); helper.register('markdown', require('./markdown')); helper.register('render', require('./render')(ctx)); helper.register('css', require('./css')); helper.register('js', require('./js')); helper.register('link_to', require('./link_to')); helper.register('mail_to', require('./mail_to')); helper.register('image_tag', require('./image_tag')); helper.register('favicon_tag', require('./favicon_tag')); helper.register('feed_tag', require('./feed_tag')); const tagcloud = require('./tagcloud'); helper.register('tagcloud', tagcloud); helper.register('tag_cloud', tagcloud); helper.register('toc', require('./toc')); helper.register('relative_url', require('./relative_url')); helper.register('url_for', require('./url_for')); helper.register('full_url_for', require('./full_url_for')); const debug = require('./debug'); helper.register('inspect', debug.inspectObject); helper.register('log', debug.log); }; ================================================ FILE: lib/plugins/helper/is.ts ================================================ import type { LocalsType } from '../../types'; function isCurrentHelper(this: LocalsType, path = '/', strict: boolean) { const currentPath = this.path.replace(/^[^/].*/, '/$&'); if (strict) { if (path.endsWith('/')) path += 'index.html'; path = path.replace(/^[^/].*/, '/$&'); return currentPath === path; } path = path.replace(/\/index\.html$/, '/'); if (path === '/') return currentPath === '/index.html'; path = path.replace(/^[^/].*/, '/$&'); return currentPath.startsWith(path); } function isHomeHelper() { return Boolean(this.page.__index); } function isHomeFirstPageHelper() { return Boolean(this.page.__index) && this.page.current === 1; } function isPostHelper() { return Boolean(this.page.__post); } function isPageHelper() { return Boolean(this.page.__page); } function isArchiveHelper() { return Boolean(this.page.archive); } function isYearHelper(year?) { const { page } = this; if (!page.archive) return false; if (year) { return page.year === year; } return Boolean(page.year); } function isMonthHelper(year?, month?) { const { page } = this; if (!page.archive) return false; if (year) { if (month) { return page.year === year && page.month === month; } return page.month === year; } return Boolean(page.year && page.month); } function isCategoryHelper(category?) { if (category) { return this.page.category === category; } return Boolean(this.page.category); } function isTagHelper(tag?) { if (tag) { return this.page.tag === tag; } return Boolean(this.page.tag); } export {isCurrentHelper as current}; export {isHomeHelper as home}; export {isHomeFirstPageHelper as home_first_page}; export {isPostHelper as post}; export {isPageHelper as page}; export {isArchiveHelper as archive}; export {isYearHelper as year}; export {isMonthHelper as month}; export {isCategoryHelper as category}; export {isTagHelper as tag}; ================================================ FILE: lib/plugins/helper/js.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import moize from 'moize'; import type { LocalsType } from '../../types'; let relative_link = true; function jsHelper(this: LocalsType, ...args: any[]) { let result = '\n'; relative_link = this.config.relative_link; args.flat(Infinity).forEach(item => { if (typeof item === 'string' || item instanceof String) { let path = item; if (!path.endsWith('.js')) { path += '.js'; } result += `<script src="${url_for.call(this, path)}"></script>\n`; } else { const newItem = { ...item }; // Custom attributes newItem.src = url_for.call(this, newItem.src); if (!newItem.src.endsWith('.js')) newItem.src += '.js'; result += htmlTag('script', newItem, '') + '\n'; } }); return result; } export = moize(jsHelper, { maxSize: 10, isDeepEqual: true, updateCacheForKey() { return relative_link; } }); ================================================ FILE: lib/plugins/helper/link_to.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; interface Options { id?: string; href?: string; title?: string; external?: boolean | null; class?: string | string[]; target?: string; rel?: string; } interface Attrs { href: string; title: string; external?: boolean | null; class?: string; target?: string; rel?: string; [key: string]: string | boolean | null | undefined; } function linkToHelper(this: LocalsType, path: string, text?: string, options: Options | boolean = {}) { if (typeof options === 'boolean') options = {external: options}; if (!text) text = path.replace(/^https?:\/\/|\/$/g, ''); const attrs = Object.assign({ href: url_for.call(this, path) as string, title: text }, options); if (attrs.external) { attrs.target = '_blank'; attrs.rel = 'noopener'; attrs.external = null; } if (attrs.class && Array.isArray(attrs.class)) { attrs.class = attrs.class.join(' '); } return htmlTag('a', attrs as Attrs, text); } export = linkToHelper; ================================================ FILE: lib/plugins/helper/list_archives.ts ================================================ import type Query from 'warehouse/dist/query'; import type { LocalsType, PostSchema } from '../../types'; import { toMomentLocale } from './date'; import { url_for, Cache } from 'hexo-util'; interface Options { format?: string; type?: string; style?: string | false; transform?: (name: string) => string; separator?: string; show_count?: boolean; class?: string; order?: number; } interface Data { name: string; year: number; month: number; count: number; } const postsCache = new Cache(); function listArchivesHelper(this: LocalsType, options: Options = {}) { const { config } = this; const archiveDir = config.archive_dir; const { timezone } = config; const lang = toMomentLocale(this.page.lang || this.page.language || config.language); let { format } = options; const type = options.type || 'monthly'; const { style = 'list', transform, separator = ', ' } = options; const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true; const className = options.class || 'archive'; const order = options.order || -1; const compareFunc = type === 'monthly' ? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB : (yearA, _monthA, yearB, _monthB) => yearA === yearB; let result = ''; if (!format) { format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY'; } const posts = config.relative_link ? postsCache.apply(`date-${order}`, () => this.site.posts.sort('date', order)) as Query<PostSchema> : this.site.posts.sort('date', order); if (!posts.length) return result; const data: Data[] = []; let length = 0; posts.forEach(post => { // Clone the date object to avoid pollution let date = post.date.clone(); if (timezone) date = date.tz(timezone); const year = date.year(); const month = date.month() + 1; const lastData = data[length - 1]; if (!lastData || !compareFunc(lastData.year, lastData.month, year, month)) { if (lang) date = date.locale(lang); const name = date.format(format); length = data.push({ name, year, month, count: 1 }); } else { lastData.count++; } }); const link = item => { let url = `${archiveDir}/${item.year}/`; if (type === 'monthly') { if (item.month < 10) url += '0'; url += `${item.month}/`; } return url_for.call(this, url); }; if (style === 'list') { result += `<ul class="${className}-list">`; for (let i = 0, len = data.length; i < len; i++) { const item = data[i]; result += `<li class="${className}-list-item">`; result += `<a class="${className}-list-link" href="${link(item)}">`; result += transform ? transform(item.name) : item.name; result += '</a>'; if (showCount) { result += `<span class="${className}-list-count">${item.count}</span>`; } result += '</li>'; } result += '</ul>'; } else { for (let i = 0, len = data.length; i < len; i++) { const item = data[i]; if (i) result += separator; result += `<a class="${className}-link" href="${link(item)}">`; result += transform ? transform(item.name) : item.name; if (showCount) { result += `<span class="${className}-count">${item.count}</span>`; } result += '</a>'; } } return result; } export = listArchivesHelper; ================================================ FILE: lib/plugins/helper/list_categories.ts ================================================ import { url_for } from 'hexo-util'; import type { CategorySchema, LocalsType } from '../../types'; import type Query from 'warehouse/dist/query'; import type Document from 'warehouse/dist/document'; interface Options { style?: string | false; class?: string; depth?: number | string; orderby?: string; order?: number; show_count?: boolean; show_current?: boolean; transform?: (name: string) => string; separator?: string; suffix?: string; children_indicator?: string | boolean; } function listCategoriesHelper(this: LocalsType, categories?: Query<CategorySchema> | Options, options?: Options) { if (!options && (!categories || !Object.prototype.hasOwnProperty.call(categories, 'length'))) { options = categories as Options; categories = this.site.categories; } categories = categories as Query<CategorySchema>; if (!categories || !categories.length) return ''; options = options || {}; const { style = 'list', transform, separator = ', ', suffix = '' } = options; const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true; const className = options.class || 'category'; const depth = options.depth ? parseInt(String(options.depth), 10) : 0; const orderby = options.orderby || 'name'; const order = options.order || 1; const showCurrent = options.show_current || false; const childrenIndicator = Object.prototype.hasOwnProperty.call(options, 'children_indicator') ? options.children_indicator : false; const prepareQuery = parent => { const query: { parent?: any } = {}; if (parent) { query.parent = parent; } else { query.parent = {$exists: false}; } return (categories as Query<CategorySchema>).find(query).sort(orderby, order); }; const hierarchicalList = (level: number, parent?: any) => { let result = ''; prepareQuery(parent).forEach((cat: Document<CategorySchema> & CategorySchema) => { let child; if (!depth || level + 1 < depth) { child = hierarchicalList(level + 1, cat._id); } let isCurrent = false; if (showCurrent && this.page) { for (let j = 0; j < cat.length; j++) { const post = cat.posts.data[j]; if (post && post._id === this.page._id) { isCurrent = true; break; } } // special case: category page isCurrent = isCurrent || (this.page.base && this.page.base.startsWith(cat.path)); } const additionalClassName = child && childrenIndicator ? ` ${childrenIndicator}` : ''; result += `<li class="${className}-list-item${additionalClassName}">`; result += `<a class="${className}-list-link${isCurrent ? ' current' : ''}" href="${url_for.call(this, cat.path)}${suffix}">`; result += transform ? transform(cat.name) : cat.name; result += '</a>'; if (showCount) { result += `<span class="${className}-list-count">${cat.length}</span>`; } if (child) { result += `<ul class="${className}-list-child">${child}</ul>`; } result += '</li>'; }); return result; }; const flatList = (level: number, parent?: any) => { let result = ''; prepareQuery(parent).forEach((cat, i) => { if (i || level) result += separator; result += `<a class="${className}-link" href="${url_for.call(this, cat.path)}${suffix}">`; result += transform ? transform(cat.name) : cat.name; if (showCount) { result += `<span class="${className}-count">${cat.length}</span>`; } result += '</a>'; if (!depth || level + 1 < depth) { result += flatList(level + 1, cat._id); } }); return result; }; if (style === 'list') { return `<ul class="${className}-list">${hierarchicalList(0)}</ul>`; } return flatList(0); } export = listCategoriesHelper; ================================================ FILE: lib/plugins/helper/list_posts.ts ================================================ import { url_for } from 'hexo-util'; import type { LocalsType, PostSchema } from '../../types'; import type Query from 'warehouse/dist/query'; interface Options { style?: string | false; class?: string; amount?: number; orderby?: string; order?: number; transform?: (name: string) => string; separator?: string; } function listPostsHelper(this: LocalsType, posts?: Query<PostSchema> | Options, options?: Options) { if (!options && (!posts || !Object.prototype.hasOwnProperty.call(posts, 'length'))) { options = posts as Options; posts = this.site.posts; } posts = posts as Query<PostSchema>; options = options || {}; const { style = 'list', transform, separator = ', ' } = options; const orderby = options.orderby || 'date'; const order = options.order || -1; const className = options.class || 'post'; const amount = options.amount || 6; // Sort the posts posts = posts.sort(orderby, order); // Limit the number of posts if (amount) posts = posts.limit(amount); let result = ''; if (style === 'list') { result += `<ul class="${className}-list">`; posts.forEach(post => { const title = post.title || post.slug; result += `<li class="${className}-list-item">`; result += `<a class="${className}-list-link" href="${url_for.call(this, post.path)}">`; result += transform ? transform(title) : title; result += '</a>'; result += '</li>'; }); result += '</ul>'; } else { posts.forEach((post, i) => { if (i) result += separator; const title = post.title || post.slug; result += `<a class="${className}-link" href="${url_for.call(this, post.path)}">`; result += transform ? transform(title) : title; result += '</a>'; }); } return result; } export = listPostsHelper; ================================================ FILE: lib/plugins/helper/list_tags.ts ================================================ import { url_for, escapeHTML } from 'hexo-util'; import moize from 'moize'; import type { LocalsType, TagSchema } from '../../types'; import type Query from 'warehouse/dist/query'; interface Options { style?: string | false; class?: any; amount?: number; orderby?: string; order?: number; transform?: (name: string) => string; separator?: string; show_count?: boolean; suffix?: string; } function listTagsHelper(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) { if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) { options = tags as Options; tags = this.site.tags; } tags = tags as Query<TagSchema>; if (!tags || !tags.length) return ''; options = options || {}; const { style = 'list', transform, separator = ', ', suffix = '' } = options; const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true; const classStyle = typeof style === 'string' ? `-${style}` : ''; let className, ulClass, liClass, aClass, labelClass, countClass, labelSpan; if (typeof options.class !== 'undefined') { if (typeof options.class === 'string') { className = options.class; } else { className = 'tag'; } ulClass = options.class.ul || `${className}${classStyle}`; liClass = options.class.li || `${className}${classStyle}-item`; aClass = options.class.a || `${className}${classStyle}-link`; labelClass = options.class.label || `${className}${classStyle}-label`; countClass = options.class.count || `${className}${classStyle}-count`; labelSpan = Object.prototype.hasOwnProperty.call(options.class, 'label'); } else { className = 'tag'; ulClass = `${className}${classStyle}`; liClass = `${className}${classStyle}-item`; aClass = `${className}${classStyle}-link`; labelClass = `${className}${classStyle}-label`; countClass = `${className}${classStyle}-count`; labelSpan = false; } const orderby = options.orderby || 'name'; const order = options.order || 1; let result = ''; // Sort the tags tags = tags.sort(orderby, order); // Limit the number of tags if (options.amount) tags = tags.limit(options.amount); if (style === 'list') { result += `<ul class="${ulClass}" itemprop="keywords">`; tags.forEach(tag => { result += `<li class="${liClass}">`; result += `<a class="${aClass}" href="${url_for.call(this, tag.path)}${suffix}" rel="tag">`; result += transform ? transform(tag.name) : escapeHTML(tag.name); result += '</a>'; if (showCount) { result += `<span class="${countClass}">${tag.length}</span>`; } result += '</li>'; }); result += '</ul>'; } else { tags.forEach((tag, i) => { if (i) result += separator; result += `<a class="${aClass}" href="${url_for.call(this, tag.path)}${suffix}" rel="tag">`; if (labelSpan) { result += `<span class="${labelClass}">${transform ? transform(tag.name) : tag.name}</span>`; } else { result += transform ? transform(tag.name) : tag.name; } if (showCount) { result += `<span class="${countClass}">${tag.length}</span>`; } result += '</a>'; }); } return result; } function listTagsHelperFactory(tags?: Query<TagSchema> | Options, options?: Options) { const transformArgs = () => { if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) { options = tags as Options; tags = this.site.tags; } tags = tags as Query<TagSchema>; return [tags.toArray(), options]; }; return moize(listTagsHelper.bind(this), { maxSize: 5, isDeepEqual: true, transformArgs }).call(this, tags, options); } export = listTagsHelperFactory; ================================================ FILE: lib/plugins/helper/mail_to.ts ================================================ import { htmlTag } from 'hexo-util'; import moize from 'moize'; interface Options { href?: string; title?: string; class?: string | string[]; subject?: string; cc?: string | string[]; bcc?: string | string[]; id?: string; body?: string; } interface Attrs { href: string; title: string; class?: string; subject?: string; cc?: string; bcc?: string; id?: string; body?: string; [key: string]: any; } function mailToHelper(path: string | string[], text?: string, options: Options = {}) { if (Array.isArray(path)) path = path.join(','); if (!text) text = path; const attrs = Object.assign({ href: `mailto:${path}`, title: text }, options); if (attrs.class && Array.isArray(attrs.class)) { attrs.class = attrs.class.join(' '); } const data = {}; ['subject', 'cc', 'bcc', 'body'].forEach(i => { const item = attrs[i]; if (item) { data[i] = Array.isArray(item) ? item.join(',') : item; attrs[i] = null; } }); const querystring = new URLSearchParams(data).toString(); if (querystring) attrs.href += `?${querystring}`; return htmlTag('a', attrs as Attrs, text); } export = moize(mailToHelper, { maxSize: 10, isDeepEqual: true }); ================================================ FILE: lib/plugins/helper/markdown.ts ================================================ import type { LocalsType } from '../../types'; function markdownHelper(this: LocalsType, text: string, options?: any) { return this.render(text, 'markdown', options); } export = markdownHelper; ================================================ FILE: lib/plugins/helper/meta_generator.ts ================================================ import type { LocalsType } from '../../types'; function metaGeneratorHelper(this: LocalsType) { return `<meta name="generator" content="Hexo ${this.env.version}">`; } export = metaGeneratorHelper; ================================================ FILE: lib/plugins/helper/number_format.ts ================================================ interface Options { delimiter?: string; separator?: string; precision?: number | false; } function numberFormatHelper(num: number, options: Options = {}) { const split = num.toString().split('.'); let before = split.shift(); let after = split.length ? split[0] : ''; const delimiter = options.delimiter || ','; const separator = options.separator || '.'; const { precision } = options; if (delimiter) { const beforeArr: string[] = []; const beforeLength = before.length; const beforeFirst = beforeLength % 3; if (beforeFirst) beforeArr.push(before.slice(0, beforeFirst)); for (let i = beforeFirst; i < beforeLength; i += 3) { beforeArr.push(before.slice(i, i + 3)); } before = beforeArr.join(delimiter); } if (precision) { const afterLength = after.length; let afterResult = ''; if (afterLength > precision) { const afterLast = after[precision]; const last = parseInt(after[precision - 1], 10); afterResult = after.substring(0, precision - 1) + (Number(afterLast) < 5 ? last : last + 1); } else { afterResult = after; for (let i = 0, len = precision - afterLength; i < len; i++) { afterResult += '0'; } } after = afterResult; } else if (precision === 0) { after = ''; } return before + (after ? separator + after : ''); } export = numberFormatHelper; ================================================ FILE: lib/plugins/helper/open_graph.ts ================================================ import { isMoment, isDate, Moment } from 'moment'; import { encodeURL, prettyUrls, stripHTML, escapeHTML } from 'hexo-util'; import moize from 'moize'; import type { LocalsType } from '../../types'; const localeMap = { 'en': 'en_US', 'de': 'de_DE', 'es': 'es_ES', 'fr': 'fr_FR', 'hu': 'hu_HU', 'id': 'id_ID', 'it': 'it_IT', 'ja': 'ja_JP', 'ko': 'ko_KR', 'nl': 'nl_NL', 'ru': 'ru_RU', 'th': 'th_TH', 'tr': 'tr_TR', 'vi': 'vi_VN' }; const localeToTerritory = moize.shallow(str => { if (str.length === 2 && localeMap[str]) return localeMap[str]; if (str.length === 5) { let territory = []; if (str.includes('-')) { territory = str.split('-'); } else { territory = str.split('_'); } if (territory.length === 2) return territory[0].toLowerCase() + '_' + territory[1].toUpperCase(); } }); const meta = (name: string, content: string | URL, escape?: boolean) => { if (escape !== false && typeof content === 'string') { content = escapeHTML(content); } if (content) return `<meta name="${name}" content="${content}">\n`; return `<meta name="${name}">\n`; }; const og = (name: string, content?: string, escape?: boolean) => { if (escape !== false && typeof content === 'string') { content = escapeHTML(content); } if (content) return `<meta property="${name}" content="${content}">\n`; return `<meta property="${name}">\n`; }; interface Options { image?: string; images?: string[]; description?: string; title?: string; type?: string; url?: string; site_name?: string; twitter_card?: string; date?: Moment | Date | false; updated?: Moment | Date | false; language?: string; author?: string; twitter_image?: string; twitter_id?: string; twitter_site?: string; fb_admins?: string; fb_app_id?: string; } function openGraphHelper(this: LocalsType, options: Options = {}) { const { config, page } = this; const { content } = page; let images = options.image || options.images || page.photos || []; let description = options.description || page.description || page.excerpt || content || config.description; let keywords = (page.tags && page.tags.length ? page.tags : undefined) || (config as any).keywords || false; const title = options.title || page.title || config.title; const type = options.type || (this.is_post() ? 'article' : 'website'); const url = prettyUrls(options.url || this.url, config.pretty_urls); const siteName = options.site_name || config.title; const twitterCard = options.twitter_card || 'summary'; const date = options.date !== false ? options.date || page.date : false; const updated = options.updated !== false ? options.updated || page.updated : false; const language = options.language || page.lang || page.language || config.language; const author = options.author || config.author; if (!Array.isArray(images)) images = [images]; if (description) { description = escapeHTML(stripHTML(description).substring(0, 200) .trim() // Remove prefixing/trailing spaces ).replace(/\n/g, ' '); // Replace new lines by spaces } if (!images.length && content) { images = images.slice(); if (content.includes('<img')) { let img; const imgPattern = /<img [^>]*src=['"]([^'"]+)([^>]*>)/gi; while ((img = imgPattern.exec(content)) !== null) { images.push(img[1]); } } } let result = ''; if (description) { result += meta('description', description); } result += og('og:type', type); result += og('og:title', title); if (url) { result += og('og:url', encodeURL(url), false); } else { result += og('og:url'); } result += og('og:site_name', siteName); if (description) { result += og('og:description', description, false); } if (language) { result += og('og:locale', localeToTerritory(language), false); } images = images.map(path => new URL(path, url || config.url).toString()) .filter(url => !url.startsWith('data:')); images.forEach(path => { result += og('og:image', path, false); }); if (date) { if ((isMoment(date) || isDate(date)) && !isNaN(date.valueOf())) { result += og('article:published_time', date.toISOString()); } } if (updated) { if ((isMoment(updated) || isDate(updated)) && !isNaN(updated.valueOf())) { result += og('article:modified_time', updated.toISOString()); } } if (author) { result += og('article:author', author); } if (keywords) { if (typeof keywords === 'string') keywords = [keywords]; keywords.map(tag => { return tag.name ? tag.name : tag; }).filter(Boolean).sort().forEach(keyword => { result += og('article:tag', keyword); }); } result += meta('twitter:card', twitterCard); if (options.twitter_image) { let twitter_image: string | URL = options.twitter_image; twitter_image = new URL(twitter_image, url || config.url); result += meta('twitter:image', twitter_image, false); } else if (images.length) { result += meta('twitter:image', images[0], false); } if (options.twitter_id) { let twitterId = options.twitter_id; if (!twitterId.startsWith('@')) twitterId = `@${twitterId}`; result += meta('twitter:creator', twitterId); } if (options.twitter_site) { result += meta('twitter:site', options.twitter_site, false); } if (options.fb_admins) { result += og('fb:admins', options.fb_admins); } if (options.fb_app_id) { result += og('fb:app_id', options.fb_app_id); } return result.trim(); } export = openGraphHelper; ================================================ FILE: lib/plugins/helper/paginator.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; interface Options { base?: string; current?: number; format?: string; total?: number; end_size?: number; mid_size?: number; space?: string; next_text?: string; prev_text?: string; prev_next?: boolean; escape?: boolean; page_class?: string; current_class?: string; space_class?: string; prev_class?: string; next_class?: string; force_prev_next?: boolean; show_all?: boolean; transform?: (i: number) => any; } const createLink = (options: Options, ctx: LocalsType) => { const { base, format } = options; return (i: number) => url_for.call(ctx, i === 1 ? base : base + format.replace('%d', String(i))); }; const createPageTag = (options: Options, ctx: LocalsType) => { const link = createLink(options, ctx); const { current, escape, transform, page_class: pageClass, current_class: currentClass } = options; return (i: number) => { if (i === current) { return htmlTag('span', { class: pageClass + ' ' + currentClass }, transform ? transform(i) : i, escape); } return htmlTag('a', { class: pageClass, href: link(i) }, transform ? transform(i) : i, escape); }; }; const showAll = (tags: string[], options: Options, ctx: LocalsType) => { const { total } = options; const pageLink = createPageTag(options, ctx); for (let i = 1; i <= total; i++) { tags.push(pageLink(i)); } }; const paginationPartShow = (tags, options, ctx: LocalsType) => { const { current, total, space, end_size: endSize, mid_size: midSize, space_class: spaceClass } = options; const leftEnd = Math.min(endSize, current - 1); const rightEnd = Math.max(total - endSize + 1, current + 1); const leftMid = Math.max(leftEnd + 1, current - midSize); const rightMid = Math.min(rightEnd - 1, current + midSize); const spaceHtml = htmlTag('span', { class: spaceClass }, space, false); const pageTag = createPageTag(options, ctx); // Display pages on the left edge for (let i = 1; i <= leftEnd; i++) { tags.push(pageTag(i)); } // Display spaces between edges and middle pages if (space && leftMid - leftEnd > 1) { tags.push(spaceHtml); } // Display left middle pages for (let i = leftMid; i < current; i++) { tags.push(pageTag(i)); } // Display the current page tags.push(pageTag(current)); // Display right middle pages for (let i = current + 1; i <= rightMid; i++) { tags.push(pageTag(i)); } // Display spaces between edges and middle pages if (space && rightEnd - rightMid > 1) { tags.push(spaceHtml); } // Display pages on the right edge for (let i = rightEnd; i <= total; i++) { tags.push(pageTag(i)); } }; function paginatorHelper(this: LocalsType, options: Options = {}) { options = Object.assign({ base: this.page.base || '', current: this.page.current || 0, format: `${this.config.pagination_dir}/%d/`, total: this.page.total || 1, end_size: 1, mid_size: 2, space: '…', next_text: 'Next', prev_text: 'Prev', prev_next: true, escape: true, page_class: 'page-number', current_class: 'current', space_class: 'space', prev_class: 'extend prev', next_class: 'extend next', force_prev_next: false }, options); const { current, total, prev_text: prevText, next_text: nextText, prev_next: prevNext, escape, prev_class: prevClass, next_class: nextClass, force_prev_next: forcePrevNext } = options; if (!current) return ''; const link = createLink(options, this); const tags = []; // Display the link to the previous page if (prevNext && current > 1) { tags.push(htmlTag('a', { class: prevClass, rel: 'prev', href: link(current - 1)}, prevText, escape)); } else if (forcePrevNext) { tags.push(htmlTag('span', { class: prevClass, rel: 'prev' }, prevText, escape)); } if (options.show_all) { showAll(tags, options, this); } else { paginationPartShow(tags, options, this); } // Display the link to the next page if (prevNext && current < total) { tags.push(htmlTag('a', { class: nextClass, rel: 'next', href: link(current + 1) }, nextText, escape)); } else if (forcePrevNext) { tags.push(htmlTag('span', { class: nextClass, rel: 'next' }, nextText, escape)); } return tags.join(''); } export = paginatorHelper; ================================================ FILE: lib/plugins/helper/partial.ts ================================================ import { dirname, join } from 'path'; import type Hexo from '../../hexo'; import type { LocalsType } from '../../types'; interface Options { cache?: boolean | string; only?: boolean; } export = (ctx: Hexo) => function partial(this: LocalsType, name: string, locals?: any, options: Options = {}) { if (typeof name !== 'string') throw new TypeError('name must be a string!'); const { cache } = options; const viewDir = this.view_dir; const currentView = this.filename.substring(viewDir.length); const path = join(dirname(currentView), name); const view = ctx.theme.getView(path) || ctx.theme.getView(name); const viewLocals: Record<string, any> = {}; if (!view) { throw new Error(`Partial ${name} does not exist. (in ${currentView})`); } if (options.only) { Object.assign(viewLocals, locals); } else { Object.assign(viewLocals, this, locals); } // Partial don't need layout viewLocals.layout = false; if (cache) { const cacheId = typeof cache === 'string' ? cache : view.path; return this.fragment_cache(cacheId, () => view.renderSync(viewLocals)); } return view.renderSync(viewLocals); }; ================================================ FILE: lib/plugins/helper/relative_url.ts ================================================ import { relative_url } from 'hexo-util'; export = function(from: string, to: string) { return relative_url(from, to); } ================================================ FILE: lib/plugins/helper/render.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => function render(text: string, engine: string, options:object = {}) { return ctx.render.renderSync({ text, engine }, options); }; ================================================ FILE: lib/plugins/helper/search_form.ts ================================================ import moize from 'moize'; import type { LocalsType } from '../../types'; interface Options { class?: string; text?: string | null; button?: string | boolean; } function searchFormHelper(this: LocalsType, options: Options = {}) { const { config } = this; const className = options.class || 'search-form'; const { text = 'Search', button } = options; return `<form action="//google.com/search" method="get" accept-charset="UTF-8" class="${className}"><input type="search" name="q" class="${className}-input"${text ? ` placeholder="${text}"` : ''}>${button ? `<button type="submit" class="${className}-submit">${typeof button === 'string' ? button : text}</button>` : ''}<input type="hidden" name="sitesearch" value="${config.url}"></form>`; } export = moize.deep(searchFormHelper); ================================================ FILE: lib/plugins/helper/tagcloud.ts ================================================ import { Color, url_for } from 'hexo-util'; import moize from 'moize'; import type { LocalsType, TagSchema } from '../../types'; import type Query from 'warehouse/dist/query'; interface Options { min_font?: number; max_font?: number; orderby?: string; order?: number; unit?: string; color?: boolean; class?: string; show_count?: boolean; count_class?: string; level?: number; transform?: (name: string) => string; separator?: string; amount?: number; start_color?: string; end_color?: string; } function tagcloudHelper(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) { if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) { options = tags as Options; tags = this.site.tags; } tags = tags as Query<TagSchema>; if (!tags || !tags.length) return ''; options = options || {}; const min = options.min_font || 10; const max = options.max_font || 20; const orderby = options.orderby || 'name'; const order = options.order || 1; const unit = options.unit || 'px'; const color = options.color; const className = options.class; const showCount = options.show_count; const countClassName = options.count_class || 'count'; const level = options.level || 10; const { transform } = options; const separator = options.separator || ' '; const result = []; let startColor, endColor; if (color) { if (!options.start_color) throw new TypeError('start_color is required!'); if (!options.end_color) throw new TypeError('end_color is required!'); startColor = new Color(options.start_color); endColor = new Color(options.end_color); } // Sort the tags if (orderby === 'random' || orderby === 'rand') { tags = tags.random(); } else { tags = tags.sort(orderby, order); } // Limit the number of tags if (options.amount) { tags = tags.limit(options.amount); } const sizes = []; tags.sort('length').forEach(tag => { const { length } = tag; if (sizes.includes(length)) return; sizes.push(length); }); const length = sizes.length - 1; tags.forEach(tag => { const ratio = length ? sizes.indexOf(tag.length) / length : 0; const size = min + ((max - min) * ratio); let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};`; const attr = className ? ` class="${className}-${Math.round(ratio * level)}"` : ''; if (color) { const midColor = startColor.mix(endColor, ratio); style += ` color: ${midColor.toString()}`; } result.push( `<a href="${url_for.call(this, tag.path)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}${showCount ? `<span class="${countClassName}">${tag.length}</span>` : ''}</a>` ); }); return result.join(separator); } function tagcloudHelperFactory(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) { const transformArgs = () => { if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) { options = tags as Options; tags = this.site.tags; } tags = tags as Query<TagSchema>; return [tags.toArray(), options]; }; return moize(tagcloudHelper.bind(this), { maxSize: 5, isDeepEqual: true, transformArgs }).call(this, tags, options); } export = tagcloudHelperFactory; ================================================ FILE: lib/plugins/helper/toc.ts ================================================ import { tocObj, escapeHTML } from 'hexo-util'; interface Options { min_depth?: number; max_depth?: number; max_items?: number; class?: string; class_item?: string; class_link?: string; class_text?: string; class_child?: string; class_number?: string; class_level?: string; list_number?: boolean; } /** * Hexo TOC helper: generates a nested <ol> list from markdown headings * @param {string} str Raw markdown/html string * @param {Options} options Configuration options */ function tocHelper(str, options: Options = {}) { // Default options options = Object.assign({ min_depth: 1, max_depth: 6, max_items: Infinity, class: 'toc', class_item: '', class_link: '', class_text: '', class_child: '', class_number: '', class_level: '', list_number: true }, options); // Extract and truncate flat TOC data const flat = getAndTruncateTocObj( str, { min_depth: options.min_depth, max_depth: options.max_depth }, options.max_items ); if (!flat.length) return ''; // Prepare class names const className = escapeHTML(options.class); const itemClassName = escapeHTML(options.class_item || options.class + '-item'); const linkClassName = escapeHTML(options.class_link || options.class + '-link'); const textClassName = escapeHTML(options.class_text || options.class + '-text'); const childClassName = escapeHTML(options.class_child || options.class + '-child'); const numberClassName = escapeHTML(options.class_number || options.class + '-number'); const levelClassName = escapeHTML(options.class_level || options.class + '-level'); const listNumber = options.list_number; // Build tree, assign numbers, render HTML const tree = buildTree(flat); if (listNumber) assignNumbers(tree); function render(list, depth = 0) { if (!list.length) return ''; const olCls = depth === 0 ? className : childClassName; let out = `<ol class="${olCls}">`; list.forEach(node => { const lvl = node.level; out += `<li class="${itemClassName} ${levelClassName}-${lvl}">`; out += `<a class="${linkClassName}"${node.id ? ` href="#${encodeURI(node.id)}"` : ''}>`; if (listNumber && !node.unnumbered) { out += `<span class="${numberClassName}">${node.number}</span> `; } out += `<span class="${textClassName}">${node.text}</span></a>`; out += render(node.children, depth + 1); out += '</li>'; }); out += '</ol>'; return out; } return render(tree); } /** * Extract flat TOC data and enforce max_items */ function getAndTruncateTocObj(str, { min_depth, max_depth }, max_items) { let data = tocObj(str, { min_depth, max_depth }); if (max_items < Infinity && data.length > max_items) { const levels = data.map(i => i.level); const min = Math.min(...levels); let curMax = Math.max(...levels); // remove deeper headings until within limit while (data.length > max_items && curMax > min) { // eslint-disable-next-line no-loop-func data = data.filter(i => i.level < curMax); curMax--; } data = data.slice(0, max_items); } return data; } /** * Build nested tree from flat heading list */ function buildTree(headings) { const root = { level: 0, children: [] }; const stack = [root]; headings.forEach(h => { // pop until parent.level < h.level while (stack[stack.length - 1].level >= h.level) { stack.pop(); } const parent = stack[stack.length - 1]; const node = { ...h, children: [] }; parent.children.push(node); stack.push(node); }); return root.children; } /** * Assign hierarchical numbering to each node */ function assignNumbers(nodes) { const counters = []; function dfs(list, depth) { counters[depth] = 0; list.forEach(node => { counters[depth]++; node.number = counters.slice(0, depth + 1).join('.') + '.'; if (node.children.length) dfs(node.children, depth + 1); }); } dfs(nodes, 0); } export = tocHelper; ================================================ FILE: lib/plugins/helper/url_for.ts ================================================ import { url_for } from 'hexo-util'; import type { LocalsType } from '../../types'; interface Options { relative?: boolean } export = function(this: LocalsType, path: string, options: Options = {}) { return url_for.call(this, path, options); } ================================================ FILE: lib/plugins/highlight/highlight.ts ================================================ import type { HighlightOptions } from '../../extend/syntax_highlight'; import type Hexo from '../../hexo'; // Lazy require highlight.js let highlight: typeof import('hexo-util').highlight; module.exports = function highlightFilter(this: Hexo, code: string, options: HighlightOptions) { const hljsCfg = this.config.highlight || {} as any; const line_threshold = options.line_threshold || hljsCfg.line_threshold || 0; const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? hljsCfg.line_number : options.line_number; const surpassesLineThreshold = options.lines_length > line_threshold; const gutter = shouldUseLineNumbers && surpassesLineThreshold; const languageAttr = typeof options.language_attr === 'undefined' ? hljsCfg.language_attr : options.language_attr; const hljsOptions = { autoDetect: hljsCfg.auto_detect, caption: options.caption, firstLine: options.firstLine as number, gutter, hljs: hljsCfg.hljs, lang: options.lang, languageAttr, mark: options.mark as number[], tab: hljsCfg.tab_replace, wrap: hljsCfg.wrap, stripIndent: hljsCfg.strip_indent }; if (hljsCfg.first_line_number === 'inline') { if (typeof options.firstLineNumber !== 'undefined') { hljsOptions.firstLine = options.firstLineNumber as number; } else { hljsOptions.gutter = false; } } if (Array.isArray(hljsCfg.exclude_languages) && hljsCfg.exclude_languages.includes(hljsOptions.lang)) { // Only wrap with <pre><code class="lang"></code></pre> hljsOptions.wrap = false; hljsOptions.gutter = false; hljsOptions.autoDetect = false; } if (!highlight) highlight = require('hexo-util').highlight; return highlight(code, hljsOptions); }; ================================================ FILE: lib/plugins/highlight/index.ts ================================================ import type Hexo from '../../hexo'; module.exports = (ctx: Hexo) => { const { highlight } = ctx.extend; highlight.register('highlight.js', require('./highlight')); highlight.register('prismjs', require('./prism')); }; ================================================ FILE: lib/plugins/highlight/prism.ts ================================================ import type { HighlightOptions } from '../../extend/syntax_highlight'; import type Hexo from '../../hexo'; // Lazy require prismjs let prismHighlight: typeof import('hexo-util').prismHighlight; module.exports = function(this: Hexo, code: string, options: HighlightOptions) { const prismjsCfg = this.config.prismjs || {} as any; const line_threshold = options.line_threshold || prismjsCfg.line_threshold || 0; const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? prismjsCfg.line_number : options.line_number; const surpassesLineThreshold = options.lines_length > line_threshold; const lineNumber = shouldUseLineNumbers && surpassesLineThreshold; const prismjsOptions = { caption: options.caption, firstLine: options.firstLine as number, isPreprocess: prismjsCfg.preprocess, lang: options.lang, lineNumber, mark: Array.isArray(options.mark) ? String(options.mark) : options.mark, tab: prismjsCfg.tab_replace, stripIndent: prismjsCfg.strip_indent }; if (!prismHighlight) prismHighlight = require('hexo-util').prismHighlight; if (Array.isArray(prismjsCfg.exclude_languages) && prismjsCfg.exclude_languages.includes(prismjsOptions.lang)) { // Only wrap with <pre><code class="lang"></code></pre> return `<pre><code class="${prismjsOptions.lang}">${require('hexo-util').escapeHTML(code)}</code></pre>`; } return prismHighlight(code, prismjsOptions); }; ================================================ FILE: lib/plugins/injector/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { injector } = ctx.extend; }; ================================================ FILE: lib/plugins/processor/asset.ts ================================================ import { adjustDateForTimezone, toDate, isExcludedFile, isMatch } from './common'; import Promise from 'bluebird'; import { parse as yfm } from 'hexo-front-matter'; import { extname, relative } from 'path'; import { Pattern } from 'hexo-util'; import { magenta } from 'picocolors'; import type { _File } from '../../box'; import type Hexo from '../../hexo'; import type { Stats } from 'fs'; import { PageSchema } from '../../types'; export = (ctx: Hexo) => { return { pattern: new Pattern(path => { if (isExcludedFile(path, ctx.config)) return; return { renderable: ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render) }; }), process: function assetProcessor(file: _File) { if (file.params.renderable) { return processPage(ctx, file); } return processAsset(ctx, file); } }; }; function processPage(ctx: Hexo, file: _File) { const Page = ctx.model('Page'); const { path } = file; const doc = Page.findOne({source: path}); const { config } = ctx; const { timezone } = config; const updated_option = config.updated_option; if (file.type === 'skip' && doc) { return; } if (file.type === 'delete') { if (doc) { return doc.remove(); } return; } return Promise.all([ file.stat(), file.read() ]).spread((stats: Stats, content: string) => { const data: PageSchema = yfm(content); const output = ctx.render.getOutput(path); data.source = path; data.raw = content; data.date = toDate(data.date) as any; if (data.date) { if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any; } else { data.date = stats.ctime as any; } data.updated = toDate(data.updated) as any; if (data.updated) { if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any; } else if (updated_option === 'date') { data.updated = data.date; } else if (updated_option === 'empty') { data.updated = undefined; } else { data.updated = stats.mtime as any; } if (data.permalink) { data.path = data.permalink; data.permalink = undefined; if (data.path.endsWith('/')) { data.path += 'index'; } if (!extname(data.path)) { data.path += `.${output}`; } } else { data.path = `${path.substring(0, path.length - extname(path).length)}.${output}`; } if (!data.layout && output !== 'html' && output !== 'htm') { data.layout = false; } if (doc) { if (file.type !== 'update') { ctx.log.warn(`Trying to "create" ${magenta(file.path)}, but the file already exists!`); } return doc.replace(data); } return Page.insert(data); }); } function processAsset(ctx: Hexo, file: _File) { const id = relative(ctx.base_dir, file.source).replace(/\\/g, '/'); const Asset = ctx.model('Asset'); const doc = Asset.findById(id); if (file.type === 'delete') { if (doc) { return doc.remove(); } return; } return Asset.save({ _id: id, path: file.path, modified: file.type !== 'skip', renderable: file.params.renderable }); } ================================================ FILE: lib/plugins/processor/common.ts ================================================ import moment from 'moment-timezone'; import micromatch from 'micromatch'; const DURATION_MINUTE = 1000 * 60; function isMatch(path: string, patterns?: string| string[]) { if (!patterns) return false; return micromatch.isMatch(path, patterns); } function isTmpFile(path: string) { return path.endsWith('%') || path.endsWith('~'); } function isHiddenFile(path: string) { return /(^|\/)[_.]/.test(path); } function isExcludedFile(path: string, config) { if (isTmpFile(path)) return true; if (isMatch(path, config.exclude)) return true; if (isHiddenFile(path) && !isMatch(path, config.include)) return true; return false; } export {isTmpFile}; export {isHiddenFile}; export {isExcludedFile}; export function toDate(date?: string | number | Date | moment.Moment): Date | undefined | moment.Moment { if (!date || moment.isMoment(date)) return date as any; if (!(date instanceof Date)) { date = new Date(date); } if (isNaN(date.getTime())) return; return date; } export function adjustDateForTimezone(date: Date | moment.Moment, timezone: string) { if (moment.isMoment(date)) date = date.toDate(); const offset = date.getTimezoneOffset(); const ms = date.getTime(); const target = moment.tz.zone(timezone).utcOffset(ms); const diff = (offset - target) * DURATION_MINUTE; return new Date(ms - diff); } export {isMatch}; ================================================ FILE: lib/plugins/processor/data.ts ================================================ import { Pattern } from 'hexo-util'; import { extname } from 'path'; import type Hexo from '../../hexo'; import type { _File } from '../../box'; export = (ctx: Hexo) => ({ pattern: new Pattern('_data/*path'), process: function dataProcessor(file: _File) { const Data = ctx.model('Data'); const { path } = file.params; const id = path.substring(0, path.length - extname(path).length); const doc = Data.findById(id); if (file.type === 'skip' && doc) { return; } if (file.type === 'delete') { if (doc) { return doc.remove(); } return; } return file.render().then(result => { if (result == null) return; return Data.save({ _id: id, data: result }); }); } }); ================================================ FILE: lib/plugins/processor/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { processor } = ctx.extend; function register(name: string) { const obj = require(`./${name}`)(ctx); processor.register(obj.pattern, obj.process); } register('asset'); register('data'); register('post'); }; ================================================ FILE: lib/plugins/processor/post.ts ================================================ import { toDate, adjustDateForTimezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } from './common'; import Promise from 'bluebird'; import { parse as yfm } from 'hexo-front-matter'; import { extname, join, posix, sep } from 'path'; import { stat, listDir } from 'hexo-fs'; import { slugize, Pattern, Permalink } from 'hexo-util'; import { magenta } from 'picocolors'; import type { _File } from '../../box'; import type Hexo from '../../hexo'; import type { Stats } from 'fs'; import { PostAssetSchema, PostSchema } from '../../types'; import type Document from 'warehouse/dist/document'; const postDir = '_posts/'; const draftDir = '_drafts/'; let permalink: Permalink; const preservedKeys = { title: true, year: true, month: true, day: true, i_month: true, i_day: true, hash: true }; export = (ctx: Hexo) => { return { pattern: new Pattern(path => { if (isTmpFile(path)) return; let result; if (path.startsWith(postDir)) { result = { published: true, path: path.substring(postDir.length) }; } else if (path.startsWith(draftDir)) { result = { published: false, path: path.substring(draftDir.length) }; } if (!result || isHiddenFile(result.path)) return; // checks only if there is a renderer for the file type or if is included in skip_render result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render); // if post_asset_folder is set, restrict renderable files to default file extension if (result.renderable && ctx.config.post_asset_folder) { result.renderable = (extname(ctx.config.new_post_name) === extname(path)); } return result; }), process: function postProcessor(file: _File) { if (file.params.renderable) { return processPost(ctx, file); } else if (ctx.config.post_asset_folder) { return processAsset(ctx, file); } } }; }; function processPost(ctx: Hexo, file: _File) { const Post = ctx.model('Post'); const { path } = file.params; const doc = Post.findOne({source: file.path}); const { config } = ctx; const { timezone, updated_option, use_slug_as_post_title } = config; let categories, tags; if (file.type === 'skip' && doc) { return; } if (file.type === 'delete') { if (doc) { return doc.remove(); } return; } return Promise.all([ file.stat(), file.read() ]).spread((stats: Stats, content: string) => { const data: PostSchema = yfm(content); const info = parseFilename(config.new_post_name, path); const keys = Object.keys(info); data.source = file.path; data.raw = content; data.slug = info.title; if (file.params.published) { if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true; } else { data.published = false; } for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!preservedKeys[key]) data[key] = info[key]; } // use `slug` as `title` of post when `title` is not specified. // https://github.com/hexojs/hexo/issues/5372 if (use_slug_as_post_title && !('title' in data)) { // @ts-expect-error - title is not in data data.title = info.title; } if (data.date) { data.date = toDate(data.date) as any; } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) { data.date = new Date( info.year, parseInt(info.month || info.i_month, 10) - 1, parseInt(info.day || info.i_day, 10) ) as any; } if (data.date) { if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any; } else { data.date = stats.birthtime as any; } data.updated = toDate(data.updated) as any; if (data.updated) { if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any; } else if (updated_option === 'date') { data.updated = data.date; } else if (updated_option === 'empty') { data.updated = undefined; } else { data.updated = stats.mtime as any; } if (data.category && !data.categories) { data.categories = data.category; data.category = undefined; } if (data.tag && !data.tags) { data.tags = data.tag; data.tag = undefined; } categories = data.categories || []; tags = data.tags || []; if (!Array.isArray(categories)) categories = [categories]; if (!Array.isArray(tags)) tags = [tags]; if (data.photo && !data.photos) { data.photos = data.photo; data.photo = undefined; } if (data.photos && !Array.isArray(data.photos)) { data.photos = [data.photos]; } if (data.permalink) { data.__permalink = data.permalink; data.permalink = undefined; } if (doc) { if (file.type !== 'update') { ctx.log.warn(`Trying to "create" ${magenta(file.path)}, but the file already exists!`); } return doc.replace(data); } return Post.insert(data); }).then((doc: PostSchema) => Promise.all([ doc.setCategories(categories), doc.setTags(tags), scanAssetDir(ctx, doc) ])); } function parseFilename(config: string, path: string) { config = config.substring(0, config.length - extname(config).length); path = path.substring(0, path.length - extname(path).length); if (!permalink || permalink.rule !== config) { permalink = new Permalink(config, { segments: { year: /(\d{4})/, month: /(\d{2})/, day: /(\d{2})/, i_month: /(\d{1,2})/, i_day: /(\d{1,2})/, hash: /([0-9a-f]{12})/ } }); } const data = permalink.parse(path) as Record<string, any>; if (data) { if (data.title !== undefined) { return data; } return Object.assign(data, { title: slugize(path) }); } return { title: slugize(path) }; } function scanAssetDir(ctx: Hexo, post: PostSchema) { if (!ctx.config.post_asset_folder) return; const assetDir = post.asset_dir; const baseDir = ctx.base_dir; const sourceDir = ctx.config.source_dir; const baseDirLength = baseDir.length; const sourceDirLength = sourceDir.length; const PostAsset = ctx.model('PostAsset'); return stat(assetDir).then(stats => { if (!stats.isDirectory()) return []; return listDir(assetDir); }).catch(err => { if (err && err.code === 'ENOENT') return []; throw err; }).filter(item => !isExcludedFile(item, ctx.config)).map(item => { const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/'); const renderablePath = id.substring(sourceDirLength + 1); const asset = PostAsset.findById(id); if (shouldSkipAsset(ctx, post, asset)) return undefined; return PostAsset.save({ _id: id, post: post._id, slug: item, modified: true, renderable: ctx.render.isRenderable(renderablePath) && !isMatch(renderablePath, ctx.config.skip_render) }); }); } function shouldSkipAsset(ctx: Hexo, post: PostSchema, asset: Document<PostAssetSchema>) { if (!ctx._showDrafts()) { if (post.published === false && asset) { // delete existing draft assets if draft posts are hidden asset.remove(); } if (post.published === false) { // skip draft assets if draft posts are hidden return true; } } return asset !== undefined; // skip already existing assets } function processAsset(ctx: Hexo, file: _File) { const PostAsset = ctx.model('PostAsset'); const Post = ctx.model('Post'); const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/'); const postAsset = PostAsset.findById(id); if (file.type === 'delete' || Post.length === 0) { if (postAsset) { return postAsset.remove(); } return; } const savePostAsset = (post: Document<PostSchema>) => { return PostAsset.save({ _id: id, slug: file.source.substring(post.asset_dir.length), post: post._id, modified: file.type !== 'skip', renderable: file.params.renderable }); }; if (postAsset) { // `postAsset.post` is `Post.id`. const post = Post.findById(postAsset.post); if (post != null && (post.published || ctx._showDrafts())) { return savePostAsset(post); } } // NOTE: Must use `posix.sep` ('/') because id is normalized to use forward slashes. // Using os-specific `sep` would fail on Windows where backslashes wouldn't match. const relativeAssetDirPath = id.slice(0, id.lastIndexOf(posix.sep)); // Convert relative path to OS-specific absolute path with trailing separator const absoluteAssetDirPath = join(ctx.base_dir, relativeAssetDirPath) + sep; /* NOTE: Using `Post.filter()` instead to ensure we get the correct post. Because `Post.findOne()` with a query function or object does not work as expected here. It returns an incorrect post even when the condition doesn't match. Examples that didn't work: - `Post.findOne(p => p.asset_dir === absoluteAssetDirPath)` // returned wrong post - `Post.findOne({asset_dir: absoluteAssetDirPath})` // returned null */ const posts = Post.filter(p => p.asset_dir === absoluteAssetDirPath); const post = posts.length === 1 ? posts.data[0] : null; if (post != null && (post.published || ctx._showDrafts())) { return savePostAsset(post); } // NOTE: Probably, unreachable. if (postAsset) { return postAsset.remove(); } } ================================================ FILE: lib/plugins/renderer/index.ts ================================================ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { renderer } = ctx.extend; const plain = require('./plain'); renderer.register('htm', 'html', plain, true); renderer.register('html', 'html', plain, true); renderer.register('css', 'css', plain, true); renderer.register('js', 'js', plain, true); renderer.register('json', 'json', require('./json'), true); const yaml = require('./yaml'); renderer.register('yml', 'json', yaml, true); renderer.register('yaml', 'json', yaml, true); const nunjucks = require('./nunjucks'); renderer.register('njk', 'html', nunjucks, true); renderer.register('j2', 'html', nunjucks, true); }; ================================================ FILE: lib/plugins/renderer/json.ts ================================================ import type { StoreFunctionData } from '../../extend/renderer'; function jsonRenderer(data: StoreFunctionData): any { return JSON.parse(data.text); } export = jsonRenderer; ================================================ FILE: lib/plugins/renderer/nunjucks.ts ================================================ import nunjucks, { Environment } from 'nunjucks'; import { readFileSync } from 'hexo-fs'; import { dirname } from 'path'; import type { StoreFunctionData } from '../../extend/renderer'; function toArray(value) { if (Array.isArray(value)) { // Return if given value is an Array return value; } else if (typeof value.toArray === 'function') { return value.toArray(); } else if (value instanceof Map) { const arr = []; value.forEach(v => arr.push(v)); return arr; } else if (value instanceof Set || typeof value === 'string') { return [...value]; } else if (typeof value === 'object' && value instanceof Object && Boolean(value)) { return Object.values(value); } return []; } function safeJsonStringify(json: any, spacer = undefined): string { if (typeof json !== 'undefined' && json !== null) { return JSON.stringify(json, null, spacer); } return '""'; } const nunjucksCfg = { autoescape: false, throwOnUndefined: false, trimBlocks: false, lstripBlocks: false }; const nunjucksAddFilter = (env: Environment): void => { env.addFilter('toarray', toArray); env.addFilter('safedump', safeJsonStringify); }; function njkCompile(data: StoreFunctionData): nunjucks.Template { let env: Environment; if (data.path) { env = nunjucks.configure(dirname(data.path), nunjucksCfg); } else { env = nunjucks.configure(nunjucksCfg); } nunjucksAddFilter(env); const text = 'text' in data ? data.text : readFileSync(data.path); return nunjucks.compile(text, env, data.path); } function njkRenderer(data: StoreFunctionData, locals?: any): string { return njkCompile(data).render(locals); } njkRenderer.compile = (data: StoreFunctionData): (locals: any) => string => { // Need a closure to keep the compiled template. return locals => njkCompile(data).render(locals); }; export = njkRenderer; ================================================ FILE: lib/plugins/renderer/plain.ts ================================================ import type { StoreFunctionData } from '../../extend/renderer'; function plainRenderer(data: StoreFunctionData): string { return data.text; } export = plainRenderer; ================================================ FILE: lib/plugins/renderer/yaml.ts ================================================ import yaml from 'js-yaml'; import { escape } from 'hexo-front-matter'; import logger from 'hexo-log'; import type { StoreFunctionData } from '../../extend/renderer'; let schema: yaml.Schema; // FIXME: workaround for https://github.com/hexojs/hexo/issues/4917 try { schema = yaml.DEFAULT_SCHEMA.extend(require('js-yaml-js-types').all); } catch (e) { if (e instanceof yaml.YAMLException) { logger().warn('YAMLException: please see https://github.com/hexojs/hexo/issues/4917'); } else { throw e; } } function yamlHelper(data: StoreFunctionData): any { return yaml.load(escape(data.text), { schema }); } export = yamlHelper; ================================================ FILE: lib/plugins/tag/asset_img.ts ================================================ import img from './img'; import { encodeURL } from 'hexo-util'; import type Hexo from '../../hexo'; /** * Asset image tag * * Syntax: * {% asset_img [class names] slug [width] [height] [title text [alt text]]%} */ export = (ctx: Hexo) => { const PostAsset = ctx.model('PostAsset'); return function assetImgTag(args: string[]) { const len = args.length; // Find image URL for (let i = 0; i < len; i++) { const asset = PostAsset.findOne({post: this._id, slug: args[i]}); if (asset) { // img tag will call url_for so no need to call it here args[i] = encodeURL(new URL(asset.path, ctx.config.url).pathname); return img(ctx)(args); } } }; }; ================================================ FILE: lib/plugins/tag/asset_link.ts ================================================ import { url_for, escapeHTML } from 'hexo-util'; import type Hexo from '../../hexo'; /** * Asset link tag * * Syntax: * {% asset_link slug [title] [escape] %} */ export = (ctx: Hexo) => { const PostAsset = ctx.model('PostAsset'); return function assetLinkTag(args: string[]) { const slug = args.shift(); if (!slug) return; const asset = PostAsset.findOne({post: this._id, slug}); if (!asset) return; let escape = args[args.length - 1]; if (escape === 'true' || escape === 'false') { args.pop(); } else { escape = 'true'; } let title = args.length ? args.join(' ') : asset.slug; const attrTitle = escapeHTML(title); if (escape === 'true') title = attrTitle; const link = url_for.call(ctx, asset.path); return `<a href="${link}" title="${attrTitle}">${title}</a>`; }; }; ================================================ FILE: lib/plugins/tag/asset_path.ts ================================================ import { url_for } from 'hexo-util'; import type Hexo from '../../hexo'; /** * Asset path tag * * Syntax: * {% asset_path slug %} */ export = (ctx: Hexo) => { const PostAsset = ctx.model('PostAsset'); return function assetPathTag(args: string[]) { const slug = args.shift(); if (!slug) return; const asset = PostAsset.findOne({post: this._id, slug}); if (!asset) return; const path = url_for.call(ctx, asset.path); return path; }; }; ================================================ FILE: lib/plugins/tag/blockquote.ts ================================================ // Based on: https://raw.github.com/imathis/octopress/master/plugins/blockquote.rb import titlecase from 'titlecase'; import type Hexo from '../../hexo'; const rFullCiteWithTitle = /(\S.*)\s+(https?:\/\/\S+)\s+(.+)/i; const rFullCite = /(\S.*)\s+(https?:\/\/\S+)/i; const rAuthorTitle = /([^,]+),\s*([^,]+)/; /** * @param {string[]} args * @param {Hexo} ctx */ const parseFooter = (args: string[], ctx: Hexo) => { const str = args.join(' '); if (!str) return ''; let author = ''; let source = ''; let title = ''; let match; if ((match = rFullCiteWithTitle.exec(str))) { author = match[1]; source = match[2]; title = ctx.config.titlecase ? titlecase(match[3]) : match[3]; } else if ((match = rFullCite.exec(str))) { author = match[1]; source = match[2]; } else if ((match = rAuthorTitle.exec(str))) { author = match[1]; title = ctx.config.titlecase ? titlecase(match[2]) : match[2]; } else { author = str; } let footer = ''; if (author) footer += `<strong>${author}</strong>`; if (source) { const link = source.replace(/^https?:\/\/|\/(index.html?)?$/g, ''); footer += `<cite><a href="${source}">${title ? title : link}</a></cite>`; } else if (title) { footer += `<cite>${title}</cite>`; } return footer; }; /** * Blockquote tag * * Syntax: * {% blockquote [author[, source]] [link] [source_link_title] %} * Quote string * {% endblockquote %} */ export = (ctx: Hexo) => function blockquoteTag(args: string[], content: string) { const footer = parseFooter(args, ctx); let result = '<blockquote>'; result += ctx.render.renderSync({text: content, engine: 'markdown'}); if (footer) result += `<footer>${footer}</footer>`; result += '</blockquote>'; return result; }; ================================================ FILE: lib/plugins/tag/code.ts ================================================ // Based on: https://raw.github.com/imathis/octopress/master/plugins/code_block.rb import { escapeHTML, htmlTag } from 'hexo-util'; import type Hexo from '../../hexo'; import type { HighlightOptions } from '../../extend/syntax_highlight'; const rCaptionUrlTitle = /(\S[\S\s]*)\s+(https?:\/\/\S+)\s+(.+)/i; const rCaptionUrl = /(\S[\S\s]*)\s+(https?:\/\/\S+)/i; const rCaption = /\S[\S\s]*/; /** * Code block tag * Syntax: * {% codeblock [options] %} * code snippet * {% endcodeblock %} * @param {String} title Caption text * @param {Object} lang Specify language * @param {String} url Source link * @param {String} link_text Text of the link * @param {Object} line_number Show line number, value must be a boolean * @param {Object} highlight Enable code highlighting, value must be a boolean * @param {Object} first_line Specify the first line number, value must be a number * @param {Object} mark Line highlight specific line(s), each value separated by a comma. Specify number range using a dash * Example: `mark:1,4-7,10` will mark line 1, 4 to 7 and 10. * @param {Object} wrap Wrap the code block in <table>, value must be a boolean * @returns {String} Code snippet with code highlighting */ function parseArgs(args: string[]): HighlightOptions { const _else = []; const len = args.length; let lang: string, language_attr: boolean, line_number: boolean, line_threshold: number, wrap: boolean; let firstLine = 1; const mark = []; for (let i = 0; i < len; i++) { const colon = args[i].indexOf(':'); if (colon === -1) { _else.push(args[i]); continue; } const key = args[i].slice(0, colon); const value = args[i].slice(colon + 1); switch (key) { case 'lang': lang = value; break; case 'line_number': line_number = value === 'true'; break; case 'line_threshold': if (!isNaN(Number(value))) line_threshold = +value; break; case 'first_line': if (!isNaN(Number(value))) firstLine = +value; break; case 'wrap': wrap = value === 'true'; break; case 'mark': { for (const cur of value.split(',')) { const hyphen = cur.indexOf('-'); if (hyphen !== -1) { let a = +cur.slice(0, hyphen); let b = +cur.slice(hyphen + 1); if (Number.isNaN(a) || Number.isNaN(b)) continue; if (b < a) { // switch a & b [a, b] = [b, a]; } for (; a <= b; a++) { mark.push(a); } } if (!isNaN(Number(cur))) mark.push(+cur); } break; } case 'language_attr': { language_attr = value === 'true'; break; } default: { _else.push(args[i]); } } } const arg = _else.join(' '); // eslint-disable-next-line one-var let match, caption = ''; if ((match = arg.match(rCaptionUrlTitle)) != null) { caption = htmlTag('span', {}, match[1]) + htmlTag('a', { href: match[2] }, match[3]); } else if ((match = arg.match(rCaptionUrl)) != null) { caption = htmlTag('span', {}, match[1]) + htmlTag('a', { href: match[2] }, 'link'); } else if ((match = arg.match(rCaption)) != null) { caption = htmlTag('span', {}, match[0]); } return { lang, language_attr, firstLine, caption, line_number, line_threshold, mark, wrap }; } export = (ctx: Hexo) => function codeTag(args: string[], content: string) { // If neither highlight.js nor prism.js is enabled, return escaped code directly if (!ctx.extend.highlight.query(ctx.config.syntax_highlighter)) { return `<pre><code>${escapeHTML(content)}</code></pre>`; } let index: number; let enableHighlight = true; if ((index = args.findIndex(item => item.startsWith('highlight:'))) !== -1) { const arg = args[index]; const highlightStr = arg.slice(10); enableHighlight = highlightStr === 'true'; args.splice(index, 1); } // If 'highlight: false' is given, return escaped code directly if (!enableHighlight) { return `<pre><code>${escapeHTML(content)}</code></pre>`; } const options = parseArgs(args); options.lines_length = content.split('\n').length; content = ctx.extend.highlight.exec(ctx.config.syntax_highlighter, { context: ctx, args: [content, options] }); return content.replace(/{/g, '{').replace(/}/g, '}'); }; ================================================ FILE: lib/plugins/tag/full_url_for.ts ================================================ import { full_url_for, htmlTag } from 'hexo-util'; import type Hexo from '../../hexo'; /** * Full url for tag * * Syntax: * {% full_url_for text path %} */ export = (ctx: Hexo) => { return function fullUrlForTag([text, path]) { const url = full_url_for.call(ctx, path); const attrs = { href: url }; return htmlTag('a', attrs, text); }; }; ================================================ FILE: lib/plugins/tag/iframe.ts ================================================ import { htmlTag } from 'hexo-util'; /** * Iframe tag * * Syntax: * {% iframe url [width] [height] %} */ function iframeTag(args: string[]) { const src = args[0]; const width = args[1] && args[1] !== 'default' ? args[1] : '100%'; const height = args[2] && args[2] !== 'default' ? args[2] : '300'; const attrs = { src, width, height, frameborder: '0', loading: 'lazy', allowfullscreen: true }; return htmlTag('iframe', attrs, ''); } export = iframeTag; ================================================ FILE: lib/plugins/tag/img.ts ================================================ import { htmlTag, url_for } from 'hexo-util'; import type Hexo from '../../hexo'; const rUrl = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\w]*))?)/; const rMetaDoubleQuote = /"?([^"]+)?"?/; const rMetaSingleQuote = /'?([^']+)?'?/; /** * Image tag * * Syntax: * {% img [class names] /path/to/image [width] [height] [title text [alt text]] %} */ export = (ctx: Hexo) => { return function imgTag(args: string[]) { const classes = []; let src, width, height, title, alt; // Find image URL and class name while (args.length > 0) { const item = args.shift(); if (rUrl.test(item) || item.startsWith('/')) { src = url_for.call(ctx, item); break; } else { classes.push(item); } } // Find image width and height if (args && args.length) { if (!/\D+/.test(args[0])) { width = args.shift(); if (args.length && !/\D+/.test(args[0])) { height = args.shift(); } } const meta = args.join(' '); const rMetaTitle = meta.startsWith('"') ? rMetaDoubleQuote : rMetaSingleQuote; const rMetaAlt = meta.endsWith('"') ? rMetaDoubleQuote : rMetaSingleQuote; const match = new RegExp(`${rMetaTitle.source}\\s*${rMetaAlt.source}`).exec(meta); // Find image title and alt if (match != null) { title = match[1]; alt = match[2]; } } const attrs = { src, class: classes.join(' '), width, height, title, alt }; return htmlTag('img', attrs); }; }; ================================================ FILE: lib/plugins/tag/include_code.ts ================================================ import { basename, extname, join } from 'path'; import { htmlTag, url_for } from 'hexo-util'; import type Hexo from '../../hexo'; const rCaptionTitleFile = /(.*)?(?:\s+|^)(\/*\S+)/; const rLang = /\s*lang:(\w+)/i; const rFrom = /\s*from:(\d+)/i; const rTo = /\s*to:(\d+)/i; /** * Include code tag * * Syntax: * {% include_code [title] [lang:language] path/to/file %} */ export = (ctx: Hexo) => function includeCodeTag(args: string[]) { let codeDir = ctx.config.code_dir; let arg = args.join(' '); // Add trailing slash to codeDir if (!codeDir.endsWith('/')) codeDir += '/'; let lang = ''; arg = arg.replace(rLang, (match, _lang) => { lang = _lang; return ''; }); let from = 0; arg = arg.replace(rFrom, (match, _from) => { from = _from - 1; return ''; }); let to = Number.MAX_VALUE; arg = arg.replace(rTo, (match, _to) => { to = _to; return ''; }); const match = arg.match(rCaptionTitleFile); // Exit if path is not defined if (!match) return; const path = match[2]; // If the language is not defined, use file extension instead lang = lang || extname(path).substring(1); const source = join(codeDir, path).replace(/\\/g, '/'); // Prevent path traversal: https://github.com/hexojs/hexo/issues/5250 const Page = ctx.model('Page'); const doc = Page.findOne({ source }); if (!doc) return; let code = doc.content; const lines = code.split('\n'); code = lines.slice(from, to).join('\n').trim(); // If the title is not defined, use file name instead const title = match[1] || basename(path); const caption = htmlTag('span', {}, title) + `<a href="${url_for.call(ctx, doc.path)}">view raw</a>`; if (ctx.extend.highlight.query(ctx.config.syntax_highlighter)) { const options = { lang, caption, lines_length: lines.length }; return ctx.extend.highlight.exec(ctx.config.syntax_highlighter, { context: ctx, args: [code, options] }); } return `<pre><code>${code}</code></pre>`; }; ================================================ FILE: lib/plugins/tag/index.ts ================================================ import moize from 'moize'; import type Hexo from '../../hexo'; export default (ctx: Hexo) => { const { tag } = ctx.extend; const blockquote = require('./blockquote')(ctx); tag.register('quote', blockquote, true); tag.register('blockquote', blockquote, true); const code = require('./code')(ctx); tag.register('code', code, true); tag.register('codeblock', code, true); tag.register('iframe', require('./iframe')); const img = require('./img')(ctx); tag.register('img', img); tag.register('image', img); const includeCode = require('./include_code')(ctx); tag.register('include_code', includeCode, {async: true}); tag.register('include-code', includeCode, {async: true}); const link = require('./link'); tag.register('a', link); tag.register('link', link); tag.register('anchor', link); tag.register('post_path', require('./post_path')(ctx)); tag.register('post_link', require('./post_link')(ctx)); tag.register('asset_path', require('./asset_path')(ctx)); tag.register('asset_link', require('./asset_link')(ctx)); const assetImg = require('./asset_img')(ctx); tag.register('asset_img', assetImg); tag.register('asset_image', assetImg); tag.register('pullquote', require('./pullquote')(ctx), true); tag.register('url_for', require('./url_for')(ctx)); tag.register('full_url_for', require('./full_url_for')(ctx)); }; // Use WeakMap to track different ctx (in case there is any) const moized = new WeakMap(); export function postFindOneFactory(ctx: Hexo) { if (moized.has(ctx)) { return moized.get(ctx); } const moizedPostFindOne = moize(createPostFindOne(ctx), { isDeepEqual: true, maxSize: 20 }); moized.set(ctx, moizedPostFindOne); return moizedPostFindOne; } function createPostFindOne(ctx: Hexo) { const Post = ctx.model('Post'); return Post.findOne.bind(Post); } ================================================ FILE: lib/plugins/tag/link.ts ================================================ import { htmlTag } from 'hexo-util'; const rUrl = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\w]*))?)/; /** * Link tag * * Syntax: * {% link text url [external] [title] %} */ function linkTag(args: string[]) { let url = ''; const text = []; let external = false; let title = ''; let i = 0; const len = args.length; // Find link URL and text for (; i < len; i++) { const item = args[i]; if (rUrl.test(item)) { url = item; break; } else { text.push(item); } } // Delete link URL and text from arguments args = args.slice(i + 1); // Check if the link should be open in a new window // and collect the last text as the link title if (args.length) { const shift = args[0]; if (shift === 'true' || shift === 'false') { external = shift === 'true'; args.shift(); } title = args.join(' '); } const attrs = { href: url, title, target: external ? '_blank' : '' }; return htmlTag('a', attrs, text.join(' ')); } export = linkTag; ================================================ FILE: lib/plugins/tag/post_link.ts ================================================ import { url_for, escapeHTML } from 'hexo-util'; import { postFindOneFactory } from './'; import type Hexo from '../../hexo'; /** * Post link tag * * Syntax: * {% post_link slug | title [title] [escape] %} */ export = (ctx: Hexo) => { return function postLinkTag(args: string[]) { let slug = args.shift(); if (!slug) { throw new Error(`Post not found: "${slug}" doesn't exist for {% post_link %}`); } let hash = ''; const parts = slug.split('#'); if (parts.length === 2) { slug = parts[0]; hash = parts[1]; } let escape = args[args.length - 1]; if (escape === 'true' || escape === 'false') { args.pop(); } else { escape = 'true'; } const factory = postFindOneFactory(ctx); const post = factory({ slug }) || factory({ title: slug }); if (!post) { throw new Error(`Post not found: post_link ${slug}.`); } let title = args.length ? args.join(' ') : post.title || post.slug; // Let attribute be the true post title so it appears in tooltip. const attrTitle = escapeHTML(post.title || post.slug); if (escape === 'true') title = escapeHTML(title); const link = url_for.call(ctx, post.path + (hash ? `#${hash}` : '')); return `<a href="${link}" title="${attrTitle}">${title}</a>`; }; }; ================================================ FILE: lib/plugins/tag/post_path.ts ================================================ import { url_for } from 'hexo-util'; import { postFindOneFactory } from './'; import type Hexo from '../../hexo'; /** * Post path tag * * Syntax: * {% post_path slug | title %} */ export = (ctx: Hexo) => { return function postPathTag(args: any[]) { const slug = args.shift(); if (!slug) return; const factory = postFindOneFactory(ctx); const post = factory({ slug }) || factory({ title: slug }); if (!post) return; const link = url_for.call(ctx, post.path); return link; }; }; ================================================ FILE: lib/plugins/tag/pullquote.ts ================================================ import type Hexo from '../../hexo'; /** * Pullquote tag * * Syntax: * {% pullquote [class] %} * Quote string * {% endpullquote %} */ export = (ctx: Hexo) => function pullquoteTag(args: string[], content: string) { args.unshift('pullquote'); const result = ctx.render.renderSync({text: content, engine: 'markdown'}); return `<blockquote class="${args.join(' ')}">${result}</blockquote>`; }; ================================================ FILE: lib/plugins/tag/url_for.ts ================================================ import { url_for, htmlTag } from 'hexo-util'; import type Hexo from '../../hexo'; /** * Url for tag * * Syntax: * {% url_for text path [relative] %} */ export = (ctx: Hexo) => { return function urlForTag([text, path, relative]) { const url = url_for.call(ctx, path, relative ? { relative: relative !== 'false' } : undefined); const attrs = { href: url }; return htmlTag('a', attrs, text); }; }; ================================================ FILE: lib/theme/index.ts ================================================ import { extname } from 'path'; import Box from '../box'; import View from './view'; import I18n from 'hexo-i18n'; import { config } from './processors/config'; import { i18n } from './processors/i18n'; import { source } from './processors/source'; import { view } from './processors/view'; import type Hexo from '../hexo'; class Theme extends Box { public config: any; public views: Record<string, Record<string, View>>; public i18n: I18n; public View: typeof View; constructor(ctx: Hexo, options?: any) { super(ctx, ctx.theme_dir, options); this.config = {}; this.views = {}; this.processors = [ config, i18n, source, view ]; let languages: string | string[] = ctx.config.language; if (!Array.isArray(languages)) languages = [languages]; languages.push('default'); this.i18n = new I18n({ languages: [...new Set(languages.filter(Boolean))] }); class _View extends View {} this.View = _View; _View.prototype._theme = this; _View.prototype._render = ctx.render; _View.prototype._helper = ctx.extend.helper; } getView(path: string): View { // Replace backslashes on Windows path = path.replace(/\\/g, '/'); const ext = extname(path); const name = path.substring(0, path.length - ext.length); const views = this.views[name]; if (!views) return; if (ext) { return views[ext]; } return views[Object.keys(views)[0]]; } setView(path: string, data: string): void { const ext = extname(path); const name = path.substring(0, path.length - ext.length); this.views[name] = this.views[name] || {}; const views = this.views[name]; views[ext] = new this.View(path, data); } removeView(path: string): void { const ext = extname(path); const name = path.substring(0, path.length - ext.length); const views = this.views[name]; if (!views) return; views[ext] = undefined; } } export = Theme; ================================================ FILE: lib/theme/processors/config.ts ================================================ import { Pattern } from 'hexo-util'; import type { _File } from '../../box'; import Theme from '..'; function process(file: _File) { if (file.type === 'delete') { (file.box as Theme).config = {}; return; } return file.render().then(result => { (file.box as Theme).config = result; this.log.debug('Theme config loaded.'); }).catch(err => { this.log.error('Theme config load failed.'); throw err; }); } const pattern = new Pattern(/^_config\.\w+$/); export const config = { pattern, process }; ================================================ FILE: lib/theme/processors/i18n.ts ================================================ import { Pattern } from 'hexo-util'; import { extname } from 'path'; import type { _File } from '../../box'; import type Theme from '..'; function process(file: _File) { const { path } = file.params; const ext = extname(path); const name = path.substring(0, path.length - ext.length); const { i18n } = (file.box as Theme); if (file.type === 'delete') { i18n.remove(name); return; } return file.render().then(data => { if (typeof data !== 'object') return; i18n.set(name, data); }); } const pattern = new Pattern('languages/*path'); export const i18n = { pattern, process }; ================================================ FILE: lib/theme/processors/source.ts ================================================ import { Pattern } from 'hexo-util'; import * as common from '../../plugins/processor/common'; import type { _File } from '../../box'; function process(file: _File) { const Asset = this.model('Asset'); const id = file.source.substring(this.base_dir.length).replace(/\\/g, '/'); const { path } = file.params; const doc = Asset.findById(id); if (file.type === 'delete') { if (doc) { return doc.remove(); } return; } return Asset.save({ _id: id, path, modified: file.type !== 'skip' }); } const pattern = new Pattern(path => { if (!path.startsWith('source/')) return false; path = path.substring(7); if (common.isHiddenFile(path) || common.isTmpFile(path) || path.includes('node_modules')) return false; return {path}; }); export const source = { pattern, process }; ================================================ FILE: lib/theme/processors/view.ts ================================================ import { Pattern } from 'hexo-util'; import type { _File } from '../../box'; import type Theme from '..'; function process(file: _File): Promise<void> { const { path } = file.params; if (file.type === 'delete') { (file.box as Theme).removeView(path); return; } return file.read().then(result => { (file.box as Theme).setView(path, result); }); } const pattern = new Pattern('layout/*path'); export const view = { pattern, process }; ================================================ FILE: lib/theme/view.ts ================================================ import { dirname, extname, join } from 'path'; import { parse as yfm } from 'hexo-front-matter'; import Promise from 'bluebird'; import type Theme from '.'; import type Render from '../hexo/render'; import type { NodeJSLikeCallback } from '../types'; import type { Helper } from '../extend'; const assignIn = (target: any, ...sources: any[]) => { const length = sources.length; if (length < 1 || target == null) return target; for (let i = 0; i < length; i++) { const source = sources[i]; for (const key in source) { target[key] = source[key]; } } return target; }; class Options { layout?: any; [key: string]: any; } class View { public path: string; public source: string; public _theme: Theme; public data: any; public _compiled: (locals: any) => Promise<any>; public _compiledSync: (locals: any) => any; public _helper: Helper; public _render: Render; constructor(path: string, data: string) { this.path = path; this.source = join(this._theme.base, 'layout', path); this.data = typeof data === 'string' ? yfm(data) : data; this._precompile(); } render(callback: NodeJSLikeCallback<any>): Promise<any>; render(options: Options, callback?: NodeJSLikeCallback<any>): Promise<any>; render(options: Options | NodeJSLikeCallback<any> = {}, callback?: NodeJSLikeCallback<any>): Promise<any> { if (!callback && typeof options === 'function') { callback = options; options = {}; } const { data } = this; const { layout = (options as Options).layout } = data; const locals = this._buildLocals(options as Options); return this._compiled(this._bindHelpers(locals)).then(result => { if (result == null || !layout) return result; const layoutView = this._resolveLayout(layout); if (!layoutView) return result; const layoutLocals = { ...locals, body: result, layout: false }; return layoutView.render(layoutLocals, callback); }).asCallback(callback); } renderSync(options: Options = {}) { const { data } = this; const { layout = options.layout } = data; const locals = this._buildLocals(options); const result = this._compiledSync(this._bindHelpers(locals)); if (result == null || !layout) return result; const layoutView = this._resolveLayout(layout); if (!layoutView) return result; const layoutLocals = { ...locals, body: result, layout: false }; return layoutView.renderSync(layoutLocals); } _buildLocals(locals: Options) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { layout, _content, ...data } = this.data; return assignIn({}, locals, data, { filename: this.source }); } _bindHelpers(locals) { const helpers = this._helper.list(); const keys = Object.keys(helpers); for (const key of keys) { locals[key] = helpers[key].bind(locals); } return locals; } _resolveLayout(name: string): View { // Relative path const layoutPath = join(dirname(this.path), name); let layoutView = this._theme.getView(layoutPath); if (layoutView && layoutView.source !== this.source) return layoutView; // Absolute path layoutView = this._theme.getView(name); if (layoutView && layoutView.source !== this.source) return layoutView; } _precompile(): void { const render = this._render; const ctx = render.context; const ext = extname(this.path); const renderer = render.getRenderer(ext); const data = { path: this.source, text: this.data._content }; function buildFilterArguments(result: any): [string, any, { context: any, args: any[] }] { const output = render.getOutput(ext) || ext; return [ `after_render:${output}`, result, { context: ctx, args: [data] } ]; } if (renderer && typeof renderer.compile === 'function') { const compiled = renderer.compile(data); this._compiledSync = locals => { const result = compiled(locals); return ctx.execFilterSync(...buildFilterArguments(result)); }; this._compiled = locals => Promise.resolve(compiled(locals)) .then(result => ctx.execFilter(...buildFilterArguments(result))); } else { this._compiledSync = locals => render.renderSync(data, locals); this._compiled = locals => render.render(data, locals); } } } export = View; ================================================ FILE: lib/types.ts ================================================ import moment from 'moment'; import type default_config from './hexo/default_config'; import type i18n from 'hexo-i18n'; import type Query from 'warehouse/dist/query'; import type css from './plugins/helper/css'; import type { date, date_xml, time, full_date, relative_date, time_tag, moment as _moment } from './plugins/helper/date'; import type { inspectObject, log } from './plugins/helper/debug'; import type favicon_tag from './plugins/helper/favicon_tag'; import type feed_tag from './plugins/helper/feed_tag'; import type { titlecase, word_wrap, truncate, stripHTML, escapeHTML } from './plugins/helper/format'; import type fragment_cache from './plugins/helper/fragment_cache'; import type full_url_for from './plugins/helper/full_url_for'; import type gravatar from './plugins/helper/gravatar'; import type image_tag from './plugins/helper/image_tag'; import type { current, home, home_first_page, post, page, archive, year, month, category, tag } from './plugins/helper/is'; import type js from './plugins/helper/js'; import type link_to from './plugins/helper/link_to'; import type list_archives from './plugins/helper/list_archives'; import type list_categories from './plugins/helper/list_categories'; import type list_posts from './plugins/helper/list_posts'; import type list_tags from './plugins/helper/list_tags'; import type mail_to from './plugins/helper/mail_to'; import type markdown from './plugins/helper/markdown'; import type meta_generator from './plugins/helper/meta_generator'; import type number_format from './plugins/helper/number_format'; import type open_graph from './plugins/helper/open_graph'; import type paginator from './plugins/helper/paginator'; import type relative_url from './plugins/helper/relative_url'; import type render from './plugins/helper/render'; import type search_form from './plugins/helper/search_form'; import type tag_cloud from './plugins/helper/tagcloud'; import type toc from './plugins/helper/toc'; import type url_for from './plugins/helper/url_for'; export type NodeJSLikeCallback<R, E = any> = (err: E, result?: R) => void export interface RenderData { engine?: string; content?: string; disableNunjucks?: boolean; markdown?: any; source?: string; titlecase?: boolean; title?: string; excerpt?: string; more?: string; } // Schema export interface TagSchema { id?: string; _id?: string; name: string; slug: string; path: string; permalink: string; posts: any; length: number; } export interface DataSchema { id?: string; data: any; } export interface CategorySchema { id?: string; _id?: string; name: string; parent?: string; slug: string; path: string; permalink: string; posts: any; length: number; } export interface PostCategorySchema { _id?: string; post_id: string; category_id: string; } export interface PostTagSchema { _id?: string; post_id: string; tag_id: string; } export interface PostAssetSchema { _id: string; slug: string; modified: boolean; post: string; renderable: boolean; path: string; source: string; } export interface BasePagePostSchema { /** * ID generated by warehouse */ _id?: string; /** * Article title */ title: string; /** * Article created date */ date: moment.Moment, /** * Article last updated date */ updated: moment.Moment, /** * Comment enabled or not */ comments: boolean; /** * Layout name */ layout: string | false; /** * The full processed content of the article */ _content: string; /** * The full processed content of the article */ content?: string; /** * The path of the source file */ source: string; /** * The URL of the article without root URL. * We usually use url_for(page.path) in theme. */ path: string; /** * The raw data of the article */ raw: string; /** * Article excerpt */ excerpt?: string; /** * Contents except article excerpt */ more?: string; /** * Full path of the source file */ full_source: string; /** * Full (encoded) URL of the article */ permalink: string; /** * The photos of the article (Used in gallery posts) */ photos?: string[]; /** * The external link of the article (Used in link posts) */ link?: string; /** * The language of the article */ lang?: string; /** * The language of the article */ language?: string; /** * Base URL */ base?: string; /** * Whether the page is a page */ __page?: boolean; /** * Whether the page is a post */ __post?: boolean; /** * Whether the page is a home page */ __index?: boolean; /** * custom variables set in front-matter. */ [key: string]: any; } export interface PostSchema extends BasePagePostSchema { /** * Post ID */ id?: string; /** * The slug of the post */ slug: string; /** * True if the post is not a draft */ published: boolean; /** * The path of the asset directory */ asset_dir: string; /** * All categories of the post */ categories: Query<CategorySchema>; /** * All tags of the post */ tags: Query<TagSchema>; /** * Inner usage */ __permalink?: string; /** * The previous post, `null` if the post is the first post */ prev?: PostSchema | null; /** * The next post, `null` if the post is the last post */ next?: PostSchema | null; notPublished: () => boolean; setTags: (tags: string[]) => any; setCategories: (cats: (string | string[])[]) => any; } export interface PageSchema extends BasePagePostSchema { /** * Posts displayed per page, only available on home page */ per_page?: number; /** * Total number of pages, only available on home page */ total?: number; /** * Current page number, only available on home page */ current?: number; /** * The URL of current page, only available on home page */ current_url?: string; /** * Posts in this page, only available on home page */ posts?: any; /** * Previous page number. `0` if the current page is the first. only available on home page */ prev?: number; /** * The URL of previous page. `''` if the current page is the first. only available on home page */ prev_link?: string; /** * Next page number. `0` if the current page is the last. only available on home page */ next?: number; /** * The URL of next page. `''` if the current page is the last. only available on home page */ next_link?: string; /** * Equals true, only available on archive page */ archive?: boolean; /** * Archive year (4-digit), only available on archive page */ year?: number; /** * Archive month (2-digit without leading zeros), only available on archive page */ month?: number; /** * Category name, only available on category page */ category?: string; /** * Tag name, only available on tag page */ tag?: string; } export interface AssetSchema { _id?: string; path: string; modified: boolean; renderable: boolean; source: string; } export interface CacheSchema { _id: string; hash: string; modified: number; } // Generator return types export interface BaseGeneratorReturn { /** * Path not including the prefixing `/`. */ path: string; /** * Data */ data?: any; /** * Layout. Specify the layouts for rendering. The value can be a string or an array. * If it’s ignored then the route will return `data` directly. */ layout?: string | string[]; } export interface SiteLocals { /** * All posts */ posts: Query<PostSchema>; /** * All pages */ pages: Query<PageSchema>; /** * All categories */ categories: Query<CategorySchema>; /** * All tags */ tags: Query<TagSchema>; data: any; } export interface LocalsType { // original properties from Locals class /** * Page specific information and custom variables set in front-matter. */ page: BasePagePostSchema; /** * Path of current page */ path: string; /** * Full URL of current page */ url: string; /** * Site configuration. */ config: typeof default_config; /** * Theme configuration. Inherits from site configuration. */ theme: any; layout: string | boolean; /** * Environment variables */ env: any; view_dir: string; /** * Sitewide information. */ site: SiteLocals; cache?: boolean; // i18n properties from i18nLocalsFilter /** * https://hexo.io/docs/internationalization#Templates */ __: ReturnType<i18n['__']>; /** * https://hexo.io/docs/internationalization#Templates */ _p: ReturnType<i18n['_p']>; // result after renderer.compile body?: string; // from _buildLocals filename?: string; // helper functions from _bindHelpers css: typeof css; date: typeof date; date_xml: typeof date_xml; escape_html: typeof escapeHTML; favicon_tag: typeof favicon_tag; feed_tag: typeof feed_tag; fragment_cache: ReturnType<typeof fragment_cache>; full_date: typeof full_date; full_url_for: typeof full_url_for; gravatar: typeof gravatar; image_tag: typeof image_tag; inspect: typeof inspectObject; is_archive: typeof archive; is_category: typeof category; is_current: typeof current; is_home: typeof home; is_home_first_page: typeof home_first_page; is_month: typeof month; is_page: typeof page; is_post: typeof post; is_tag: typeof tag; is_year: typeof year; js: typeof js; link_to: typeof link_to; list_archives: typeof list_archives; list_categories: typeof list_categories; list_posts: typeof list_posts; list_tags: typeof list_tags; log: typeof log; mail_to: typeof mail_to; markdown: typeof markdown; meta_generator: typeof meta_generator; moment: typeof _moment; number_format: typeof number_format; open_graph: typeof open_graph; paginator: typeof paginator; partial: ReturnType<typeof render>; relative_date: typeof relative_date; relative_url: typeof relative_url; render: ReturnType<typeof render>; search_form: typeof search_form; strip_html: typeof stripHTML; tag_cloud: typeof tag_cloud; tagcloud: typeof tag_cloud; time: typeof time; time_tag: typeof time_tag; titlecase: typeof titlecase; toc: typeof toc; trim: typeof stripHTML; truncate: typeof truncate; url_for: typeof url_for; word_wrap: typeof word_wrap; } export interface FilterOptions { context?: any; args?: any[]; } ================================================ FILE: package.json ================================================ { "name": "hexo", "version": "8.1.1", "description": "A fast, simple & powerful blog framework, powered by Node.js.", "main": "dist/hexo", "bin": { "hexo": "./bin/hexo" }, "scripts": { "prepublishOnly": "npm install && npm run clean && npm run build", "build": "tsc -b", "clean": "tsc -b --clean", "eslint": "eslint lib test", "test": "mocha test/scripts/**/*.ts --require ts-node/register", "test-cov": "c8 --reporter=lcovonly npm test -- --no-parallel", "prepare": "husky" }, "files": [ "dist/", "bin/" ], "types": "./dist/hexo/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/hexojs/hexo.git" }, "homepage": "https://hexo.io/", "funding": { "type": "opencollective", "url": "https://opencollective.com/hexo" }, "keywords": [ "website", "blog", "cms", "framework", "hexo" ], "author": "Tommy Chen <tommy351@gmail.com> (https://zespia.tw)", "maintainers": [ "Abner Chou <hi@abnerchou.me> (https://abnerchou.me)" ], "license": "MIT", "dependencies": { "abbrev": "^3.0.0", "bluebird": "^3.7.2", "fast-archy": "^1.0.0", "fast-text-table": "^1.0.1", "hexo-cli": "^4.3.2", "hexo-front-matter": "^4.2.1", "hexo-fs": "^5.0.0", "hexo-i18n": "^2.0.0", "hexo-log": "^4.1.0", "hexo-util": "^4.0.0", "js-yaml": "^4.1.0", "js-yaml-js-types": "^1.0.1", "micromatch": "^4.0.8", "moize": "^6.1.6", "moment": "^2.30.1", "moment-timezone": "^0.5.46", "nunjucks": "^3.2.4", "picocolors": "^1.1.1", "pretty-hrtime": "^1.0.3", "strip-ansi": "^7.1.0", "tildify": "^2.0.0", "titlecase": "^1.1.3", "warehouse": "^6.0.0" }, "devDependencies": { "@types/abbrev": "^1.1.3", "@types/bluebird": "^3.5.37", "@types/chai": "^4.3.11", "@types/graceful-fs": "^4.1.9", "@types/js-yaml": "^4.0.9", "@types/micromatch": "^4.0.7", "@types/mocha": "^10.0.9", "@types/node": "^20.17.6", "@types/nunjucks": "^3.2.2", "@types/sinon": "^17.0.3", "0x": "^5.1.2", "c8": "^9.0.0", "chai": "^4.3.6", "cheerio": "1.0.0", "decache": "^4.6.1", "eslint": "^9.17.0", "eslint-config-hexo": "^6.0.0", "hexo-renderer-marked": "^6.0.0", "husky": "^9.1.7", "lint-staged": "^15.3.0", "mocha": "^10.0.0", "sinon": "^17.0.1", "ts-node": "^10.9.1", "typescript": "^5.3.2" }, "engines": { "node": ">=20.19.0" } } ================================================ FILE: test/benchmark.js ================================================ const { performance, PerformanceObserver } = require('perf_hooks'); const { spawn } = require('child_process'); const { spawn: spawnAsync } = require('hexo-util'); const { rmdir, exists } = require('hexo-fs'); const { appendFileSync: appendFile } = require('fs'); const { resolve } = require('path'); const log = require('hexo-log').default(); const { red } = require('picocolors'); const hooks = [ { regex: /Hexo version/, tag: 'hexo-begin' }, { regex: /Start processing/, tag: 'processing' }, { regex: /Rendering post/, tag: 'render-post' }, { regex: /Files loaded/, tag: 'file-loaded' }, { regex: /generated in/, tag: 'generated' }, { regex: /Database saved/, tag: 'database-saved' } ]; const isWin32 = require('os').platform() === 'win32'; const npmScript = isWin32 ? 'npm.cmd' : 'npm'; const testDir = resolve('.tmp-hexo-theme-unit-test'); const zeroEksDir = resolve(testDir, '0x'); const hexoBin = resolve(testDir, 'node_modules/.bin/hexo'); const isGitHubActions = process.env.GITHUB_ACTIONS; const zeroEks = require('0x'); let isProfiling = process.argv.join(' ').includes('--profiling'); let isBenchmark = process.argv.join(' ').includes('--benchmark'); if (!isProfiling && !isBenchmark) { isProfiling = true; isBenchmark = true; } (async () => { await init(); if (isBenchmark) { log.info('Running benchmark'); if (isGitHubActions) { log.info('Running in GitHub Actions.'); appendFile(process.env.GITHUB_STEP_SUMMARY, '# Benchmark Result\n'); } await cleanUp(); await run_benchmark('Cold processing'); await run_benchmark('Hot processing'); await cleanUp(); await run_benchmark('Another Cold processing'); } if (isProfiling) { await cleanUp(); await profiling(); } })(); async function run_benchmark(name) { let measureFinished = false; return new Promise(resolve => { const result = {}; const obs = new PerformanceObserver(list => { list .getEntries() .sort((a, b) => a.detail - b.detail) .forEach(entry => { const { name, duration: _duration } = entry; const duration = _duration / 1000; result[name] = { 'Cost time (s)': `${duration.toFixed(2)}s` }; if (duration > 20) { log.fatal(red('!! Performance regression detected !!')); } }); if (measureFinished) { obs.disconnect(); if (isGitHubActions) { appendFile(process.env.GITHUB_STEP_SUMMARY, `\n## ${name}\n\n| Step | Cost time (s) |\n| --- | --- |\n${Object.keys(result).map(name => `| ${name} | ${result[name]['Cost time (s)']} |`).join('\n')}\n`); } console.log(name); console.table(result); resolve(result); } }); obs.observe({ entryTypes: ['measure'] }); const hexo = spawn('node', [hexoBin, 'g', '--debug'], { cwd: testDir }); hooks.forEach(({ regex, tag }) => { hexo.stdout.on('data', function listener(data) { const string = data.toString('utf-8'); if (regex.test(string)) { performance.mark(tag); hexo.stdout.removeListener('data', listener); } }); }); hexo.on('close', () => { performance.measure('Load Plugin/Scripts/Database', 'hexo-begin', 'processing'); if (name === 'Hot processing') { performance.measure('Process Source', { start: 'processing', end: 'file-loaded', detail: 0 }); } else { performance.measure('Process Source', { start: 'processing', end: 'render-post', detail: 1 }); performance.measure('Render Posts', { start: 'render-post', end: 'file-loaded', detail: 2 }); } performance.measure('Render Files', { start: 'file-loaded', end: 'generated', detail: 3 }); performance.measure('Save Database', { start: 'generated', end: 'database-saved', detail: 4 }); performance.measure('Total time', { start: 'hexo-begin', end: 'database-saved', detail: 5 }); measureFinished = true; }); }); } async function cleanUp() { return spawnAsync(hexoBin, ['clean'], { cwd: testDir }); } async function gitClone(repo, dir, depth = 1) { return spawnAsync('git', ['clone', repo, dir, `--depth=${depth}`]); } async function init() { if (await exists(testDir)) { log.info(`"${testDir}" already exists. Skipping benchmark environment setup.`); } else { log.info('Setting up a dummy hexo site with 500 posts'); await gitClone('https://github.com/hexojs/hexo-theme-unit-test.git', testDir); await gitClone('https://github.com/hexojs/hexo-theme-landscape', resolve(testDir, 'themes', 'landscape')); await gitClone('https://github.com/hexojs/hexo-many-posts.git', resolve(testDir, 'source', '_posts', 'hexo-many-posts')); } log.info('Installing dependencies'); // Always re-install dependencies if (await exists(resolve(testDir, 'node_modules'))) await rmdir(resolve(testDir, 'node_modules')); await spawnAsync(npmScript, ['install', '--silent'], { cwd: testDir }); log.info('Build hexo'); await spawnAsync(npmScript, ['run', 'build']); log.info('Replacing hexo'); await rmdir(resolve(testDir, 'node_modules', 'hexo')); if (isWin32) { await spawnAsync('cmd', [ '/s', '/c', 'mklink', '/D', resolve(testDir, 'node_modules', 'hexo'), resolve(__dirname, '..') ]); await rmdir(resolve(testDir, 'node_modules', 'hexo-cli')); await spawnAsync('cmd', [ '/s', '/c', 'mklink', '/D', resolve(testDir, 'node_modules', 'hexo-cli'), resolve(__dirname, '..', 'node_modules', 'hexo-cli') ]); } else { await spawnAsync('ln', [ '-sf', resolve(__dirname, '..'), resolve(testDir, 'node_modules', 'hexo') ]); } } async function profiling() { // Clean up 0x dir before profiling if (await exists(zeroEksDir)) await rmdir(zeroEksDir); const zeroEksOpts = { argv: [hexoBin, 'g', '--cwd', testDir], workingDir: '.', // A workaround for https://github.com/davidmarkclements/0x/issues/228 outputDir: zeroEksDir, title: 'Hexo Flamegraph' }; log.info('Profiling'); const file = await zeroEks(zeroEksOpts); // A small hack that workaround 0x's broken stdout handling console.log(''); log.info(file); } ================================================ FILE: test/fixtures/_config.json ================================================ { "author": "waldo", "favorites": { "food": "ice cream" } } ================================================ FILE: test/fixtures/hello.njk ================================================ Hello {{ name }}! ================================================ FILE: test/fixtures/post_render.ts ================================================ import { highlight } from 'hexo-util'; const code = [ 'if tired && night:', ' sleep()' ].join('\n'); export const content = [ '# Title', '``` python', code, '```', 'some content', '', '## Another title', '{% blockquote %}', 'quote content 1', '{% endblockquote %}', '', '{% quote Hello World %}', 'quote content 2', '{% endquote %}' ].join('\n'); export const expected = [ '<h1 id="Title"><a href="#Title" class="headerlink" title="Title"></a>Title</h1>', highlight(code, {lang: 'python'}), '\n<p>some content</p>\n', '<h2 id="Another-title"><a href="#Another-title" class="headerlink" title="Another title"></a>Another title</h2>', '<blockquote>', '<p>quote content 1</p>\n', '</blockquote>\n\n', '<blockquote><p>quote content 2</p>\n', '<footer><strong>Hello World</strong></footer></blockquote>' ].join(''); export const expected_disable_nunjucks = [ '<h1 id="Title"><a href="#Title" class="headerlink" title="Title"></a>Title</h1>', highlight(code, {lang: 'python'}), '\n<p>some content</p>\n', '<h2 id="Another-title"><a href="#Another-title" class="headerlink" title="Another title"></a>Another title</h2>', '<p>{% blockquote %}<br>', 'quote content 1<br>', '{% endblockquote %}</p>\n', '<p>{% quote Hello World %}<br>', 'quote content 2<br>', '{% endquote %}</p>' ].join(''); export const content_for_issue_3346 = [ '# Title', '```', '{% test1 %}', '{{ test2 }}', '```', 'some content', '', '## Another title', '{% blockquote %}', 'quote content', '{% endblockquote %}' ].join('\n'); export const expected_for_issue_3346 = [ '<h1 id="Title"><a href="#Title" class="headerlink" title="Title"></a>Title</h1>', highlight('{% test1 %}\n{{ test2 }}').replace(/{/g, '{').replace(/}/g, '}'), // Escaped by backtick_code_block '\n<p>some content</p>\n', '<h2 id="Another-title"><a href="#Another-title" class="headerlink" title="Another title"></a>Another title</h2>', '<blockquote>', '<p>quote content</p>\n', '</blockquote>' ].join(''); export const content_for_issue_4460 = [ '```html', '<body>', '<!-- here goes the rest of the page -->', '</body>', '```' ].join('\n'); ================================================ FILE: test/scripts/box/box.ts ================================================ import { join, sep } from 'path'; import { appendFile, mkdir, mkdirs, rename, rmdir, stat, unlink, writeFile } from 'hexo-fs'; import { hash, Pattern } from 'hexo-util'; import { spy, match, assert as sinonAssert } from 'sinon'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import Box from '../../../lib/box'; import chai from 'chai'; const should = chai.should(); describe('Box', () => { const baseDir = join(__dirname, 'box_tmp'); const newBox = (path?, config?) => { const hexo = new Hexo(baseDir, { silent: true }); hexo.config = Object.assign(hexo.config, config); const base = path ? join(baseDir, path) : baseDir; return new Box(hexo, base); }; before(() => mkdir(baseDir)); after(() => rmdir(baseDir)); it('constructor - add trailing "/" to the base path', () => { const box = newBox('foo'); box.base.should.eql(join(baseDir, 'foo') + sep); }); it('addProcessor() - no pattern', () => { const box = newBox(); box.addProcessor(() => 'test'); const p = box.processors[0]; p.pattern.match('').should.eql({}); p.process().should.eql('test'); }); it('addProcessor() - with regex', () => { const box = newBox(); box.addProcessor(/^foo/, () => 'test'); const p = box.processors[0]; p.pattern.match('foobar').should.be.ok; p.pattern.should.be.an.instanceof(Pattern); p.process().should.eql('test'); }); it('addProcessor() - with pattern', () => { const box = newBox(); box.addProcessor(new Pattern(/^foo/), () => 'test'); const p = box.processors[0]; p.pattern.match('foobar').should.be.ok; p.pattern.should.be.an.instanceof(Pattern); p.process().should.eql('test'); }); it('addProcessor() - no fn', () => { const box = newBox(); // @ts-expect-error should.throw(() => box.addProcessor('test'), 'fn must be a function'); }); it('process()', async () => { const box = newBox('test'); const data: Record<string, any> = {}; box.addProcessor(file => { data[file.path] = file; }); await BluebirdPromise.all([ writeFile(join(box.base, 'a.txt'), 'a'), writeFile(join(box.base, 'b', 'c.js'), 'c') ]); await box.process(); for (const [key, item] of Object.entries(data)) { item.path.should.eql(key); item.source.should.eql(join(box.base, key)); item.type.should.eql('create'); item.params.should.eql({}); } await rmdir(box.base); }); it('process() - do nothing if target does not exist', async () => { const box = newBox('test'); return box.process(); }); it('process() - create', async () => { const box = newBox('test'); const name = 'a.txt'; const path = join(box.base, name); const processor = spy(); box.addProcessor(processor); await writeFile(path, 'a'); await box.process(); sinonAssert.calledWithMatch(processor, { type: 'create', path: name }); await rmdir(box.base); }); it('process() - update (mtime changed and hash changed)', async () => { const box = newBox('test'); const name = 'a.txt'; const path = join(box.base, name); const cacheId = 'test/' + name; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(path, 'a'), box.Cache.insert({ _id: cacheId, modified: 0, hash: hash('b').toString('hex') }) ]); await box.process(); sinonAssert.calledWithMatch(processor, { type: 'update', path: name }); await rmdir(box.base); }); it('process() - skip (mtime changed but hash matched)', async () => { const box = newBox('test'); const name = 'a.txt'; const path = join(box.base, name); const cacheId = 'test/' + name; const processor = spy(); box.addProcessor(processor); await writeFile(path, 'a'); await stat(path); await box.Cache.insert({ _id: cacheId, modified: 0, hash: hash('a').toString('hex') }); await box.process(); sinonAssert.calledWithMatch(processor, { type: 'skip', path: name }); await rmdir(box.base); }); it('process() - skip (hash changed but mtime matched)', async () => { const box = newBox('test'); const name = 'a.txt'; const path = join(box.base, name); const cacheId = 'test/' + name; const processor = spy(); box.addProcessor(processor); await writeFile(path, 'a'); const stats = await stat(path); await box.Cache.insert({ _id: cacheId, modified: stats.mtime, hash: hash('b').toString('hex') }); await box.process(); sinonAssert.calledWithMatch(processor, { type: 'skip', path: name }); await rmdir(box.base); }); it('process() - skip (mtime matched and hash matched)', async () => { const box = newBox('test'); const name = 'a.txt'; const path = join(box.base, name); const cacheId = 'test/' + name; const processor = spy(); box.addProcessor(processor); await writeFile(path, 'a'); const stats = await stat(path); await box.Cache.insert({ _id: cacheId, modified: stats.mtime, hash: hash('a').toString('hex') }); await box.process(); sinonAssert.calledWithMatch(processor, { type: 'skip', path: name }); await rmdir(box.base); }); it('process() - delete', async () => { const box = newBox('test'); const cacheId = 'test/a.txt'; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ mkdirs(box.base), box.Cache.insert({ _id: cacheId }) ]); await box.process(); sinonAssert.calledWith(processor, match.has('type', 'delete')); processor.calledOnce.should.be.true; await rmdir(box.base); }); it('process() - params', async () => { const box = newBox('test'); const path = join(box.base, 'posts', '123456'); const processor = spy(); box.addProcessor('posts/:id', processor); await writeFile(path, 'a'); await box.process(); sinonAssert.calledWith(processor, match.has('params', match.has('id', '123456'))); processor.calledOnce.should.be.true; await rmdir(box.base); }); it('process() - handle null ignore', async () => { const box = newBox('test', { ignore: null }); const data = {}; box.addProcessor(file => { data[file.path] = file; }); await writeFile(join(box.base, 'foo.txt'), 'foo'); await box.process(); data.should.have.all.keys(['foo.txt']); await rmdir(box.base); }); it('process() - error ignore - 1', async () => { const box = newBox('test', { ignore: [null] }); box.options.ignored.should.eql([]); }); it('process() - error ignore - 2', async () => { const box = newBox('test', { ignore: [111] }); box.options.ignored.should.eql([]); }); it('process() - skip files if they match a glob epression in ignore', async () => { const box = newBox('test', { ignore: '**/ignore_me' }); const data: object = {}; box.addProcessor(file => { data[file.path] = file; }); await BluebirdPromise.all([ writeFile(join(box.base, 'foo.txt'), 'foo'), writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me') ]); await box.process(); data.should.have.all.keys(['foo.txt']); await rmdir(box.base); }); it('process() - skip files if they match any of the glob expressions in ignore', async () => { const box = newBox('test', { ignore: ['**/ignore_me', '**/ignore_me_too.txt'] }); const data = {}; box.addProcessor(file => { data[file.path] = file; }); await BluebirdPromise.all([ writeFile(join(box.base, 'foo.txt'), 'foo'), writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me'), writeFile(join(box.base, 'ignore_me_too.txt'), 'ignore_me_too') ]); await box.process(); data.should.have.all.keys(['foo.txt']); await rmdir(box.base); }); it('watch() - create', async () => { const box = newBox('test'); const path = 'a.txt'; const src = join(box.base, path); const processor = spy(); box.addProcessor(processor); await writeFile(src, 'a'); await box.watch(); box.isWatching().should.be.true; await BluebirdPromise.delay(500); sinonAssert.calledWithMatch(processor.firstCall, { source: src, path: path, type: 'create', params: {} }); box.unwatch(); await rmdir(box.base); }); it('watch() - update', async () => { const box = newBox('test'); const path = 'a.txt'; const src = join(box.base, path); const cacheId = 'test/' + path; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src, 'a'), Cache.insert({_id: cacheId}) ]); await box.watch(); await appendFile(src, 'b'); await BluebirdPromise.delay(500); sinonAssert.calledWithMatch(processor.lastCall, { source: src, path: path, type: 'update', params: {} }); box.unwatch(); await rmdir(box.base); }); it('watch() - delete', async () => { const box = newBox('test'); const path = 'a.txt'; const src = join(box.base, path); const cacheId = 'test/' + path; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src, 'a'), Cache.insert({_id: cacheId}) ]); await box.watch(); await unlink(src); await BluebirdPromise.delay(500); sinonAssert.calledWithMatch(processor.lastCall, { source: src, path: path, type: 'delete', params: {} }); box.unwatch(); await rmdir(box.base); }); it('watch() - rename file', async () => { const box = newBox('test'); const path = 'a.txt'; const src = join(box.base, path); const newPath = 'b.txt'; const newSrc = join(box.base, newPath); const cacheId = 'test/' + path; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src, 'a'), Cache.insert({_id: cacheId}) ]); await box.watch(); await rename(src, newSrc); await BluebirdPromise.delay(500); for (const [file] of processor.args.slice(-2)) { switch (file.type) { case 'create': file.source.should.eql(newSrc); file.path.should.eql(newPath); break; case 'delete': file.source.should.eql(src); file.path.should.eql(path); break; } } box.unwatch(); await rmdir(box.base); }); it('watch() - rename folder', async () => { const box = newBox('test'); const path = 'a/b.txt'; const src = join(box.base, path); const newPath = 'b/b.txt'; const newSrc = join(box.base, newPath); const cacheId = 'test/' + path; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src, 'a'), Cache.insert({_id: cacheId}) ]); await box.watch(); await rename(join(box.base, 'a'), join(box.base, 'b')); await BluebirdPromise.delay(500); for (const [file] of processor.args.slice(-2)) { switch (file.type) { case 'create': file.source.should.eql(newSrc); file.path.should.eql(newPath); break; case 'delete': file.source.should.eql(src); file.path.should.eql(path); break; } } box.unwatch(); await rmdir(box.base); }); it('watch() - update with simple "ignore" option', async () => { const box = newBox('test', {ignore: '**/ignore_me/**'}); const path1 = 'a.txt'; const path2 = 'b.txt'; const src1 = join(box.base, path1); const src2 = join(box.base, 'ignore_me', path2); const cacheId1 = 'test/' + path1; const cacheId2 = 'test/ignore_me/' + path2; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src1, 'a'), Cache.insert({_id: cacheId1}) ]); await BluebirdPromise.all([ writeFile(src2, 'b'), Cache.insert({_id: cacheId2}) ]); await box.watch(); await appendFile(src1, 'aaa'); await BluebirdPromise.delay(500); const file = processor.lastCall.args[0]; file.should.deep.include({ source: src1, path: path1, type: 'update', params: {} }); await appendFile(src2, 'bbb'); await BluebirdPromise.delay(500); const file2 = processor.lastCall.args[0]; file2.should.eql(file); // not changed box.unwatch(); await rmdir(box.base); }); it('watch() - update with complex "ignore" option', async () => { const box = newBox('test', {ignore: ['**/ignore_me/**', '**/ignore_me_too.txt']}); const path1 = 'a.txt'; const path2 = 'b.txt'; const path3 = 'ignore_me_too.txt'; const src1 = join(box.base, path1); const src2 = join(box.base, 'ignore_me', path2); const src3 = join(box.base, path3); const cacheId1 = 'test/' + path1; const cacheId2 = 'test/ignore_me/' + path2; const cacheId3 = 'test/' + path3; const { Cache } = box; const processor = spy(); box.addProcessor(processor); await BluebirdPromise.all([ writeFile(src1, 'a'), Cache.insert({_id: cacheId1}) ]); await BluebirdPromise.all([ writeFile(src2, 'b'), Cache.insert({_id: cacheId2}) ]); await BluebirdPromise.all([ writeFile(src3, 'c'), Cache.insert({_id: cacheId3}) ]); await box.watch(); await appendFile(src1, 'aaa'); await BluebirdPromise.delay(500); const file = processor.lastCall.args[0]; file.should.deep.include({ source: src1, path: path1, type: 'update', params: {} }); await appendFile(src2, 'bbb'); await BluebirdPromise.delay(500); processor.lastCall.args[0].should.eql(file); // not changed await appendFile(src3, 'ccc'); await BluebirdPromise.delay(500); processor.lastCall.args[0].should.eql(file); // not changed box.unwatch(); await rmdir(box.base); }); it('watch() - watcher has started', async () => { const box = newBox(); await box.watch(); await box.watch().then(() => { should.fail('Return value must be rejected'); }, err => { err.should.property('message', 'Watcher has already started.'); }); box.unwatch(); }); it('watch() - run process() before start watching', async () => { const box = newBox('test'); const data: string[] = []; box.addProcessor(file => { data.push(file.path); }); await BluebirdPromise.all([ writeFile(join(box.base, 'a.txt'), 'a'), writeFile(join(box.base, 'b', 'c.js'), 'c') ]); await box.watch(); data.should.have.members(['a.txt', 'b/c.js']); box.unwatch(); await rmdir(box.base); }); it('unwatch()', async () => { const box = newBox('test'); const processor = spy(); await box.watch(); box.addProcessor(processor); box.unwatch(); await writeFile(join(box.base, 'a.txt'), 'a'); processor.called.should.be.false; box.unwatch(); await rmdir(box.base); }); it('isWatching()', async () => { const box = newBox(); box.isWatching().should.be.false; await box.watch(); box.isWatching().should.be.true; box.unwatch(); box.isWatching().should.be.false; box.unwatch(); }); it('processBefore & processAfter events', async () => { const box = newBox('test'); const beforeSpy = spy(); const afterSpy = spy(); box.on('processBefore', beforeSpy); box.on('processAfter', afterSpy); await writeFile(join(box.base, 'a.txt'), 'a'); await box.process(); sinonAssert.calledWithMatch(beforeSpy, { type: 'create', path: 'a.txt' }); sinonAssert.calledWithMatch(afterSpy, { type: 'create', path: 'a.txt' }); beforeSpy.calledOnce.should.be.true; afterSpy.calledOnce.should.be.true; await rmdir(box.base); }); }); ================================================ FILE: test/scripts/box/file.ts ================================================ import { join } from 'path'; import { rmdir, stat, statSync, writeFile } from 'hexo-fs'; import { load } from 'js-yaml'; import Hexo from '../../../lib/hexo'; import Box from '../../../lib/box'; describe('File', () => { const hexo = new Hexo(__dirname); const box = new Box(hexo, join(hexo.base_dir, 'file_test')); const { File } = box; const body = [ 'name:', ' first: John', ' last: Doe', '', 'age: 23', '', 'list:', '- Apple', '- Banana' ].join('\n'); const obj = load(body); const path = 'test.yml'; const makeFile = (path, props) => { return new File(Object.assign({ source: join(box.base, path), path }, props)); }; const file = makeFile(path, { source: join(box.base, path), path, type: 'create', params: {foo: 'bar'} }); // NOTE: Do not use `arrow function` here. // See https://mochajs.org/#arrow-functions before(async function() { this.timeout(20000); await Promise.all([ writeFile(file.source, body), hexo.init() ]); stat(file.source); }); after(() => rmdir(box.base)); it('read()', async () => { const result = await file.read(); result.should.eql(body); }); it('readSync()', () => { file.readSync().should.eql(body); }); it('stat()', async () => { const stats = await Promise.all([ stat(file.source), file.stat() ]); stats[0].should.eql(stats[1]); }); it('statSync()', () => { file.statSync().should.eql(statSync(file.source)); }); it('render()', async () => { const result = await file.render(); result.should.eql(obj); }); it('renderSync()', () => { file.renderSync().should.eql(obj); }); }); ================================================ FILE: test/scripts/console/clean.ts ================================================ import { exists, mkdirs, unlink, writeFile } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import cleanConsole from '../../../lib/plugins/console/clean'; type OriginalParams = Parameters<typeof cleanConsole>; type OriginalReturn = ReturnType<typeof cleanConsole>; describe('clean', () => { let hexo: Hexo, clean: (...args: OriginalParams) => OriginalReturn; beforeEach(() => { hexo = new Hexo(__dirname, {silent: true}); clean = cleanConsole.bind(hexo); }); it('delete database', async () => { const dbPath = hexo.database.options.path; await writeFile(dbPath, ''); await clean(); const exist = await exists(dbPath); exist.should.be.false; }); it('delete public folder', async () => { const publicDir = hexo.public_dir; await mkdirs(publicDir); await clean(); const exist = await exists(publicDir); exist.should.be.false; }); it('execute corresponding filter', async () => { const extraDbPath = hexo.database.options.path + '.tmp'; hexo.extend.filter.register('after_clean', () => { return unlink(extraDbPath); }); await writeFile(extraDbPath, ''); await clean(); const exist = await exists(extraDbPath); exist.should.be.false; }); }); ================================================ FILE: test/scripts/console/config.ts ================================================ import { mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import { join } from 'path'; import { load } from 'js-yaml'; import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import configConsole from '../../../lib/plugins/console/config'; type OriginalParams = Parameters<typeof configConsole>; type OriginalReturn = ReturnType<typeof configConsole>; import chai from 'chai'; const should = chai.should(); describe('config', () => { const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true}); const config: (...args: OriginalParams) => OriginalReturn = configConsole.bind(hexo); before(async () => { await mkdirs(hexo.base_dir); hexo.init(); }); beforeEach(() => writeFile(hexo.config_path, '')); after(() => rmdir(hexo.base_dir)); it('read all config', async () => { const logStub = stub(console, 'log'); try { await config({_: []}); } finally { logStub.restore(); } sinonAssert.calledWith(logStub, hexo.config); }); it('read config', async () => { const logStub = stub(console, 'log'); try { await config({_: ['title']}); } finally { logStub.restore(); } sinonAssert.calledWith(logStub, hexo.config.title); }); it('read nested config', async () => { const logStub = stub(console, 'log'); try { (hexo.config as any).server = { port: 12345 }; await config({_: ['server.port']}); sinonAssert.calledWith(logStub, (hexo.config as any).server.port); } finally { delete(hexo.config as any).server; logStub.restore(); } }); async function writeConfig(...args) { await config({_: args}); const content = await readFile(hexo.config_path); return load(content) as any; } it('write config', async () => { const config = await writeConfig('title', 'My Blog'); config.title.should.eql('My Blog'); }); it('write config: number', async () => { const config = await writeConfig('server.port', '5000'); config.server.port.should.eql(5000); }); it('write config: false', async () => { const config = await writeConfig('post_asset_folder', 'false'); config.post_asset_folder.should.be.false; }); it('write config: true', async () => { const config = await writeConfig('post_asset_folder', 'true'); config.post_asset_folder.should.be.true; }); it('write config: null', async () => { const config = await writeConfig('language', 'null'); should.not.exist(config.language); }); it('write config: undefined', async () => { const config = await writeConfig('meta_generator', 'undefined'); should.not.exist(config.meta_generator); }); it('write config: json', async () => { const configPath = join(hexo.base_dir, '_config.json'); hexo.config_path = join(hexo.base_dir, '_config.json'); await writeFile(configPath, '{}'); await config({_: ['title', 'My Blog']}); return readFile(configPath).then(content => { const json = JSON.parse(content); json.title.should.eql('My Blog'); hexo.config_path = join(hexo.base_dir, '_config.yml'); return unlink(configPath); }); }); it('create config if not exist', async () => { await unlink(hexo.config_path); const config = await writeConfig('subtitle', 'Hello world'); config.subtitle.should.eql('Hello world'); }); }); ================================================ FILE: test/scripts/console/deploy.ts ================================================ import { exists, mkdirs, readFile, rmdir, writeFile } from 'hexo-fs'; import { join } from 'path'; import { spy, stub, assert as sinonAssert } from 'sinon'; import chai from 'chai'; const should = chai.should(); import Hexo from '../../../lib/hexo'; import deployConsole from '../../../lib/plugins/console/deploy'; type OriginalParams = Parameters<typeof deployConsole>; type OriginalReturn = ReturnType<typeof deployConsole>; describe('deploy', () => { const hexo = new Hexo(join(__dirname, 'deploy_test'), { silent: true }); const deploy: (...args: OriginalParams) => OriginalReturn = deployConsole.bind(hexo); before(async () => { await mkdirs(hexo.public_dir); hexo.init(); }); beforeEach(() => { hexo.config.deploy = { type: 'foo' }; hexo.extend.deployer.register('foo', () => { }); }); after(() => rmdir(hexo.base_dir)); it('no deploy config', () => { delete(hexo.config as any).deploy; const logStub = stub(console, 'log'); try { should.not.exist(deploy({ test: true })); } finally { logStub.restore(); } sinonAssert.calledWithMatch( logStub, 'You should configure deployment settings in _config.yml first!' ); }); it('single deploy setting', async () => { hexo.config.deploy = { type: 'foo', foo: 'bar' }; const deployer = spy(); const beforeListener = spy(); const afterListener = spy(); hexo.once('deployBefore', beforeListener); hexo.once('deployAfter', afterListener); hexo.extend.deployer.register('foo', deployer); await deploy({ foo: 'foo', bar: 'bar' }); deployer.calledOnce.should.be.true; beforeListener.calledOnce.should.be.true; afterListener.calledOnce.should.be.true; sinonAssert.calledWith(deployer, { type: 'foo', foo: 'foo', bar: 'bar' }); }); it('multiple deploy setting', async () => { const deployer1 = spy(); const deployer2 = spy(); hexo.config.deploy = [ { type: 'foo', foo: 'foo' }, { type: 'bar', bar: 'bar' } ]; hexo.extend.deployer.register('foo', deployer1); hexo.extend.deployer.register('bar', deployer2); await deploy({ test: true }); deployer1.calledOnce.should.be.true; deployer2.calledOnce.should.be.true; sinonAssert.calledWith(deployer1, { type: 'foo', foo: 'foo', test: true }); sinonAssert.calledWith(deployer2, { type: 'bar', bar: 'bar', test: true }); }); it('deployer not found', async () => { const logSpy = spy(); const hexo = new Hexo(join(__dirname, 'deploy_test')); hexo.log.error = logSpy; const deploy: (...args: OriginalParams) => OriginalReturn = deployConsole.bind(hexo); hexo.extend.deployer.register('baz', () => { }); hexo.config.deploy = { type: 'foo', foo: 'bar' }; await deploy({}); logSpy.called.should.be.true; logSpy.args[0][0].should.contains('Deployer not found: %s'); logSpy.args[0][1].should.contains('foo'); }); it('generate', async () => { await writeFile(join(hexo.source_dir, 'test.txt'), 'test'); await deploy({ generate: true }); const content = await readFile(join(hexo.public_dir, 'test.txt')); content.should.eql('test'); await rmdir(hexo.source_dir); }); it('run generate if public directory not exist', async () => { await rmdir(hexo.public_dir); await deploy({}); const exist = await exists(hexo.public_dir); exist.should.be.true; }); }); ================================================ FILE: test/scripts/console/generate.ts ================================================ import { join } from 'path'; import { emptyDir, exists, mkdirs, readFile, rmdir, stat, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import { spy } from 'sinon'; import chai from 'chai'; const should = chai.should(); import Hexo from '../../../lib/hexo'; import generateConsole from '../../../lib/plugins/console/generate'; type OriginalParams = Parameters<typeof generateConsole>; type OriginalReturn = ReturnType<typeof generateConsole>; describe('generate', () => { let hexo: Hexo, generate: (...args: OriginalParams) => OriginalReturn; beforeEach(async function() { this.timeout(5000); hexo = new Hexo(join(__dirname, 'generate_test'), {silent: true}); generate = generateConsole.bind(hexo); await mkdirs(hexo.base_dir); await hexo.init(); }); afterEach(async () => { const exist = await exists(hexo.base_dir); if (exist) { await emptyDir(hexo.base_dir); await rmdir(hexo.base_dir); } }); const testGenerate = async (options?: any) => { await BluebirdPromise.all([ // Add some source files writeFile(join(hexo.source_dir, 'test.txt'), 'test'), writeFile(join(hexo.source_dir, 'faz', 'yo.txt'), 'yoooo'), // Add some files to public folder writeFile(join(hexo.public_dir, 'foo.txt'), 'foo'), writeFile(join(hexo.public_dir, 'bar', 'boo.txt'), 'boo'), writeFile(join(hexo.public_dir, 'faz', 'yo.txt'), 'yo') ]); await generate(options); const result = await BluebirdPromise.all([ readFile(join(hexo.public_dir, 'test.txt')), readFile(join(hexo.public_dir, 'faz', 'yo.txt')), exists(join(hexo.public_dir, 'foo.txt')), exists(join(hexo.public_dir, 'bar', 'boo.txt')) ]); // Check the new file result[0].should.eql('test'); // Check the updated file result[1].should.eql('yoooo'); // Old files should not be deleted result[2].should.be.true; result[3].should.be.true; }; it('default', () => testGenerate()); it('public_dir is not a directory', async () => { await BluebirdPromise.all([ // Add some source files writeFile(join(hexo.source_dir, 'test.txt'), 'test'), // Add some files to public folder writeFile(join(hexo.public_dir, 'foo.txt'), 'foo') ]); const old = hexo.public_dir; hexo.public_dir = join(hexo.public_dir, 'foo.txt'); try { await generate(); } catch (e) { e.message.split(' ').slice(1).join(' ').should.eql('is not a directory'); } hexo.public_dir = old; }); it('write file if not exist', async () => { const src = join(hexo.source_dir, 'test.txt'); const dest = join(hexo.public_dir, 'test.txt'); const content = 'test'; // Add some source files await writeFile(src, content); // First generation await generate(); // Delete generated files await unlink(dest); // Second generation await generate(); const result = await readFile(dest); result.should.eql(content); // Remove source files and generated files await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('don\'t write if file unchanged', async () => { const src = join(hexo.source_dir, 'test.txt'); const dest = join(hexo.public_dir, 'test.txt'); const content = 'test'; const newContent = 'newtest'; // Add some source files await writeFile(src, content); // First generation await generate(); // Change the generated file await writeFile(dest, newContent); // Second generation await generate(); // Read the generated file const result = await readFile(dest); // Make sure the generated file didn't change result.should.eql(newContent); // Remove source files and generated files await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('force regenerate', async () => { const src = join(hexo.source_dir, 'test.txt'); const dest = join(hexo.public_dir, 'test.txt'); const content = 'test'; await writeFile(src, content); // First generation await generate(); // Read file status let stats = await stat(dest); const mtime = stats.mtime.getTime(); await BluebirdPromise.delay(1000); // Force regenerate await generate({ force: true }); stats = await stat(dest); stats.mtime.getTime().should.above(mtime); // Remove source files and generated files await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('watch - update', async () => { const src = join(hexo.source_dir, 'test.txt'); const dest = join(hexo.public_dir, 'test.txt'); const content = 'test'; await testGenerate({ watch: true }); // Update the file await writeFile(src, content); await BluebirdPromise.delay(300); // Check the updated file const result = await readFile(dest); result.should.eql(content); // Stop watching hexo.unwatch(); await BluebirdPromise.delay(300); }); it('deploy', async () => { const deployer = spy(); hexo.extend.deployer.register('test', deployer); hexo.config.deploy = { type: 'test' }; await generate({ deploy: true }); deployer.calledOnce.should.be.true; }); it('update theme source files', async () => { // Add some source files await BluebirdPromise.all([ // Add some source files writeFile(join(hexo.theme_dir, 'source', 'a.txt'), 'a'), writeFile(join(hexo.theme_dir, 'source', 'b.txt'), 'b'), writeFile(join(hexo.theme_dir, 'source', 'c.njk'), 'c') ]); await BluebirdPromise.delay(300); await generate(); // Update source file await BluebirdPromise.all([ writeFile(join(hexo.theme_dir, 'source', 'b.txt'), 'bb'), writeFile(join(hexo.theme_dir, 'source', 'c.njk'), 'cc') ]); await BluebirdPromise.delay(300); // Generate again await generate(); await BluebirdPromise.delay(300); // Read the updated source file const result = await BluebirdPromise.all([ readFile(join(hexo.public_dir, 'b.txt')), readFile(join(hexo.public_dir, 'c.html')) ]); result[0].should.eql('bb'); result[1].should.eql('cc'); }); it('proceeds after error when bail option is not set', async () => { hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception'))); hexo.extend.generator.register('test_page', () => [ { path: 'testing-path', layout: 'post', data: {} } ] ); await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post'); return generate(); }); it('proceeds after error when bail option is set to false', async () => { hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception'))); hexo.extend.generator.register('test_page', () => [ { path: 'testing-path', layout: 'post', data: {} } ] ); await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post'); return generate({ bail: false }); }); it('breaks after error when bail option is set to true', async () => { hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception'))); hexo.extend.generator.register('test_page', () => [ { path: 'testing-path', layout: 'post', data: {} } ] ); await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post'); return generate({ bail: true }).then(() => { should.fail('Return value must be rejected'); }, err => { err.should.property('message', 'Testing unhandled exception'); }); }); it('should generate all files when bail option is set to true and no errors', async () => { // Test cases for hexojs/hexo#4499 hexo.extend.generator.register('resource', () => [ { path: 'resource-1', data: 'string' }, { path: 'resource-2', data: {} }, { path: 'resource-3', data: () => BluebirdPromise.resolve(Buffer.from('string')) } ] ); return generate({ bail: true }); }); it('should generate all files even when concurrency is set', async () => { await generate({ concurrency: '1' }); return generate({ concurrency: '2' }); }); }); // #3975 workaround for Windows describe('generate - watch (delete)', () => { const hexo = new Hexo(join(__dirname, 'generate_test'), {silent: true}); const generate: (...args: OriginalParams) => OriginalReturn = generateConsole.bind(hexo); beforeEach(async () => { await mkdirs(hexo.base_dir); await hexo.init(); }); afterEach(async () => { const exist = await exists(hexo.base_dir); if (exist) { await emptyDir(hexo.base_dir); await BluebirdPromise.delay(500); await rmdir(hexo.base_dir); } }); const testGenerate = async options => { await BluebirdPromise.all([ // Add some source files writeFile(join(hexo.source_dir, 'test.txt'), 'test'), writeFile(join(hexo.source_dir, 'faz', 'yo.txt'), 'yoooo'), // Add some files to public folder writeFile(join(hexo.public_dir, 'foo.txt'), 'foo'), writeFile(join(hexo.public_dir, 'bar', 'boo.txt'), 'boo'), writeFile(join(hexo.public_dir, 'faz', 'yo.txt'), 'yo') ]); await generate(options); const result = await BluebirdPromise.all([ readFile(join(hexo.public_dir, 'test.txt')), readFile(join(hexo.public_dir, 'faz', 'yo.txt')), exists(join(hexo.public_dir, 'foo.txt')), exists(join(hexo.public_dir, 'bar', 'boo.txt')) ]); // Check the new file result[0].should.eql('test'); // Check the updated file result[1].should.eql('yoooo'); // Old files should not be deleted result[2].should.be.true; result[3].should.be.true; }; it('watch - delete', async () => { await testGenerate({ watch: true }); await unlink(join(hexo.source_dir, 'test.txt')); await BluebirdPromise.delay(500); const exist = await exists(join(hexo.public_dir, 'test.txt')); exist.should.be.false; }); }); ================================================ FILE: test/scripts/console/list.ts ================================================ import { spy, stub, assert as sinonAssert, SinonSpy } from 'sinon'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import listConsole from '../../../lib/plugins/console/list'; type OriginalParams = Parameters<typeof listConsole>; type OriginalReturn = ReturnType<typeof listConsole>; describe('Console list', () => { const hexo = new Hexo(__dirname); it('no args', () => { hexo.call = spy(); const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo); list({ _: [''] }); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._[0].should.eql('list'); }); it('has args', async () => { const logStub = stub(console, 'log'); hexo.load = () => BluebirdPromise.resolve(); const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo); await list({ _: ['page'] }); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'No pages.'); logStub.restore(); }); it('list type not found', () => { hexo.call = spy(); const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo); list({ _: ['test'] }); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._[0].should.eql('list'); }); }); ================================================ FILE: test/scripts/console/list_categories.ts ================================================ import BluebirdPromise from 'bluebird'; import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import listCategory from '../../../lib/plugins/console/list/category'; type OriginalParams = Parameters<typeof listCategory>; type OriginalReturn = ReturnType<typeof listCategory>; describe('Console list', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const listCategories: (...args: OriginalParams) => OriginalReturn = listCategory.bind(hexo); let logStub; before(() => { logStub = stub(console, 'log'); }); afterEach(() => { logStub.reset(); }); after(() => { logStub.restore(); }); it('no categories', () => { listCategories(); sinonAssert.calledWithMatch(logStub, 'Name'); sinonAssert.calledWithMatch(logStub, 'Posts'); sinonAssert.calledWithMatch(logStub, 'No categories.'); }); it('categories', async () => { const posts = [ {source: 'foo', slug: 'foo', title: 'Its', date: 1e8}, {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1}, {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1} ]; await hexo.init(); const output = await Post.insert(posts); await BluebirdPromise.each([ ['foo'], ['baz'], ['baz'] ], (tags, i) => output[i].setCategories(tags)); await hexo.locals.invalidate(); listCategories(); sinonAssert.calledWithMatch(logStub, 'Name'); sinonAssert.calledWithMatch(logStub, 'Posts'); sinonAssert.calledWithMatch(logStub, 'baz'); sinonAssert.calledWithMatch(logStub, 'foo'); }); }); ================================================ FILE: test/scripts/console/list_page.ts ================================================ import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import listPage from '../../../lib/plugins/console/list/page'; type OriginalParams = Parameters<typeof listPage>; type OriginalReturn = ReturnType<typeof listPage>; describe('Console list', () => { const hexo = new Hexo(); const Page = hexo.model('Page'); const listPages: (...args: OriginalParams) => OriginalReturn = listPage.bind(hexo); hexo.config.permalink = ':title/'; let logStub; before(() => { logStub = stub(console, 'log'); }); afterEach(() => { logStub.reset(); }); after(() => { logStub.restore(); }); it('no page', () => { listPages(); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'No pages.'); }); it('page', async () => { await Page.insert({ source: 'foo', title: 'Hello World', path: 'bar' }); listPages(); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'Hello World'); sinonAssert.calledWithMatch(logStub, 'foo'); }); it('page with unicode', async () => { await Page.insert({ source: 'foo', title: '\u0100', path: 'bar' }); listPages(); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, '\u0100'); sinonAssert.calledWithMatch(logStub, 'foo'); }); }); ================================================ FILE: test/scripts/console/list_post.ts ================================================ import BluebirdPromise from 'bluebird'; import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import listPost from '../../../lib/plugins/console/list/post'; type OriginalParams = Parameters<typeof listPost>; type OriginalReturn = ReturnType<typeof listPost>; describe('Console list', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const listPosts: (...args: OriginalParams) => OriginalReturn = listPost.bind(hexo); let logStub; before(() => { logStub = stub(console, 'log'); }); afterEach(() => { logStub.reset(); }); after(() => { logStub.restore(); }); it('no post', () => { listPosts(); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'Category'); sinonAssert.calledWithMatch(logStub, 'Tags'); sinonAssert.calledWithMatch(logStub, 'No posts.'); }); it('post', async () => { const posts = [ {source: 'foo', slug: 'foo', title: 'Its', date: 1e8}, {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1}, {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1} ]; const tags = [ ['foo'], ['baz'], ['baz'] ]; await hexo.init(); const output = await Post.insert(posts); await BluebirdPromise.each(tags, (tags, i) => output[i].setTags(tags)); await hexo.locals.invalidate(); listPosts(); sinonAssert.calledWithMatch(logStub, 'Date'); sinonAssert.calledWithMatch(logStub, 'Title'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'Category'); sinonAssert.calledWithMatch(logStub, 'Tags'); for (let i = 0; i < posts.length; i++) { sinonAssert.calledWithMatch(logStub, posts[i].source); sinonAssert.calledWithMatch(logStub, posts[i].slug); sinonAssert.calledWithMatch(logStub, posts[i].title); sinonAssert.calledWithMatch(logStub, tags[i][0]); } }); }); ================================================ FILE: test/scripts/console/list_route.ts ================================================ import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import listRoute from '../../../lib/plugins/console/list/route'; type OriginalParams = Parameters<typeof listRoute>; type OriginalReturn = ReturnType<typeof listRoute>; describe('Console list', () => { const hexo = new Hexo(__dirname); const listRoutes: (...args: OriginalParams) => OriginalReturn = listRoute.bind(hexo); const { route } = hexo; let logStub; before(() => { logStub = stub(console, 'log'); }); afterEach(() => { logStub.reset(); }); after(() => { logStub.restore(); }); it('no route', () => { listRoutes(); sinonAssert.calledWithMatch(logStub, 'Total: 0'); }); it('route', async () => { route.set('test', 'foo'); listRoutes(); sinonAssert.calledWithMatch(logStub, 'Total: 1'); route.remove('test'); }); it('route with nodes', async () => { route.set('test0/test1', 'foo'); listRoutes(); sinonAssert.calledWithMatch(logStub, 'Total: 1'); sinonAssert.calledWithMatch(logStub, '└─┬ test0'); sinonAssert.calledWithMatch(logStub, ' └── test1'); }); }); ================================================ FILE: test/scripts/console/list_tags.ts ================================================ import BluebirdPromise from 'bluebird'; import { stub, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import listTag from '../../../lib/plugins/console/list/tag'; type OriginalParams = Parameters<typeof listTag>; type OriginalReturn = ReturnType<typeof listTag>; describe('Console list', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const listTags: (...args: OriginalParams) => OriginalReturn = listTag.bind(hexo); hexo.config.permalink = ':title/'; let logStub; before(() => { logStub = stub(console, 'log'); }); afterEach(() => { logStub.reset(); }); after(() => { logStub.restore(); }); it('no tags', () => { listTags(); sinonAssert.calledWithMatch(logStub, 'Name'); sinonAssert.calledWithMatch(logStub, 'Posts'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'No tags.'); }); it('tags', async () => { const posts = [ {source: 'foo', slug: 'foo', title: 'Its', date: 1e8}, {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1}, {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1} ]; await hexo.init(); const output = await Post.insert(posts); await BluebirdPromise.each([ ['foo'], ['baz'], ['baz'] ], (tags, i) => output[i].setTags(tags)); await hexo.locals.invalidate(); listTags(); sinonAssert.calledWithMatch(logStub, 'Name'); sinonAssert.calledWithMatch(logStub, 'Posts'); sinonAssert.calledWithMatch(logStub, 'Path'); sinonAssert.calledWithMatch(logStub, 'baz'); sinonAssert.calledWithMatch(logStub, 'foo'); sinonAssert.calledWithMatch(logStub, 'tags/baz'); sinonAssert.calledWithMatch(logStub, 'tags/foo'); }); }); ================================================ FILE: test/scripts/console/migrate.ts ================================================ import { spy, assert as sinonAssert, stub, SinonSpy } from 'sinon'; import Hexo from '../../../lib/hexo'; import migrateConsole from '../../../lib/plugins/console/migrate'; type OriginalParams = Parameters<typeof migrateConsole>; type OriginalReturn = ReturnType<typeof migrateConsole>; describe('migrate', () => { const hexo = new Hexo(__dirname, { silent: true }); const migrate: (...args: OriginalParams) => OriginalReturn = migrateConsole.bind(hexo); it('default', async () => { const migrator = spy(); hexo.extend.migrator.register('test', migrator); await migrate({ _: ['test'], foo: 1, bar: 2 }); sinonAssert.calledWithMatch(migrator, { foo: 1, bar: 2 }); migrator.calledOnce.should.be.true; }); it('no args', async () => { const hexo = new Hexo(__dirname, { silent: true }); hexo.call = spy(); const migrate: (...args: OriginalParams) => OriginalReturn = migrateConsole.bind(hexo); await migrate({ _: [] }); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._[0].should.eql('migrate'); }); it('migrator not found', async () => { const logStub = stub(console, 'log'); await migrate({ _: ['foo'] }); logStub.calledOnce.should.be.true; logStub.args[0][0].should.contains('migrator plugin is not installed.'); logStub.args[0][0].should.contains('Installed migrator plugins:'); logStub.restore(); }); }); ================================================ FILE: test/scripts/console/new.ts ================================================ import { exists, mkdirs, readFile, rmdir, unlink } from 'hexo-fs'; import moment from 'moment'; import { join } from 'path'; import BluebirdPromise from 'bluebird'; import { useFakeTimers, spy, SinonSpy } from 'sinon'; import Hexo from '../../../lib/hexo'; import newConsole from '../../../lib/plugins/console/new'; type OriginalParams = Parameters<typeof newConsole>; type OriginalReturn = ReturnType<typeof newConsole>; describe('new', () => { const hexo = new Hexo(join(__dirname, 'new_test'), {silent: true}); const n: (...args: OriginalParams) => OriginalReturn = newConsole.bind(hexo); const post = hexo.post; const now = Date.now(); let clock; before(async () => { clock = useFakeTimers(now); await mkdirs(hexo.base_dir); await hexo.init(); await BluebirdPromise.all([ hexo.scaffold.set('post', [ 'title: {{ title }}', 'date: {{ date }}', 'tags:', '---' ].join('\n')), hexo.scaffold.set('draft', [ 'title: {{ title }}', 'tags:', '---' ].join('\n')) ]); }); after(() => { clock.restore(); return rmdir(hexo.base_dir); }); it('no args', async () => { hexo.call = spy(); await n({ _: [] }); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._[0].should.eql('new'); }); it('title', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'] }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('layout', async () => { const path = join(hexo.source_dir, '_drafts', 'Hello-World.md'); const body = [ 'title: Hello World', 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['draft', 'Hello World'] }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('slug', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'foo.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'], slug: 'foo' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('slug - s', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'foo.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'], s: 'foo' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('path', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'bar.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'], slug: 'foo', path: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('path - p', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'bar.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'], slug: 'foo', p: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('without _', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'bar.md'); const body = [ 'title: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: [], path: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('rename if target existed', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md'); await post.create({ title: 'Hello World' }); await n({ _: ['Hello World'] }); const exist = await exists(path); exist.should.be.true; await BluebirdPromise.all([ unlink(path), unlink(join(hexo.source_dir, '_posts', 'Hello-World.md')) ]); }); it('replace existing files', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await post.create({ title: 'Hello World' }); await n({ _: ['Hello World'], replace: true }); const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md')); exist.should.be.false; const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('replace existing files - r', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await post.create({ title: 'Hello World' }); await n({ _: ['Hello World'], r: true }); const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md')); exist.should.be.false; const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('extra data', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: Hello World', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['Hello World'], foo: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('special character - 1', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: \'[Hello] World\'', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['[Hello] World'], foo: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('special character - 2', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: \'{Hello} World\'', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['{Hello} World'], foo: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('special character - 3', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: \'\'\'Hello\'\' World\'', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['\'Hello\' World'], foo: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); it('special character - 4', async () => { const date = moment(now); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const body = [ 'title: \'"Hello" World\'', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await n({ _: ['"Hello" World'], foo: 'bar' }); const content = await readFile(path); content.should.eql(body); await unlink(path); }); }); ================================================ FILE: test/scripts/console/publish.ts ================================================ import { exists, mkdirs, readFile, rmdir, unlink } from 'hexo-fs'; import moment from 'moment'; import { join } from 'path'; import BluebirdPromise from 'bluebird'; import { useFakeTimers, spy, SinonSpy, SinonFakeTimers } from 'sinon'; import Hexo from '../../../lib/hexo'; import publishConsole from '../../../lib/plugins/console/publish'; type OriginalParams = Parameters<typeof publishConsole>; type OriginalReturn = ReturnType<typeof publishConsole>; describe('publish', () => { const hexo = new Hexo(join(__dirname, 'publish_test'), {silent: true}); const publish: (...args: OriginalParams) => OriginalReturn = publishConsole.bind(hexo); const post = hexo.post; const now = Date.now(); let clock: SinonFakeTimers; before(async () => { clock = useFakeTimers(now); await mkdirs(hexo.base_dir); await hexo.init(); await hexo.scaffold.set('post', [ '---', 'title: {{ title }}', 'date: {{ date }}', 'tags:', '---' ].join('\n')); await hexo.scaffold.set('draft', [ '---', 'title: {{ title }}', 'tags:', '---' ].join('\n')); }); after(() => { clock.restore(); return rmdir(hexo.base_dir); }); beforeEach(() => post.create({ title: 'Hello World', layout: 'draft' })); it('slug', async () => { const draftPath = join(hexo.source_dir, '_drafts', 'Hello-World.md'); const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await publish({ _: ['Hello-World'] }); const exist = await exists(draftPath); const data = await readFile(path); exist.should.be.false; data.should.eql(content); await unlink(path); }); it('no args', async () => { const hexo = new Hexo(join(__dirname, 'publish_test'), {silent: true}); hexo.call = spy(); const publish: (...args: OriginalParams) => OriginalReturn = publishConsole.bind(hexo); await publish({_: []}); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._[0].should.eql('publish'); }); it('layout', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'layout: photo', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await publish({ _: ['photo', 'Hello-World'] }); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('rename if target existed', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md'); await post.create({ title: 'Hello World' }); await publish({ _: ['Hello-World'] }); const exist = await exists(path); exist.should.be.true; await BluebirdPromise.all([ unlink(path), unlink(join(hexo.source_dir, '_posts', 'Hello-World.md')) ]); }); it('replace existing target', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); await post.create({ title: 'Hello World' }); await publish({ _: ['Hello-World'], replace: true }); const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md')); exist.should.be.false; await unlink(path); }); }); ================================================ FILE: test/scripts/console/render.ts ================================================ import { mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import { join } from 'path'; import BluebirdPromise from 'bluebird'; import { spy, SinonSpy } from 'sinon'; import Hexo from '../../../lib/hexo'; import renderConsole from '../../../lib/plugins/console/render'; type OriginalParams = Parameters<typeof renderConsole>; type OriginalReturn = ReturnType<typeof renderConsole>; describe('render', () => { const hexo = new Hexo(join(__dirname, 'render_test'), {silent: true}); const render: (...args: OriginalParams) => OriginalReturn = renderConsole.bind(hexo); before(async () => { await mkdirs(hexo.base_dir); hexo.init(); }); after(() => rmdir(hexo.base_dir)); const body = [ 'foo: 1', 'bar:', ' boo: 2' ].join('\n'); it('no args', async () => { const hexo = new Hexo(join(__dirname, 'render_test'), {silent: true}); hexo.call = spy(); const render: (...args: OriginalParams) => OriginalReturn = renderConsole.bind(hexo); await render({_: []}); (hexo.call as SinonSpy).calledOnce.should.be.true; (hexo.call as SinonSpy).args[0][0].should.eql('help'); (hexo.call as SinonSpy).args[0][1]._.should.eql('render'); }); it('relative path', async () => { const src = join(hexo.base_dir, 'test.yml'); const dest = join(hexo.base_dir, 'result.json'); await writeFile(src, body); await render({_: ['test.yml'], output: 'result.json'}); const result = await readFile(dest); JSON.parse(result).should.eql({ foo: 1, bar: { boo: 2 } }); await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('absolute path', async () => { const src = join(hexo.base_dir, 'test.yml'); const dest = join(hexo.base_dir, 'result.json'); await writeFile(src, body); await render({_: [src], output: 'result.json'}); const result = await readFile(dest); JSON.parse(result).should.eql({ foo: 1, bar: { boo: 2 } }); await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('absolute output', async () => { const src = join(hexo.base_dir, 'test.yml'); const dest = join(hexo.base_dir, 'result.json'); await writeFile(src, body); await render({_: ['test.yml'], output: dest}); const result = await readFile(dest); JSON.parse(result).should.eql({ foo: 1, bar: { boo: 2 } }); await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); // it('output'); missing-unit-test it('engine', async () => { const src = join(hexo.base_dir, 'test'); const dest = join(hexo.base_dir, 'result.json'); await writeFile(src, body); await render({_: ['test'], output: 'result.json', engine: 'yaml'}); const result = await readFile(dest); JSON.parse(result).should.eql({ foo: 1, bar: { boo: 2 } }); await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); it('pretty', async () => { const src = join(hexo.base_dir, 'test.yml'); const dest = join(hexo.base_dir, 'result.json'); await writeFile(src, body); await render({_: ['test.yml'], output: 'result.json', pretty: true}); const result = await readFile(dest); result.should.eql(JSON.stringify({ foo: 1, bar: { boo: 2 } }, null, ' ')); await BluebirdPromise.all([ unlink(src), unlink(dest) ]); }); }); ================================================ FILE: test/scripts/extend/console.ts ================================================ import Console from '../../../lib/extend/console'; import chai from 'chai'; const should = chai.should(); describe('Console', () => { const ctx = {}; it('register()', () => { const c = new Console(); // no name // @ts-expect-error should.throw(() => c.register(), TypeError, 'name is required'); // name, fn c.register('test', () => {}); c.get('test').should.exist; // name, not fn // @ts-expect-error should.throw(() => c.register('test'), TypeError, 'fn must be a function'); // name, desc, fn c.register('test', 'this is a test', () => {}); c.get('test').should.exist; c.get('test').desc!.should.eql('this is a test'); // name, desc, not fn // @ts-expect-error should.throw(() => c.register('test', 'this is a test'), TypeError, 'fn must be a function'); // name, options, fn c.register('test', {init: true}, () => {}); c.get('test').should.exist; c.get('test').options!.init!.should.be.true; // name, desc, options, fn c.register('test', 'this is a test', {init: true}, () => {}); c.get('test').should.exist; c.get('test').desc!.should.eql('this is a test'); c.get('test').options!.init!.should.be.true; // name, desc, options, not fn // @ts-expect-error should.throw(() => c.register('test', 'this is a test', {init: true}), TypeError, 'fn must be a function'); }); it('register() - alias', () => { const c = new Console(); c.register('test', () => {}); c.alias.should.eql({ t: 'test', te: 'test', tes: 'test', test: 'test' }); }); it('register() - promisify', () => { const c = new Console(); c.register('test', (args, callback) => { args.should.eql({foo: 'bar'}); callback && callback(null, 'foo'); }); c.get('test').call(ctx, { _: [], foo: 'bar' }).then(result => { result.should.eql('foo'); }); }); it('list()', () => { const c = new Console(); c.register('test', () => {}); c.list().should.have.all.keys(['test']); }); it('get()', () => { const c = new Console(); c.register('test', () => {}); c.get('test').should.exist; c.get('t').should.exist; c.get('te').should.exist; c.get('tes').should.exist; }); }); ================================================ FILE: test/scripts/extend/deployer.ts ================================================ import Deployer from '../../../lib/extend/deployer'; import chai from 'chai'; const should = chai.should(); describe('Deployer', () => { const ctx = {}; it('register()', () => { const d = new Deployer(); // name, fn d.register('test', () => {}); d.get('test').should.exist; // no name // @ts-expect-error should.throw(() => d.register(), TypeError, 'name is required'); // no fn // @ts-expect-error should.throw(() => d.register('test'), TypeError, 'fn must be a function'); }); it('register() - promisify', () => { const d = new Deployer(); d.register('test', (args, callback) => { args.should.eql({foo: 'bar'}); callback && callback(null, 'foo'); }); d.get('test').call(ctx, { type: '', foo: 'bar' }).then(result => { result.should.eql('foo'); }); }); it('register() - Promise.method', () => { const d = new Deployer(); d.register('test', args => { args.should.eql({foo: 'bar'}); return 'foo'; }); d.get('test').call(ctx, { type: '', foo: 'bar' }).then(result => { result.should.eql('foo'); }); }); it('list()', () => { const d = new Deployer(); d.register('test', () => {}); d.list().should.have.all.keys(['test']); }); it('get()', () => { const d = new Deployer(); d.register('test', () => {}); d.get('test').should.exist; }); }); ================================================ FILE: test/scripts/extend/filter.ts ================================================ import Filter from '../../../lib/extend/filter'; import { spy } from 'sinon'; import chai from 'chai'; const should = chai.should(); describe('Filter', () => { it('register()', () => { const f = new Filter(); // type, fn f.register('test', () => {}); f.list('test')[0].should.exist; f.list('test')[0].priority!.should.eql(10); // type, fn, priority f.register('test2', () => {}, 50); f.list('test2')[0].priority!.should.eql(50); // fn f.register(() => {}); f.list('after_post_render')[0].should.exist; f.list('after_post_render')[0].priority!.should.eql(10); // fn, priority f.register(() => {}, 50); f.list('after_post_render')[1].priority!.should.eql(50); // no fn // @ts-expect-error should.throw(() => f.register(), TypeError, 'fn must be a function'); }); it('register() - type alias', () => { const f = new Filter(); // pre f.register('pre', () => {}); f.list('before_post_render')[0].should.exist; // post f.register('post', () => {}); f.list('after_post_render')[0].should.exist; }); it('register() - priority', () => { const f = new Filter(); f.register('test', () => {}); f.register('test', () => {}, 5); f.register('test', () => {}, 15); f.list('test').map(item => item.priority).should.eql([5, 10, 15]); }); it('unregister()', async () => { const f = new Filter(); const filter = spy(); f.register('test', filter); f.unregister('test', filter); await f.exec('test', ''); filter.called.should.be.false; }); it('unregister() - type is required', () => { const f = new Filter(); // @ts-expect-error should.throw(() => f.unregister(), 'type is required'); }); it('unregister() - fn must be a function', () => { const f = new Filter(); // @ts-expect-error should.throw(() => f.unregister('test'), 'fn must be a function'); }); it('list()', () => { const f = new Filter(); f.register('test', () => {}); f.list().test.should.exist; f.list('test')[0].should.exist; f.list('foo').should.have.lengthOf(0); }); it('exec()', async () => { const f = new Filter(); const filter1 = spy(data => { data.should.eql(''); return data + 'foo'; }); const filter2 = spy(data => { data.should.eql('foo'); return data + 'bar'; }); f.register('test', filter1); f.register('test', filter2); const data = await f.exec('test', ''); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; filter2.calledAfter(filter1).should.be.true; data.should.eql('foobar'); }); it('exec() - pointer', async () => { const f = new Filter(); const filter1 = spy(data => { data.should.eql({}); data.foo = 1; }); const filter2 = spy(data => { data.should.eql({foo: 1}); data.bar = 2; }); f.register('test', filter1); f.register('test', filter2); const data = await f.exec('test', {}); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; filter2.calledAfter(filter1).should.be.true; data.should.eql({ foo: 1, bar: 2 }); }); it('exec() - args', async () => { const f = new Filter(); const filter1 = spy((data, arg1, arg2) => { arg1.should.eql(1); arg2.should.eql(2); }); const filter2 = spy((data, arg1, arg2) => { arg1.should.eql(1); arg2.should.eql(2); }); f.register('test', filter1); f.register('test', filter2); await f.exec('test', {}, { args: [1, 2] }); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; }); it('exec() - context', async () => { const f = new Filter(); const ctx = {foo: 1, bar: 2}; const filter1 = spy(); const filter2 = spy(); f.register('test', filter1); f.register('test', filter2); await f.exec('test', {}, { context: ctx }); filter1.alwaysCalledOn(ctx).should.be.true; filter2.alwaysCalledOn(ctx).should.be.true; filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; }); it('execSync()', () => { const f = new Filter(); const filter1 = spy(data => { data.should.eql(''); return data + 'foo'; }); const filter2 = spy(data => { data.should.eql('foo'); return data + 'bar'; }); f.register('test', filter1); f.register('test', filter2); f.execSync('test', '').should.eql('foobar'); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; filter2.calledAfter(filter1).should.be.true; }); it('execSync() - pointer', () => { const f = new Filter(); const filter1 = spy(data => { data.should.eql({}); data.foo = 1; }); const filter2 = spy(data => { data.should.eql({foo: 1}); data.bar = 2; }); f.register('test', filter1); f.register('test', filter2); f.execSync('test', {}).should.eql({foo: 1, bar: 2}); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; filter2.calledAfter(filter1).should.be.true; }); it('execSync() - args', () => { const f = new Filter(); const filter1 = spy((data, arg1, arg2) => { arg1.should.eql(1); arg2.should.eql(2); }); const filter2 = spy((data, arg1, arg2) => { arg1.should.eql(1); arg2.should.eql(2); }); f.register('test', filter1); f.register('test', filter2); f.execSync('test', {}, { args: [1, 2] }); filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; }); it('execSync() - context', () => { const f = new Filter(); const ctx = {foo: 1, bar: 2}; const filter1 = spy(); const filter2 = spy(); f.register('test', filter1); f.register('test', filter2); f.execSync('test', {}, {context: ctx}); filter1.alwaysCalledOn(ctx).should.be.true; filter2.alwaysCalledOn(ctx).should.be.true; filter1.calledOnce.should.be.true; filter2.calledOnce.should.be.true; }); }); ================================================ FILE: test/scripts/extend/generator.ts ================================================ import Generator from '../../../lib/extend/generator'; import chai from 'chai'; const should = chai.should(); describe('Generator', () => { it('register()', () => { const g = new Generator(); // name, fn g.register('test', () => []); g.get('test').should.exist; // fn g.register(() => []); g.get('generator-0').should.exist; // no fn // @ts-expect-error should.throw(() => g.register('test'), TypeError, 'fn must be a function'); }); it('register() - promisify', async () => { const g = new Generator(); g.register('test', (_locals, callback) => { callback && callback(null, 'foo'); return []; }); const result = await g.get('test')({} as any); result.should.eql('foo'); }); it('get()', () => { const g = new Generator(); g.register('test', () => []); g.get('test').should.exist; }); it('list()', () => { const g = new Generator(); g.register('test', () => []); g.list().should.have.all.keys(['test']); }); }); ================================================ FILE: test/scripts/extend/helper.ts ================================================ import Helper from '../../../lib/extend/helper'; import chai from 'chai'; const should = chai.should(); describe('Helper', () => { it('register()', () => { const h = new Helper(); // name, fn h.register('test', () => ''); h.get('test').should.exist; // no fn // @ts-expect-error should.throw(() => h.register('test'), TypeError, 'fn must be a function'); // no name // @ts-expect-error should.throw(() => h.register(), TypeError, 'name is required'); }); it('list()', () => { const h = new Helper(); h.register('test', () => ''); h.list().should.have.all.keys(['test']); }); it('get()', () => { const h = new Helper(); h.register('test', () => ''); h.get('test').should.exist; }); }); ================================================ FILE: test/scripts/extend/injector.ts ================================================ import Injector from '../../../lib/extend/injector'; describe('Injector', () => { const content = [ '<!DOCTYPE html>', '<html lang="en">', '<head id="head"><title>Test', '', '', '
', '

', '', '' ].join(''); it('register() - entry is required', () => { const i = new Injector(); // no name try { // @ts-expect-error i.register(); } catch (err) { err.should.be .instanceOf(TypeError) .property('message', 'entry is required'); } }); it('register() - string', () => { const i = new Injector(); const str = ''; i.register('head_begin', str); i.register('head_end', str, 'home'); i.get('head_begin').should.contains(str); i.get('head_begin', 'default').should.contains(str); i.get('head_end', 'home').should.contains(str); }); it('register() - function', () => { const i = new Injector(); const fn = () => ''; i.register('head_begin', fn); i.get('head_begin').should.contains(fn()); }); it('register() - fallback when entry not exists', () => { const i = new Injector(); const str = ''; // @ts-expect-error i.register('foo', str); i.get('head_end').should.contains(str); }); it('list()', () => { const i = new Injector(); i.register('body_begin', ''); i.list().body_begin.default.should.be.instanceOf(Set); [...i.list().body_begin.default].should.not.be.empty; }); it('get()', () => { const i = new Injector(); const str = ''; i.register('body_begin', str); i.register('body_end', str, 'home'); i.get('body_begin').should.be.instanceOf(Array); i.get('body_begin').should.contains(str); i.get('body_end', 'home').should.be.instanceOf(Array); i.get('body_end', 'home').should.contains(str); i.get('head_end').should.be.instanceOf(Array); i.get('head_end').should.eql([]); }); it('getText()', () => { const i = new Injector(); const str = ''; i.register('head_end', str); i.register('body_end', str, 'home'); i.getText('body_end', 'home').should.eql(str); i.getText('body_end').should.eql(''); }); it('getSize()', () => { const i = new Injector(); const str = ''; i.register('head_end', str); i.register('body_end', str); i.register('body_end', str, 'home'); i.getSize('head_begin').should.eql(0); i.getSize('head_end').should.eql(1); i.getSize('body_end').should.eql(2); }); it('exec() - default', () => { const i = new Injector(); const result = i.exec(content); result.should.contain('Test'); result.should.contain('

'); }); it('exec() - default', () => { const i = new Injector(); const result = i.exec(content); result.should.contain('Test'); result.should.contain('

'); }); it('exec() - insert code', () => { const i = new Injector(); i.register('head_begin', ''); i.register('head_end', ''); i.register('head_end', ''); i.register('body_begin', ''); i.register('body_end', ''); const result = i.exec(content); result.should.contain(''); result.should.contain(''); result.should.contain(''); result.should.contain(''); }); it('exec() - no duplicate insert', () => { const content = [ '', '', '', 'Test', '', '', '
', '

', '', '' ].join(''); const i = new Injector(); i.register('head_begin', ''); i.register('head_end', ''); i.register('head_end', ''); i.register('body_begin', ''); i.register('body_end', ''); const result = i.exec(content); result.should.contain(''); result.should.contain(''); result.should.contain(''); result.should.contain(''); }); it('exec() - multi-line head & body', () => { const content = [ '', '', 'Test', '', '', '
', '

', '', '' ].join('\n'); const i = new Injector(); i.register('head_begin', ''); i.register('head_end', ''); i.register('head_end', ''); i.register('body_begin', ''); i.register('body_end', ''); const result = i.exec(content); result.should.contain(''); result.should.contain(''); result.should.contain(''); result.should.contain(''); }); it('exec() - inject on specific page', () => { const content = [ '', '', 'Test', '', '', '
', '

', '', '' ].join('\n'); const i = new Injector(); i.register('head_begin', ''); i.register('head_begin', '', 'home'); i.register('head_begin', '', 'post'); i.register('head_begin', '', 'page'); i.register('head_begin', '', 'archive'); i.register('head_begin', '', 'category'); i.register('head_begin', '', 'tag'); const result1 = i.exec(content, { page: {} }); const result2 = i.exec(content, { page: { __index: true } }); const result3 = i.exec(content, { page: { __post: true } }); const result4 = i.exec(content, { page: { __page: true } }); const result5 = i.exec(content, { page: { archive: true } }); const result6 = i.exec(content, { page: { category: true } }); const result7 = i.exec(content, { page: { tag: true } }); // home result1.should.not.contain(''); result2.should.contain(''); // post result1.should.not.contain(''); result3.should.contain(''); // page result1.should.not.contain(''); result4.should.contain(''); // archive result1.should.not.contain(''); result5.should.contain(''); // category result1.should.not.contain(''); result6.should.contain(''); // tag result1.should.not.contain(''); result7.should.contain(''); }); }); ================================================ FILE: test/scripts/extend/migrator.ts ================================================ import Migrator from '../../../lib/extend/migrator'; import chai from 'chai'; const should = chai.should(); describe('Migrator', () => { const ctx = {}; it('register()', () => { const d = new Migrator(); // name, fn d.register('test', () => {}); d.get('test').should.exist; // no name // @ts-expect-error should.throw(() => d.register(), TypeError, 'name is required'); // no fn // @ts-expect-error should.throw(() => d.register('test'), TypeError, 'fn must be a function'); }); it('register() - promisify', () => { const d = new Migrator(); d.register('test', (args, callback) => { args.should.eql({foo: 'bar'}); callback && callback(null, 'foo'); }); d.get('test').call(ctx, { foo: 'bar' }).then(result => { result.should.eql('foo'); }); }); it('register() - Promise.method', async () => { const d = new Migrator(); d.register('test', args => { args.should.eql({foo: 'bar'}); return 'foo'; }); const result = await d.get('test').call(ctx, { foo: 'bar' }); result.should.eql('foo'); }); it('list()', () => { const d = new Migrator(); d.register('test', () => {}); d.list().should.have.all.keys(['test']); }); it('get()', () => { const d = new Migrator(); d.register('test', () => {}); d.get('test').should.exist; }); }); ================================================ FILE: test/scripts/extend/processor.ts ================================================ import Processor from '../../../lib/extend/processor'; import chai from 'chai'; const should = chai.should(); describe('Processor', () => { it('register()', () => { const p = new Processor(); // pattern, fn p.register('test', () => {}); p.list()[0].should.exist; // fn p.register(() => {}); p.list()[1].should.exist; // more than one arg // @ts-expect-error p.register((_a, _b) => {}); p.list()[1].should.exist; // no fn // @ts-expect-error should.throw(() => p.register(), TypeError, 'fn must be a function'); }); it('list()', () => { const p = new Processor(); p.register('test', () => {}); p.list().should.have.lengthOf(1); }); }); ================================================ FILE: test/scripts/extend/renderer.ts ================================================ import Renderer from '../../../lib/extend/renderer'; import BluebirdPromise from 'bluebird'; import chai from 'chai'; const should = chai.should(); describe('Renderer', () => { it('register()', () => { const r = new Renderer(); // name, output, fn r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.get('yaml').should.exist; r.get('yaml').output!.should.eql('json'); // name, output, fn, sync r.register('yaml', 'json', () => {}, true); r.get('yaml').should.exist; r.get('yaml').output!.should.eql('json'); r.get('yaml', true).should.exist; r.get('yaml', true).output!.should.eql('json'); // no fn // @ts-expect-error should.throw(() => r.register('yaml', 'json'), TypeError, 'fn must be a function'); // no output // @ts-expect-error should.throw(() => r.register('yaml'), TypeError, 'output is required'); // no name // @ts-expect-error should.throw(() => r.register(), TypeError, 'name is required'); }); it('register() - promisify', async () => { const r = new Renderer(); // async r.register('yaml', 'json', (_data, _options, callback) => { callback && callback(null, 'foo'); return BluebirdPromise.resolve(); }); const yaml = await r.get('yaml')({}, {}); yaml.should.eql('foo'); // sync r.register('swig', 'html', (_data, _options) => 'foo', true); const swig = await r.get('swig')({}, {}); swig.should.eql('foo'); }); it('register() - compile', () => { const r = new Renderer(); function renderer(_data, _locals) { return BluebirdPromise.resolve(); } renderer.compile = _ => { return () => {}; }; r.register('swig', 'html', renderer); r.get('swig').compile!.should.eql(renderer.compile); }); it('getOutput()', () => { const r = new Renderer(); r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.getOutput('yaml').should.eql('json'); r.getOutput('.yaml').should.eql('json'); r.getOutput('config.yaml').should.eql('json'); r.getOutput('foo.xml').should.not.ok; }); it('isRenderable()', () => { const r = new Renderer(); r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.isRenderable('yaml').should.be.true; r.isRenderable('.yaml').should.be.true; r.isRenderable('config.yaml').should.be.true; r.isRenderable('foo.xml').should.be.false; }); it('isRenderableSync()', () => { const r = new Renderer(); r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.isRenderableSync('yaml').should.be.false; r.register('njk', 'html', () => {}, true); r.isRenderableSync('njk').should.be.true; r.isRenderableSync('.njk').should.be.true; r.isRenderableSync('layout.njk').should.be.true; r.isRenderableSync('foo.html').should.be.false; }); it('get()', () => { const r = new Renderer(); r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.get('yaml').should.exist; r.get('.yaml').should.exist; r.get('config.yaml').should.exist; should.not.exist(r.get('foo.xml')); should.not.exist(r.get('yaml', true)); r.register('swig', 'html', () => {}, true); r.get('swig').should.exist; r.get('swig', true).should.exist; }); it('list()', () => { const r = new Renderer(); r.register('yaml', 'json', () => BluebirdPromise.resolve()); r.register('swig', 'html', () => {}, true); r.list().should.have.all.keys(['yaml', 'swig']); r.list(true).should.have.all.keys(['swig']); }); }); ================================================ FILE: test/scripts/extend/tag.ts ================================================ import { join } from 'path'; import Tag from '../../../lib/extend/tag'; import chai from 'chai'; import Hexo from '../../../lib/hexo'; import defaultConfig from '../../../lib/hexo/default_config'; import posts from '../../../lib/plugins/processor/post'; import Filter from '../../../lib/extend/filter'; import renderPostFilter from '../../../lib/plugins/filter/before_generate/render_post'; import { mkdirs, rmdir, writeFile } from 'hexo-fs'; // @ts-ignore import Promise from 'bluebird'; const should = chai.should(); type PostParams = Parameters['process']> type PostReturn = ReturnType['process']> describe('Tag', () => { const tag = new Tag(); const baseDir = join(__dirname, 'post_test'); const hexo = new Hexo(baseDir); const post = posts(hexo); const process: (...args: PostParams) => Promise = Promise.method(post.process.bind(hexo)); const { source } = hexo; const { File } = source; function newFile(options) { const { path } = options; options.path = (options.published ? '_posts' : '_drafts') + '/' + path; options.source = join(source.base, options.path); options.params = { published: options.published, path, renderable: options.renderable }; return new File(options); } before(async () => { await mkdirs(baseDir); hexo.init(); }); beforeEach(() => { hexo.config = Object.assign({}, defaultConfig); }); after(() => rmdir(baseDir)); it('register()', async () => { const tag = new Tag(); tag.register('test', (args, _content) => args.join(' ')); const result = await tag.render('{% test foo.bar | abcdef > fn(a, b, c) < fn() %}'); result.should.eql('foo.bar | abcdef > fn(a, b, c) < fn()'); }); it('register() - async', async () => { const tag = new Tag(); tag.register('test', async (args, _content) => args.join(' '), { async: true }); const result = await tag.render('{% test foo bar %}'); result.should.eql('foo bar'); }); it('register() - block', async () => { const tag = new Tag(); tag.register('test', (args, content) => args.join(' ') + ' ' + content, true); const str = [ '{% test foo bar %}', 'test content', '{% endtest %}' ].join('\n'); const result = await tag.render(str); result.should.eql('foo bar test content'); }); it('register() - async block', async () => { const tag = new Tag(); tag.register('test', async (args, content) => args.join(' ') + ' ' + content, { ends: true, async: true }); const str = [ '{% test foo bar %}', 'test content', '{% endtest %}' ].join('\n'); const result = await tag.render(str); result.should.eql('foo bar test content'); }); it('register() - nested test', async () => { const tag = new Tag(); tag.register('test', (_args, content) => content, true); const str = [ '{% test %}', '123456', ' {% raw %}', ' raw', ' {% endraw %}', ' {% test %}', ' test', ' {% endtest %}', '789012', '{% endtest %}' ].join('\n'); const result = await tag.render(str); result.replace(/\s/g, '').should.eql('123456rawtest789012'); }); it('register() - nested async / async test', async () => { const tag = new Tag(); tag.register('test', (args, content) => content, {ends: true, async: true}); tag.register('async', async (args, content) => args.join(' ') + ' ' + content, { ends: true, async: true }); const str = [ '{% test %}', '123456', ' {% async %}', ' async', ' {% endasync %}', '789012', '{% endtest %}' ].join('\n'); const result = await tag.render(str); result.replace(/\s/g, '').should.eql('123456async789012'); }); it('register() - strip indention', async () => { const tag = new Tag(); tag.register('test', (args, content) => content, true); const str = [ '{% test %}', ' test content', '{% endtest %}' ].join('\n'); const result = await tag.render(str); result.should.eql('test content'); }); it('register() - async callback', async () => { const tag = new Tag(); tag.register('test', async (args, _content, callback) => { callback && callback(null, args.join(' ')); return ''; }, { async: true }); const result = await tag.render('{% test foo bar %}'); result.should.eql('foo bar'); }); it('register() - name is required', () => { // @ts-expect-error should.throw(() => tag.register(), 'name is required'); }); it('register() - fn must be a function', () => { // @ts-expect-error should.throw(() => tag.register('test'), 'fn must be a function'); }); it('unregister()', () => { const tag = new Tag(); tag.register('test', async (args, _content) => args.join(' '), {async: true}); tag.unregister('test'); return tag.render('{% test foo bar %}') .then(result => { console.log(result); throw new Error('should return error'); }) .catch(err => { err.should.have.property('type', 'unknown block tag: test'); }); }); it('unregister() - name is required', () => { // @ts-expect-error should.throw(() => tag.unregister(), 'name is required'); }); it('render() - context', async () => { const tag = new Tag(); tag.register('test', function() { return this.foo; }); const result = await tag.render('{% test %}', { foo: 'bar' }); result.should.eql('bar'); }); it('render() - callback', () => { const tag = new Tag(); // spy() is not a function let spy = false; const callback = () => { spy = true; }; tag.register('test', () => 'foo'); return tag.render('{% test %}', callback).then(result => { result.should.eql('foo'); spy.should.eql(true); }); }); it('tag should get right locals', async () => { let count = 0; hexo.extend.filter = new Filter(); hexo.extend.tag = new Tag(); hexo.extend.tag.register('series', () => { count = hexo.locals.get('posts').length; return ''; }, {ends: false}); hexo.extend.filter.register('before_generate', renderPostFilter.bind(hexo)); const body1 = [ 'title: "test1"', 'date: 2023-09-03 16:59:42', 'tags: foo', '---', '{% series %}' ].join('\n'); const file = newFile({ path: 'test1.html', published: true, type: 'create', renderable: true }); const body2 = [ '---', 'title: test2', 'date: 2023-09-03 16:59:46', 'tags: foo', '---' ].join('\n'); const file2 = newFile({ path: 'test2.html', published: true, type: 'create', renderable: true }); const body3 = [ 'title: test3', 'date: 2023-09-03 16:59:49', 'tags: foo', '---' ].join('\n'); const file3 = newFile({ path: 'test3.html', published: true, type: 'create', renderable: true }); await Promise.all([ writeFile(file.source, body1), writeFile(file2.source, body2), writeFile(file3.source, body3) ]); await Promise.all([ process(file), process(file2), process(file3) ]); await hexo._generate({ cache: false }); count.should.eql(3); }); }); ================================================ FILE: test/scripts/extend/tag_errors.ts ================================================ import Tag from '../../../lib/extend/tag'; describe('Tag Errors', () => { const assertNunjucksError = (err, line, type) => { err.should.have.property('name', 'Nunjucks Error'); err.should.have.property('message'); err.should.have.property('line', line); err.should.have.property('type', type); }; it('unknown tag', async () => { const tag = new Tag(); const body = [ '{% abc %}', ' content', '{% endabc %}' ].join('\n'); try { await tag.render(body); } catch (err) { assertNunjucksError(err, 1, 'unknown block tag: abc'); } }); it('no closing tag 1', async () => { const tag = new Tag(); tag.register('test', (_args, _content) => { return ''; }, { ends: true }); const body = [ '{% test %}', ' content' ].join('\n'); try { await tag.render(body); } catch (err) { err.should.have.property('name', 'Template render error'); err.should.have.property('message'); err.message.should.have.string('unexpected end of file'); } }); it('no closing tag 2', async () => { const tag = new Tag(); tag.register('test', (_args, _content) => { return ''; }, { ends: true }); const body = [ '{% test %}', ' content', '{% test %}' ].join('\n'); try { await tag.render(body); } catch (err) { err.should.have.property('name', 'Template render error'); err.should.have.property('message'); err.message.should.have.string('unexpected end of file'); } }); it('curly braces', async () => { const tag = new Tag(); const body = [ '{{docker ps -aq | map docker inspect -f "{{.Name}} {{.Mounts}}"}}' ].join('\n'); try { await tag.render(body); } catch (err) { assertNunjucksError(err, 1, 'expected variable end'); } }); it('nested curly braces', async () => { const tag = new Tag(); tag.register('test', (_args, _content) => { return ''; }, { ends: true }); const body = [ '{% test %}', ' {{docker ps -aq | map docker inspect -f "{{.Name}} {{.Mounts}}"}}', '{% endtest %}' ].join('\n'); try { await tag.render(body); } catch (err) { assertNunjucksError(err, 2, 'expected variable end'); } }); it('source file path', async () => { const source = '_posts/hello-world.md'; const tag = new Tag(); tag.register('test', (_args, _content) => { return ''; }, { ends: true }); const body = [ '{% test %}', ' {{docker ps -aq | map docker inspect -f "{{.Name}} {{.Mounts}}"}}', '{% endtest %}' ].join('\n'); try { // Add { source } as option await tag.render(body, { source }); } catch (err) { err.message.should.contains(source); } }); it('source file path 2', async () => { const source = '_posts/hello-world.md'; const tag = new Tag(); tag.register('test', (_args, _content) => { return ''; }, { ends: true }); const body = [ '{% test %}', '${#var}', '{% endtest %}' ].join('\n'); try { await tag.render(body, { source }); } catch (err) { err.should.have.property('message'); err.message.should.contains(source); } }); }); ================================================ FILE: test/scripts/filters/backtick_code_block.ts ================================================ import { highlight as highlightJs, prismHighlight, escapeHTML } from 'hexo-util'; import defaultConfig from '../../../lib/hexo/default_config'; import Hexo from '../../../lib/hexo'; import defaultCodeBlock from '../../../lib/plugins/filter/before_post_render/backtick_code_block'; import chai from 'chai'; const should = chai.should(); describe('Backtick code block', () => { const hexo = new Hexo(); require('../../../lib/plugins/highlight/')(hexo); const codeBlock = defaultCodeBlock(hexo); const code = [ 'if (tired && night) {', ' sleep();', '}' ].join('\n'); const escapeSwigTag = (str: string) => str.replace(/{/g, '{').replace(/}/g, '}'); function highlight(code: string, options?) { return highlightJs(code, options || {}) .replace(/{/g, '{') .replace(/}/g, '}'); } function prism(code: string, options?) { return prismHighlight(code, options || {}) .replace(/{/g, '{') .replace(/}/g, '}'); } function createCodeWithOptions(options: string, source = code) { return [ '```' + options, source, '```' ].join('\n'); } beforeEach(() => { // Reset config hexo.config.highlight = Object.assign({}, defaultConfig.highlight); hexo.config.prismjs = Object.assign({}, defaultConfig.prismjs); }); after(() => { // Reset config for further test hexo.config.highlight = defaultConfig.highlight; hexo.config.prismjs = defaultConfig.prismjs; }); it('disabled', () => { const content = [ '``` js', code, '```' ].join('\n'); const data = {content}; hexo.config.syntax_highlighter = ''; codeBlock(data); data.content.should.eql(content); }); it('with no config (disabled)', () => { const content = [ '``` js', code, '```' ].join('\n'); const data = {content}; const oldHljsCfg = hexo.config.highlight; const oldPrismCfg = hexo.config.prismjs; delete(hexo.config as any).highlight; delete(hexo.config as any).prismjs; codeBlock(data); data.content.should.eql(content); hexo.config.highlight = oldHljsCfg; hexo.config.prismjs = oldPrismCfg; }); describe('highlightjs', () => { beforeEach(() => { hexo.config.syntax_highlighter = 'highlight.js'; }); it('shorthand', () => { const data = { content: 'Hello, world!' }; should.not.exist(codeBlock(data)); }); it('default', () => { const data = { content: [ '``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql('' + highlight(code, {lang: 'js'}) + ''); }); it('without language name', () => { const data = { content: [ '```', code, '```' ].join('\n') }; const expected = highlight(code); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('without language name - ignore tab character', () => { const data = { content: [ '``` \t', code, '```' ].join('\n') }; const expected = highlight(code); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('title', () => { const data = { content: [ '``` js Hello world', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', caption: 'Hello world' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('url', () => { const data = { content: [ '``` js Hello world https://hexo.io/', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', caption: 'Hello worldlink' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('link text', () => { const data = { content: [ '``` js Hello world https://hexo.io/ Hexo', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', caption: 'Hello worldHexo' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('indent', () => { const indentCode = code.split('\n').map(line => ' ' + line).join('\n'); const data = { content: [ '``` js Hello world https://hexo.io/', indentCode, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', caption: 'Hello worldlink' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number false', () => { hexo.config.highlight.line_number = false; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number false, don`t first_line_number always1', () => { hexo.config.highlight.line_number = false; hexo.config.highlight.first_line_number = 'always1'; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('only wrap with pre and code', () => { hexo.config.highlight.exclude_languages = ['js']; hexo.config.highlight.hljs = true; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: false, hljs: true, wrap: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number false, don`t care first_line_number inline', () => { hexo.config.highlight.line_number = false; hexo.config.highlight.first_line_number = 'inline'; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number true', () => { hexo.config.highlight.line_number = true; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number, first_line_number always1, js=', () => { hexo.config.highlight.line_number = true; hexo.config.highlight.first_line_number = 'always1'; const data = { content: [ '``` js=', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: true, firstLine: 1 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number, first_line_number inline, js', () => { hexo.config.highlight.line_number = true; hexo.config.highlight.first_line_number = 'inline'; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: false, firstLine: 0 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number, first_line_number inline, js=1', () => { hexo.config.highlight.line_number = true; hexo.config.highlight.first_line_number = 'inline'; const data = { content: [ '``` js=1', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: true, firstLine: 1 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number, first_line_number inline, js=2', () => { hexo.config.highlight.line_number = true; hexo.config.highlight.first_line_number = 'inline'; const data = { content: [ '``` js=2', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', gutter: true, firstLine: 2 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('tab replace', () => { hexo.config.highlight.tab_replace = ' '; const code = [ 'if (tired && night){', '\tsleep();', '}' ].join('\n'); const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = highlight(code, { lang: 'js', tab: ' ' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('wrap', () => { hexo.config.highlight.wrap = false; const data = { content: [ '``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql('' + highlight(code, { lang: 'js', wrap: false }) + ''); hexo.config.highlight.wrap = true; }); // test for Issue #4220 it('skip a Swig template', () => { const data = { content: [ '```foo```', '', '```', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql('```foo```\n\n' + highlight(code, {}) + ''); }); // test for Issue #4190 it('ignore triple backticks at the line which is started by extra characters', () => { const data = { content: [ '```', code, 'foo```', '', 'bar```', 'baz', '```' ].join('\n') }; codeBlock(data); data.content.should.eql('' + highlight(code + '\nfoo```\n\nbar```\nbaz', {}) + ''); }); // test for Issue #4573 it('ignore trailing spaces', () => { const data = { content: [ '``` js', code, '``` ', '``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.not.contain('`'); }); // test for Issue #4573 it('ignore trailing spaces but not newlines', () => { const data = { content: [ '``` js', code, '```', '', '# New line' ].join('\n') }; codeBlock(data); data.content.should.contain('\n\n# New line'); }); it('highlight disable', () => { const data = { content: createCodeWithOptions('js highlight:false') }; const expected = escapeSwigTag(data.content); codeBlock(data); data.content.should.eql(expected); }); it('line_number', () => { let data = { content: createCodeWithOptions('js line_number:false') }; let expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true') }; expected = highlight(code, { lang: 'js', gutter: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line_threshold', () => { let data = { content: createCodeWithOptions('js line_number:false line_threshold:1') }; let expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:1') }; expected = highlight(code, { lang: 'js', gutter: true }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:3') }; expected = highlight(code, { lang: 'js', gutter: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('first_line', () => { let data = { content: createCodeWithOptions('js first_line:1234') }; let expected = highlight(code, { lang: 'js', firstLine: 1234 }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js') }; expected = highlight(code, { lang: 'js', firstLine: 1 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('mark', () => { const source = [ 'const http = require(\'http\');', '', 'const hostname = \'127.0.0.1\';', 'const port = 1337;', '', 'http.createServer((req, res) => {', ' res.writeHead(200, { \'Content-Type\': \'text/plain\' });', ' res.end(\'Hello World\n\');', '}).listen(port, hostname, () => {', ' console.log(`Server running at http://${hostname}:${port}/`);', '});' ].join('\n'); let data = { content: createCodeWithOptions('js mark:1,7-9,11', source) }; let expected = highlight(source, { lang: 'js', mark: [1, 7, 8, 9, 11] }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js mark:11,9-7,1', source) }; expected = highlight(source, { lang: 'js', mark: [1, 7, 8, 9, 11] }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('wrap', () => { let data = { content: createCodeWithOptions('js wrap:false') }; let expected = highlight(code, { lang: 'js', wrap: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js wrap:true') }; expected = highlight(code, { lang: 'js', wrap: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('language_attr', () => { const data = { content: createCodeWithOptions('js language_attr:true') }; const expected = highlight(code, { lang: 'js', languageAttr: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('hybrid', () => { let data = { content: createCodeWithOptions('js Hello world https://hexo.io/ Hexo line_number:true line_threshold:1') }; const expected = highlight(code, { lang: 'js', caption: 'Hello worldHexo', gutter: true }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:1 Hello world https://hexo.io/ Hexo') }; codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js Hello world line_number:true line_threshold:1 https://hexo.io/ Hexo') }; codeBlock(data); data.content.should.eql('' + expected + ''); }); // https://github.com/hexojs/hexo/issues/5423 it('with ordered list', () => { const data = { content: [ '1. ``` js', code, '```', '2. ``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql([ '1. ' + highlight(code, { lang: 'js' }) + '', '2. ' + highlight(code, { lang: 'js' }) + '' ].join('\n')); }); // https://github.com/hexojs/hexo/issues/5423 it('with unordered list', () => { let data = { content: [ '- ``` js', code, '```', '- ``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql([ '- ' + highlight(code, { lang: 'js' }) + '', '- ' + highlight(code, { lang: 'js' }) + '' ].join('\n')); data = { content: [ '* ``` js', code, '```', '* ``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql([ '* ' + highlight(code, { lang: 'js' }) + '', '* ' + highlight(code, { lang: 'js' }) + '' ].join('\n')); data = { content: [ '+ ``` js', code, '```', '+ ``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql([ '+ ' + highlight(code, { lang: 'js' }) + '', '+ ' + highlight(code, { lang: 'js' }) + '' ].join('\n')); }); }); describe('prismjs', () => { beforeEach(() => { hexo.config.syntax_highlighter = 'prismjs'; }); it('default', () => { const data = { content: [ '``` js', code, '```' ].join('\n') }; codeBlock(data); data.content.should.eql('' + prism(code, {lang: 'js'}) + ''); }); it('without language name', () => { const data = { content: [ '```', code, '```' ].join('\n') }; const expected = prism(code); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('without language name - ignore tab character', () => { const data = { content: [ '``` \t', code, '```' ].join('\n') }; const expected = prism(code); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('indent', () => { const indentCode = code.split('\n').map(line => ' ' + line).join('\n'); const data = { content: [ '``` js', indentCode, '```' ].join('\n') }; const expected = prism(code, { lang: 'js' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line number false', () => { hexo.config.prismjs.line_number = false; const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = prism(code, { lang: 'js', lineNumber: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('tab replace', () => { hexo.config.prismjs.tab_replace = ' '; const code = [ 'if (tired && night){', '\tsleep();', '}' ].join('\n'); const data = { content: [ '``` js', code, '```' ].join('\n') }; const expected = prism(code, { lang: 'js', tab: ' ' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('title', () => { const data = { content: [ '``` js Hello world', code, '```' ].join('\n') }; const expected = prism(code, { lang: 'js', caption: 'Hello world' }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('prism only wrap with pre and code', () => { hexo.config.prismjs.exclude_languages = ['js']; const data = { content: [ '``` js', code, '```' ].join('\n') }; const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}'); const expected = `
${escapeSwigTag(escapeHTML(code))}
`; codeBlock(data); data.content.should.eql('' + expected + ''); hexo.config.prismjs.exclude_languages = []; }); it('highlight disable', () => { const data = { content: createCodeWithOptions('js highlight:false') }; const expected = escapeSwigTag(data.content); codeBlock(data); data.content.should.eql(expected); }); it('line_number', () => { let data = { content: createCodeWithOptions('js line_number:false') }; let expected = prism(code, { lang: 'js', lineNumber: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true') }; expected = prism(code, { lang: 'js', lineNumber: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('line_threshold', () => { let data = { content: createCodeWithOptions('js line_number:false line_threshold:1') }; let expected = prism(code, { lang: 'js', lineNumber: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:1') }; expected = prism(code, { lang: 'js', lineNumber: true }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:3') }; expected = prism(code, { lang: 'js', lineNumber: false }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('first_line', () => { let data = { content: createCodeWithOptions('js first_line:1234') }; let expected = prism(code, { lang: 'js', firstLine: 1234 }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js') }; expected = prism(code, { lang: 'js', firstLine: 1 }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('mark', () => { const source = [ 'const http = require(\'http\');', '', 'const hostname = \'127.0.0.1\';', 'const port = 1337;', '', 'http.createServer((req, res) => {', ' res.writeHead(200, { \'Content-Type\': \'text/plain\' });', ' res.end(\'Hello World\n\');', '}).listen(port, hostname, () => {', ' console.log(`Server running at http://${hostname}:${port}/`);', '});' ].join('\n'); let data = { content: createCodeWithOptions('js mark:1,7-9,11', source) }; let expected = prism(source, { lang: 'js', mark: [1, 7, 8, 9, 11] }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js mark:11,9-7,1', source) }; expected = prism(source, { lang: 'js', mark: [1, 7, 8, 9, 11] }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('wrap', () => { let data = { content: createCodeWithOptions('js wrap:false') }; let expected = prism(code, { lang: 'js', wrap: false }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js wrap:true') }; expected = prism(code, { lang: 'js', wrap: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('language_attr', () => { const data = { content: createCodeWithOptions('js language_attr:true') }; const expected = prism(code, { lang: 'js', languageAttr: true }); codeBlock(data); data.content.should.eql('' + expected + ''); }); it('hybrid', () => { let data = { content: createCodeWithOptions('js Hello world https://hexo.io/ Hexo line_number:true line_threshold:1') }; const expected = prism(code, { lang: 'js', caption: 'Hello worldHexo', lineNumber: true }); codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js line_number:true line_threshold:1 Hello world https://hexo.io/ Hexo') }; codeBlock(data); data.content.should.eql('' + expected + ''); data = { content: createCodeWithOptions('js Hello world line_number:true line_threshold:1 https://hexo.io/ Hexo') }; codeBlock(data); data.content.should.eql('' + expected + ''); }); }); }); ================================================ FILE: test/scripts/filters/excerpt.ts ================================================ import Hexo from '../../../lib/hexo'; import excerptFilter from '../../../lib/plugins/filter/after_post_render/excerpt'; type ExcerptFilterParams = Parameters; type ExcerptFilterReturn = ReturnType; describe('Excerpt', () => { const hexo = new Hexo(); const excerpt: (...args: ExcerptFilterParams) => ExcerptFilterReturn = excerptFilter.bind(hexo); it('without ', () => { const content = [ 'foo', 'bar', 'baz' ].join('\n'); const data: { content: string; excerpt?: string; more?: string; } = { content }; excerpt(data); data.content.should.eql(content); data.excerpt!.should.eql(''); data.more!.should.eql(content); }); it('with ', () => { const _moreCases = [ '', '', '', '' ]; _moreCases.forEach(moreCase => _test(moreCase)); function _test(more) { const content = [ 'foo', 'bar', more, 'baz' ].join('\n'); const data: { content: string; excerpt?: string; more?: string; } = { content }; excerpt(data); data.content.should.eql([ 'foo', 'bar', '', 'baz' ].join('\n')); data.excerpt!.should.eql([ 'foo', 'bar' ].join('\n')); data.more!.should.eql([ 'baz' ].join('\n')); } }); it('multiple ', () => { const content = [ 'foo', '', 'bar', '', 'baz' ].join('\n'); const data: { content: string; excerpt?: string; more?: string; } = { content }; excerpt(data); data.content.should.eql([ 'foo', '', 'bar', '', 'baz' ].join('\n')); data.excerpt!.should.eql([ 'foo' ].join('\n')); data.more!.should.eql([ 'bar', '', 'baz' ].join('\n')); }); it('skip processing if post/page.excerpt is present in the front-matter', () => { const content = [ 'foo', '', 'bar' ].join('\n'); const data: { content: string; excerpt: string; more?: string; } = { content, excerpt: 'baz' }; excerpt(data); data.content.should.eql([ 'foo', '', 'bar' ].join('\n')); data.excerpt.should.eql([ 'baz' ].join('\n')); data.more!.should.eql([ 'foo', '', 'bar' ].join('\n')); }); }); ================================================ FILE: test/scripts/filters/external_link.ts ================================================ import Hexo from '../../../lib/hexo'; import decache from 'decache'; import externalLinkFilter from '../../../lib/plugins/filter/after_render/external_link'; import externalLinkPostFilter from '../../../lib/plugins/filter/after_post_render/external_link'; import chai from 'chai'; const should = chai.should(); type ExternalLinkParams = Parameters; type ExternalLinkReturn = ReturnType; type ExternalLinkPostParams = Parameters; type ExternalLinkPostReturn = ReturnType; describe('External link', () => { const hexo = new Hexo(); let externalLink: (...args: ExternalLinkParams) => ExternalLinkReturn; beforeEach(() => { decache('../../../lib/plugins/filter/after_render/external_link'); externalLink = require('../../../lib/plugins/filter/after_render/external_link').bind(hexo); }); hexo.config = { url: 'https://example.com', external_link: { enable: true, field: 'site', exclude: '' } } as any; it('disabled', () => { const content = 'foo' + 'Hexo' + 'bar'; hexo.config.external_link.enable = false; should.not.exist(externalLink(content)); hexo.config.external_link.enable = true; }); it('field is post', () => { const content = 'foo' + 'Hexo' + 'bar'; hexo.config.external_link.field = 'post'; should.not.exist(externalLink(content)); hexo.config.external_link.field = 'site'; }); it('enabled', () => { const content = [ '# External link test', '1. External link', 'Hexo', '2. External link with "rel" Attribute', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', '3. External link with Other Attributes', 'Hexo', 'Hexo', '4. Internal link', 'Link', '5. Ignore links have "target" attribute', 'Hexo', '6. Ignore links don\'t have "href" attribute', 'Anchor', '7. Ignore links whose hostname is same as config', 'Example Domain' ].join('\n'); const result = externalLink(content); result.should.eql([ '# External link test', '1. External link', 'Hexo', '2. External link with "rel" Attribute', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', '3. External link with Other Attributes', 'Hexo', 'Hexo', '4. Internal link', 'Link', '5. Ignore links have "target" attribute', 'Hexo', '6. Ignore links don\'t have "href" attribute', 'Anchor', '7. Ignore links whose hostname is same as config', 'Example Domain' ].join('\n')); }); it('exclude - string', () => { const content = [ 'Hexo', 'Hexo', 'Hexo' ].join('\n'); hexo.config.external_link.exclude = 'foo.com'; const result = externalLink(content); result.should.eql([ 'Hexo', 'Hexo', 'Hexo' ].join('\n')); hexo.config.external_link.exclude = ''; }); it('exclude - array', () => { const content = [ 'Hexo', 'Hexo', 'Hexo' ].join('\n'); // @ts-expect-error hexo.config.external_link.exclude = ['foo.com', 'bar.com']; const result = externalLink(content); result.should.eql([ 'Hexo', 'Hexo', 'Hexo' ].join('\n')); hexo.config.external_link.exclude = ''; }); }); describe('External link - post', () => { const Hexo = require('../../../lib/hexo'); const hexo = new Hexo(); let externalLink: (...args: ExternalLinkPostParams) => ExternalLinkPostReturn; beforeEach(() => { decache('../../../lib/plugins/filter/after_post_render/external_link'); externalLink = require('../../../lib/plugins/filter/after_post_render/external_link').bind(hexo); }); hexo.config = { url: 'https://example.com', external_link: { enable: true, field: 'post', exclude: '' } }; it('disabled', () => { const content = 'fooHexobar'; const data = {content}; hexo.config.external_link.enable = false; externalLink(data); data.content.should.eql(content); hexo.config.external_link.enable = true; }); it('field is site', () => { const content = 'foo' + 'Hexo' + 'bar'; const data = {content}; hexo.config.external_link.field = 'site'; externalLink(data); data.content.should.eql(content); hexo.config.external_link.field = 'post'; }); it('enabled', () => { const content = [ '# External link test', '1. External link', 'Hexo', '2. Link with hash (#), mailto: , javascript: shouldn\'t be processed', 'Hexo', 'Hexo', 'Hexo', '3. External link with "rel" Attribute', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', '4. External link with Other Attributes', 'Hexo', 'Hexo', '5. Internal link', 'Link', '6. Ignore links have "target" attribute', 'Hexo', '7. Ignore links don\'t have "href" attribute', 'Anchor', '8. Ignore links whose hostname is same as config', 'Example Domain' ].join('\n'); const data = {content}; externalLink(data); data.content.should.eql([ '# External link test', '1. External link', 'Hexo', '2. Link with hash (#), mailto: , javascript: shouldn\'t be processed', 'Hexo', 'Hexo', 'Hexo', '3. External link with "rel" Attribute', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', 'Hexo', '4. External link with Other Attributes', 'Hexo', 'Hexo', '5. Internal link', 'Link', '6. Ignore links have "target" attribute', 'Hexo', '7. Ignore links don\'t have "href" attribute', 'Anchor', '8. Ignore links whose hostname is same as config', 'Example Domain' ].join('\n')); }); it('backward compatibility', () => { const content = 'foo' + 'Hexo' + 'bar'; const data = {content}; hexo.config.external_link = false; externalLink(data); data.content.should.eql(content); hexo.config.external_link = { enable: true, field: 'post', exclude: '' }; }); it('exclude - string', () => { const content = [ 'Hexo', 'Hexo', 'Hexo' ].join('\n'); hexo.config.external_link.exclude = 'foo.com'; const data = {content}; externalLink(data); data.content.should.eql([ 'Hexo', 'Hexo', 'Hexo' ].join('\n')); hexo.config.external_link.exclude = ''; }); it('exclude - array', () => { const content = [ 'Hexo', 'Hexo', 'Hexo' ].join('\n'); hexo.config.external_link.exclude = ['foo.com', 'bar.com']; const data = {content}; externalLink(data); data.content.should.eql([ 'Hexo', 'Hexo', 'Hexo' ].join('\n')); hexo.config.external_link.exclude = ''; }); }); ================================================ FILE: test/scripts/filters/i18n_locals.ts ================================================ import Hexo from '../../../lib/hexo'; import i18nLocalsFilter from '../../../lib/plugins/filter/template_locals/i18n'; type I18nLocalsFilterParams = Parameters; type I18nLocalsFilterReturn = ReturnType; describe('i18n locals', () => { const hexo = new Hexo(); const i18nFilter: (...args: I18nLocalsFilterParams) => I18nLocalsFilterReturn = i18nLocalsFilter.bind(hexo); const theme = hexo.theme; const i18n = theme.i18n; // Default language i18n.languages = ['en', 'default']; // Fixtures i18n.set('de', { Home: 'Zuhause' }); i18n.set('default', { Home: 'Default Home' }); i18n.set('en', { Home: 'Home' }); i18n.set('zh-tw', { Home: '首頁' }); it('page.lang set', () => { const locals = { config: hexo.config, page: { lang: 'zh-tw' } } as any; i18nFilter(locals); locals.__('Home').should.eql('首頁'); }); it('page.language set', () => { const locals = { config: hexo.config, page: { language: 'zh-tw' } } as any; i18nFilter(locals); locals.__('Home').should.eql('首頁'); }); it('detect by path (lang found)', () => { const locals = { config: hexo.config, page: {} as any, path: 'zh-tw/index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('zh-tw'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('首頁'); }); it('detect by path (lang not found)', () => { const locals = { config: hexo.config, page: {} as any, path: 'news/index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('en'); locals.page.canonical_path.should.eql('news/index.html'); locals.__('Home').should.eql('Home'); }); it('use config by default', () => { const locals = { config: hexo.config, page: {} as any, path: 'index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('en'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('Home'); }); it('use config by default - with multiple languages, first language should be used', () => { const oldConfig = i18n.languages; i18n.languages = ['zh-tw', 'en', 'default']; const locals = { config: hexo.config, page: {} as any, path: 'index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('zh-tw'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('首頁'); i18n.languages = oldConfig; }); it('use config by default - with no languages, default language should be used', () => { const oldConfig = i18n.languages; i18n.languages = ['default']; const locals = { config: hexo.config, page: {} as any, path: 'index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('default'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('Default Home'); i18n.languages = oldConfig; }); it('use config by default - with unknown language, default language should be used', () => { const oldConfig = i18n.languages; i18n.languages = ['fr', 'default']; const locals = { config: hexo.config, page: {} as any, path: 'index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('fr'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('Default Home'); i18n.languages = oldConfig; }); it('use config by default - with no set language and no default file take first available', () => { const oldConfig = i18n.languages; const oldSet = i18n.get('default'); i18n.remove('default'); i18n.languages = ['default']; const locals = { config: hexo.config, page: {} as any, path: 'index.html' } as any; i18nFilter(locals); locals.page.lang.should.eql('default'); locals.page.canonical_path.should.eql('index.html'); locals.__('Home').should.eql('Zuhause'); i18n.set('default', oldSet); i18n.languages = oldConfig; }); }); ================================================ FILE: test/scripts/filters/meta_generator.ts ================================================ import Hexo from '../../../lib/hexo'; import decache from 'decache'; import * as cheerio from 'cheerio'; import type hexoMetaGeneratorInject from '../../../lib/plugins/filter/after_render/meta_generator'; import chai from 'chai'; const should = chai.should(); type hexoMetaGeneratorInjectParams = Parameters; type hexoMetaGeneratorInjectReturn = ReturnType; describe('Meta Generator', () => { const hexo = new Hexo(); let metaGenerator: (...args: hexoMetaGeneratorInjectParams) => hexoMetaGeneratorInjectReturn; beforeEach(() => { decache('../../../lib/plugins/filter/after_render/meta_generator'); metaGenerator = require('../../../lib/plugins/filter/after_render/meta_generator').bind(hexo); }); it('default', () => { const content = ''; const result = metaGenerator(content); const $ = cheerio.load(result); $('meta[name="generator"]').should.have.lengthOf(1); $('meta[name="generator"]').attr('content')!.should.eql(`Hexo ${hexo.version}`); }); it('disable meta_generator', () => { const content = ''; hexo.config.meta_generator = false; const result = metaGenerator(content); should.not.exist(result); }); it('no duplicate generator tag', () => { hexo.config.meta_generator = true; should.not.exist(metaGenerator('')); should.not.exist(metaGenerator('')); }); // Test for Issue #3777 it('multi-line head', () => { const content = '\n\n'; hexo.config.meta_generator = true; const result = metaGenerator(content); const $ = cheerio.load(result); $('meta[name="generator"]').should.have.lengthOf(1); const expected = '\n\n'; result.should.eql(expected); }); }); ================================================ FILE: test/scripts/filters/new_post_path.ts ================================================ import { join } from 'path'; import moment from 'moment'; import { createSha1Hash } from 'hexo-util'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import newPostPathFilter from '../../../lib/plugins/filter/new_post_path'; type NewPostPathFilterParams = Parameters; type NewPostPathFilterReturn = ReturnType; describe('new_post_path', () => { const hexo = new Hexo(join(__dirname, 'new_post_path_test')); const newPostPath: (...args: NewPostPathFilterParams) => NewPostPathFilterReturn = newPostPathFilter.bind(hexo); const sourceDir = hexo.source_dir; const draftDir = join(sourceDir, '_drafts'); const postDir = join(sourceDir, '_posts'); before(async () => { hexo.config.new_post_name = ':title.md'; await mkdirs(hexo.base_dir); hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('page layout + path', async () => { const target = await newPostPath({ path: 'foo', layout: 'page' }); target.should.eql(join(sourceDir, 'foo.md')); }); it('draft layout + path', async () => { const target = await newPostPath({ path: 'foo', layout: 'draft' }); target.should.eql(join(draftDir, 'foo.md')); }); it('default layout + path', async () => { const target = await newPostPath({ path: 'foo' }); target.should.eql(join(postDir, 'foo.md')); }); it('page layout + slug', async () => { const target = await newPostPath({ slug: 'foo', layout: 'page' }); target.should.eql(join(sourceDir, 'foo', 'index.md')); }); it('draft layout + slug', async () => { const target = await newPostPath({ slug: 'foo', layout: 'draft' }); target.should.eql(join(draftDir, 'foo.md')); }); it('default layout + slug', async () => { const now = moment(); hexo.config.new_post_name = ':year-:month-:day-:title.md'; const target = await newPostPath({ slug: 'foo' }); target.should.eql(join(postDir, now.format('YYYY-MM-DD') + '-foo.md')); }); it('date', async () => { const date = moment([2014, 0, 1]); hexo.config.new_post_name = ':year-:i_month-:i_day-:title.md'; const target = await newPostPath({ slug: 'foo', date: date.toDate() as any }); target.should.eql(join(postDir, date.format('YYYY-M-D') + '-foo.md')); }); it('extra data', async () => { hexo.config.new_post_name = ':foo-:bar-:title.md'; const target = await newPostPath({ slug: 'foo', foo: 'oh', bar: 'ya' }); target.should.eql(join(postDir, 'oh-ya-foo.md')); }); it('append extension name if not existed', async () => { hexo.config.new_post_name = ':title'; const target = await newPostPath({ slug: 'foo' }); target.should.eql(join(postDir, 'foo.md')); }); it('hash', async () => { const now = moment(); const slug = 'foo'; const sha1 = createSha1Hash(); const hash = sha1.update(slug + now.unix().toString()) .digest('hex').slice(0, 12); hexo.config.new_post_name = ':title-:hash'; const target = await newPostPath({ slug, title: 'tree', date: now.format('YYYY-MM-DD HH:mm:ss') as any }); target.should.eql(join(postDir, `${slug}-${hash}.md`)); }); it('don\'t append extension name if existed', async () => { const target = await newPostPath({ path: 'foo.markdown' }); target.should.eql(join(postDir, 'foo.markdown')); }); it('replace existing files', async () => { const filename = 'test.md'; const path = join(postDir, filename); await writeFile(path, ''); const target = await newPostPath({ path: filename }, true); target.should.eql(path); await unlink(path); }); it('rename if target existed', async () => { const filename = [ 'test.md', 'test-1.md', 'test-2.md', 'test-foo.md' ]; const path = filename.map(item => join(postDir, item)); await Promise.all(path.map(item => writeFile(item, ''))); const target = await newPostPath({ path: filename[0] }); target.should.eql(join(postDir, 'test-3.md')); await Promise.all(path.map(item => unlink(item))); }); it('data is required', async () => { try { await newPostPath(); } catch (err) { err.message.should.have.string('Either data.path or data.slug is required!'); } }); }); ================================================ FILE: test/scripts/filters/post_permalink.ts ================================================ import moment from 'moment'; import Hexo from '../../../lib/hexo'; import postPermalinkFilter from '../../../lib/plugins/filter/post_permalink'; type PostPermalinkFilterParams = Parameters; type PostPermalinkFilterReturn = ReturnType; describe('post_permalink', () => { const hexo = new Hexo(); const postPermalink: (...args: PostPermalinkFilterParams) => PostPermalinkFilterReturn = postPermalinkFilter.bind(hexo); const Post = hexo.model('Post'); let post; before(async () => { hexo.config.permalink = ':year/:month/:day/:title/'; hexo.config.permalink_defaults = {}; await hexo.init(); const apost = await Post.insert({ source: 'foo.md', slug: 'foo', date: moment('2014-01-02') }); const id = apost._id; await apost.setCategories(['foo', 'bar']); post = Post.findById(id); }); it('default', () => { postPermalink(post).should.eql('2014/01/02/foo/'); }); it('categories', () => { hexo.config.permalink = ':category/:title/'; postPermalink(post).should.eql('foo/bar/foo/'); }); it('uncategorized', async () => { hexo.config.permalink = ':category/:title/'; const post = await Post.insert({ source: 'bar.md', slug: 'bar' }); postPermalink(post).should.eql(hexo.config.default_category + '/bar/'); Post.removeById(post._id); }); it('extra data', () => { hexo.config.permalink = ':layout/:title/'; postPermalink(post).should.eql(post.layout + '/foo/'); }); it('id', () => { hexo.config.permalink = ':id'; postPermalink(post).should.eql(post._id); post.id = 1; postPermalink(post).should.eql('1'); }); it('name', async () => { hexo.config.permalink = ':title/:name'; const post = await Post.insert({ source: 'sub/bar.md', slug: 'sub/bar' }); postPermalink(post).should.eql('sub/bar/bar'); Post.removeById(post._id); }); it('post_title', async () => { hexo.config.permalink = ':year/:month/:day/:post_title/'; const post = await Post.insert({ source: 'sub/2015-05-06-my-new-post.md', slug: '2015-05-06-my-new-post', title: 'My New Post', date: moment('2015-05-06') }); postPermalink(post).should.eql('2015/05/06/my-new-post/'); Post.removeById(post._id); }); it('hour minute and second', async () => { hexo.config.permalink = ':year/:month/:day/:hour/:minute/:second/:post_title/'; const post = await Post.insert({ source: 'sub/2015-05-06-my-new-post.md', slug: '2015-05-06-my-new-post', title: 'My New Post', date: moment('2015-05-06 12:13:14') }); postPermalink(post).should.eql('2015/05/06/12/13/14/my-new-post/'); Post.removeById(post._id); }); it('timestamp', async () => { hexo.config.permalink = ':timestamp/:slug'; const timestamp = '1736401514'; const dates = [ moment('2025-01-09 05:45:14Z'), moment('2025-01-08 22:45:14-07') ]; const posts = await Post.insert( dates.map((date, idx) => { return { source: `test${idx}.md`, slug: `test${idx}`, date: date }; }) ); postPermalink(posts[0]).should.eql(`${timestamp}/test0`); postPermalink(posts[1]).should.eql(`${timestamp}/test1`); return Promise.all( posts.map(post => { return Post.removeById(post._id); }) ); }); it('time is omitted in front-matter', async () => { hexo.config.permalink = ':year/:month/:day/:hour/:minute/:second/:post_title/'; const post = await Post.insert({ source: 'sub/2015-05-06-my-new-post.md', slug: '2015-05-06-my-new-post', title: 'My New Post', date: moment('2015-05-06') }); postPermalink(post).should.eql('2015/05/06/00/00/00/my-new-post/'); Post.removeById(post._id); }); it('permalink_defaults', async () => { hexo.config.permalink = 'posts/:lang/:title/'; hexo.config.permalink_defaults = {lang: 'en'}; const posts = await Post.insert([{ source: 'my-new-post.md', slug: 'my-new-post', title: 'My New Post1' }, { source: 'my-new-fr-post.md', slug: 'my-new-fr-post', title: 'My New Post2', lang: 'fr' }]); postPermalink(posts[0]).should.eql('posts/en/my-new-post/'); postPermalink(posts[1]).should.eql('posts/fr/my-new-fr-post/'); await Promise.all(posts.map(post => Post.removeById(post._id))); }); it('permalink_defaults - null', async () => { hexo.config.permalink = 'posts/:lang/:title/'; hexo.config.permalink_defaults = null as any; const posts = await Post.insert([{ source: 'my-new-post.md', slug: 'my-new-post', title: 'My New Post1', lang: 'en' }, { source: 'my-new-post-2.md', slug: 'my-new-post-2', title: 'My New Post2', lang: 'fr' }]); postPermalink(posts[0]).should.eql('posts/en/my-new-post/'); postPermalink(posts[1]).should.eql('posts/fr/my-new-post-2/'); await Promise.all(posts.map(post => Post.removeById(post._id))); }); it('permalink - should override everything', async () => { hexo.config.permalink = ':year/:month/:day/:title/'; const posts = await Post.insert([{ source: 'my-new-post.md', slug: 'hexo/permalink-test', __permalink: 'hexo/permalink-test', title: 'Permalink Test', date: moment('2014-01-02') }, { source: 'another-new-post.md', slug: '/hexo-hexo/permalink-test-2', __permalink: '/hexo-hexo/permalink-test-2', title: 'Permalink Test', date: moment('2014-01-02') }]); postPermalink(posts[0]).should.eql('/hexo/permalink-test'); postPermalink(posts[1]).should.eql('/hexo-hexo/permalink-test-2'); await Promise.all(posts.map(post => Post.removeById(post._id))); }); it('permalink - should end with / or .html - 1', async () => { hexo.config.post_asset_folder = true; hexo.config.permalink = ':year/:month/:day/:title'; const post = await Post.insert({ source: 'foo.md', slug: 'foo', date: moment('2014-01-02') }); postPermalink(post).should.eql('2014/01/02/foo/'); Post.removeById(post._id); hexo.config.post_asset_folder = false; }); it('permalink - should end with / or .html - 2', async () => { hexo.config.post_asset_folder = true; const posts = await Post.insert([{ source: 'my-new-post.md', slug: 'hexo/permalink-test', __permalink: 'hexo/permalink-test', title: 'Permalink Test', date: moment('2014-01-02') }, { source: 'another-new-post.md', slug: '/hexo-hexo/permalink-test-2', __permalink: '/hexo-hexo/permalink-test-2/', title: 'Permalink Test', date: moment('2014-01-02') }, { source: 'another-another-new-post.md', slug: '/hexo-hexo/permalink-test-3', __permalink: '/hexo-hexo/permalink-test-3.html', title: 'Permalink Test', date: moment('2014-01-02') }]); postPermalink(posts[0]).should.eql('/hexo/permalink-test/'); postPermalink(posts[1]).should.eql('/hexo-hexo/permalink-test-2/'); postPermalink(posts[2]).should.eql('/hexo-hexo/permalink-test-3.html'); await Promise.all(posts.map(post => Post.removeById(post._id))); hexo.config.post_asset_folder = false; }); }); ================================================ FILE: test/scripts/filters/render_post.ts ================================================ import Hexo from '../../../lib/hexo'; import renderPostFilter from '../../../lib/plugins/filter/before_generate/render_post'; import { content, expected } from '../../fixtures/post_render'; type RenderPostFilterParams = Parameters; type RenderPostFilterReturn = ReturnType; describe('Render post', () => { const hexo = new Hexo(); const Post = hexo.model('Post'); const Page = hexo.model('Page'); const renderPost: (...args: RenderPostFilterParams) => RenderPostFilterReturn = renderPostFilter.bind(hexo); before(async () => { await hexo.init(); await hexo.loadPlugin(require.resolve('hexo-renderer-marked')); }); it('post', async () => { let post = await Post.insert({ source: 'foo.md', slug: 'foo', _content: content }); const id = post._id; await renderPost(); post = Post.findById(id); post.content.trim().should.eql(expected); post.remove(); }); it('page', async () => { let page = await Page.insert({ source: 'foo.md', path: 'foo.html', _content: content }); const id = page._id; await renderPost(); page = Page.findById(id); page.content.trim().should.eql(expected); page.remove(); }); }); ================================================ FILE: test/scripts/filters/save_database.ts ================================================ import Hexo from '../../../lib/hexo'; import { exists, unlink } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import saveDatabaseFilter from '../../../lib/plugins/filter/before_exit/save_database'; type SaveDatabaseFilterParams = Parameters type SaveDatabaseFilterReturn = ReturnType describe('Save database', () => { const hexo = new Hexo(); const saveDatabase: (...args: SaveDatabaseFilterParams) => BluebirdPromise = BluebirdPromise.method(saveDatabaseFilter).bind(hexo); const dbPath = hexo.database.options.path; it('default', async () => { hexo.env.init = true; hexo._dbLoaded = true; await saveDatabase(); const exist = await exists(dbPath); exist.should.be.true; unlink(dbPath); }); it('do nothing if hexo is not initialized', async () => { hexo.env.init = false; hexo._dbLoaded = true; await saveDatabase(); const exist = await exists(dbPath); exist.should.be.false; }); it('do nothing if database is not loaded', async () => { hexo.env.init = true; hexo._dbLoaded = false; await saveDatabase(); const exist = await exists(dbPath); exist.should.be.false; }); }); ================================================ FILE: test/scripts/filters/titlecase.ts ================================================ import Hexo from '../../../lib/hexo'; import titlecaseFilter from '../../../lib/plugins/filter/before_post_render/titlecase'; type titlecaseFilterParams = Parameters; type titlecaseFilterReturn = ReturnType; describe('Titlecase', () => { const hexo = new Hexo(); const titlecase: (...args: titlecaseFilterParams) => titlecaseFilterReturn = titlecaseFilter.bind(hexo); it('disabled', () => { const title = 'Today is a good day'; const data = {title}; hexo.config.titlecase = false; titlecase(data); data.title.should.eql(title); }); it('enabled', () => { const title = 'Today is a good day'; const data = {title}; hexo.config.titlecase = true; titlecase(data); data.title.should.eql('Today Is a Good Day'); }); it('enabled globally but disabled in a specify post', () => { const title = 'Today is a good day'; const data = {title, titlecase: false}; hexo.config.titlecase = true; titlecase(data); data.title.should.eql('Today is a good day'); }); it('disabled globally but enabled in a specify post', () => { const title = 'Today is a good day'; const data = {title, titlecase: true}; hexo.config.titlecase = false; titlecase(data); data.title.should.eql('Today Is a Good Day'); }); }); ================================================ FILE: test/scripts/generators/asset.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import { readStream } from '../../util'; import Hexo from '../../../lib/hexo'; import assetGenerator from '../../../lib/plugins/generator/asset'; import { spy } from 'sinon'; import chai from 'chai'; const should = chai.should(); type AssetParams = Parameters type AssetReturn = ReturnType describe('asset', () => { const hexo = new Hexo(join(__dirname, 'asset_test'), {silent: true}); const generator: (...args: AssetParams) => AssetReturn = assetGenerator.bind(hexo); const Asset = hexo.model('Asset'); const checkStream = async (stream, expected) => { const data = await readStream(stream); data.should.eql(expected); }; before(async () => { await mkdirs(hexo.base_dir); hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('renderable', async () => { const path = 'test.yml'; const source = join(hexo.base_dir, path); const content = 'foo: bar'; await Promise.all([ Asset.insert({_id: path, path}), writeFile(source, content) ]); const data = await generator(); data[0].path.should.eql('test.json'); data[0].data.modified.should.be.true; const result = await data[0].data.data!(); result.should.eql('{"foo":"bar"}'); await Promise.all([ Asset.removeById(path), unlink(source) ]); }); it('renderable - error', async () => { const logSpy = spy(); hexo.log.error = logSpy; const path = 'test.yml'; const source = join(hexo.base_dir, path); const content = 'foo: :'; await Promise.all([ Asset.insert({_id: path, path}), writeFile(source, content) ]); const data = await generator(); data[0].path.should.eql('test.json'); data[0].data.modified.should.be.true; await data[0].data.data!(); logSpy.called.should.be.true; logSpy.args[0][1].should.contains('Asset render failed: %s'); logSpy.args[0][2].should.contains('test.json'); await Promise.all([ Asset.removeById(path), unlink(source) ]); }); it('not renderable', async () => { const path = 'test.txt'; const source = join(hexo.base_dir, path); const content = 'test content'; await Promise.all([ Asset.insert({_id: path, path}), writeFile(source, content) ]); const data = await generator(); data[0].path.should.eql(path); data[0].data.modified.should.be.true; await checkStream(data[0].data.data!(), content); await Promise.all([ Asset.removeById(path), unlink(source) ]); }); it('skip render', async () => { const path = 'test.yml'; const source = join(hexo.base_dir, path); const content = 'foo: bar'; await Promise.all([ Asset.insert({_id: path, path, renderable: false}), writeFile(source, content) ]); const data = await generator(); data[0].path.should.eql('test.yml'); data[0].data.modified.should.be.true; await checkStream(data[0].data.data!(), content); await Promise.all([ Asset.removeById(path), unlink(source) ]); }); it('remove assets which does not exist', async () => { const path = 'test.txt'; await Asset.insert({ _id: path, path }); await generator(); should.not.exist(Asset.findById(path)); }); it('don\'t remove extension name', async () => { const path = 'test.min.js'; const source = join(hexo.base_dir, path); await Promise.all([ Asset.insert({_id: path, path}), writeFile(source, '') ]); const data = await generator(); data[0].path.should.eql('test.min.js'); await Promise.all([ Asset.removeById(path), unlink(source) ]); }); }); ================================================ FILE: test/scripts/generators/page.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import pageGenerator from '../../../lib/plugins/generator/page'; import chai from 'chai'; import { BaseGeneratorReturn } from '../../../lib/types'; const should = chai.should(); type PageGeneratorParams = Parameters; type PageGeneratorReturn = ReturnType; describe('page', () => { const hexo = new Hexo(__dirname, {silent: true}); const Page = hexo.model('Page'); const generator: (...args: PageGeneratorParams) => BluebirdPromise = BluebirdPromise.method(pageGenerator.bind(hexo)); const locals = (): any => { hexo.locals.invalidate(); return hexo.locals.toObject(); }; it('default layout', async () => { const page = await Page.insert({ source: 'foo', path: 'bar' }); const data = await generator(locals()); page.__page = true; data.should.eql([ { path: page.path, layout: ['page', 'post', 'index'], data: page } ]); page.remove(); }); it('custom layout', async () => { const page = await Page.insert({ source: 'foo', path: 'bar', layout: 'photo' }); const data = await generator(locals()) as BaseGeneratorReturn[]; data[0].layout!.should.eql(['photo', 'page', 'post', 'index']); page.remove(); }); [false, 'false', 'off'].forEach(layout => { it('layout = ' + JSON.stringify(layout), async () => { const page = await Page.insert({ source: 'foo', path: 'bar', layout }); const data = await generator(locals()) as BaseGeneratorReturn[]; should.not.exist(data[0].layout); page.remove(); }); }); }); ================================================ FILE: test/scripts/generators/post.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import postGenerator from '../../../lib/plugins/generator/post'; import { BaseGeneratorReturn } from '../../../lib/types'; import chai from 'chai'; const should = chai.should(); type PostGeneratorParams = Parameters; type PostGeneratorReturn = ReturnType; describe('post', () => { const hexo = new Hexo(__dirname, {silent: true}); const Post = hexo.model('Post'); const generator: (...args: PostGeneratorParams) => BluebirdPromise = BluebirdPromise.method(postGenerator.bind(hexo)); hexo.config.permalink = ':title/'; const locals = (): any => { hexo.locals.invalidate(); return hexo.locals.toObject(); }; before(() => hexo.init()); it('default layout', async () => { const post = await Post.insert({ source: 'foo', slug: 'bar' }); const data = await generator(locals()); post.__post = true; data.should.eql([ { path: 'bar/', layout: ['post', 'page', 'index'], data: post } ]); post.remove(); }); it('custom layout', async () => { const post = await Post.insert({ source: 'foo', slug: 'bar', layout: 'photo' }); const data = await generator(locals()) as BaseGeneratorReturn[]; data[0].layout!.should.eql(['photo', 'post', 'page', 'index']); post.remove(); }); it('layout disabled', async () => { const post = await Post.insert({ source: 'foo', slug: 'bar', layout: false }); const data = await generator(locals()) as BaseGeneratorReturn[]; should.not.exist(data[0].layout); post.remove(); }); it('prev/next post', async () => { const posts = await Post.insert([ {source: 'foo', slug: 'foo', date: 1e8}, {source: 'bar', slug: 'bar', date: 1e8 + 1}, {source: 'baz', slug: 'baz', date: 1e8 - 1} ]); const data = await generator(locals()) as BaseGeneratorReturn[]; should.not.exist(data[0].data.prev); data[0].data.next!._id!.should.eq(posts[0]._id); data[1].data.prev!._id!.should.eq(posts[1]._id); data[1].data.next!._id!.should.eq(posts[2]._id); data[2].data.prev!._id!.should.eq(posts[0]._id); should.not.exist(data[2].data.next); await BluebirdPromise.all(posts.map(post => post.remove())); }); }); ================================================ FILE: test/scripts/helpers/css.ts ================================================ import * as cheerio from 'cheerio'; import Hexo from '../../../lib/hexo'; import cssHelper from '../../../lib/plugins/helper/css'; type CssHelperParams = Parameters; type CssHelperReturn = ReturnType; describe('css', () => { const hexo = new Hexo(__dirname); const ctx: any = { config: hexo.config }; const css: (...args: CssHelperParams) => CssHelperReturn = cssHelper.bind(ctx); function assertResult(result, expected) { const $ = cheerio.load(result); if (!Array.isArray(expected)) { expected = [expected]; } expected.forEach((item, index) => { if (typeof item === 'string' || item instanceof String) { $('link').eq(index).attr('href')!.should.eql(item); } else { for (const attribute in item) { $('link').eq(index).attr(attribute)!.should.eql(item[attribute]); } } }); } it('a string', () => { assertResult(css('style'), '/style.css'); assertResult(css('style.css'), '/style.css'); assertResult(css('https://hexo.io/style.css'), 'https://hexo.io/style.css'); assertResult(css('//hexo.io/style.css'), '//hexo.io/style.css'); }); it('an array', () => { assertResult(css(['//hexo.io/style.css']), '//hexo.io/style.css'); assertResult(css(['foo', 'bar', 'baz']), ['/foo.css', '/bar.css', '/baz.css']); }); it('multiple strings', () => { assertResult(css('foo', 'bar', 'baz'), ['/foo.css', '/bar.css', '/baz.css']); }); it('multiple arrays', () => { assertResult(css(['foo', 'bar'], ['baz']), ['/foo.css', '/bar.css', '/baz.css']); }); it('mixed', () => { assertResult(css(['foo', 'bar'], 'baz'), ['/foo.css', '/bar.css', '/baz.css']); }); it('an object', () => { assertResult(css({href: 'script.css'}), {href: '/script.css'}); assertResult(css({href: '/script.css'}), {href: '/script.css'}); assertResult(css({href: 'script'}), {href: '/script.css'}); assertResult(css({href: '/script.css', foo: 'bar'}), {href: '/script.css', foo: 'bar'}); }); it('multiple objects', () => { assertResult(css({href: '/foo.css'}, {href: '/bar.css'}), [{href: '/foo.css'}, {href: '/bar.css'}]); assertResult(css({href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}), [{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]); }); it('an array of objects', () => { assertResult(css([{href: '/foo.css'}, {href: '/bar.css'}]), [{href: '/foo.css'}, {href: '/bar.css'}]); assertResult(css([{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]), [{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]); }); it('relative link', () => { ctx.config.relative_link = true; ctx.config.root = '/'; ctx.path = ''; assertResult(css('style'), 'style.css'); ctx.path = 'foo/bar/'; assertResult(css('style'), '../../style.css'); ctx.config.relative_link = false; }); }); ================================================ FILE: test/scripts/helpers/date.ts ================================================ import moment from 'moment-timezone'; import { useFakeTimers } from 'sinon'; import Hexo from '../../../lib/hexo'; import { date as dateHelper, date_xml, relative_date, time as timeHelper, full_date, time_tag, toMomentLocale } from '../../../lib/plugins/helper/date'; type DateHelperParams = Parameters; type DateHelperReturn = ReturnType; type TimeHelperParams = Parameters; type TimeHelperReturn = ReturnType; type FullDateHelperParams = Parameters; type FullDateHelperReturn = ReturnType; type TimeTagHelperParams = Parameters; type TimeTagHelperReturn = ReturnType; type RelativeDateHelperParams = Parameters; type RelativeDateHelperReturn = ReturnType; describe('date', () => { const hexo = new Hexo(); let clock; before(() => { clock = useFakeTimers(Date.now()); }); after(() => { clock.restore(); }); it('date', () => { const ctx: any = { config: hexo.config, page: {} }; const date: (...args: DateHelperParams) => DateHelperReturn = dateHelper.bind(ctx); // now date().should.eql(moment().format(hexo.config.date_format)); // moment date(moment()).should.eql(moment().format(hexo.config.date_format)); date(moment(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // date date(new Date()).should.eql(moment().format(hexo.config.date_format)); date(new Date(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // number date(Date.now()).should.eql(moment().format(hexo.config.date_format)); date(Date.now(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // page.lang ctx.page.lang = 'zh-tw'; date(Date.now(), 'MMM D YYYY').should.eql(moment().locale('zh-tw').format('MMM D YYYY')); ctx.page.lang = ''; // config.language ctx.config.language = 'ja'; date(Date.now(), 'MMM D YYYY').should.eql(moment().locale('ja').format('MMM D YYYY')); ctx.config.language = ''; // timezone ctx.config.timezone = 'UTC'; date(Date.now(), 'LLL').should.eql(moment().tz('UTC').format('LLL')); ctx.config.timezone = ''; }); it('date_xml', () => { const dateXML = date_xml; // now dateXML().should.eql(moment().toISOString()); // moment dateXML(moment()).should.eql(moment().toISOString()); // date dateXML(new Date()).should.eql(moment().toISOString()); // number dateXML(Date.now()).should.eql(moment().toISOString()); }); it('relative_date', () => { const ctx = { config: hexo.config, page: {} }; const relativeDate: (...args: RelativeDateHelperParams) => RelativeDateHelperReturn = relative_date.bind(ctx); // now relativeDate().should.eql(moment().fromNow()); // moment relativeDate(moment()).should.eql(moment().fromNow()); // date relativeDate(new Date()).should.eql(moment().fromNow()); // number relativeDate(Date.now()).should.eql(moment().fromNow()); }); it('time', () => { const ctx: any = { config: hexo.config, page: {} }; const time: (...args: TimeHelperParams) => TimeHelperReturn = timeHelper.bind(ctx); // now time().should.eql(moment().format(hexo.config.time_format)); // moment time(moment()).should.eql(moment().format(hexo.config.time_format)); time(moment(), 'H:mm').should.eql(moment().format('H:mm')); // date time(new Date()).should.eql(moment().format(hexo.config.time_format)); time(new Date(), 'H:mm').should.eql(moment().format('H:mm')); // number time(Date.now()).should.eql(moment().format(hexo.config.time_format)); time(Date.now(), 'H:mm').should.eql(moment().format('H:mm')); // page.lang ctx.page.lang = 'zh-tw'; time(Date.now(), 'A H:mm').should.eql(moment().locale('zh-tw').format('A H:mm')); ctx.page.lang = ''; // config.language ctx.config.language = 'ja'; time(Date.now(), 'A H:mm').should.eql(moment().locale('ja').format('A H:mm')); ctx.config.language = ''; // timezone ctx.config.timezone = 'UTC'; time().should.eql(moment().tz('UTC').format(hexo.config.time_format)); ctx.config.timezone = ''; }); it('full_date', () => { const ctx: any = { config: hexo.config, date: dateHelper, time: timeHelper, page: {} }; const fullDate: (...args: FullDateHelperParams) => FullDateHelperReturn = full_date.bind(ctx); const fullDateFormat = hexo.config.date_format + ' ' + hexo.config.time_format; // now fullDate().should.eql(moment().format(fullDateFormat)); // moment fullDate(moment()).should.eql(moment().format(fullDateFormat)); fullDate(moment(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // date fullDate(new Date()).should.eql(moment().format(fullDateFormat)); fullDate(new Date(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // number fullDate(Date.now()).should.eql(moment().format(fullDateFormat)); fullDate(Date.now(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY')); // page.lang ctx.page.lang = 'zh-tw'; fullDate(Date.now(), 'LLL').should.eql(moment().locale('zh-tw').format('LLL')); ctx.page.lang = ''; // config.language ctx.config.language = 'ja'; fullDate(Date.now(), 'LLL').should.eql(moment().locale('ja').format('LLL')); ctx.config.language = ''; // timezone ctx.config.timezone = 'UTC'; fullDate().should.eql(moment().tz('UTC').format(fullDateFormat)); ctx.config.timezone = ''; }); it('time_tag', () => { const ctx: any = { config: hexo.config, date: dateHelper, page: {} }; const timeTag: (...args: TimeTagHelperParams) => TimeTagHelperReturn = time_tag.bind(ctx); function result(date?, format?) { date = date || new Date(); format = format || hexo.config.date_format; return ''; } function check(date, format?) { format = format || hexo.config.date_format; timeTag(date, format).should.eql(result(date, format)); } // now timeTag().should.eql(result()); // moment check(moment()); check(moment(), 'MMM-D-YYYY'); // date check(new Date()); check(new Date(), 'MMM-D-YYYY'); // number check(Date.now()); check(Date.now(), 'MMM-D-YYYY'); // page.lang ctx.page.lang = 'zh-tw'; timeTag(Date.now(), 'LLL').should.eql(''); ctx.page.lang = ''; // config.language ctx.config.language = 'ja'; timeTag(Date.now(), 'LLL').should.eql(''); ctx.config.language = ''; // timezone ctx.config.timezone = 'UTC'; timeTag(Date.now(), 'LLL').should.eql(''); ctx.config.timezone = ''; }); it('toMomentLocale', () => { (toMomentLocale(undefined) === undefined).should.be.true; // @ts-ignore toMomentLocale(null)!.should.eql('en'); toMomentLocale('')!.should.eql('en'); toMomentLocale('en')!.should.eql('en'); toMomentLocale('default')!.should.eql('en'); toMomentLocale('zh-CN')!.should.eql('zh-cn'); toMomentLocale('zh_CN')!.should.eql('zh-cn'); }); }); ================================================ FILE: test/scripts/helpers/debug.ts ================================================ import { stub } from 'sinon'; import { inspectObject, log } from '../../../lib/plugins/helper/debug'; import { inspect } from 'util'; describe('debug', () => { it('inspect simple object', () => { const obj = { foo: 'bar' }; inspectObject(obj).should.eql(inspect(obj)); }); it('inspect circular object', () => { const obj: any = { foo: 'bar' }; obj.circular = obj; inspectObject(obj).should.eql(inspect(obj)); }); it('inspect deep object', () => { const obj = { baz: { thud: 'narf', dur: { foo: 'bar', baz: { bang: 'zoom' } } } }; inspectObject(obj, {depth: 2}).should.not.eql(inspect(obj, {depth: 5})); inspectObject(obj, {depth: 5}).should.eql(inspect(obj, {depth: 5})); }); it('log should print to console', () => { const logStub = stub(console, 'log'); try { log('Hello %s from debug.log()!', 'World'); } finally { logStub.restore(); } logStub.calledWithExactly('Hello %s from debug.log()!', 'World').should.be.true; }); }); ================================================ FILE: test/scripts/helpers/escape_html.ts ================================================ import { escapeHTML } from '../../../lib/plugins/helper/format'; describe('escape_html', () => { it('default', () => { escapeHTML('

Hello "world".

').should.eql('<p class="foo">Hello "world".</p>'); }); it('str must be a string', () => { escapeHTML.should.throw('str must be a string!'); }); it('avoid double escape', () => { escapeHTML('<foo>bar; type faviconTagReturn = ReturnType; describe('favicon_tag', () => { const hexo = new Hexo(__dirname); const ctx = { config: hexo.config }; const favicon: (...args: faviconTagParams) => faviconTagReturn = faviconTag.bind(ctx); it('path', () => { favicon('favicon.ico').should.eql(''); }); }); ================================================ FILE: test/scripts/helpers/feed_tag.ts ================================================ import feedTag from '../../../lib/plugins/helper/feed_tag'; import chai from 'chai'; const should = chai.should(); type FeedTagParams = Parameters; type FeedTagReturn = ReturnType; describe('feed_tag', () => { const ctx: any = { config: { title: 'Hexo', url: 'http://example.com', root: '/', feed: {} } }; beforeEach(() => { ctx.config.feed = {}; }); const feed: (...args: FeedTagParams) => FeedTagReturn = feedTag.bind(ctx); it('path - atom', () => { feed('atom.xml').should.eql(''); }); it('path - rss', () => { feed('rss2.xml').should.eql(''); }); it('title', () => { feed('atom.xml', {title: 'RSS Feed'}).should.eql(''); }); it('type', () => { feed('rss.xml', {type: 'rss'}).should.eql(''); }); it('type - null', () => { feed('foo.xml', {type: null}).should.eql(''); }); it('invalid input - number', () => { // @ts-expect-error should.throw(() => feed(123), 'path must be a string!'); }); it('invalid input - undefined', () => { delete ctx.config.feed; feed().should.eql(''); }); it('invalid input - empty', () => { ctx.config.feed = {}; feed().should.eql(''); }); it('feed - parse argument if available', () => { ctx.config.feed = { type: 'atom', path: 'atom.xml' }; feed('atomic.xml').should.eql(''); }); it('feed - atom', () => { ctx.config.feed = { type: 'atom', path: 'atom.xml' }; feed().should.eql(''); }); it('feed - rss2', () => { ctx.config.feed = { type: 'rss2', path: 'rss.xml' }; feed().should.eql(''); }); it('feed - rss2', () => { ctx.config.feed = { type: ['atom', 'rss2'], path: ['atom.xml', 'rss.xml'] }; feed().should.eql([ '', '' ].join('')); }); }); ================================================ FILE: test/scripts/helpers/fragment_cache.ts ================================================ import Hexo from '../../../lib/hexo'; import fragmentCache from '../../../lib/plugins/helper/fragment_cache'; describe('fragment_cache', () => { const hexo = new Hexo(__dirname); const fragment_cache = fragmentCache(hexo); fragment_cache.call({cache: true}, 'foo', () => 123); it('cache enabled', () => { fragment_cache.call({cache: true}, 'foo').should.eql(123); }); it('cache disabled', () => { fragment_cache.call({cache: false}, 'foo', () => 456).should.eql(456); }); it('should reset cache on generateBefore', () => { fragment_cache.call({cache: true}, 'foo', () => 789).should.eql(456); // reset cache hexo.emit('generateBefore'); fragment_cache.call({cache: true}, 'foo', () => 789).should.eql(789); }); }); ================================================ FILE: test/scripts/helpers/full_url_for.ts ================================================ import fullUrlForHelper from '../../../lib/plugins/helper/full_url_for'; type FullUrlForHelperParams = Parameters; type FullUrlForHelperReturn = ReturnType; describe('full_url_for', () => { const ctx: any = { config: { url: 'https://example.com' } }; const fullUrlFor: (...args: FullUrlForHelperParams) => FullUrlForHelperReturn = fullUrlForHelper.bind(ctx); it('no path input', () => { fullUrlFor().should.eql(ctx.config.url + '/'); }); it('internal url', () => { fullUrlFor('index.html').should.eql(ctx.config.url + '/index.html'); fullUrlFor('/').should.eql(ctx.config.url + '/'); fullUrlFor('/index.html').should.eql(ctx.config.url + '/index.html'); }); it('internal url (pretty_urls.trailing_index disabled)', () => { ctx.config.pretty_urls = { trailing_index: false }; fullUrlFor('index.html').should.eql(ctx.config.url + '/'); fullUrlFor('/index.html').should.eql(ctx.config.url + '/'); }); it('external url', () => { [ 'https://hexo.io/', '//google.com/', // 'index.html' in external link should not be removed '//google.com/index.html' ].forEach(url => { fullUrlFor(url).should.eql(url); }); }); it('only hash', () => { fullUrlFor('#test').should.eql(ctx.config.url + '/#test'); }); }); ================================================ FILE: test/scripts/helpers/gravatar.ts ================================================ import crypto from 'crypto'; import gravatarHelper from '../../../lib/plugins/helper/gravatar'; describe('gravatar', () => { function md5(str) { return crypto.createHash('md5').update(str).digest('hex'); } const gravatar = gravatarHelper; const email = 'abc@abc.com'; const hash = md5(email); it('default', () => { gravatar(email).should.eql('https://www.gravatar.com/avatar/' + hash); }); it('size', () => { gravatar(email, 100).should.eql('https://www.gravatar.com/avatar/' + hash + '?s=100'); }); it('options', () => { gravatar(email, { s: 200, r: 'pg', d: 'mm' }).should.eql('https://www.gravatar.com/avatar/' + hash + '?s=200&r=pg&d=mm'); }); }); ================================================ FILE: test/scripts/helpers/image_tag.ts ================================================ import Hexo from '../../../lib/hexo'; import imageTag from '../../../lib/plugins/helper/image_tag'; type imageTagParams = Parameters; type imageTagReturn = ReturnType; describe('image_tag', () => { const hexo = new Hexo(__dirname); const ctx: any = { config: hexo.config }; const img: (...args: imageTagParams) => imageTagReturn = imageTag.bind(ctx); it('path', () => { img('https://hexo.io/image.jpg').should.eql(''); }); it('class (string)', () => { img('https://hexo.io/image.jpg', {class: 'foo'}) .should.eql(''); }); it('class (array)', () => { img('https://hexo.io/image.jpg', {class: ['foo', 'bar']}) .should.eql(''); }); it('alt', () => { img('https://hexo.io/image.jpg', {alt: 'Image caption'}) .should.eql('Image caption'); }); }); ================================================ FILE: test/scripts/helpers/is.ts ================================================ import Hexo from '../../../lib/hexo'; import { current, home, home_first_page, post, page, archive, year, month, category, tag } from '../../../lib/plugins/helper/is'; describe('is', () => { const hexo = new Hexo(__dirname); it('is_current', async () => { await current.call({path: 'index.html', config: hexo.config}).should.be.true; await current.call({path: 'tags/index.html', config: hexo.config}).should.be.false; await current.call({path: 'index.html', config: hexo.config}, '/').should.be.true; await current.call({path: 'index.html', config: hexo.config}, 'index.html').should.be.true; await current.call({path: 'tags/index.html', config: hexo.config}, '/').should.be.false; await current.call({path: 'tags/index.html', config: hexo.config}, '/index.html').should.be.false; await current.call({path: 'index.html', config: hexo.config}, '/', true).should.be.true; await current.call({path: 'index.html', config: hexo.config}, '/index.html', true).should.be.true; await current.call({path: 'foo/bar', config: hexo.config}, 'foo', true).should.be.false; await current.call({path: 'foo/bar', config: hexo.config}, 'foo').should.be.true; await current.call({path: 'foo/bar', config: hexo.config}, 'foo/bar').should.be.true; await current.call({path: 'foo/bar', config: hexo.config}, 'foo/baz').should.be.false; }); it('is_home', async () => { await home.call({page: {__index: true}}).should.be.true; await home.call({page: {}}).should.be.false; }); it('is_home_first_page', async () => { await home_first_page.call({page: {__index: true, current: 1}}).should.be.true; await home_first_page.call({page: {__index: true, current: 2}}).should.be.false; await home_first_page.call({page: {__index: true}}).should.be.false; await home_first_page.call({page: {}}).should.be.false; }); it('is_post', async () => { await post.call({page: {__post: true}}).should.be.true; await post.call({page: {}}).should.be.false; }); it('is_page', async () => { await page.call({page: {__page: true}}).should.be.true; await page.call({page: {}}).should.be.false; }); it('is_archive', async () => { await archive.call({page: {}}).should.be.false; await archive.call({page: {archive: true}}).should.be.true; await archive.call({page: {archive: false}}).should.be.false; }); it('is_year', async () => { await year.call({page: {}}).should.be.false; await year.call({page: {archive: true}}).should.be.false; await year.call({page: {archive: true, year: 2014}}).should.be.true; await year.call({page: {archive: true, year: 2014}}, 2014).should.be.true; await year.call({page: {archive: true, year: 2014}}, 2015).should.be.false; await year.call({page: {archive: true, year: 2014, month: 10}}).should.be.true; }); it('is_month', async () => { await month.call({page: {}}).should.be.false; await month.call({page: {archive: true}}).should.be.false; await month.call({page: {archive: true, year: 2014}}).should.be.false; await month.call({page: {archive: true, year: 2014, month: 10}}).should.be.true; await month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 10).should.be.true; await month.call({page: {archive: true, year: 2014, month: 10}}, 2015, 10).should.be.false; await month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 12).should.be.false; await month.call({page: {archive: true, year: 2014, month: 10}}, 10).should.be.true; await month.call({page: {archive: true, year: 2014, month: 10}}, 12).should.be.false; }); it('is_category', async () => { await category.call({page: {category: 'foo'}}).should.be.true; await category.call({page: {category: 'foo'}}, 'foo').should.be.true; await category.call({page: {category: 'foo'}}, 'bar').should.be.false; await category.call({page: {}}).should.be.false; }); it('is_tag', async () => { await tag.call({page: {tag: 'foo'}}).should.be.true; await tag.call({page: {tag: 'foo'}}, 'foo').should.be.true; await tag.call({page: {tag: 'foo'}}, 'bar').should.be.false; await tag.call({page: {}}).should.be.false; }); }); ================================================ FILE: test/scripts/helpers/js.ts ================================================ import * as cheerio from 'cheerio'; import Hexo from '../../../lib/hexo'; import jsHelper from '../../../lib/plugins/helper/js'; type JsHelperParams = Parameters; type JsHelperReturn = ReturnType; describe('js', () => { const hexo = new Hexo(__dirname); const ctx: any = { config: hexo.config }; const js: (...args: JsHelperParams) => JsHelperReturn = jsHelper.bind(ctx); function assertResult(result, expected) { const $ = cheerio.load(result); if (!Array.isArray(expected)) { expected = [expected]; } expected.forEach((item, index) => { if (typeof item === 'string' || item instanceof String) { $('script').eq(index).attr('src')!.should.eql(item); } else { for (const attribute in item) { if (item[attribute] === true) { $('script').eq(index).attr(attribute)!.should.eql(attribute); } else { $('script').eq(index).attr(attribute)!.should.eql(item[attribute]); } } } }); } it('a string', () => { assertResult(js('script'), '/script.js'); assertResult(js('script.js'), '/script.js'); assertResult(js('https://hexo.io/script.js'), 'https://hexo.io/script.js'); assertResult(js('//hexo.io/script.js'), '//hexo.io/script.js'); }); it('an array', () => { assertResult(js(['//hexo.io/script.js']), '//hexo.io/script.js'); assertResult(js(['foo', 'bar', 'baz']), ['/foo.js', '/bar.js', '/baz.js']); }); it('multiple strings', () => { assertResult(js('foo', 'bar', 'baz'), ['/foo.js', '/bar.js', '/baz.js']); }); it('multiple arrays', () => { assertResult(js(['foo', 'bar'], ['baz']), ['/foo.js', '/bar.js', '/baz.js']); }); it('mixed', () => { assertResult(js(['foo', 'bar'], 'baz'), ['/foo.js', '/bar.js', '/baz.js']); }); it('an object', () => { assertResult(js({src: 'script.js'}), {src: '/script.js'}); assertResult(js({src: '/script.js'}), {src: '/script.js'}); assertResult(js({src: 'script'}), {src: '/script.js'}); assertResult(js({src: '/script.js', foo: 'bar'}), {src: '/script.js', foo: 'bar'}); }); it('multiple objects', () => { assertResult(js({src: '/foo.js'}, {src: '/bar.js'}), [{src: '/foo.js'}, {src: '/bar.js'}]); assertResult(js({src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}), [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]); }); it('an array of objects', () => { assertResult(js([{src: '/foo.js'}, {src: '/bar.js'}]), [{src: '/foo.js'}, {src: '/bar.js'}]); assertResult(js([{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]), [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]); }); it('async and defer attributes', () => { assertResult(js({src: '/foo.js', 'async': true}), {src: '/foo.js', 'async': true}); assertResult(js({src: '/bar.js', 'defer': true}), {src: '/bar.js', 'defer': true}); }); it('relative link', () => { ctx.config.relative_link = true; ctx.config.root = '/'; ctx.path = ''; assertResult(js('script'), 'script.js'); ctx.path = 'foo/bar/'; assertResult(js('script'), '../../script.js'); ctx.config.relative_link = false; }); }); ================================================ FILE: test/scripts/helpers/link_to.ts ================================================ import Hexo from '../../../lib/hexo'; import linkToHelper from '../../../lib/plugins/helper/link_to'; type LinkToHelperParams = Parameters; type LinkToHelperReturn = ReturnType; describe('link_to', () => { const hexo = new Hexo(__dirname); const ctx: any = { config: hexo.config }; const linkTo: (...args: LinkToHelperParams) => LinkToHelperReturn = linkToHelper.bind(ctx); it('path', () => { linkTo('https://hexo.io/').should.eql('hexo.io'); }); it('title', () => { linkTo('https://hexo.io/', 'Hexo').should.eql('Hexo'); }); it('external (boolean)', () => { linkTo('https://hexo.io/', 'Hexo', true) .should.eql('Hexo'); }); it('external (object)', () => { linkTo('https://hexo.io/', 'Hexo', {external: true}) .should.eql('Hexo'); }); it('class (string)', () => { linkTo('https://hexo.io/', 'Hexo', {class: 'foo'}) .should.eql('Hexo'); }); it('class (array)', () => { linkTo('https://hexo.io/', 'Hexo', {class: ['foo', 'bar']}) .should.eql('Hexo'); }); it('id', () => { linkTo('https://hexo.io/', 'Hexo', {id: 'foo'}) .should.eql('Hexo'); }); }); ================================================ FILE: test/scripts/helpers/list_archives.ts ================================================ import Hexo from '../../../lib/hexo'; import listArchivesHelper from '../../../lib/plugins/helper/list_archives'; type ListArchivesHelperParams = Parameters; type ListArchivesHelperReturn = ReturnType; describe('list_archives', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const ctx: any = { config: hexo.config, page: {} }; const listArchives: (...args: ListArchivesHelperParams) => ListArchivesHelperReturn = listArchivesHelper.bind(ctx); function resetLocals() { hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); } before(async () => { await hexo.init(); await Post.insert([ {source: 'foo', slug: 'foo', date: new Date(2014, 1, 2)}, {source: 'bar', slug: 'bar', date: new Date(2013, 5, 6)}, {source: 'baz', slug: 'baz', date: new Date(2013, 9, 10)}, {source: 'boo', slug: 'boo', date: new Date(2013, 5, 8)} ]); resetLocals(); }); it('default', () => { const result = listArchives(); result.should.eql([ '' ].join('')); }); it('type: yearly', () => { const result = listArchives({ type: 'yearly' }); result.should.eql([ '' ].join('')); }); it('format', () => { const result = listArchives({ format: 'YYYY/M' }); result.should.eql([ '' ].join('')); }); it('style: false', () => { const result = listArchives({ style: false }); result.should.eql([ 'February 20141', 'October 20131', 'June 20132' ].join(', ')); }); it('show_count', () => { const result = listArchives({ show_count: false }); result.should.eql([ '' ].join('')); }); it('show_count + style: false', () => { const result = listArchives({ style: false, show_count: false }); result.should.eql([ 'February 2014', 'October 2013', 'June 2013' ].join(', ')); }); it('order', () => { const result = listArchives({ order: 1 }); result.should.eql([ '' ].join('')); }); it('transform', () => { const result = listArchives({ transform(str) { return str.toUpperCase(); } }); result.should.eql([ '' ].join('')); }); it('transform + style: false', () => { const result = listArchives({ style: false, transform(str) { return str.toUpperCase(); } }); result.should.eql([ 'FEBRUARY 20141', 'OCTOBER 20131', 'JUNE 20132' ].join(', ')); }); it('separator', () => { const result = listArchives({ style: false, separator: '' }); result.should.eql([ 'February 20141', 'October 20131', 'June 20132' ].join('')); }); it('class', () => { const result = listArchives({ class: 'test' }); result.should.eql([ '' ].join('')); }); it('page.lang', () => { ctx.page.lang = 'zh-tw'; const result = listArchives(); ctx.page.lang = ''; result.should.eql([ '' ].join('')); }); it('config.language', () => { ctx.config.language = 'de'; const result = listArchives(); ctx.config.language = ''; result.should.eql([ '' ].join('')); }); it('timezone', () => { ctx.config.timezone = 'Asia/Tokyo'; const result = listArchives({ format: 'YYYY MM ZZ' }); result.should.eql([ '' ].join('')); ctx.config.timezone = ''; }); }); ================================================ FILE: test/scripts/helpers/list_categories.ts ================================================ import Hexo from '../../../lib/hexo'; import listCategoriesHelper from '../../../lib/plugins/helper/list_categories'; type ListCategoriesHelperParams = Parameters; type ListCategoriesHelperReturn = ReturnType; describe('list_categories', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const Category = hexo.model('Category'); const ctx: any = { config: hexo.config }; const listCategories: (...args: ListCategoriesHelperParams) => ListCategoriesHelperReturn = listCategoriesHelper.bind(ctx); before(async () => { await hexo.init(); const posts = await Post.insert([ {source: 'foo', slug: 'foo'}, {source: 'bar', slug: 'bar'}, {source: 'baz', slug: 'baz'}, {source: 'boo', slug: 'boo'}, {source: 'bat', slug: 'bat'} ]); await Promise.all([ ['baz'], ['baz', 'bar'], ['foo'], ['baz'], ['bat', ['baz', 'bar']] ].map((cats, i) => posts[i].setCategories(cats))); hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); ctx.page = ctx.site.posts.data[1]; }); it('default', () => { const result = listCategories(); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); it('specified collection', () => { const result = listCategories(Category.find({ parent: {$exists: false} })); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); it('style: false', () => { const result = listCategories({ style: false }); result.should.eql([ 'bat1', 'baz4', 'bar2', 'foo1' ].join(', ')); }); it('show_count: false', () => { const result = listCategories({ show_count: false }); result.should.eql([ '
    ', '
  • ', 'bat', '
  • ', '
  • ', 'baz', '
      ', '
    • ', 'bar', '
    • ', '
    ', '
  • ', '
  • ', 'foo', '
  • ', '
' ].join('')); }); it('class', () => { const result = listCategories({ class: 'test' }); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); it('depth', () => { const result = listCategories({ depth: 1 }); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); it('orderby', () => { const result = listCategories({ orderby: 'length' }); result.should.eql([ '
    ', '
  • ', 'foo1', '
  • ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
' ].join('')); }); it('order', () => { const result = listCategories({ order: -1 }); result.should.eql([ '
    ', '
  • ', 'foo1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
  • ', 'bat1', '
  • ', '
' ].join('')); }); it('transform', () => { const result = listCategories({ transform(name) { return name.toUpperCase(); } }); result.should.eql([ '
    ', '
  • ', 'BAT1', '
  • ', '
  • ', 'BAZ4', '
      ', '
    • ', 'BAR2', '
    • ', '
    ', '
  • ', '
  • ', 'FOO1', '
  • ', '
' ].join('')); }); it('separator (blank)', () => { const result = listCategories({ style: false, separator: '' }); result.should.eql([ 'bat1', 'baz4', 'bar2', 'foo1' ].join('')); }); it('separator (non-blank)', () => { const result = listCategories({ style: false, separator: '|' }); result.should.eql([ 'bat1|', 'baz4|', 'bar2|', 'foo1' ].join('')); }); it('children-indicator', () => { const result = listCategories({ children_indicator: 'has-children' }); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); it('show-current', () => { const result = listCategories({ show_current: true }); result.should.eql([ '
    ', '
  • ', 'bat1', '
  • ', '
  • ', 'baz4', '
      ', '
    • ', 'bar2', '
    • ', '
    ', '
  • ', '
  • ', 'foo1', '
  • ', '
' ].join('')); }); }); ================================================ FILE: test/scripts/helpers/list_posts.ts ================================================ import Hexo from '../../../lib/hexo'; import listPostsHelper from '../../../lib/plugins/helper/list_posts'; type ListPostsHelperParams = Parameters; type ListPostsHelperReturn = ReturnType; describe('list_posts', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const ctx: any = { config: hexo.config }; const listPosts: (...args: ListPostsHelperParams) => ListPostsHelperReturn = listPostsHelper.bind(ctx); hexo.config.permalink = ':title/'; before(async () => { await hexo.init(); await Post.insert([ {source: 'foo', slug: 'foo', title: 'Its', date: 1e8}, {source: 'bar', slug: 'bar', title: 'Chemistry', date: 1e8 + 1}, {source: 'baz', slug: 'baz', title: 'Bitch', date: 1e8 - 1} ]); hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); }); it('default', () => { const result = listPosts(); result.should.eql([ '' ].join('')); }); it('specified collection', () => { const result = listPosts(Post.find({ title: 'Its' })); result.should.eql([ '' ].join('')); }); it('style: false', () => { const result = listPosts({ style: false }); result.should.eql([ 'Chemistry', 'Its', 'Bitch' ].join(', ')); }); it('orderby', () => { const result = listPosts({ orderby: 'title' }); result.should.eql([ '' ].join('')); }); it('order', () => { const result = listPosts({ order: 1 }); result.should.eql([ '' ].join('')); }); it('class', () => { const result = listPosts({ class: 'test' }); result.should.eql([ '' ].join('')); }); it('transform', () => { const result = listPosts({ transform(str) { return str.toUpperCase(); } }); result.should.eql([ '' ].join('')); }); it('separator', () => { const result = listPosts({ style: false, separator: '' }); result.should.eql([ 'Chemistry', 'Its', 'Bitch' ].join('')); }); it('amount', () => { const result = listPosts({ amount: 2 }); result.should.eql([ '' ].join('')); }); }); ================================================ FILE: test/scripts/helpers/list_tags.ts ================================================ import Hexo from '../../../lib/hexo'; import listTagsHelper from '../../../lib/plugins/helper/list_tags'; type ListTagsHelperParams = Parameters; type ListTagsHelperReturn = ReturnType; describe('list_tags', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const Tag = hexo.model('Tag'); const ctx: any = { config: hexo.config }; const listTags: (...args: ListTagsHelperParams) => ListTagsHelperReturn = listTagsHelper.bind(ctx); before(async () => { await hexo.init(); const posts = await Post.insert([ {source: 'foo', slug: 'foo'}, {source: 'bar', slug: 'bar'}, {source: 'baz', slug: 'baz'}, {source: 'boo', slug: 'boo'} ]); // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem await Promise.all([ ['foo'], ['baz'], ['baz'], ['bar'] ].map((tags, i) => posts[i].setTags(tags))); hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); }); it('default', () => { const result = listTags(); result.should.eql([ '' ].join('')); }); it('specified collection', () => { const result = listTags(Tag.find({ name: /^b/ })); result.should.eql([ '' ].join('')); }); it('style: false', () => { const result = listTags({ style: false }); result.should.eql([ '', '', '' ].join(', ')); }); it('show_count: false', () => { const result = listTags({ show_count: false }); result.should.eql([ '' ].join('')); }); it('class', () => { const result = listTags({ class: 'test' }); result.should.eql([ '' ].join('')); }); it('custom class', () => { const result = listTags({ class: { ul: 'lorem', li: 'ipsum', a: 'tempor', count: 'dolor' } }); result.should.eql([ '' ].join('')); }); it('custom class not list', () => { const result = listTags({ style: false, show_count: true, separator: '', class: { a: 'tempor', label: 'lorem', count: 'dolor' } }); result.should.eql([ '', '', '' ].join('')); }); it('orderby', () => { const result = listTags({ orderby: 'length' }); result.should.eql([ '' ].join('')); }); it('order', () => { const result = listTags({ order: -1 }); result.should.eql([ '' ].join('')); }); it('transform', () => { const result = listTags({ transform(name) { return name.toUpperCase(); } }); result.should.eql([ '' ].join('')); }); it('separator', () => { const result = listTags({ style: false, separator: '' }); result.should.eql([ '', '', '' ].join('')); }); it('amount', () => { const result = listTags({ amount: 2 }); result.should.eql([ '' ].join('')); }); }); describe('list_tags transform', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const ctx: any = { config: hexo.config }; const listTags: (...args: ListTagsHelperParams) => ListTagsHelperReturn = listTagsHelper.bind(ctx); before(async () => { await hexo.init(); const posts = await Post.insert([ {source: 'foo', slug: 'foo'} ]); // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem await Promise.all([ ['badHTML'] ].map((tags, i) => posts[i].setTags(tags))); hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); }); // no transform should escape HTML it('no transform', () => { const result = listTags(); result.should.eql([ '' ].join('')); }); }); ================================================ FILE: test/scripts/helpers/mail_to.ts ================================================ import Hexo from '../../../lib/hexo'; import mailToHelper from '../../../lib/plugins/helper/mail_to'; type MailToHelperParams = Parameters; type MailToHelperReturn = ReturnType; describe('mail_to', () => { const hexo = new Hexo(__dirname); const ctx: any = { config: hexo.config }; const mailto: (...args: MailToHelperParams) => MailToHelperReturn = mailToHelper.bind(ctx); it('path', () => { mailto('abc@example.com').should.eql('abc@example.com'); }); it('path - array', () => { const emails = ['abc@example.com', 'foo@example.com']; const emailsStr = 'abc@example.com,foo@example.com'; mailto(emails).should.eql(`${emailsStr}`); }); it('text', () => { mailto('abc@example.com', 'Email').should.eql('Email'); }); it('subject', () => { mailto('abc@example.com', 'Email', {subject: 'Hello'}) .should.eql('Email'); }); it('cc (string)', () => { const data = {cc: 'abc@abc.com'}; const querystring = new URLSearchParams(data).toString(); mailto('abc@example.com', 'Email', {cc: 'abc@abc.com'}) .should.eql('Email'); }); it('cc (array)', () => { const data = {cc: 'abc@abc.com,bcd@bcd.com'}; const querystring = new URLSearchParams(data).toString(); mailto('abc@example.com', 'Email', {cc: ['abc@abc.com', 'bcd@bcd.com']}) .should.eql('Email'); }); it('bcc (string)', () => { const data = {bcc: 'abc@abc.com'}; const querystring = new URLSearchParams(data).toString(); mailto('abc@example.com', 'Email', {bcc: 'abc@abc.com'}) .should.eql('Email'); }); it('bcc (array)', () => { const data = {bcc: 'abc@abc.com,bcd@bcd.com'}; const querystring = new URLSearchParams(data).toString(); mailto('abc@example.com', 'Email', {bcc: ['abc@abc.com', 'bcd@bcd.com']}) .should.eql('Email'); }); it('body', () => { mailto('abc@example.com', 'Email', {body: 'Hello'}) .should.eql('Email'); }); it('class (string)', () => { mailto('abc@example.com', 'Email', {class: 'foo'}) .should.eql('Email'); }); it('class (array)', () => { mailto('abc@example.com', 'Email', {class: ['foo', 'bar']}) .should.eql('Email'); }); it('id', () => { mailto('abc@example.com', 'Email', {id: 'foo'}) .should.eql('Email'); }); }); ================================================ FILE: test/scripts/helpers/markdown.ts ================================================ import Hexo from '../../../lib/hexo'; import renderHelper from '../../../lib/plugins/helper/render'; import markdownHelper from '../../../lib/plugins/helper/markdown'; type MarkdownHelperParams = Parameters; type MarkdownHelperReturn = ReturnType; describe('markdown', () => { const hexo = new Hexo(__dirname); const ctx = { render: renderHelper(hexo) }; const markdown: (...args: MarkdownHelperParams) => MarkdownHelperReturn = markdownHelper.bind(ctx); before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked')))); it('default', () => { markdown('123456 **bold** and *italic*').should.eql('

123456 bold and italic

\n'); }); }); ================================================ FILE: test/scripts/helpers/meta_generator.ts ================================================ import Hexo from '../../../lib/hexo'; import metaGeneratorHelper from '../../../lib/plugins/helper/meta_generator'; import chai from 'chai'; const should = chai.should(); type MetaGeneratorHelperParams = Parameters; type MetaGeneratorHelperReturn = ReturnType; describe('meta_generator', () => { const hexo = new Hexo(); const metaGenerator: (...args: MetaGeneratorHelperParams) => MetaGeneratorHelperReturn = metaGeneratorHelper.bind(hexo); it('default', () => { const { version } = hexo; should.exist(version); metaGenerator().should.eql(``); }); }); ================================================ FILE: test/scripts/helpers/number_format.ts ================================================ import numberFormat from '../../../lib/plugins/helper/number_format'; describe('number_format', () => { it('default', () => { numberFormat(1234.567).should.eql('1,234.567'); }); it('int', () => { numberFormat(1234567).should.eql('1,234,567'); }); it('precision', () => { numberFormat(1234.567, {precision: false}).should.eql('1,234.567'); numberFormat(1234.567, {precision: 0}).should.eql('1,234'); numberFormat(1234.567, {precision: 1}).should.eql('1,234.6'); numberFormat(1234.567, {precision: 2}).should.eql('1,234.57'); numberFormat(1234.567, {precision: 3}).should.eql('1,234.567'); numberFormat(1234.567, {precision: 4}).should.eql('1,234.5670'); }); it('delimiter', () => { numberFormat(1234.567, {delimiter: ' '}).should.eql('1 234.567'); }); it('separator', () => { numberFormat(1234.567, {separator: '*'}).should.eql('1,234*567'); }); }); ================================================ FILE: test/scripts/helpers/open_graph.ts ================================================ import moment from 'moment'; import * as cheerio from 'cheerio'; import { encodeURL, htmlTag as tag } from 'hexo-util'; import defaultConfig from '../../../lib/hexo/default_config'; import Hexo from '../../../lib/hexo'; import openGraph from '../../../lib/plugins/helper/open_graph'; import { post as isPost } from '../../../lib/plugins/helper/is'; describe('open_graph', () => { const hexo = new Hexo(); const Post = hexo.model('Post'); function meta(options) { return tag('meta', options); } before(() => { return hexo.init(); }); beforeEach(() => { // Reset config hexo.config = { ...defaultConfig }; hexo.config.permalink = ':title'; }); it('default', async () => { let post = await Post.insert({ source: 'foo.md', slug: 'bar' }); await post.setTags(['optimize', 'web']); post = await Post.findById(post._id); const result = openGraph.call({ page: post, config: hexo.config, is_post: isPost }); result.should.eql([ meta({property: 'og:type', content: 'website'}), meta({property: 'og:title', content: hexo.config.title}), meta({property: 'og:url'}), meta({property: 'og:site_name', content: hexo.config.title}), meta({property: 'og:locale', content: 'en_US'}), meta({property: 'article:published_time', content: post.date.toISOString()}), // page.updated will no longer exist by default // See https://github.com/hexojs/hexo/pull/4278 // meta({property: 'article:modified_time', content: post.updated.toISOString()}), meta({property: 'article:author', content: hexo.config.author}), meta({property: 'article:tag', content: 'optimize'}), meta({property: 'article:tag', content: 'web'}), meta({name: 'twitter:card', content: 'summary'}) ].join('\n')); await Post.removeById(post._id); }); it('title - page', () => { const ctx = { page: {title: 'Hello world'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); result.should.have.string(meta({property: 'og:title', content: ctx.page.title})); }); it('title - options', () => { const result = openGraph.call({ page: {title: 'Hello world'}, config: hexo.config, is_post: isPost }, {title: 'test'}); result.should.have.string(meta({property: 'og:title', content: 'test'})); }); it('type - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {type: 'photo'}); result.should.have.string(meta({property: 'og:type', content: 'photo'})); }); it('type - is_post', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post() { return true; } }); result.should.have.string(meta({property: 'og:type', content: 'article'})); }); it('url - context', () => { const ctx = { page: {}, config: hexo.config, is_post: isPost, url: 'https://hexo.io/foo' }; const result = openGraph.call(ctx); result.should.have.string(meta({property: 'og:url', content: ctx.url})); }); it('url - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost, url: 'https://hexo.io/foo' }, {url: 'https://hexo.io/bar'}); result.should.have.string(meta({property: 'og:url', content: 'https://hexo.io/bar'})); }); it('url - pretty_urls.trailing_index', () => { hexo.config.pretty_urls.trailing_index = false; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost, url: 'http://example.com/page/index.html' }); const $ = cheerio.load(result); $('meta[property="og:url"]').attr('content')!.endsWith('index.html').should.be.false; hexo.config.pretty_urls.trailing_index = true; }); it('url - pretty_urls.trailing_html', () => { hexo.config.pretty_urls.trailing_html = false; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost, url: 'http://example.com/page/about.html' }); const $ = cheerio.load(result); $('meta[property="og:url"]').attr('content')!.endsWith('.html').should.be.false; hexo.config.pretty_urls.trailing_html = true; }); it('url - null pretty_urls', () => { hexo.config.pretty_urls = null as any; const url = 'http://example.com/page/about.html'; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost, url }); const $ = cheerio.load(result); $('meta[property="og:url"]').attr('content')!.should.eql(url); hexo.config.pretty_urls = { trailing_index: true, trailing_html: true }; }); it('url - IDN', () => { const ctx = { page: {}, config: hexo.config, is_post: isPost, url: 'https://foô.com/bár' }; const result = openGraph.call(ctx); result.should.have.string(meta({property: 'og:url', content: encodeURL(ctx.url)})); }); it('images - content', () => { const result = openGraph.call({ page: { content: [ '

123456789

', '', '', '' ].join('') }, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'})); }); it('images - content with data-uri', () => { const result = openGraph.call({ page: { content: '' }, config: hexo.config, is_post: isPost }); result.should.not.have.string(meta({property: 'og:image', content: 'data:image/svg+xml;utf8,...'})); }); it('images - string', () => { const result = openGraph.call({ page: { photos: 'https://hexo.io/test.jpg' }, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'})); }); it('images - array', () => { const result = openGraph.call({ page: { photos: [ 'https://hexo.io/foo.jpg', 'https://hexo.io/bar.jpg' ] }, config: hexo.config, is_post: isPost }); result.should.have.string([ meta({property: 'og:image', content: 'https://hexo.io/foo.jpg'}), meta({property: 'og:image', content: 'https://hexo.io/bar.jpg'}) ].join('\n')); }); it('images - don\'t pollute context', () => { const ctx = { page: { content: [ '

123456789

', '', '', '' ].join(''), photos: [] }, config: hexo.config, is_post: isPost }; openGraph.call(ctx); ctx.page.photos.should.eql([]); }); it('images - options.image', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {image: 'https://hexo.io/test.jpg'}); result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'})); }); it('images - options.images', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {images: 'https://hexo.io/test.jpg'}); result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'})); }); it('images - prepend config.url to the path (without prefixing /)', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {images: 'test.jpg'}); result.should.have.string(meta({property: 'og:image', content: hexo.config.url + '/test.jpg'})); }); it('images - prepend config.url to the path (with prefixing /)', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {images: '/test.jpg'}); result.should.have.string(meta({property: 'og:image', content: hexo.config.url + '/test.jpg'})); }); it('images - resolve relative path when site is hosted in subdirectory', () => { const config = hexo.config; config.url = new URL('blog', config.url).toString(); config.root = 'blog'; const postUrl = new URL('/foo/bar/index.html', config.url).toString(); const result = openGraph.call({ page: {}, config, is_post: isPost, url: postUrl }, {images: 'test.jpg'}); result.should.have.string(meta({property: 'og:image', content: new URL('/foo/bar/test.jpg', config.url).toString()})); }); it('twitter_image - default same as og:image', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {images: 'image.jpg'}); result.should.have.string(meta({name: 'twitter:image', content: hexo.config.url + '/image.jpg'})); }); it('twitter_image - different URLs for og:image and twitter:image', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_image: 'twitter.jpg', images: 'image.jpg'}); result.should.have.string(meta({name: 'twitter:image', content: hexo.config.url + '/twitter.jpg'})); }); it('images - twitter_image absolute url', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_image: 'https://hexo.io/twitter.jpg', images: 'image.jpg'}); result.should.have.string(meta({name: 'twitter:image', content: 'https://hexo.io/twitter.jpg'})); }); it('site_name - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {site_name: 'foo'}); result.should.have.string(meta({property: 'og:site_name', content: 'foo'})); }); it('description - page', () => { const ctx = { page: {description: 'test'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); result.should.have.string(meta({name: 'description', content: ctx.page.description})); result.should.have.string(meta({property: 'og:description', content: ctx.page.description})); }); it('description - options', () => { const ctx = { page: {description: 'test'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx, {description: 'foo'}); result.should.have.string(meta({name: 'description', content: 'foo'})); result.should.have.string(meta({property: 'og:description', content: 'foo'})); }); it('description - excerpt', () => { const ctx = { page: {excerpt: 'test'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); result.should.have.string(meta({name: 'description', content: ctx.page.excerpt})); result.should.have.string(meta({property: 'og:description', content: ctx.page.excerpt})); }); it('description - content', () => { const ctx = { page: {content: 'test'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); result.should.have.string(meta({name: 'description', content: ctx.page.content})); result.should.have.string(meta({property: 'og:description', content: ctx.page.content})); }); it('description - config', () => { const ctx = { page: {}, config: hexo.config, is_post: isPost }; hexo.config.description = 'test'; const result = openGraph.call(ctx); result.should.have.string(meta({name: 'description', content: hexo.config.description})); result.should.have.string(meta({property: 'og:description', content: hexo.config.description})); hexo.config.description = ''; }); it('description - escape', () => { const ctx = { page: {description: 'Important! Today is "not" \'Xmas\'!'}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const escaped = 'Important! Today is "not" 'Xmas'!'; result.should.have.string(meta({name: 'description', content: escaped})); result.should.have.string(meta({property: 'og:description', content: escaped})); }); it('twitter_card - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_card: 'photo'}); result.should.have.string(meta({name: 'twitter:card', content: 'photo'})); }); it('twitter_id - options (without prefixing @)', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_id: 'hexojs'}); result.should.have.string(meta({name: 'twitter:creator', content: '@hexojs'})); }); it('twitter_id - options (with prefixing @)', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_id: '@hexojs'}); result.should.have.string(meta({name: 'twitter:creator', content: '@hexojs'})); }); it('twitter_site - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {twitter_site: 'Hello'}); result.should.have.string(meta({name: 'twitter:site', content: 'Hello'})); }); it('fb_admins - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {fb_admins: '123456789'}); result.should.have.string(meta({property: 'fb:admins', content: '123456789'})); }); it('fb_app_id - options', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {fb_app_id: '123456789'}); result.should.have.string(meta({property: 'fb:app_id', content: '123456789'})); }); it('updated - options', () => { const result = openGraph.call({ page: { updated: moment('2016-05-23T21:20:21.372Z') }, config: hexo.config, is_post: isPost }, { }); result.should.have.string(meta({property: 'article:modified_time', content: '2016-05-23T21:20:21.372Z'})); }); it('updated - options - allow overriding article:modified_time', () => { const result = openGraph.call({ page: { updated: moment('2016-05-23T21:20:21.372Z') }, config: hexo.config, is_post: isPost }, { updated: moment('2015-04-22T20:19:20.371Z') }); result.should.have.string(meta({property: 'article:modified_time', content: '2015-04-22T20:19:20.371Z'})); }); it('updated - options - allow disabling article:modified_time', () => { const result = openGraph.call({ page: { updated: moment('2016-05-23T21:20:21.372Z') }, config: hexo.config, is_post: isPost }, { updated: false }); result.should.not.have.string(meta({property: 'article:modified_time', content: '2016-05-23T21:20:21.372Z'})); }); it('description - do not add /(?:og:)?description/ meta tags if there is no description', () => { const result = openGraph.call({ page: { }, config: hexo.config, is_post: isPost }, { }); result.should.not.have.string(meta({property: 'og:description'})); result.should.not.have.string(meta({property: 'description'})); }); it('keywords - page keywords array', () => { const ctx = { page: { tags: ['optimize', 'web'] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['optimize', 'web']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); }); it('keywords - page keywords string', () => { const ctx = { page: { tags: 'optimize' }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['optimize']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); }); it('keywords - page tags', () => { const ctx = { page: { tags: ['optimize', 'web'] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['optimize', 'web']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); }); // https://github.com/hexojs/hexo/issues/5458 it('keywords - page tags sorted', () => { const ctx = { page: { tags: ['web', 'optimize'] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['web', 'optimize'].sort(); result.should.have.string(meta({ property: 'article:tag', content: keywords[0] }) + '\n' + meta({ property: 'article:tag', content: keywords[1] })); }); it('keywords - config keywords array', () => { hexo.config.keywords = ['optimize', 'web']; const ctx = { page: {}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['optimize', 'web']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); }); it('keywords - page tags first', () => { hexo.config.keywords = ['web3', 'web4']; const ctx = { page: { tags: ['web1', 'web2'] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['web1', 'web2']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); }); it('keywords - use config.keywords if no tags', () => { hexo.config.keywords = ['web5', 'web6']; const ctx = { page: { tags: [] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['web5', 'web6']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); }); it('keywords - null', () => { const ctx = { page: {}, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); result.should.not.have.string(' { const ctx = { page: { tags: ['optimize', 'web&<>"\'/', 'site'] }, config: hexo.config, is_post: isPost }; const result = openGraph.call(ctx); const keywords = ['optimize', 'web&<>"\'/', 'site']; result.should.have.string(meta({property: 'article:tag', content: keywords[0]})); result.should.have.string(meta({property: 'article:tag', content: keywords[1]})); result.should.have.string(meta({property: 'article:tag', content: keywords[2]})); }); it('og:locale - options.language', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {language: 'es-cr'}); result.should.have.string(meta({property: 'og:locale', content: 'es_CR'})); }); it('og:locale - options.language (incorrect format)', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {language: 'foo-bar'}); result.should.have.string(meta({property: 'og:locale', content: undefined})); }); it('og:locale - page.lang', () => { const result = openGraph.call({ page: { lang: 'es-mx' }, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:locale', content: 'es_MX'})); }); it('og:locale - page.language', () => { const result = openGraph.call({ page: { language: 'es-gt' }, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:locale', content: 'es_GT'})); }); it('og:locale - config.language', () => { hexo.config.language = 'es-pa'; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:locale', content: 'es_PA'})); }); it('og:locale - convert territory to uppercase', () => { hexo.config.language = 'fr-fr'; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'og:locale', content: 'fr_FR'})); }); it('og:locale - no language set', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }); result.should.not.have.string(meta({property: 'og:locale'})); }); it('og:locale - language is not in lang-TERRITORY format', () => { hexo.config.language = 'en'; openGraph.call({ page: {}, config: hexo.config, is_post: isPost }).should.have.string(meta({property: 'og:locale', content: 'en_US'})); hexo.config.language = 'Fr_fr'; openGraph.call({ page: {}, config: hexo.config, is_post: isPost }).should.have.string(meta({property: 'og:locale', content: 'fr_FR'})); hexo.config.language = 'zh-CN'; openGraph.call({ page: {}, config: hexo.config, is_post: isPost }).should.have.string(meta({property: 'og:locale', content: 'zh_CN'})); }); it('article:author - options.author', () => { const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }, {author: 'Jane Doe'}); result.should.have.string(meta({property: 'article:author', content: 'Jane Doe'})); }); it('article:author - config.language', () => { hexo.config.language = 'es-pa'; const result = openGraph.call({ page: {}, config: hexo.config, is_post: isPost }); result.should.have.string(meta({property: 'article:author', content: 'John Doe'})); }); it('article:author - no author set', () => { const result = openGraph.call({ page: {}, config: { author: undefined }, is_post: isPost }); result.should.not.have.string(meta({property: 'article:author'})); }); }); ================================================ FILE: test/scripts/helpers/paginator.ts ================================================ import { url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import paginatorHelper from '../../../lib/plugins/helper/paginator'; type PaginatorHelperParams = Parameters; type PaginatorHelperReturn = ReturnType; describe('paginator', () => { const hexo = new Hexo(__dirname); const ctx: any = { page: { base: '', total: 10 }, site: hexo.locals, config: hexo.config }; const paginator: (...args: PaginatorHelperParams) => PaginatorHelperReturn = paginatorHelper.bind(ctx); function link(i) { return url_for.call(ctx, i === 1 ? '' : 'page/' + i + '/'); } function checkResult(result, data) { let expected = ''; const current = data.current; const total = data.total; const pages = data.pages; const space = data.space || '…'; const prevNext = Object.prototype.hasOwnProperty.call(data, 'prev_next') ? data.prev_next : true; let num; if (prevNext && current > 1) { expected += ''; } for (let i = 0, len = pages.length; i < len; i++) { num = pages[i]; if (!num) { expected += '' + space + ''; } else if (num === current) { expected += '' + current + ''; } else { expected += '' + num + ''; } } if (prevNext && current < total) { expected += ''; } result.should.eql(expected); } [ [1, 2, 3, 0, 10], [1, 2, 3, 4, 0, 10], [1, 2, 3, 4, 5, 0, 10], [1, 2, 3, 4, 5, 6, 0, 10], [1, 0, 3, 4, 5, 6, 7, 0, 10], [1, 0, 4, 5, 6, 7, 8, 0, 10], [1, 0, 5, 6, 7, 8, 9, 10], [1, 0, 6, 7, 8, 9, 10], [1, 0, 7, 8, 9, 10], [1, 0, 8, 9, 10] ].forEach((pages, i, arr) => { const current = i + 1; const total = arr.length; it('current = ' + current, () => { const result = paginator({ current, total }); checkResult(result, { current, total, pages }); }); }); it('show_all', () => { const result = paginator({ current: 5, show_all: true }); checkResult(result, { current: 5, total: 10, pages: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }); }); it('end_size', () => { const result = paginator({ current: 5, end_size: 2 }); checkResult(result, { current: 5, total: 10, pages: [1, 2, 3, 4, 5, 6, 7, 0, 9, 10] }); }); it('end_size = 0', () => { const result = paginator({ current: 5, end_size: 0 }); checkResult(result, { current: 5, total: 10, pages: [0, 3, 4, 5, 6, 7, 0] }); }); it('mid_size', () => { const result = paginator({ current: 5, mid_size: 1 }); checkResult(result, { current: 5, total: 10, pages: [1, 0, 4, 5, 6, 0, 10] }); }); it('mid_size = 0', () => { const result = paginator({ current: 5, mid_size: 0 }); checkResult(result, { current: 5, total: 10, pages: [1, 0, 5, 0, 10] }); }); it('space', () => { const result = paginator({ current: 5, space: '~' }); checkResult(result, { current: 5, total: 10, pages: [1, 0, 3, 4, 5, 6, 7, 0, 10], space: '~' }); }); it('no space', () => { const result = paginator({ current: 5, space: '' }); checkResult(result, { current: 5, total: 10, pages: [1, 3, 4, 5, 6, 7, 10] }); }); it('base', () => { const result = paginator({ current: 1, base: 'archives/' }); result.should.eql([ '1', '2', '3', '', '10', '' ].join('')); }); it('format', () => { const result = paginator({ current: 1, format: 'index-%d.html' }); result.should.eql([ '1', '2', '3', '', '10', '' ].join('')); }); it('prev_text / next_text', () => { const result = paginator({ current: 2, prev_text: 'Newer', next_text: 'Older' }); result.should.eql([ '', '1', '2', '3', '4', '', '10', '' ].join('')); }); it('prev_next', () => { const result = paginator({ current: 2, prev_next: false }); result.should.eql([ '1', '2', '3', '4', '', '10' ].join('')); }); it('transform', () => { const result = paginator({ current: 2, transform(page) { return 'Page ' + page; } }); result.should.eql([ '', 'Page 1', 'Page 2', 'Page 3', 'Page 4', '', 'Page 10', '' ].join('')); }); it('context', () => { ctx.page.current = 5; const result = paginator({ space: '' }); checkResult(result, { current: 5, total: 10, pages: [1, 3, 4, 5, 6, 7, 10] }); }); it('current = 0', () => { ctx.page.current = 0; const result = paginator({}); result.should.eql(''); }); it('escape', () => { const result = paginator({ current: 2, prev_text: '', next_text: '', escape: false }); result.should.eql([ '', '1', '2', '3', '4', '', '10', '' ].join('')); }); it('custom_class', () => { const result = paginator({ current: 2, current_class: 'current-class', space_class: 'space-class', page_class: 'page-class', prev_class: 'prev-class', next_class: 'next-class' }); result.should.eql([ '', '1', '2', '3', '4', '', '10', '' ].join('')); }); it('force_prev_next', () => { const result = paginator({ current: 1, force_prev_next: true }); result.should.eql([ '', '1', '2', '3', '', '10', '' ].join('')); }); it('force_prev_next - 2', () => { const result = paginator({ current: 1, prev_next: false, force_prev_next: true }); result.should.eql([ '', '1', '2', '3', '', '10', '' ].join('')); }); }); ================================================ FILE: test/scripts/helpers/partial.ts ================================================ import pathFn from 'path'; import { mkdirs, writeFile, rmdir } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import fragmentCache from '../../../lib/plugins/helper/fragment_cache'; import partialHelper from '../../../lib/plugins/helper/partial'; import chai from 'chai'; const should = chai.should(); type PartialHelperParams = Parameters>; type PartialHelperReturn = ReturnType>; describe('partial', () => { const hexo = new Hexo(pathFn.join(__dirname, 'partial_test'), {silent: true}); const themeDir = pathFn.join(hexo.base_dir, 'themes', 'test'); const viewDir = pathFn.join(themeDir, 'layout') + pathFn.sep; const viewName = 'article.njk'; const ctx: any = { site: hexo.locals, config: hexo.config, view_dir: viewDir, filename: pathFn.join(viewDir, 'post', viewName), foo: 'foo', cache: true }; ctx.fragment_cache = fragmentCache(hexo); hexo.env.init = true; const partial: (...args: PartialHelperParams) => PartialHelperReturn = partialHelper(hexo).bind(ctx); before(async () => { await BluebirdPromise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); await hexo.init(); hexo.theme.setView('widget/tag.njk', 'tag widget'); }); after(() => rmdir(hexo.base_dir)); it('default', () => { // relative path partial('../widget/tag').should.eql('tag widget'); // absolute path partial('widget/tag').should.eql('tag widget'); // not found should.throw( () => partial('foo'), `Partial foo does not exist. (in ${pathFn.join('post', viewName)})` ); }); it('locals', () => { hexo.theme.setView('test.njk', '{{ foo }}'); partial('test', {foo: 'bar'}).should.eql('bar'); }); it('cache', () => { hexo.theme.setView('test.njk', '{{ foo }}'); partial('test', {foo: 'bar'}, {cache: true}).should.eql('bar'); partial('test', {}, {cache: true}).should.eql('bar'); partial('test', {foo: 'baz'}, {cache: 'ash'}).should.eql('baz'); partial('test', {}, {cache: 'ash'}).should.eql('baz'); }); it('only', () => { hexo.theme.setView('test.njk', '{{ foo }}{{ bar }}'); partial('test', {bar: 'bar'}, {only: true}).should.eql('bar'); }); it('a partial in another partial', () => { hexo.theme.setView('partial/a.njk', '{{ partial("b") }}'); hexo.theme.setView('partial/b.njk', '{{ partial("c") }}'); hexo.theme.setView('partial/c.njk', 'c'); partial('partial/a').should.eql('c'); }); it('name must be a string', () => { // @ts-expect-error should.throw(() => partial(), 'name must be a string!'); }); }); ================================================ FILE: test/scripts/helpers/relative_url.ts ================================================ import relativeURL from '../../../lib/plugins/helper/relative_url'; describe('relative_url', () => { it('from root', () => { relativeURL('', 'css/style.css').should.eql('css/style.css'); relativeURL('index.html', 'css/style.css').should.eql('css/style.css'); }); it('from same root', () => { relativeURL('foo/', 'foo/style.css').should.eql('style.css'); relativeURL('foo/index.html', 'foo/style.css').should.eql('style.css'); relativeURL('foo/bar/', 'foo/bar/style.css').should.eql('style.css'); relativeURL('foo/bar/index.html', 'foo/bar/style.css').should.eql('style.css'); }); it('from different root', () => { relativeURL('foo/', 'css/style.css').should.eql('../css/style.css'); relativeURL('foo/index.html', 'css/style.css').should.eql('../css/style.css'); relativeURL('foo/bar/', 'css/style.css').should.eql('../../css/style.css'); relativeURL('foo/bar/index.html', 'css/style.css').should.eql('../../css/style.css'); }); it('to root', () => { relativeURL('index.html', '/').should.eql('index.html'); relativeURL('foo/', '/').should.eql('../index.html'); relativeURL('foo/index.html', '/').should.eql('../index.html'); }); it('should encode path', () => { relativeURL('foo/', 'css/fôo.css').should.eql('../css/f%C3%B4o.css'); }); }); ================================================ FILE: test/scripts/helpers/render.ts ================================================ import Hexo from '../../../lib/hexo'; import renderHelper from '../../../lib/plugins/helper/render'; describe('render', () => { const hexo = new Hexo(__dirname); const render = renderHelper(hexo); before(() => hexo.init()); it('default', () => { const body = [ 'foo: 1', 'bar:', '\tbaz: 3' ].join('\n'); const result = render(body, 'yaml'); result.should.eql({ foo: 1, bar: { baz: 3 } }); }); }); ================================================ FILE: test/scripts/helpers/search_form.ts ================================================ import searchFormHelper from '../../../lib/plugins/helper/search_form'; type SearchFormHelperParams = Parameters; type SearchFormHelperReturn = ReturnType; describe('search_form', () => { const searchForm: (...args: SearchFormHelperParams) => SearchFormHelperReturn = searchFormHelper.bind({ config: {url: 'https://hexo.io'} }); it('default', () => { searchForm().should.eql('
' + '' + '' + '
'); }); it('class', () => { searchForm({class: 'foo'}).should.eql('
' + '' + '' + '
'); }); it('text', () => { searchForm({text: 'Find'}).should.eql('
' + '' + '' + '
'); }); it('text - null', () => { searchForm({text: null}).should.eql('
' + '' + '' + '
'); }); it('button - true', () => { searchForm({button: true, text: 'Find'}).should.eql('
' + '' + '' + '' + '
'); }); it('button - string', () => { searchForm({button: 'Go', text: 'Find'}).should.eql('
' + '' + '' + '' + '
'); }); it('button - ignore incorrect type', () => { // @ts-expect-error searchForm({button: {}, text: 'Find'}).should.eql('
' + '' + '' + '' + '
'); }); }); ================================================ FILE: test/scripts/helpers/tagcloud.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import tagcloudHelper from '../../../lib/plugins/helper/tagcloud'; import chai from 'chai'; const should = chai.should(); type TagcloudHelperParams = Parameters; type TagcloudHelperReturn = ReturnType; describe('tagcloud', () => { const hexo = new Hexo(__dirname); const Post = hexo.model('Post'); const Tag = hexo.model('Tag'); const ctx: any = { config: hexo.config }; const tagcloud: (...args: TagcloudHelperParams) => TagcloudHelperReturn = tagcloudHelper.bind(ctx); before(async () => { await hexo.init(); const posts = await Post.insert([ {source: 'foo', slug: 'foo'}, {source: 'bar', slug: 'bar'}, {source: 'baz', slug: 'baz'}, {source: 'boo', slug: 'boo'} ]); // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem await BluebirdPromise.all([ ['bcd'], ['bcd', 'cde'], ['bcd', 'cde', 'abc'], ['bcd', 'cde', 'abc', 'def'] ].map((tags, i) => posts[i].setTags(tags))); hexo.locals.invalidate(); ctx.site = hexo.locals.toObject(); }); it('default', () => { const result = tagcloud(); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('no tags', async () => { const hexo = new Hexo(__dirname); await hexo.init(); hexo.locals.invalidate(); // @ts-expect-error hexo.site = hexo.locals.toObject(); const tagcloud: (...args: TagcloudHelperParams) => TagcloudHelperReturn = tagcloudHelper.bind(hexo); const result = tagcloud(); result.should.eql(''); }); it('specified collection', () => { const result = tagcloud(Tag.find({ name: /bc/ })); result.should.eql([ 'abc', 'bcd' ].join(' ')); }); it('font size', () => { const result = tagcloud({ min_font: 15, max_font: 30 }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('font size - when every tag has the same number of posts, font-size should be minimum.', () => { const result = tagcloud(Tag.find({ name: /abc/ }), { min_font: 15, max_font: 30 }); result.should.eql([ 'abc' ].join(' ')); }); it('font unit', () => { const result = tagcloud({ unit: 'em' }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('orderby - length', () => { const result = tagcloud({ orderby: 'length' }); result.should.eql([ 'def', 'abc', 'cde', 'bcd' ].join(' ')); }); it('orderby - random', () => { const result1 = tagcloud({ orderby: 'random' }); const result2 = tagcloud({ orderby: 'rand' }); result1.should.have.string('def'); result1.should.have.string('abc'); result1.should.have.string('cde'); result1.should.have.string('bcd'); result2.should.have.string('def'); result2.should.have.string('abc'); result2.should.have.string('cde'); result2.should.have.string('bcd'); }); it('order', () => { const result = tagcloud({ order: -1 }); result.should.eql([ 'def', 'cde', 'bcd', 'abc' ].join(' ')); }); it('amount', () => { const result = tagcloud({ amount: 2 }); result.should.eql([ 'abc', 'bcd' ].join(' ')); }); it('transform', () => { const result = tagcloud({ transform(name) { return name.toUpperCase(); } }); result.should.eql([ 'ABC', 'BCD', 'CDE', 'DEF' ].join(' ')); }); it('color: name', () => { const result = tagcloud({ color: true, start_color: 'red', end_color: 'pink' }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('color: hex', () => { const result = tagcloud({ color: true, start_color: '#f00', // red end_color: '#ffc0cb' // pink }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('color: RGBA', () => { const result = tagcloud({ color: true, start_color: 'rgba(70, 130, 180, 0.3)', // steelblue end_color: 'rgb(70, 130, 180)' }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('color: HSLA', () => { const result = tagcloud({ color: true, start_color: 'hsla(207, 44%, 49%, 0.3)', // rgba(70, 130, 180, 0.3) end_color: 'hsl(207, 44%, 49%)' // rgb(70, 130, 180) }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('color - when every tag has the same number of posts, start_color should be used.', () => { const result = tagcloud(Tag.find({ name: /abc/ }), { color: true, start_color: 'red', end_color: 'pink' }); result.should.eql([ 'abc' ].join(' ')); }); it('color - missing start_color', () => { try { tagcloud({ color: true, end_color: 'pink' }); should.fail(); } catch (err) { err.message.should.eql('start_color is required!'); } }); it('separator', () => { const result = tagcloud({ separator: ', ' }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(', ')); }); it('class name', () => { const result = tagcloud({ class: 'tag-cloud' }); result.should.eql([ 'abc', 'bcd', 'cde', 'def' ].join(' ')); }); it('show_count', () => { const result = tagcloud({ show_count: true }); result.should.eql([ 'abc2', 'bcd4', 'cde3', 'def1' ].join(' ')); }); it('show_count with custom class', () => { const result = tagcloud({ show_count: true, count_class: 'tag-count' }); result.should.eql([ 'abc2', 'bcd4', 'cde3', 'def1' ].join(' ')); }); }); ================================================ FILE: test/scripts/helpers/toc.ts ================================================ import { escapeHTML } from 'hexo-util'; import toc from '../../../lib/plugins/helper/toc'; describe('toc', () => { const html = [ '

Title 1

', '

Title 1.1

', '

Title 1.1.1

', '

Title 1.2

', '

Title 1.3

', '

Title 1.3.1

', '

Title 2

', '

Title 2.1

', '

Title should escape &, <, ', and "

', '

Chapter 1 should be printed to toc

' ].join(''); it('default', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', '
      ', '
    1. ', '', '1.1. ', // list_number enabled 'Title 1.1', '', '
        ', '
      1. ', '', '1.1.1. ', // list_number enabled 'Title 1.1.1', '', '
      2. ', '
      ', '
    2. ', '
    3. ', '', '1.2. ', // list_number enabled 'Title 1.2', '', '
    4. ', '
    5. ', '', '1.3. ', // list_number enabled 'Title 1.3', '', '
        ', '
      1. ', '', '1.3.1. ', // list_number enabled 'Title 1.3.1', '', '
      2. ', '
      ', '
    6. ', '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', '
      ', '
    1. ', '', '2.1. ', // list_number enabled 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', '3. ', // list_number enabled 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', // list_number enabled 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html).should.eql(expected); }); it('class', () => { const className = 'foo'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', '
      ', '
    1. ', '', '1.1. ', // list_number enabled 'Title 1.1', '', '
        ', '
      1. ', '', '1.1.1. ', // list_number enabled 'Title 1.1.1', '', '
      2. ', '
      ', '
    2. ', '
    3. ', '', '1.2. ', // list_number enabled 'Title 1.2', '', '
    4. ', '
    5. ', '', '1.3. ', // list_number enabled 'Title 1.3', '', '
        ', '
      1. ', '', '1.3.1. ', // list_number enabled 'Title 1.3.1', '', '
      2. ', '
      ', '
    6. ', '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', '
      ', '
    1. ', '', '2.1. ', // list_number enabled 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', '3. ', // list_number enabled 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', // list_number enabled 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { class: 'foo' }).should.eql(expected); }); it('list_number', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', // '1. ', 'Title 1', '', '
      ', '
    1. ', '', // '1.1. ', 'Title 1.1', '', '
        ', '
      1. ', '', // '1.1.1. ', 'Title 1.1.1', '', '
      2. ', '
      ', '
    2. ', '
    3. ', '', // '1.2. ', 'Title 1.2', '', '
    4. ', '
    5. ', '', // '1.3. ', 'Title 1.3', '', '
        ', '
      1. ', '', // '1.3.1. ', 'Title 1.3.1', '', '
      2. ', '
      ', '
    6. ', '
    ', '
  2. ', '
  3. ', '', // '2. ', 'Title 2', '', '
      ', '
    1. ', '', // '2.1. ', 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', // '3. ', 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', // '4. ', 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { list_number: false }).should.eql(expected); }); it('max_depth', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', 'Title 1', '', '
      ', '
    1. ', '', '1.1. ', 'Title 1.1', '', '
    2. ', '
    3. ', '', '1.2. ', 'Title 1.2', '', '
    4. ', '
    5. ', '', '1.3. ', 'Title 1.3', '', '
    6. ', '
    ', '
  2. ', '
  3. ', '', '2. ', 'Title 2', '', '
      ', '
    1. ', '', '2.1. ', 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', '3. ', 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { max_depth: 2 }).should.eql(expected); }); it('min_depth', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', 'Title 1.1', '', '
      ', '
    1. ', '', '1.1. ', 'Title 1.1.1', '', '
    2. ', '
    ', '
  2. ', '
  3. ', '', '2. ', 'Title 1.2', '', '
  4. ', '
  5. ', '', '3. ', 'Title 1.3', '', '
      ', '
    1. ', '', '3.1. ', 'Title 1.3.1', '', '
    2. ', '
    ', '
  6. ', '
  7. ', '', '4. ', 'Title 2.1', '', '
  8. ', '
' ].join(''); toc(html, { min_depth: 2 }).should.eql(expected); }); it('No id attribute', () => { const className = 'f'; const input = [ '

foo

', '

bar

' ].join(''); const expected = [ `
    `, `
  1. `, `foo`, '
  2. ', `
  3. `, `bar`, '
' ].join(''); toc(input, { list_number: false, class: className }).should.eql(expected); }); it('non-ASCII id', () => { const className = 'f'; const zh = '这是-H1-标题'; const zhs = zh.replace(/-/g, ' '); const de = 'Ich-♥-Deutsch'; const des = de.replace(/-/g, ' '); const ru = 'Я-люблю-русский'; const rus = ru.replace(/-/g, ' '); const special = '%20'; const input = [ `

${zhs}

`, `

${des}

`, `

${rus}

`, `

${special}

` ].join(''); const expected = [ `
    `, `
  1. `, `${zhs}`, '
  2. ', `
  3. `, `${des}`, '
  4. ', `
  5. `, `${rus}`, '
  6. ', `
  7. `, `${special}`, '
' ].join(''); toc(input, { list_number: false, class: className }).should.eql(expected); }); it('escape unsafe class name', () => { const className = 'f"b'; const esClass = escapeHTML(className); const input = '

bar

'; const expected = [ `
    `, `
  1. `, `bar`, '
' ].join(''); toc(input, { list_number: false, class: className }).should.eql(expected); }); it('invalid input', () => { const input = 'bar'; toc(input).should.eql(''); }); it('skipping heading level', () => { const input = [ '

Title 1

', '

Title 3

', '

Title 4

', '

Title 2

', '
Title 5
', '

Title 1

' ].join(''); toc(input).should.eql('
  1. 1. Title 1
    1. 1.1. Title 3
      1. 1.1.1. Title 4
    2. 1.2. Title 2
      1. 1.2.1. Title 5
  2. 2. Title 1
'); }); it('unnumbered headings', () => { const className = 'toc'; const input = [ '

Title 1

', '

Title 2

', '

Title 2.1

', '

Reference

' ].join(''); const expected = [ `
    `, `
  1. `, `1. `, `Title 1`, '', '
  2. ', `
  3. `, ``, `2. `, `Title 2`, '', `
      `, `
    1. `, ``, `2.1. `, `Title 2.1`, '', '
    2. ', '
    ', '
  4. ', `
  5. `, ``, `Reference`, '', '
  6. ', '
' ].join(''); toc(input, { list_number: true, class: className }).should.eql(expected); }); it('custom class', () => { const className = 'foo'; const childClassName = 'bar'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', '
      ', '
    1. ', '', '1.1. ', // list_number enabled 'Title 1.1', '', '
        ', '
      1. ', '', '1.1.1. ', // list_number enabled 'Title 1.1.1', '', '
      2. ', '
      ', '
    2. ', '
    3. ', '', '1.2. ', // list_number enabled 'Title 1.2', '', '
    4. ', '
    5. ', '', '1.3. ', // list_number enabled 'Title 1.3', '', '
        ', '
      1. ', '', '1.3.1. ', // list_number enabled 'Title 1.3.1', '', '
      2. ', '
      ', '
    6. ', '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', '
      ', '
    1. ', '', '2.1. ', // list_number enabled 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', '3. ', // list_number enabled 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', // list_number enabled 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { class: 'foo', class_child: 'bar' }).should.eql(expected); }); it('max_items - result contains only h1 items', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', // '
      ', // // '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', // '
      ', // // '
    ', '
  4. ', '
  5. ', '', '3. ', // list_number enabled 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', // list_number enabled 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { max_items: 4}).should.eql(expected); // The number of `h1` is 4 toc(html, { max_items: 7}).should.eql(expected); // Maximum number 7 cannot display up to `h2` }); it('max_items - result contains h1 and h2 items', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', '
      ', '
    1. ', '', '1.1. ', // list_number enabled 'Title 1.1', '', // '
        ', // // '
      ', '
    2. ', '
    3. ', '', '1.2. ', // list_number enabled 'Title 1.2', '', '
    4. ', '
    5. ', '', '1.3. ', // list_number enabled 'Title 1.3', '', // '
        ', // // '
      ', '
    6. ', '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', '
      ', '
    1. ', '', '2.1. ', // list_number enabled 'Title 2.1', '', '
    2. ', '
    ', '
  4. ', '
  5. ', '', '3. ', // list_number enabled 'Title should escape &, <, ', and "', '', '
  6. ', '
  7. ', '', '4. ', // list_number enabled 'Chapter 1 should be printed to toc', '', '
  8. ', '
' ].join(''); toc(html, { max_items: 8}).should.eql(expected); // Maximum number 8 can display up to `h2` toc(html, { max_items: 9}).should.eql(expected); // Maximum number 10 is required to display up to `h3` }); it('max_items - result of h1 was truncated', () => { const className = 'toc'; const expected = [ '
    ', '
  1. ', '', '1. ', // list_number enabled 'Title 1', '', // '
      ', // // '
    ', '
  2. ', '
  3. ', '', '2. ', // list_number enabled 'Title 2', '', '
  4. ', // '
' ].join(''); toc(html, { max_items: 2}).should.eql(expected); // `h1` is truncated from the end }); }); ================================================ FILE: test/scripts/helpers/url_for.ts ================================================ import urlForHelper from '../../../lib/plugins/helper/url_for'; import relativeUrlHelper from '../../../lib/plugins/helper/relative_url'; type UrlForHelperParams = Parameters; type UrlForHelperReturn = ReturnType; describe('url_for', () => { const ctx: any = { config: { url: 'https://example.com' }, relative_url: relativeUrlHelper }; const urlFor: (...args: UrlForHelperParams) => UrlForHelperReturn = urlForHelper.bind(ctx); it('should encode path', () => { ctx.config.root = '/'; urlFor('fôo.html').should.eql('/f%C3%B4o.html'); ctx.config.root = '/fôo/'; urlFor('bár.html').should.eql('/f%C3%B4o/b%C3%A1r.html'); }); it('internal url (relative off)', () => { ctx.config.root = '/'; urlFor('index.html').should.eql('/index.html'); urlFor('/').should.eql('/'); urlFor('/index.html').should.eql('/index.html'); ctx.config.root = '/blog/'; urlFor('index.html').should.eql('/blog/index.html'); urlFor('/').should.eql('/blog/'); urlFor('/index.html').should.eql('/blog/index.html'); }); it('internal url (relative on)', () => { ctx.config.relative_link = true; ctx.config.root = '/'; ctx.path = ''; urlFor('index.html').should.eql('index.html'); ctx.path = 'foo/bar/'; urlFor('index.html').should.eql('../../index.html'); ctx.config.relative_link = false; }); it('internal url (options.relative)', () => { ctx.path = ''; urlFor('index.html', {relative: true}).should.eql('index.html'); ctx.config.relative_link = true; urlFor('index.html', {relative: false}).should.eql('/index.html'); ctx.config.relative_link = false; }); it('internal url (pretty_urls.trailing_index disabled)', () => { ctx.config.pretty_urls = { trailing_index: false }; ctx.path = ''; ctx.config.root = '/'; urlFor('index.html').should.eql('/'); urlFor('/index.html').should.eql('/'); ctx.config.root = '/blog/'; urlFor('index.html').should.eql('/blog/'); urlFor('/index.html').should.eql('/blog/'); }); it('external url', () => { [ 'https://hexo.io/', '//google.com/', // 'index.html' in external link should not be removed '//google.com/index.html' ].forEach(url => { urlFor(url).should.eql(url); }); }); it('only hash', () => { urlFor('#test').should.eql('#test'); }); }); ================================================ FILE: test/scripts/hexo/hexo.ts ================================================ import { sep, join } from 'path'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import { spy } from 'sinon'; import { readStream } from '../../util'; import { full_url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Hexo', () => { const base_dir = join(__dirname, 'hexo_test'); const hexo = new Hexo(base_dir, { silent: true }); const coreDir = join(__dirname, '../../..'); const { version } = require('../../../package.json'); const Post = hexo.model('Post'); const Page = hexo.model('Page'); const Data = hexo.model('Data'); const { route } = hexo; async function checkStream(stream, expected) { const data = await readStream(stream); data.should.eql(expected); } function loadAssetGenerator() { hexo.extend.generator.register('asset', require('../../../lib/plugins/generator/asset')); } before(async () => { await mkdirs(hexo.base_dir); await hexo.init(); }); beforeEach(() => { // Unregister all generators hexo.extend.generator.store = {}; // Remove all routes route.routes = {}; }); after(() => rmdir(hexo.base_dir)); hexo.extend.console.register('test', args => args); it('constructor', () => { const hexo = new Hexo(__dirname); /* eslint-disable no-path-concat */ hexo.core_dir.should.eql(coreDir + sep); hexo.lib_dir.should.eql(join(coreDir, 'lib') + sep); hexo.version.should.eql(version); hexo.base_dir.should.eql(__dirname + sep); hexo.public_dir.should.eql(join(__dirname, 'public') + sep); hexo.source_dir.should.eql(join(__dirname, 'source') + sep); hexo.plugin_dir.should.eql(join(__dirname, 'node_modules') + sep); hexo.script_dir.should.eql(join(__dirname, 'scripts') + sep); hexo.scaffold_dir.should.eql(join(__dirname, 'scaffolds') + sep); /* eslint-enable no-path-concat */ hexo.env.should.eql({ args: {}, debug: false, safe: false, silent: false, env: process.env.NODE_ENV || 'development', version, cmd: '', init: false }); hexo.config_path.should.eql(join(__dirname, '_config.yml')); }); it('constructs multi-config', () => { const configs = ['../../../fixtures/_config.json', '../../../fixtures/_config.json']; const args = { _: [], config: configs.join(',') }; const hexo = new Hexo(base_dir, args); hexo.config_path.should.eql(join(base_dir, '_multiconfig.yml')); }); it('call()', async () => { const data = await hexo.call('test', {foo: 'bar'}); data.should.eql({foo: 'bar'}); }); it('call() - callback', callback => { hexo.call('test', { foo: 'bar' }, (err, data) => { should.not.exist(err); data.should.eql({ foo: 'bar' }); callback(); }); }); it('call() - callback without args', callback => { hexo.call('test', (err, data) => { should.not.exist(err); data.should.eql({}); callback(); }); }); it('call() - console not registered', async () => { try { await hexo.call('nothing'); should.fail('Return value must be rejected'); } catch (err) { err.should.property('message', 'Console `nothing` has not been registered yet!'); } }); it('init()', async () => { const hexo = new Hexo(join(__dirname, 'hexo_test'), {silent: true}); const hook = spy(); hexo.extend.filter.register('after_init', hook); await hexo.init(); hook.calledOnce.should.be.true; }); // it('model()'); missing-unit-test it('_showDrafts()', () => { hexo._showDrafts().should.be.false; hexo.env.args.draft = true; hexo._showDrafts().should.be.true; hexo.env.args.draft = false; hexo.env.args.drafts = true; hexo._showDrafts().should.be.true; hexo.env.args.drafts = false; hexo.config.render_drafts = true; hexo._showDrafts().should.be.true; hexo.config.render_drafts = false; }); async function testLoad(path) { const target = join(path, 'test.txt'); const body = 'test'; loadAssetGenerator(); await writeFile(target, body); await hexo.load(); await checkStream(route.get('test.txt'), body); await unlink(target); } it('load() - source', async () => await testLoad(hexo.source_dir)); it('load() - theme', async () => await testLoad(join(hexo.theme_dir, 'source'))); it('load() - load database', async () => { hexo._dbLoaded = false; const dbPath = hexo.database.options.path; const fixture = { meta: { version: 1, warehouse: require('warehouse').version }, models: { PostTag: [ { _id: 'cuid111111111111111111113', post_id: 'cuid111111111111111111111', tag_id: 'cuid111111111111111111112' } ], Tag: [ { _id: 'cuid111111111111111111112', name: 'foo' } ], Post: [ { _id: 'cuid111111111111111111111', source: 'test', slug: 'test' } ] } }; await writeFile(dbPath, JSON.stringify(fixture)); await hexo.load(); // check Model hexo.model('PostTag').toArray({lean: true}).length.should.eql(fixture.models.PostTag.length); hexo.model('Tag').toArray({lean: true}).length.should.eql(fixture.models.Tag.length); hexo.model('Post').toArray({lean: true}).length.should.eql(fixture.models.Post.length); hexo._binaryRelationIndex.post_tag.keyIndex.size.should.eql(1); hexo._binaryRelationIndex.post_tag.valueIndex.size.should.eql(1); await unlink(dbPath); // clean up await hexo.model('PostTag').removeById('cuid111111111111111111113'); await hexo.model('Tag').removeById('cuid111111111111111111112'); await hexo.model('Post').removeById('cuid111111111111111111111'); hexo._binaryRelationIndex.post_tag.keyIndex.clear(); hexo._binaryRelationIndex.post_tag.valueIndex.clear(); }); // Issue #3964 it('load() - merge theme config - deep clone', async () => { const hexo = new Hexo(__dirname, { silent: true }); hexo.theme.config = { a: { b: 1, c: 2 } }; hexo.config.theme_config = { a: { b: 3 } }; await hexo.load(); const { config: themeConfig } = hexo.theme; themeConfig.a.should.have.own.property('c'); themeConfig.a.b.should.eql(3); const Locals = hexo._generateLocals(); const { theme: themeLocals } = new Locals('', {path: '', layout: [], data: {}}); themeLocals.a.should.have.own.property('c'); themeLocals.a.b.should.eql(3); }); it('load() - merge theme config - null theme.config', async () => { const hexo = new Hexo(__dirname, { silent: true }); hexo.theme.config = null; hexo.config.theme_config = { c: 3 }; await hexo.load(); const { config: themeConfig } = hexo.theme; themeConfig.should.have.own.property('c'); themeConfig.c.should.eql(3); const Locals = hexo._generateLocals(); const { theme: themeLocals } = new Locals('', {path: '', layout: [], data: {}}); themeLocals.should.have.own.property('c'); themeLocals.c.should.eql(3); }); // Filters should be able to read the theme_config: // - before_post_render // - after_post_render // - before_generate it('load() - merge theme config - filter', async () => { const hexo = new Hexo(__dirname, { silent: true }); const validateThemeConfig = function() { this.theme.config.a.b.should.eql(3); }; hexo.theme.config = { a: { b: 1, c: 2 } }; hexo.config.theme_config = { a: { b: 3 } }; hexo.extend.filter.register('before_post_render', validateThemeConfig); hexo.extend.filter.register('after_post_render', validateThemeConfig); hexo.extend.filter.register('before_generate', validateThemeConfig); await hexo.load(); hexo.extend.filter.unregister('before_post_render', validateThemeConfig); hexo.extend.filter.unregister('after_post_render', validateThemeConfig); hexo.extend.filter.unregister('before_generate', validateThemeConfig); }); async function testWatch(path) { const target = join(path, 'test.txt'); const body = 'test'; const newBody = body + body; loadAssetGenerator(); await writeFile(target, body); await hexo.watch(); await checkStream(route.get('test.txt'), body); // Test for first generation await writeFile(target, newBody); // Update the file await BluebirdPromise.delay(300); await checkStream(route.get('test.txt'), newBody); // Check the new route hexo.unwatch(); // Stop watching await unlink(target); // Delete the file } it('watch() - source', async () => await testWatch(hexo.source_dir)); it('watch() - theme', async () => await testWatch(join(hexo.theme_dir, 'source'))); it('watch() - merge theme config', () => { const theme_config_1 = [ 'a:', ' b: 1', ' c: 2' ].join('\n'); const theme_config_2 = [ 'a:', ' b: 1', ' c: 3' ].join('\n'); const hexo = new Hexo(__dirname, { silent: true }); hexo.config.theme_config = { a: { b: 3, d: 4 } }; const theme_config_path = join(hexo.theme_dir, '_config.yml'); return writeFile(theme_config_path, theme_config_1) .then(() => hexo.init()) .then(() => hexo.watch()) .then(() => { hexo.theme.config.a.should.have.own.property('d'); hexo.theme.config.a.d.should.eql(4); }) .then(() => writeFile(theme_config_path, theme_config_2)) .delay(300) .then(() => { hexo.theme.config.a.should.have.own.property('d'); hexo.theme.config.a.d.should.eql(4); }) .then(() => hexo.unwatch()) .delay(300) .then(() => unlink(theme_config_path)) .delay(300); }); // it('unwatch()'); missing-unit-test it('exit()', async () => { const hook = spy(); const listener = spy(); hexo.extend.filter.register('before_exit', hook); hexo.once('exit', listener); await hexo.exit(); hook.calledOnce.should.be.true; listener.calledOnce.should.be.true; }); it('exit() - error handling - callback', callback => { hexo.once('exit', err => { err.should.eql({ foo: 'bar' }); callback(); }); hexo.exit({ foo: 'bar' }); }); it('exit() - error handling - promise', () => { return BluebirdPromise.all([ hexo.exit({ foo: 'bar' }), new BluebirdPromise((resolve, reject) => { hexo.once('exit', err => { try { err.should.eql({ foo: 'bar' }); resolve(); } catch (e) { reject(e); } }); }) ]); }); it('draft visibility', async () => { const posts = await Post.insert([ {source: 'foo', slug: 'foo', published: true}, {source: 'bar', slug: 'bar', published: false} ]); hexo.locals.invalidate(); hexo.locals.get('posts').toArray().should.eql(posts.slice(0, 1)); // draft visible hexo.config.render_drafts = true; hexo.locals.invalidate(); hexo.locals.get('posts').toArray().should.eql(posts); hexo.config.render_drafts = false; posts.map(post => Post.removeById(post._id)); }); it('future posts', async () => { const posts = await Post.insert([ {source: 'foo', slug: 'foo', date: Date.now() - 3600}, {source: 'bar', slug: 'bar', date: Date.now() + 3600} ]); function mapper(post) { return post._id; } // future on hexo.config.future = true; hexo.locals.invalidate(); hexo.locals.get('posts').map(mapper).should.eql(posts.map(mapper)); // future off hexo.config.future = false; hexo.locals.invalidate(); hexo.locals.get('posts').map(mapper).should.eql([posts[0]._id]); posts.map(post => Post.removeById(post._id)); }); it('future pages', async () => { const pages = await Page.insert([ {source: 'foo', path: 'foo', date: Date.now() - 3600}, {source: 'bar', path: 'bar', date: Date.now() + 3600} ]); function mapper(page) { return page._id; } // future on hexo.config.future = true; hexo.locals.invalidate(); hexo.locals.get('pages').map(mapper).should.eql(pages.map(mapper)); // future off hexo.config.future = false; hexo.locals.invalidate(); hexo.locals.get('pages').map(mapper).should.eql([pages[0]._id]); pages.map(page => Page.removeById(page._id)); }); it('locals.data', async () => { const data = await Data.insert([ {_id: 'users', data: {foo: 1}}, {_id: 'comments', data: {bar: 2}} ]); hexo.locals.invalidate(); hexo.locals.get('data').should.eql({ users: { foo: 1 }, comments: { bar: 2 } }); data.map(data => data.remove()); }); it('_generate()', async () => { // object hexo.extend.generator.register('test_obj', (locals: any) => { locals.test.should.eql('foo'); return { path: 'foo', data: 'foo' }; }); // array hexo.extend.generator.register('test_arr', (locals: any) => { locals.test.should.eql('foo'); return [ { path: 'bar', data: 'bar' }, { path: 'baz', data: 'baz' } ]; }); const beforeListener = spy(); const afterListener = spy(); const afterHook = spy(); const beforeHook = spy(() => { hexo.locals.set('test', 'foo'); }); hexo.once('generateBefore', beforeListener); hexo.once('generateAfter', afterListener); hexo.extend.filter.register('before_generate', beforeHook); hexo.extend.filter.register('after_generate', afterHook); await hexo._generate(); route.list().should.eql(['foo', 'bar', 'baz']); beforeListener.calledOnce.should.be.true; afterListener.calledOnce.should.be.true; beforeHook.calledOnce.should.be.true; afterHook.calledOnce.should.be.true; await BluebirdPromise.all([ checkStream(route.get('foo'), 'foo'), checkStream(route.get('bar'), 'bar'), checkStream(route.get('baz'), 'baz') ]); }); it('_generate() - layout', async () => { hexo.theme.setView('test.njk', [ '{{ config.title }}', '{{ page.foo }}', '{{ layout }}', '{{ view_dir }}' ].join('\n')); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test', data: { foo: 'bar' } })); const expected = [ hexo.config.title, 'bar', 'layout', join(hexo.theme_dir, 'layout') + sep ].join('\n'); await hexo._generate(); await checkStream(route.get('test'), expected); }); it('_generate() - layout array', async () => { hexo.theme.setView('baz.njk', 'baz'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: ['foo', 'bar', 'baz'] })); await hexo._generate(); await checkStream(route.get('test'), 'baz'); }); it('_generate() - layout not exist', async () => { hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'nothing' })); await hexo._generate(); await checkStream(route.get('test'), ''); }); it('_generate() - remove old routes', async () => { hexo.extend.generator.register('test', () => ({ path: 'bar', data: 'newbar' })); route.set('foo', 'foo'); route.set('bar', 'bar'); route.set('baz', 'baz'); await hexo._generate(); should.not.exist(route.get('foo')); should.not.exist(route.get('baz')); await checkStream(route.get('bar'), 'newbar'); }); it('_generate() - _after_html_render filter', async () => { const hook = spy(result => result.replace('foo', 'bar')); hexo.extend.filter.register('after_render:html', hook); hexo.theme.setView('test.njk', 'foo'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test' })); await hexo._generate(); await checkStream(route.get('test'), 'bar'); hook.called.should.eql(true); }); it('_generate() - after_render:html is alias of _after_html_render', async () => { const hook = spy(result => result.replace('foo', 'bar')); hexo.extend.filter.register('after_render:html', hook); hexo.theme.setView('test.njk', 'foo'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test' })); await hexo._generate(); await checkStream(route.get('test'), 'bar'); hook.called.should.eql(true); }); it('_generate() - return nothing in generator', async () => { // @ts-expect-error hexo.extend.generator.register('test_nothing', () => { // }); hexo.extend.generator.register('test_normal', () => ({ path: 'bar', data: 'bar' })); await hexo._generate(); await checkStream(route.get('bar'), 'bar'); }); it('_generate() - validate locals', async () => { hexo.theme.setView('test.njk', [ '{{ path }}', '{{ url }}', '{{ view_dir }}' ].join('\n')); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test' })); await hexo._generate(); await checkStream(route.get('test'), [ 'test', hexo.config.url + '/test', join(hexo.theme_dir, 'layout') + sep ].join('\n')); }); it('_generate() - should encode url', async () => { const path = 'bár'; hexo.config.url = 'http://fôo.com'; hexo.theme.setView('test.njk', '{{ url }}'); hexo.extend.generator.register('test', () => ({ path, layout: 'test' })); await hexo._generate(); await checkStream(route.get(path), full_url_for.call(hexo, path)); }); it('_generate() - do nothing if it\'s generating', () => { const hook = spy(); hexo.extend.generator.register('test', hook); hexo._isGenerating = true; hexo._generate(); hook.called.should.be.false; hexo._isGenerating = false; }); it('_generate() - reset cache for new route', async () => { let count = 0; hexo.theme.setView('test.njk', '{{ page.count() }}'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test', data: { count: () => count++ } })); await hexo._generate({cache: true}); // First generate await checkStream(route.get('test'), '0'); await checkStream(route.get('test'), '0'); // should return cached result await hexo._generate({cache: true}); // Second generate await checkStream(route.get('test'), '1'); await checkStream(route.get('test'), '1'); // should return cached result }); it('_generate() - cache disabled and use new route', async () => { let count = 0; hexo.theme.setView('test.njk', '{{ page.count() }}'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test', data: { count: () => count++ } })); await hexo._generate({ cache: false }); // First generate await checkStream(route.get('test'), '0'); await checkStream(route.get('test'), '1'); await hexo._generate({ cache: false }); // Second generate await checkStream(route.get('test'), '2'); await checkStream(route.get('test'), '3'); }); it('_generate() - cache disabled & update template', async () => { hexo.theme.setView('test.njk', '0'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test' })); await hexo._generate({ cache: false }); await checkStream(route.get('test'), '0'); hexo.theme.setView('test.njk', '1'); await checkStream(route.get('test'), '1'); }); it('_generate() - cache enabled & update template', async () => { hexo.theme.setView('test.njk', '0'); hexo.extend.generator.register('test', () => ({ path: 'test', layout: 'test' })); await hexo._generate({ cache: true }); await checkStream(route.get('test'), '0'); hexo.theme.setView('test.njk', '1'); await checkStream(route.get('test'), '0'); // should return cached result }); it('execFilter()', async () => { const fn = str => { return str + 'foo'; }; hexo.extend.filter.register('exec_test', fn); const result = await hexo.execFilter('exec_test', ''); result.should.eql('foo'); hexo.extend.filter.unregister('exec_test', fn); }); it('execFilter() - promise', async () => { const fn = str => { return new BluebirdPromise((resolve, _reject) => { resolve(str + 'bar'); }); }; hexo.extend.filter.register('exec_test', fn); const result = await hexo.execFilter('exec_test', 'foo'); result.should.eql('foobar'); hexo.extend.filter.unregister('exec_test', fn); }); it('execFilterSync()', () => { hexo.extend.filter.register('exec_sync_test', data => { data.should.eql(''); return data + 'foo'; }); hexo.execFilterSync('exec_sync_test', '').should.eql('foo'); }); }); ================================================ FILE: test/scripts/hexo/load_config.ts ================================================ import { join, sep, resolve } from 'path'; import { writeFile, unlink, mkdirs, rmdir } from 'hexo-fs'; import { makeRe } from 'micromatch'; import loadConfig from '../../../lib/hexo/load_config'; import defaultConfig from '../../../lib/hexo/default_config'; import Hexo from '../../../lib/hexo'; describe('Load config', () => { const hexo = new Hexo(join(__dirname, 'config_test'), { silent: true }); hexo.env.init = true; before(() => mkdirs(hexo.base_dir).then(() => hexo.init())); after(() => rmdir(hexo.base_dir)); beforeEach(() => { hexo.config_path = join(hexo.base_dir, '_config.yml'); hexo.config = JSON.parse(JSON.stringify(defaultConfig)); }); it('config file does not exist', async () => { await loadConfig(hexo); hexo.config.should.eql(defaultConfig); }); it('_config.yml exists', async () => { const configPath = join(hexo.base_dir, '_config.yml'); try { await writeFile(configPath, 'foo: 1'); await loadConfig(hexo); hexo.config.foo.should.eql(1); } finally { await unlink(configPath); } }); it('_config.json exists', async () => { const configPath = join(hexo.base_dir, '_config.json'); try { await writeFile(configPath, '{"baz": 3}'); await loadConfig(hexo); hexo.config.baz.should.eql(3); hexo.config_path.should.eql(configPath); } finally { await unlink(configPath); } }); it('_config.txt exists', async () => { const configPath = join(hexo.base_dir, '_config.txt'); try { await writeFile(configPath, 'foo: 1'); await loadConfig(hexo); hexo.config.should.eql(defaultConfig); hexo.config_path.should.not.eql(configPath); } finally { await unlink(configPath); } }); it('custom config path', async () => { const configPath = join(__dirname, 'werwerwer.yml'); hexo.config_path = join(__dirname, 'werwerwer.yml'); try { await writeFile(configPath, 'foo: 1'); await loadConfig(hexo); hexo.config.foo.should.eql(1); } finally { hexo.config_path = join(hexo.base_dir, '_config.yml'); await unlink(configPath); } }); it('custom config path with different extension name', async () => { const realPath = join(__dirname, 'werwerwer.json'); hexo.config_path = join(__dirname, 'werwerwer.yml'); try { await writeFile(realPath, '{"foo": 2}'); await loadConfig(hexo); hexo.config.foo.should.eql(2); hexo.config_path.should.eql(realPath); } finally { hexo.config_path = join(hexo.base_dir, '_config.yml'); await unlink(realPath); } }); it('handle trailing "/" of url', async () => { const content = [ 'root: foo', 'url: https://hexo.io/' ].join('\n'); try { await writeFile(hexo.config_path, content); await loadConfig(hexo); hexo.config.root.should.eql('foo/'); hexo.config.url.should.eql('https://hexo.io'); } finally { await unlink(hexo.config_path); } }); it('handle root is not exist', async () => { try { const content = 'url: https://hexo.io/'; await writeFile(hexo.config_path, content); await loadConfig(hexo); hexo.config.url.should.eql('https://hexo.io'); hexo.config.root.should.eql('/'); } finally { await unlink(hexo.config_path); } try { const content = 'url: https://hexo.io/foo/'; await writeFile(hexo.config_path, content); await loadConfig(hexo); hexo.config.url.should.eql('https://hexo.io/foo'); hexo.config.root.should.eql('/foo/'); } finally { await unlink(hexo.config_path); } }); it('custom public_dir', async () => { try { await writeFile(hexo.config_path, 'public_dir: foo'); await loadConfig(hexo); hexo.public_dir.should.eql(resolve(hexo.base_dir, 'foo') + sep); } finally { await unlink(hexo.config_path); } }); it('custom source_dir', async () => { try { await writeFile(hexo.config_path, 'source_dir: bar'); await loadConfig(hexo); hexo.source_dir.should.eql(resolve(hexo.base_dir, 'bar') + sep); } finally { await unlink(hexo.config_path); } }); it('custom theme - default theme_dir', async () => { try { await writeFile(hexo.config_path, 'theme: test'); await loadConfig(hexo); hexo.config.theme.should.eql('test'); hexo.theme_dir.should.eql(join(hexo.base_dir, 'themes', 'landscape') + sep); hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep); hexo.theme.base.should.eql(hexo.theme_dir); } finally { await unlink(hexo.config_path); } }); it('custom theme - base_dir/themes/[theme]', async () => { try { await writeFile(hexo.config_path, 'theme: test'); await mkdirs(join(hexo.base_dir, 'themes', 'test')); await loadConfig(hexo); hexo.config.theme.should.eql('test'); hexo.theme_dir.should.eql(join(hexo.base_dir, 'themes', 'test') + sep); hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep); hexo.theme.base.should.eql(hexo.theme_dir); const ignore = ['**/themes/*/node_modules/**', '**/themes/*/.git/**']; hexo.theme.ignore.should.eql(ignore); hexo.theme.options.ignored.should.eql(ignore.map(item => makeRe(item))); } finally { await rmdir(join(hexo.base_dir, 'themes', 'test')); await unlink(hexo.config_path); } }); it('custom theme - base_dir/node_modules/hexo-theme-[theme]', async () => { try { await writeFile(hexo.config_path, 'theme: test'); await mkdirs(join(hexo.plugin_dir, 'hexo-theme-test')); await loadConfig(hexo); hexo.config.theme.should.eql('test'); hexo.theme_dir.should.eql(join(hexo.plugin_dir, 'hexo-theme-test') + sep); hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep); hexo.theme.base.should.eql(hexo.theme_dir); const ignore = ['**/node_modules/hexo-theme-*/node_modules/**', '**/node_modules/hexo-theme-*/.git/**']; hexo.theme.ignore.should.eql(ignore); hexo.theme.options.ignored.should.eql(ignore.map(item => makeRe(item))); } finally { await rmdir(join(hexo.plugin_dir, 'hexo-theme-test')); await unlink(hexo.config_path); } }); it('merge config', async () => { const content = [ 'highlight:', ' tab_replace: yoooo' ].join('\n'); try { await writeFile(hexo.config_path, content); await loadConfig(hexo); hexo.config.highlight.line_number.should.be.true; hexo.config.highlight.tab_replace.should.eql('yoooo'); } finally { await unlink(hexo.config_path); } }); }); ================================================ FILE: test/scripts/hexo/load_database.ts ================================================ import { join } from 'path'; import Hexo from '../../../lib/hexo'; import { exists, mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import loadDatabase from '../../../lib/hexo/load_database'; describe('Load database', () => { const hexo = new Hexo(join(__dirname, 'db_test'), {silent: true}); const dbPath = hexo.database.options.path; const fixture = { meta: { version: 1, warehouse: require('warehouse').version }, models: { Test: [ {_id: 'A'}, {_id: 'B'}, {_id: 'C'} ] } }; before(() => mkdirs(hexo.base_dir)); beforeEach(() => { hexo._dbLoaded = false; }); after(async () => { const exist = await exists(dbPath); if (exist) await unlink(dbPath); rmdir(hexo.base_dir); }); it('database does not exist', () => loadDatabase(hexo)); it('database load success', async () => { await writeFile(dbPath, JSON.stringify(fixture)); await loadDatabase(hexo); hexo._dbLoaded.should.be.true; hexo.model('Test').toArray({lean: true}).should.eql(fixture.models.Test); hexo.model('Test').destroy(); await unlink(dbPath); }); it('don\'t load database if loaded', async () => { hexo._dbLoaded = true; await writeFile(dbPath, JSON.stringify(fixture)); await loadDatabase(hexo); hexo.model('Test').should.have.lengthOf(0); await unlink(dbPath); }); }); // #3975 workaround for Windows // Clean-up is not necessary (unlike the above tests), // because the db file is already removed if invalid describe('Load database - load failed', () => { const hexo = new Hexo(join(__dirname), {silent: true}); const dbPath = hexo.database.options.path; it('database load failed', async () => { hexo._dbLoaded = false; await writeFile(dbPath, '{1423432: 324'); await loadDatabase(hexo); hexo._dbLoaded.should.be.false; const exist = await exists(dbPath); exist.should.be.false; }); }); ================================================ FILE: test/scripts/hexo/load_plugins.ts ================================================ import { join, dirname } from 'path'; import { writeFile, mkdir, rmdir, unlink } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import loadPlugins from '../../../lib/hexo/load_plugins'; import BluebirdPromise from 'bluebird'; import chai from 'chai'; import { spy } from 'sinon'; const should = chai.should(); describe('Load plugins', () => { const hexo = new Hexo(join(__dirname, 'plugin_test'), { silent: true }) as any; const script = [ 'hexo._script_test = {', ' filename: __filename,', ' dirname: __dirname,', ' module: module,', ' require: require', '}' ].join('\n'); const asyncScript = [ 'async function afunc() {', ' return new Promise(resolve => resolve());', '}', 'await afunc()', 'hexo._script_test = {', ' filename: __filename,', ' dirname: __dirname,', ' module: module,', ' require: require', '}' ].join('\n'); function validate(path) { const result = hexo._script_test; result.filename.should.eql(path); result.dirname.should.eql(dirname(path)); result.module.id.should.eql(path); result.module.filename.should.eql(path); delete hexo._script_test; } function createPackageFile(name, path?) { const pkg = { name: 'hexo-site', version: '0.0.0', private: true, dependencies: { [name]: '*' } }; path = path || join(hexo.base_dir, 'package.json'); return writeFile(path, JSON.stringify(pkg, null, ' ')); } function createPackageFileWithDevDeps(name) { const pkg = { name: 'hexo-site', version: '0.0.0', private: true, dependencies: {}, devDependencies: { [name]: '*' } }; return writeFile(join(hexo.base_dir, 'package.json'), JSON.stringify(pkg, null, ' ')); } hexo.env.init = true; hexo.theme_script_dir = join(hexo.base_dir, 'themes', 'test', 'scripts'); before(() => mkdir(hexo.base_dir)); after(() => rmdir(hexo.base_dir)); afterEach(async () => { await createPackageFile('hexo-another-plugin'); }); it('load plugins', () => { const name = 'hexo-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('fail to load plugins', () => { const logSpy = spy(); hexo.log.error = logSpy; const name = 'hexo-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name), writeFile(path, 'throw new Error("test")') ]).then(() => loadPlugins(hexo)).then(() => { logSpy.args[0][1].should.contains('Plugin load failed: %s'); logSpy.args[0][2].should.contains('hexo-plugin-test'); }); }); it('load async plugins', () => { const name = 'hexo-async-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name), writeFile(path, asyncScript) ]).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('load scoped plugins', () => { const name = '@some-scope/hexo-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('load devDep plugins', () => { const name = 'hexo-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFileWithDevDeps(name), writeFile(path, script) ]).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('load plugins in the theme\'s package.json', async () => { const name = 'hexo-plugin-test'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name, join(hexo.theme_dir, 'package.json')), writeFile(path, script) ]).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('ignore plugin whose name is started with "hexo-theme-"', async () => { const script = 'hexo._script_test = true'; const name = 'hexo-theme-test_theme'; const path = join(hexo.plugin_dir, name, 'index.js'); await BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]); await loadPlugins(hexo); should.not.exist(hexo._script_test); delete hexo.config.theme; return unlink(path); }); it('ignore scoped plugin whose name is started with "hexo-theme-"', async () => { const script = 'hexo._script_test = true'; const name = '@hexojs/hexo-theme-test_theme'; const path = join(hexo.plugin_dir, name, 'index.js'); await BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]); await loadPlugins(hexo); should.not.exist(hexo._script_test); delete hexo.config.theme; return unlink(path); }); it('ignore plugins whose name is not started with "hexo-"', async () => { const script = 'hexo._script_test = true'; const name = 'another-plugin'; const path = join(hexo.plugin_dir, name, 'index.js'); await BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]); await loadPlugins(hexo); should.not.exist(hexo._script_test); return unlink(path); }); it('ignore plugins which is typescript definition', () => { const script = 'hexo._script_test = true'; const name = '@types/hexo-test-plugin'; const path = join(hexo.plugin_dir, name, 'index.js'); return BluebirdPromise.all([ createPackageFile(name), writeFile(path, script) ]).then(() => loadPlugins(hexo)).then(() => { should.not.exist(hexo._script_test); return unlink(path); }); }); it('ignore plugins which are in package.json but not exist actually', () => createPackageFile('hexo-plugin-test').then(() => loadPlugins(hexo))); it('load scripts', async () => { const path = join(hexo.script_dir, 'test.js'); writeFile(path, script); await loadPlugins(hexo); validate(path); return unlink(path); }); it('fail to load scripts', async () => { const logSpy = spy(); hexo.log.error = logSpy; const path = join(hexo.script_dir, 'test.js'); writeFile(path, 'throw new Error("test")'); await loadPlugins(hexo); logSpy.args[0][1].should.contains('Script load failed: %s'); logSpy.args[0][2].should.contains('test.js'); return unlink(path); }); it('load theme scripts', () => { const path = join(hexo.theme_script_dir, 'test.js'); return writeFile(path, script).then(() => loadPlugins(hexo)).then(() => { validate(path); return unlink(path); }); }); it('don\'t load plugins in safe mode', () => { const script = 'hexo._script_test = true'; const path = join(hexo.script_dir, 'test.js'); return writeFile(path, script).then(() => { hexo.env.safe = true; return loadPlugins(hexo); }).then(() => { hexo.env.safe = false; should.not.exist(hexo._script_test); return unlink(path); }); }); // Issue #4251 it('load scripts with sourcemap EOF', async () => { const path = join(hexo.script_dir, 'test.js'); const script = [ '(() => {', ' hexo._script_test = true;', '})();', '//# sourceMappingURL=data:application/json;' ].join('\n'); writeFile(path, script); await loadPlugins(hexo); hexo._script_test.should.eql(true); return unlink(path); }); }); ================================================ FILE: test/scripts/hexo/load_theme_config.ts ================================================ import { join } from 'path'; import { mkdirs, unlink, writeFile, rmdir } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Load alternate theme config', () => { const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true}); const loadThemeConfig = require('../../../lib/hexo/load_theme_config'); hexo.env.init = true; before(() => mkdirs(hexo.base_dir).then(() => hexo.init())); after(() => rmdir(hexo.base_dir)); beforeEach(() => { hexo.config.theme_config = { foo: { bar: 'ahhhhhh' } }; hexo.config.theme = 'test_theme'; }); it('hexo.config.theme does not exist', async () => { // @ts-ignore hexo.config.theme = undefined; await loadThemeConfig(hexo); hexo.config.theme_config.foo.bar.should.eql('ahhhhhh'); hexo.config.theme_config = {}; }); it('_config.[theme].yml does not exist', () => loadThemeConfig(hexo).then(() => { hexo.config.theme_config = {}; })); it('_config.[theme].yml exists', () => { const configPath = join(hexo.base_dir, '_config.test_theme.yml'); return writeFile(configPath, 'bar: 1').then(() => loadThemeConfig(hexo)).then(() => { hexo.config.theme_config.bar.should.eql(1); }).finally(() => unlink(configPath)); }); it('_config.[theme].json exists', () => { const configPath = join(hexo.base_dir, '_config.test_theme.json'); return writeFile(configPath, '{"baz": 3}').then(() => loadThemeConfig(hexo)).then(() => { hexo.config.theme_config.baz.should.eql(3); }).finally(() => unlink(configPath)); }); it('_config.[theme].txt exists', () => { const configPath = join(hexo.base_dir, '_config.test_theme.txt'); return writeFile(configPath, 'qux: 1').then(() => loadThemeConfig(hexo)).then(() => { should.not.exist(hexo.config.theme_config.qux); }).finally(() => unlink(configPath)); }); it('merge config', () => { const configPath = join(hexo.base_dir, '_config.test_theme.yml'); const content = [ 'foo:', ' bar: yoooo', ' baz: true' ].join('\n'); return writeFile(configPath, content).then(() => loadThemeConfig(hexo)).then(() => { hexo.config.theme_config.foo.baz.should.eql(true); hexo.config.theme_config.foo.bar.should.eql('ahhhhhh'); hexo.config.theme_config.foo.bar.should.not.eql('yoooo'); }).finally(() => unlink(configPath)); }); it('hexo.config.theme_config does not exist', async () => { const configPath = join(hexo.base_dir, '_config.test_theme.yml'); hexo.config.theme_config = undefined; const content = [ 'foo:', ' bar: yoooo', ' baz: true' ].join('\n'); await writeFile(configPath, content); await loadThemeConfig(hexo); hexo.config.theme_config.foo.baz.should.eql(true); hexo.config.theme_config.foo.bar.should.eql('yoooo'); }); }); ================================================ FILE: test/scripts/hexo/locals.ts ================================================ import Locals from '../../../lib/hexo/locals'; import chai from 'chai'; const should = chai.should(); describe('Locals', () => { const locals = new Locals(); it('get() - name must be a string', () => { // @ts-expect-error should.throw(() => locals.get(), 'name must be a string!'); }); it('set() - function', () => { locals.set('foo', () => 'foo'); // cache should be clear after new data is set locals.cache.has('foo').should.be.false; locals.get('foo').should.eql('foo'); // cache should be saved once it's get locals.cache.get('foo').should.eql('foo'); }); it('set() - not function', () => { locals.set('foo', 'foo'); locals.get('foo').should.eql('foo'); }); it('set() - name must be a string', () => { // @ts-expect-error should.throw(() => locals.set(), 'name must be a string!'); }); it('set() - value is required', () => { // @ts-expect-error should.throw(() => locals.set('test'), 'value is required!'); }); it('remove()', () => { locals.set('foo', 'foo'); locals.get('foo'); locals.remove('foo'); should.not.exist(locals.getters.foo); locals.cache.has('foo').should.be.false; }); it('remove() - name must be a string', () => { // @ts-expect-error should.throw(() => locals.remove(), 'name must be a string!'); }); it('toObject()', () => { const locals = new Locals(); locals.set('foo', 'foo'); locals.set('bar', 'bar'); locals.remove('bar'); locals.toObject().should.eql({foo: 'foo'}); }); it('invalidate()', () => { locals.set('foo', 'foo'); locals.get('foo'); locals.invalidate(); locals.cache.has('foo').should.be.false; }); }); ================================================ FILE: test/scripts/hexo/multi_config_path.ts ================================================ import pathFn from 'path'; import osFn from 'os'; import { writeFileSync, rmdirSync, unlinkSync, readFileSync } from 'hexo-fs'; import yml from 'js-yaml'; import Hexo from '../../../lib/hexo'; import multiConfigPath from '../../../lib/hexo/multi_config_path'; describe('config flag handling', () => { const hexo = new Hexo(pathFn.join(__dirname, 'test_dir')) as any; const mcp = multiConfigPath(hexo); const base = hexo.base_dir; function ConsoleReader() { this.reader = []; this.d = function(...args) { const type = 'debug'; let message = ''; for (let i = 0; i < args.length;) { message += args[i]; if (++i < args.length) { message += ' '; } } this.reader.push({ type, msg: message }); }.bind(this); this.i = function(...args) { const type = 'info'; let message = ''; for (let i = 0; i < args.length;) { message += args[i]; if (++i < args.length) { message += ' '; } } this.reader.push({ type, msg: message }); }.bind(this); this.w = function(...args) { const type = 'warning'; let message = ''; for (let i = 0; i < args.length;) { message += args[i]; if (++i < args.length) { message += ' '; } } this.reader.push({ type, msg: message }); }.bind(this); this.e = function(...args) { const type = 'error'; let message = ''; for (let i = 0; i < args.length;) { message += args[i]; if (++i < args.length) { message += ' '; } } this.reader.push({ type, msg: message }); }.bind(this); } hexo.log = new ConsoleReader(); const testYaml1 = [ 'author: foo', 'type: dinosaur', 'favorites:', ' food: sushi', ' color: purple' ].join('\n'); const testYaml2 = [ 'author: bar', 'favorites:', ' food: candy', ' ice_cream: chocolate' ].join('\n'); const testJson1 = [ '{', '"author": "dinosaur",', '"type": "elephant",', '"favorites": {"food": "burgers"}', '}' ].join('\n'); const testJson2 = [ '{', '"author": "waldo",', '"favorites": {', ' "food": "ice cream",', ' "ice_cream": "strawberry"', ' }', '}' ].join('\n'); const testJson3 = [ '{', '"author": "james bond",', '"favorites": {', ' "food": "martini",', ' "ice_cream": "vanilla"', ' }', '}' ].join('\n'); before(() => { writeFileSync(base + 'test1.yml', testYaml1); writeFileSync(base + 'test2.yml', testYaml2); writeFileSync(base + 'test1.json', testJson1); writeFileSync(base + 'test2.json', testJson2); // not supported type writeFileSync(base + 'test1.xml', ''); writeFileSync('/tmp/test3.json', testJson3); }); afterEach(() => { hexo.log.reader = []; }); after(() => { rmdirSync(hexo.base_dir); unlinkSync('/tmp/test3.json'); }); it('no file', () => { mcp(base).should.equal(base + '_config.yml'); hexo.log.reader[0].type.should.eql('warning'); hexo.log.reader[0].msg.should.eql('No config file entered.'); }); it('not supported type', () => { mcp(base, 'test1.xml,test1.json').should.equal(base + '_multiconfig.yml'); hexo.log.reader[0].type.should.eql('warning'); hexo.log.reader[0].msg.should.eql('Config file test1.xml not supported type.'); }); it('1 file', () => { mcp(base, 'test1.yml').should.eql( pathFn.resolve(base + 'test1.yml')); mcp(base, 'test1.json').should.eql( pathFn.resolve(base + 'test1.json')); mcp(base, '/tmp/test3.json').should.eql('/tmp/test3.json'); }); it('1 not found file warning', () => { const notFile = 'not_a_file.json'; mcp(base, notFile).should.eql(pathFn.join(base, '_config.yml')); hexo.log.reader[0].type.should.eql('warning'); hexo.log.reader[0].msg.should.eql('Config file ' + notFile + ' not found, using default.'); }); it('1 not found file warning absolute', () => { const notFile = '/tmp/not_a_file.json'; mcp(base, notFile).should.eql(pathFn.join(base, '_config.yml')); hexo.log.reader[0].type.should.eql('warning'); hexo.log.reader[0].msg.should.eql('Config file ' + notFile + ' not found, using default.'); }); it('combined config output', () => { const combinedPath = pathFn.join(base, '_multiconfig.yml'); mcp(base, 'test1.yml').should.not.eql(combinedPath); mcp(base, 'test1.yml,test2.yml').should.eql(combinedPath); mcp(base, 'test1.yml,test1.json').should.eql(combinedPath); mcp(base, 'test1.json,test2.json').should.eql(combinedPath); mcp(base, 'notafile.yml,test1.json').should.eql(combinedPath); hexo.log.reader[0].type.should.eql('info'); hexo.log.reader[0].msg.should.eql('Config based on 2 files'); hexo.log.reader[6].type.should.eql('warning'); hexo.log.reader[6].msg.should.eql('Config file notafile.yml not found.'); hexo.log.reader[7].type.should.eql('info'); hexo.log.reader[7].msg.should.eql('Config based on 1 files'); // because who cares about grammar anyway? mcp(base, 'notafile.yml,alsonotafile.json').should.not.eql(combinedPath); hexo.log.reader[11].type.should.eql('error'); hexo.log.reader[11].msg.should.eql('No config files found. Using _config.yml.'); }); it('combine config output with absolute paths', () => { const combinedPath = pathFn.join(base, '_multiconfig.yml'); mcp(base, 'test1.json,/tmp/test3.json').should.eql(combinedPath); hexo.log.reader[0].type.should.eql('info'); hexo.log.reader[0].msg.should.eql('Config based on 2 files'); }); it('2 YAML overwrite', () => { const configFile = mcp(base, 'test1.yml,test2.yml'); let config: any = readFileSync(configFile); config = yml.load(config); config.author.should.eql('bar'); config.favorites.food.should.eql('candy'); config.type.should.eql('dinosaur'); config = readFileSync(mcp(base, 'test2.yml,test1.yml')); config = yml.load(config); config.author.should.eql('foo'); config.favorites.food.should.eql('sushi'); config.type.should.eql('dinosaur'); }); it('2 JSON overwrite', () => { let config: any = readFileSync(mcp(base, 'test1.json,test2.json')); config = yml.load(config); config.author.should.eql('waldo'); config.favorites.food.should.eql('ice cream'); config.type.should.eql('elephant'); config = readFileSync(mcp(base, 'test2.json,test1.json')); config = yml.load(config); config.author.should.eql('dinosaur'); config.favorites.food.should.eql('burgers'); config.type.should.eql('elephant'); }); it('JSON & YAML overwrite', () => { let config: any = readFileSync(mcp(base, 'test1.yml,test1.json')); config = yml.load(config); config.author.should.eql('dinosaur'); config.favorites.food.should.eql('burgers'); config.type.should.eql('elephant'); config = readFileSync(mcp(base, 'test1.json,test1.yml')); config = yml.load(config); config.author.should.eql('foo'); config.favorites.food.should.eql('sushi'); config.type.should.eql('dinosaur'); }); it('write multiconfig to specified path', () => { const outputPath = osFn.tmpdir(); const combinedPath = pathFn.join(outputPath, '_multiconfig.yml'); mcp(base, 'test1.yml', outputPath).should.not.eql(combinedPath); mcp(base, 'test1.yml,test2.yml', outputPath).should.eql(combinedPath); mcp(base, 'test1.yml,test1.json', outputPath).should.eql(combinedPath); mcp(base, 'test1.json,test2.json', outputPath).should.eql(combinedPath); mcp(base, 'notafile.yml,test1.json', outputPath).should.eql(combinedPath); mcp(base, 'notafile.yml,alsonotafile.json', outputPath).should.not.eql(combinedPath); // delete /tmp/_multiconfig.yml unlinkSync(combinedPath); hexo.log.reader[1].type.should.eql('debug'); hexo.log.reader[1].msg.should.eql(`Writing _multiconfig.yml to ${combinedPath}`); hexo.log.reader[2].type.should.eql('info'); hexo.log.reader[2].msg.should.eql('Config based on 2 files'); hexo.log.reader[6].type.should.eql('warning'); hexo.log.reader[6].msg.should.eql('Config file notafile.yml not found.'); hexo.log.reader[7].type.should.eql('info'); hexo.log.reader[7].msg.should.eql('Config based on 1 files'); hexo.log.reader[11].type.should.eql('error'); hexo.log.reader[11].msg.should.eql('No config files found. Using _config.yml.'); }); }); ================================================ FILE: test/scripts/hexo/post.ts ================================================ import { join } from 'path'; import moment from 'moment'; import { readFile, mkdirs, unlink, rmdir, writeFile, exists, stat, listDir } from 'hexo-fs'; import { spy, useFakeTimers } from 'sinon'; import { parse as yfm } from 'hexo-front-matter'; import { expected, content, expected_disable_nunjucks, content_for_issue_3346, expected_for_issue_3346, content_for_issue_4460 } from '../../fixtures/post_render'; import { highlight, deepMerge } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}'); describe('Post', () => { const hexo = new Hexo(join(__dirname, 'post_test')); require('../../../lib/plugins/highlight/')(hexo); const { post } = hexo; const now = Date.now(); let clock; let defaultCfg = {}; before(async () => { clock = useFakeTimers(now); await mkdirs(hexo.base_dir); await hexo.init(); // Load marked renderer for testing await hexo.loadPlugin(require.resolve('hexo-renderer-marked')); await hexo.scaffold.set('post', [ '---', 'title: {{ title }}', 'date: {{ date }}', 'tags:', '---' ].join('\n')); await hexo.scaffold.set('draft', [ '---', 'title: {{ title }}', 'tags:', '---' ].join('\n')); defaultCfg = JSON.parse(JSON.stringify(hexo.config)); }); after(() => { clock.restore(); return rmdir(hexo.base_dir); }); afterEach(() => { hexo.config = JSON.parse(JSON.stringify(defaultCfg)); }); it('create()', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const listener = spy(); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; hexo.once('new', listener); const result = await post.create({ title: 'Hello World' }); result.path.should.eql(path); result.content.should.eql(content); listener.calledOnce.should.be.true; const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - slug', async () => { const path = join(hexo.source_dir, '_posts', 'foo.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const result = await post.create({ title: 'Hello World', slug: 'foo' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - filename_case', async () => { hexo.config.filename_case = 1; const path = join(hexo.source_dir, '_posts', 'hello-world.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const result = await post.create({ title: 'Hello World' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - layout', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'layout: photo', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const result = await post.create({ title: 'Hello World', layout: 'photo' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - extra data', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'foo: bar', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const result = await post.create({ title: 'Hello World', foo: 'bar' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - rename if target existed', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md'); await post.create({ title: 'Hello World' }); const result = await post.create({ title: 'Hello World' }); result.path.should.eql(path); const exist = await exists(path); exist.should.be.true; await Promise.all([ unlink(path), unlink(join(hexo.source_dir, '_posts', 'Hello-World.md')) ]); }); it('create() - replace existing files', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); await post.create({ title: 'Hello World' }); const result = await post.create({ title: 'Hello World' }, true); result.path.should.eql(path); await unlink(path); }); it('create() - asset folder', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World'); hexo.config.post_asset_folder = true; await post.create({ title: 'Hello World' }); const stats = await stat(path); stats.isDirectory().should.be.true; await unlink(path + '.md'); }); it('create() - page', async () => { const path = join(hexo.source_dir, 'Hello-World/index.md'); hexo.config.post_asset_folder = true; const result = await post.create({ title: 'Hello World', layout: 'page' }); result.path.should.eql(path); try { await stat(join(hexo.source_dir, 'Hello-World/index')); should.fail(); } catch (err) { err.code.should.eql('ENOENT'); } finally { await unlink(path); } }); it('create() - follow the separator style in the scaffold', async () => { const scaffold = [ '---', 'title: {{ title }}', '---' ].join('\n'); await hexo.scaffold.set('test', scaffold); const result = await post.create({ title: 'Hello World', layout: 'test' }); result.content.should.eql([ '---', 'title: Hello World', '---' ].join('\n') + '\n'); await Promise.all([ unlink(result.path), hexo.scaffold.remove('test') ]); }); // #4511 it('create() - avoid quote if unnecessary', async () => { const scaffold = [ '---', 'title: {{ title }}', '---' ].join('\n'); await hexo.scaffold.set('test', scaffold); const result = await post.create({ title: 'Hello World', layout: 'test' }); const data = await readFile(result.path); data.should.eql([ '---', 'title: Hello World', '---' ].join('\n') + '\n'); await Promise.all([ unlink(result.path), hexo.scaffold.remove('test') ]); }); // #4511 it('create() - wrap with quote when necessary', async () => { const scaffold = [ '---', 'title: {{ title }}', '---' ].join('\n'); await hexo.scaffold.set('test', scaffold); const result = await post.create({ title: 'Hello: World', layout: 'test' }); const data = await readFile(result.path); data.should.eql([ '---', 'title: \'Hello: World\'', '---' ].join('\n') + '\n'); await Promise.all([ unlink(result.path), hexo.scaffold.remove('test') ]); }); // #4511 it('create() - wrap with quote when necessary - yaml tag', async () => { const scaffold = [ '---', 'title: {{ title }}', '---' ].join('\n'); await hexo.scaffold.set('test', scaffold); const result = await post.create({ // https://github.com/nodeca/js-yaml#supported-yaml-types title: '!!js/regexp /pattern/gim', layout: 'test' }); const data = await readFile(result.path); data.should.eql([ '---', 'title: \'!!js/regexp /pattern/gim\'', '---' ].join('\n') + '\n'); await Promise.all([ unlink(result.path), hexo.scaffold.remove('test') ]); }); it('create() - JSON front-matter', async () => { const scaffold = [ '"title": {{ title }}', ';;;' ].join('\n'); await hexo.scaffold.set('test', scaffold); const result = await post.create({ title: 'Hello World', layout: 'test', lang: 'en' }); result.content.should.eql([ '"title": "Hello World",', '"lang": "en"', ';;;' ].join('\n') + '\n'); await Promise.all([ unlink(result.path), hexo.scaffold.remove('test') ]); }); // #1100 it('create() - non-string title', async () => { const path = join(hexo.source_dir, '_posts', '12345.md'); const result = await post.create({ title: 12345 }); result.path.should.eql(path); await unlink(path); }); it('create() - escape title', async () => { const data = await post.create({ title: 'Foo: Bar' }); data.content.should.eql([ // js-yaml use single-quotation for dumping since 3.3 '---', 'title: \'Foo: Bar\'', 'date: ' + moment(now).format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'); await unlink(data.path); }); it('create() - with content', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---', '', 'Hello hexo' ].join('\n'); const result = await post.create({ title: 'Hello World', content: 'Hello hexo' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('create() - with callback', done => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; post.create({ title: 'Hello World' }, (err, post) => { if (err) { done(err); return; } try { post.path.should.eql(path); post.content.should.eql(content); readFile(path).asCallback((err, data: any) => { if (err) { done(err); return; } try { data.should.eql(content); unlink(path).asCallback(done); } catch (e) { done(e); } }); } catch (e) { done(e); } }); }); it('publish()', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const data = await post.create({ title: 'Hello World', layout: 'draft' }); const draftPath = data.path; const result = await post.publish({ slug: 'Hello-World' }); result.path.should.eql(path); result.content.should.eql(content); const exist = await exists(draftPath); exist.should.be.false; const newdata = await readFile(path); newdata.should.eql(content); await unlink(path); }); it('publish() - layout', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'layout: photo', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; await post.create({ title: 'Hello World', layout: 'draft' }); const result = await post.publish({ slug: 'Hello-World', layout: 'photo' }); result.path.should.eql(path); result.content.should.eql(content); const data = await readFile(path); data.should.eql(content); await unlink(path); }); it('publish() - rename if target existed', async () => { const paths = [join(hexo.source_dir, '_posts', 'Hello-World-1.md')]; const result = await Promise.all([ post.create({ title: 'Hello World', layout: 'draft' }), post.create({ title: 'Hello World' }) ]); paths.push(result[1].path); const data = await post.publish({ slug: 'Hello-World' }); data.path.should.eql(paths[0]); for (const path of paths) { await unlink(path); } }); it('publish() - replace existing files', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); await Promise.all([ post.create({ title: 'Hello World', layout: 'draft' }), post.create({ title: 'Hello World' }) ]); const data = await post.publish({ slug: 'Hello-World' }, true); data.path.should.eql(path); await unlink(path); }); it('publish() - asset folder', async () => { const assetDir = join(hexo.source_dir, '_drafts', 'Hello-World'); const newAssetDir = join(hexo.source_dir, '_posts', 'Hello-World'); hexo.config.post_asset_folder = true; await post.create({ title: 'Hello World', layout: 'draft' }); // Put some files into the asset folder await Promise.all([ writeFile(join(assetDir, 'a.txt'), 'a'), writeFile(join(assetDir, 'b.txt'), 'b') ]); const result = await post.publish({ slug: 'Hello-World' }); const exist = await exists(assetDir); exist.should.be.false; const files = await listDir(newAssetDir); files.should.have.members(['a.txt', 'b.txt']); await unlink(result.path); await rmdir(newAssetDir); }); // #1100 it('publish() - non-string title', async () => { const path = join(hexo.source_dir, '_posts', '12345.md'); await post.create({ title: 12345, layout: 'draft' }); const data = await post.publish({ slug: 12345 }); data.path.should.eql(path); await unlink(path); }); it('publish() - with callback', async () => { const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); const date = moment(now); const content = [ '---', 'title: Hello World', 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), 'tags:', '---' ].join('\n') + '\n'; const callback = spy(); const data = await post.create({ title: 'Hello World', layout: 'draft' }); const draftPath = data.path; await post.publish({ slug: 'Hello-World' }, callback); callback.calledOnce.should.be.true; callback.calledWithMatch(null, { path, content }).should.true; const exist = await exists(draftPath); exist.should.be.false; const newdata = await readFile(path); newdata.should.eql(content); await unlink(path); }); // #1139 it('publish() - preserve non-null data in drafts', async () => { await post.create({ title: 'foo', layout: 'draft', tags: ['tag', 'test'] }); const data = await post.publish({ slug: 'foo' }); const meta = yfm(data.content); meta.tags.should.eql(['tag', 'test']); await unlink(data.path); }); // https:// github.com/hexojs/hexo/issues/5155 it('publish() - merge front-matter', async () => { const prefixTags = ['prefixTag1', 'fooo']; const customTags = ['customTag', 'fooo']; await hexo.scaffold.set('customscaff', [ '---', 'title: {{ title }}', 'date: {{ date }}', `tags: ${JSON.stringify(prefixTags)}`, 'qwe: 123', 'zxc: zxc', '---' ].join('\n')); const path = join(hexo.source_dir, '_posts', 'fooo.md'); await post.create({ title: 'fooo', layout: 'draft', tags: customTags, qwe: 456, asd: 'asd' }); const result = await post.publish({ slug: 'fooo', layout: 'customscaff' }); const fmt = yfm(result.content); fmt.tags.sort().should.eql(deepMerge(prefixTags, customTags).sort()); fmt.qwe.should.eql(456); fmt.asd.should.eql('asd'); fmt.zxc.should.eql('zxc'); await unlink(path); }); it('render()', async () => { // TODO: validate data const beforeHook = spy(); const afterHook = spy(); hexo.extend.filter.register('before_post_render', beforeHook); hexo.extend.filter.register('after_post_render', afterHook); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(expected); beforeHook.calledOnce.should.be.true; afterHook.calledOnce.should.be.true; }); it('render() - callback', done => { post.render('', { content, engine: 'markdown' }, err => { done(err); }); }); it('render() - file', async () => { const content = '**file test**'; const path = join(hexo.base_dir, 'render_test.md'); await writeFile(path, content); const data = await post.render(path); data.content.trim().should.eql('

file test

'); await unlink(path); }); it('render() - skip js', async () => { const content = 'let a = "{{ 1 + 1 }}"'; const data = await post.render('', { content, source: 'render_test.js' }); data.content.trim().should.eql(content); }); it('render() - toString', async () => { const content = 'foo: 1'; const data = await post.render('', { content, engine: 'yaml' }); data.content.should.eql('{"foo":1}'); }); it('render() - skip render phase if it\'s nunjucks file', async () => { const content = [ '{% quote Hello World %}', 'quote content', '{% endquote %}' ].join('\n'); const data = await post.render('', { content, engine: 'njk' }); data.content.trim().should.eql([ '

quote content

\n', '
Hello World
' ].join('')); }); it('render() - escaping nunjucks blocks with similar names', async () => { const code = 'alert("Hello world")'; const highlighted = highlight(code); const content = [ '{% codeblock %}', code, '{% endcodeblock %}', '', '{% code %}', code, '{% endcode %}' ].join('\n'); const data = await post.render('', { content }); data.content.trim().should.eql([ highlighted, '', highlighted ].join('\n')); }); it('render() - recover escaped nunjucks blocks which is html escaped', async () => { const content = '`{% raw %}{{ test }}{% endraw %}`, {%raw%}{{ test }}{%endraw%}'; const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql('

{{ test }}, {{ test }}

'); }); it.skip('render() - recover escaped nunjucks blocks which is html escaped before post_render', async () => { const content = '`{% raw %}{{ test }}{% endraw %}`'; const filter = spy(); hexo.extend.filter.register('after_render:html', filter); await post.render('', { content, engine: 'markdown' }); filter.calledOnce.should.be.true; filter.firstCall.args[0].trim().should.eql('

{{ test }}

'); hexo.extend.filter.unregister('after_render:html', filter); }); it('render() - callback - not path and file', callback => { post.render('', {}, (err, result) => { try { err.should.be.exist; err.should.be.instanceof(Error); err.should.be.have.property('message', 'No input file or string!'); should.not.exist(result); } catch (e) { callback(e); return; } callback(); }); }); // #3573 it('render() - (disableNunjucks === true)', async () => { const renderer = hexo.render.renderer.get('markdown'); renderer.disableNunjucks = true; try { const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(expected_disable_nunjucks); } finally { renderer.disableNunjucks = false; } }); // #3573 it('render() - (disableNunjucks === false)', async () => { const renderer = hexo.render.renderer.get('markdown'); renderer.disableNunjucks = false; try { const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(expected); } finally { renderer.disableNunjucks = false; } }); // #4498 it('render() - (disableNunjucks === true) - sync', async () => { const content = '{% link foo http://bar.com %}'; const loremFn = data => { return data.text.toUpperCase(); }; loremFn.disableNunjucks = true; hexo.extend.renderer.register('coffee', 'js', loremFn, true); const data = await post.render('', { content, engine: 'coffee' }); data.content.should.eql(content.toUpperCase()); }); // #4498 it('render() - (disableNunjucks === false) - sync', async () => { const content = '{% link foo http://bar.com %}'; const loremFn = data => { return data.text.toUpperCase(); }; loremFn.disableNunjucks = false; hexo.extend.renderer.register('coffee', 'js', loremFn, true); const data = await post.render('', { content, engine: 'coffee' }); data.content.should.not.eql(content.toUpperCase()); }); it('render() - (disableNunjucks === true) - front-matter', async () => { const renderer = hexo.render.renderer.get('markdown'); renderer.disableNunjucks = true; try { const data = await post.render('', { content, engine: 'markdown', disableNunjucks: false }); data.content.trim().should.eql(expected); } finally { renderer.disableNunjucks = false; } }); it('render() - (disableNunjucks === false) - front-matter', async () => { const renderer = hexo.render.renderer.get('markdown'); renderer.disableNunjucks = false; try { const data = await post.render('', { content, engine: 'markdown', disableNunjucks: true }); data.content.trim().should.eql(expected_disable_nunjucks); } finally { renderer.disableNunjucks = false; } }); // Only boolean type of front-matter's disableNunjucks is valid it('render() - (disableNunjucks === null) - front-matter', async () => { const renderer = hexo.render.renderer.get('markdown'); renderer.disableNunjucks = true; try { const data = await post.render('', { content, engine: 'markdown', // @ts-ignore disableNunjucks: null }); data.content.trim().should.eql(expected_disable_nunjucks); } finally { renderer.disableNunjucks = false; } }); it('render() - nested swig tag', async () => { const content = [ '{% blockquote %}', 'test1', '{% quote test2 %}', 'test3', '{% endquote %}', 'test4', '{% endblockquote %}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

test1

', '

test3

', '
test2
', 'test4
' ].join('\n')); }); it('render() - swig comments', async () => { const content = '{# blockquote #}'; const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(''); }); it('render() - shouln\'t break curly brackets', async () => { hexo.config.syntax_highlighter = 'prismjs'; const content = [ '\\begin{equation}', 'E=h\\nu', '\\end{equation}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.include('\\begin{equation}'); data.content.should.include('\\end{equation}'); hexo.config.syntax_highlighter = 'highlight.js'; }); // #2321 it('render() - allow backtick code block in "blockquote" tag plugin', async () => { const code = 'alert("Hello world")'; const highlighted = highlight(code); const content = [ '{% blockquote %}', '```', code, '```', '{% endblockquote %}' ].join('\n'); const data = await post.render('', { content }); data.content.trim().should.eql([ '
' + highlighted + '
' ].join('\n')); }); // #2969 it('render() - backtick cocde block in blockquote', async () => { const code = 'alert("Hello world")'; const highlighted = highlight(code); const quotedContent = [ 'This is a code-block', '', '```', code, '```' ]; const content = [ 'Hello', '', ...quotedContent.map(s => '> ' + s) ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

Hello

', '
', '

This is a code-block

', highlighted + '
' ].join('\n')); }); // #2969 it('render() - "lang=dos" backtick cocde block in blockquote', async () => { const code = '> dir'; const highlighted = highlight(code); const quotedContent = [ 'This is a code-block', '', '```', code, '```' ]; const content = [ 'Hello', '', ...quotedContent.map(s => '> ' + s) ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

Hello

', '
', '

This is a code-block

', highlighted + '
' ].join('\n')); }); // #3767 it('render() - backtick cocde block (followed by a paragraph) in blockquote', async () => { const code = 'alert("Hello world")'; const highlighted = highlight(code); const quotedContent = [ 'This is a code-block', '', '```', code, '```', '', 'This is a following paragraph' ]; const content = [ 'Hello', '', ...quotedContent.map(s => '> ' + s) ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

Hello

', '
', '

This is a code-block

', highlighted, '', '

This is a following paragraph

', '
' ].join('\n')); }); // #3769 it('render() - blank lines in backtick cocde block in blockquote', async () => { const code = [ '', '', '', '{', ' "test": 123', '', '', '}', '' ]; const highlighted = highlight(code.join('\n')); const addQuote = s => '>' + (s ? ` ${s}` : ''); const code2 = code.map((s, i) => { if (i === 0 || i === 2 || i === 6) return addQuote(s); return s; }); const quotedContent = [ 'This is a code-block', '', '> ```', ...code2, '```', '', 'This is a following paragraph' ]; const content = [ 'Hello', '', ...quotedContent.map(addQuote) ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

Hello

', '
', '

This is a code-block

', '
', highlighted.replace('{', '{').replace('}', '}'), '
', '

This is a following paragraph

', '
' ].join('\n')); }); // #4161 it('render() - adjacent tags', async () => { const content = [ '{% pullquote %}content1{% endpullquote %}', '', 'This is a following paragraph', '', '{% pullquote %}content2{% endpullquote %}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

content1

\n
\n\n', '

This is a following paragraph

\n', '

content2

\n
' ].join('')); }); // #4161 it('render() - adjacent tags with args', async () => { const content = [ '{% pullquote center %}content1{% endpullquote %}', '', 'This is a following paragraph', '', '{% pullquote center %}content2{% endpullquote %}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ '

content1

\n
\n\n', '

This is a following paragraph

\n', '

content2

\n
' ].join('')); }); // #3346 it('render() - swig tag inside backtick code block', async () => { const content = content_for_issue_3346; const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(expected_for_issue_3346); }); // test for https://github.com/hexojs/hexo/pull/4171#issuecomment-594412367 it('render() - markdown content right after swig tag', async () => { const content = [ '{% pullquote warning %}', 'Text', '{% endpullquote %}', '# Title 0', '{% pullquote warning %}', 'Text', '{% endpullquote %}', '{% pullquote warning %}', 'Text', '{% endpullquote %}', '# Title 1', '{% pullquote warning %}', 'Text', '{% endpullquote %}', '{% pullquote warning %}Text{% endpullquote %}', '# Title 2', '{% pullquote warning %}Text{% endpullquote %}', '{% pullquote warning %}Text{% endpullquote %}', '# Title 3', '{% pullquote warning %}Text{% endpullquote %}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); // We only to make sure markdown content is rendered correctly data.content.trim().should.include('

Title 0

'); data.content.trim().should.include('

Title 1

'); data.content.trim().should.include('

Title 2

'); data.content.trim().should.include('

Title 3

'); }); // #3259 it('render() - "{{" & "}}" inside inline code', async () => { const content = 'In Go\'s templates, blocks look like this: `{{block "template name" .}} (content) {{end}}`.'; const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(`

In Go’s templates, blocks look like this: ${escapeSwigTag('{{block "template name" .}} (content) {{end}}')}.

`); }); // https://github.com/hexojs/hexo/issues/3346#issuecomment-595497849 it('render() - swig var inside inline code', async () => { const content = '`{{ 1 + 1 }}` {{ 1 + 1 }}'; const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql(`

${escapeSwigTag('{{ 1 + 1 }}')} 2

`); }); // #3543 it('render() - issue #3543', async () => { // Adopted from #3459 const js = 'alert("Foo")'; const html = '
'; const highlightedJs = highlight(js, { lang: 'js' }); const highlightedHtml = highlight(html, { lang: 'html' }); const content = [ '```js', js, '```', '{% raw %}', '

Foo

', '{% endraw %}', '```html', html, '```' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.contains(highlightedJs); data.content.trim().should.contains('

Foo

'); data.content.trim().should.not.contains('{% raw %}'); data.content.trim().should.not.contains('{% endraw %}'); data.content.trim().should.contains(highlightedHtml); }); it('render() - escape & recover multi {% raw %} and backticks', async () => { const content = [ '`{{ 1 + 1 }}` {{ 1 + 2 }} `{{ 2 + 2 }}`', 'Text', '{% raw %}', 'Raw 1', '{% endraw %}', 'Another Text', '{% raw %}', 'Raw 2', '{% endraw %}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.trim().should.eql([ `

${escapeSwigTag('{{ 1 + 1 }}')} 3 ${escapeSwigTag('{{ 2 + 2 }}')}
Text

`, '', 'Raw 1', '', '

Another Text

', '', 'Raw 2' ].join('\n')); }); // #4087 it('render() - issue #4087', async () => { // Adopted from https://github.com/hexojs/hexo/issues/4087#issuecomment-596999486 const content = [ '## Quote', '', ' {% pullquote %}foo foo foo{% endpullquote %}', '', 'test001', '', '{% pullquote %}bar bar bar{% endpullquote %}', '', '## Insert', '', 'test002', '' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); // indented pullquote data.content.trim().should.contains(`
${escapeSwigTag('{% pullquote %}foo foo foo{% endpullquote %}')}\n
`); data.content.trim().should.contains('

test001

'); // pullquote tag data.content.trim().should.contains('

bar bar bar

\n
'); data.content.trim().should.contains('

test002

'); }); // #4385 it('render() - no double escape in code block (issue #4385)', async () => { const content = [ '```rust', 'fn main() {', ' println!("Hello, world!");', '}', '```' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('
'); data.content.should.contains('{'); data.content.should.contains('}'); data.content.should.not.contains('&#123'); data.content.should.not.contains('&#125'); }); it('render() - issue #4460', async () => { hexo.config.syntax_highlighter = 'prismjs'; const content = content_for_issue_4460; const data = await post.render('', { content, engine: 'markdown' }); data.content.should.not.include('hexoPostRenderEscape'); hexo.config.syntax_highlighter = 'highlight.js'; }); it('render() - empty tag name', async () => { hexo.config.syntax_highlighter = 'prismjs'; const content = 'Disable rendering of Nunjucks tag `{{ }}` / `{% %}`'; const data = await post.render('', { content, engine: 'markdown' }); data.content.should.include(escapeSwigTag('{{ }}')); data.content.should.include(escapeSwigTag('{% %}')); hexo.config.syntax_highlighter = 'highlight.js'; }); // https://github.com/hexojs/hexo/issues/5301 it('render() - dont escape incomplete tags', async () => { const content = 'dont drop `{% }` 11111 `{# }` 22222 `{{ }` 33333'; const data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('11111'); data.content.should.contains('22222'); data.content.should.contains('33333'); data.content.should.not.contains('`'); // ` }); it('render() - should support quotes in tags', async () => { let content = '{{ "{{ }" }}'; let data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql('{{ }'); content = '{% blockquote "{% }" %}test{% endblockquote %}'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql('

test

\n
{% }
'); }); it('render() - dont escape incomplete tags with complete tags', async () => { // lost one character let content = '{{ 1 }} \n `{% "%}" }` 22222'; let data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{% "%}" }'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{% "%}" %` 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{% "%}" %'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{# }` 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{# }'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{{ "}}" }` 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{{ "}}" }'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{{ %}` 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{{ %}'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{% custom %}` 22222 `{% endcustom }`'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('1'); data.content.should.contains('{% custom %}'); data.content.should.contains('22222'); data.content.should.contains('{% endcustom }'); // lost two characters content = '{{ 1 }} \n `{#` \n 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{#'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{%` \n 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('{%'); data.content.should.contains('1'); data.content.should.contains('22222'); content = '{{ 1 }} \n `{{ ` 22222'; data = await post.render('', { content, engine: 'markdown' }); data.content.should.contains('1'); data.content.should.contains('{{ '); data.content.should.contains('22222'); }); it('render() - tags with swig character', async () => { const tagSpy = spy(); hexo.extend.tag.register('testTag', (args, content) => { tagSpy(args, content); return ''; }, { ends: true }); let content = '{% testTag 111 222 %}\n3333\n{% endtestTag %}'; await post.render('', { content, engine: 'markdown' }); tagSpy.calledOnce.should.be.true; tagSpy.firstCall.args[0].should.eql(['111', '222']); tagSpy.firstCall.args[1].should.eql('3333'); content = '{% testTag 111% % 222 %}\n333\n{% endtestTag %}'; await post.render('', { content, engine: 'markdown' }); tagSpy.calledTwice.should.be.true; tagSpy.secondCall.args[0].should.eql(['111%', '%', '222']); tagSpy.secondCall.args[1].should.eql('333'); content = '{% testTag 111 } 222} %}\n333\n{% endtestTag %}'; await post.render('', { content, engine: 'markdown' }); tagSpy.calledThrice.should.be.true; tagSpy.thirdCall.args[0].should.eql(['111', '}', '222}']); tagSpy.thirdCall.args[1].should.eql('333'); content = '{% testTag 111 222 %}\n333% % } %}\n{% endtestTag %}'; await post.render('', { content, engine: 'markdown' }); tagSpy.callCount.should.eql(4); tagSpy.getCall(3).args[0].should.eql(['111', '222']); tagSpy.getCall(3).args[1].should.eql('333% % } %}'); hexo.extend.tag.unregister('testTag'); }); it('render() - incomplete tags throw error', async () => { const content = 'nunjucks should throw {# } error'; try { await post.render('', { content, engine: 'markdown' }); should.fail(); } catch {} }); // https://github.com/hexojs/hexo/issues/5401 it('render() - tags in different lines', async () => { const content = [ '{% link', 'foobar', 'https://hexo.io/', 'tttitle', '%}' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql('foobar'); }); // https://github.com/hexojs/hexo/issues/5433 it('render() - nunjucks nesting in comments', async () => { const content = [ 'foo', '', 'bar' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql([ '

foo

', '', '

bar

', '' ].join('\n')); }); it('render() - incomplete comments', async () => { const content = [ 'foo', '', 'bar', '{% endtestTag %}' ].join('\n'); let data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql(''); tagSpy.calledOnce.should.be.true; tagSpy.firstCall.args[1].should.eql([ 'foo', '', 'bar' ].join('\n')); content = [ '{% testTag %}', 'foo', '', 'bar', '{% endtestTag %}' ].join('\n'); data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql(''); tagSpy.calledTwice.should.be.true; tagSpy.secondCall.args[1].should.eql([ 'foo', '', 'bar' ].join('\n')); hexo.extend.tag.unregister('testTag'); }); // https://github.com/hexojs/hexo/issues/5433 it('render() - code fence nesting in comments', async () => { const code = 'alert("Hello world")'; const content = [ 'foo', '', 'bar' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql([ '

foo

', '', '

bar

', '' ].join('\n')); }); // https://github.com/hexojs/hexo/issues/5715 it('render() - comment nesting in code fence', async () => { const code = 'alert("Hello world")'; const content = [ 'foo', '```', '', '```', 'bar' ].join('\n'); const data = await post.render('', { content, engine: 'markdown' }); data.content.should.eql([ '

foo

', '
1
2
3
<!--
alert("Hello world")
-->
', '

bar

', '' ].join('\n')); }); }); ================================================ FILE: test/scripts/hexo/render.ts ================================================ import { writeFile, rmdir } from 'hexo-fs'; import { join } from 'path'; import yaml from 'js-yaml'; import { spy, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Render', () => { const hexo = new Hexo(join(__dirname, 'render_test')); hexo.config.meta_generator = false; const body = [ 'name:', ' first: John', ' last: Doe', '', 'age: 23', '', 'list:', '- Apple', '- Banana' ].join('\n'); const obj = yaml.load(body); const path = join(hexo.base_dir, 'test.yml'); before(async () => { await writeFile(path, body); await hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('isRenderable()', () => { hexo.render.isRenderable('test.txt').should.be.false; // html hexo.render.isRenderable('test.htm').should.be.true; hexo.render.isRenderable('test.html').should.be.true; // swig hexo.render.isRenderable('test.swig').should.be.false; hexo.render.isRenderable('test.njk').should.be.true; // yaml hexo.render.isRenderable('test.yml').should.be.true; hexo.render.isRenderable('test.yaml').should.be.true; }); it('isRenderableSync()', () => { hexo.render.isRenderableSync('test.txt').should.be.false; // html hexo.render.isRenderableSync('test.htm').should.be.true; hexo.render.isRenderableSync('test.html').should.be.true; // swig hexo.render.isRenderableSync('test.swig').should.be.false; hexo.render.isRenderableSync('test.njk').should.be.true; // yaml hexo.render.isRenderableSync('test.yml').should.be.true; hexo.render.isRenderableSync('test.yaml').should.be.true; }); it('getOutput()', () => { hexo.render.getOutput('test.txt').should.not.ok; // html hexo.render.getOutput('test.htm').should.eql('html'); hexo.render.getOutput('test.html').should.eql('html'); // swig hexo.render.getOutput('test.njk').should.eql('html'); // yaml hexo.render.getOutput('test.yml').should.eql('json'); hexo.render.getOutput('test.yaml').should.eql('json'); }); it('render() - path', async () => { const result = await hexo.render.render({path}); result.should.eql(obj); }); it('render() - text (without engine)', async () => { const result = await hexo.render.render({text: body}); result.should.eql(body); }); it('render() - text (with engine)', async () => { const result = await hexo.render.render({text: body, engine: 'yaml'}); result.should.eql(obj); }); it('render() - no path and text', async () => { try { // @ts-expect-error await hexo.render.render(); should.fail('Return value must be rejected'); } catch (err) { err.message.should.eql('No input file or string!'); } }); it('render() - null path and text', async () => { try { // @ts-ignore await hexo.render.render({text: null, engine: null}); should.fail('Return value must be rejected'); } catch (err) { err.message.should.eql('No input file or string!'); } }); it('render() - options', async () => { const result = await hexo.render.render({ text: [ '{{ title }}', '{{ content }}' ].join('\n'), engine: 'njk' }, { title: 'Hello world', content: 'foobar' }); result.should.eql([ 'Hello world', 'foobar' ].join('\n')); }); it('render() - toString', async () => { const content = await hexo.render.render({ text: body, engine: 'yaml', toString: true }); content.should.eql(JSON.stringify(obj)); }); it('render() - custom toString method', async () => { const content = await hexo.render.render({ text: body, engine: 'yaml', toString(data) { return JSON.stringify(data, null, ' '); } }); content.should.eql(JSON.stringify(obj, null, ' ')); }); it.skip('render() - after_render filter', async () => { const data = { text: ' 123456 ', engine: 'njk' }; const filter = spy((result, obj) => { result.should.eql(data.text); obj.should.eql(data); return result.trim(); }); hexo.extend.filter.register('after_render:html', filter); const result = await hexo.render.render(data); filter.calledOnce.should.be.true; result.should.eql(data.text.trim()); hexo.extend.filter.unregister('after_render:html', filter); }); it('render() - after_render filter: use the given output extension if not found', async () => { const data = { text: 'foo', engine: 'txt' }; const filter = spy(); hexo.extend.filter.register('after_render:txt', filter); await hexo.render.render(data); filter.calledOnce.should.be.true; hexo.extend.filter.unregister('after_render:txt', filter); }); it('render() - onRenderEnd method', async () => { const onRenderEnd = spy(result => result + 'bar'); const data = { text: 'foo', engine: 'txt', onRenderEnd }; const filter = spy(); hexo.extend.filter.register('after_render:txt', filter); await hexo.render.render(data); onRenderEnd.calledOnce.should.be.true; filter.calledOnce.should.be.true; sinonAssert.calledWith(filter, 'foobar'); hexo.extend.filter.unregister('after_render:txt', filter); }); it('render() - options as callback', async () => { const cbSpy = spy(); const data = { text: ' 123456 ', engine: 'njk' }; await hexo.render.render(data, cbSpy); cbSpy.calledOnce.should.be.true; }); it('renderSync() - path', () => { const result = hexo.render.renderSync({path}); result.should.eql(obj); }); it('renderSync() - text (without engine)', () => { const result = hexo.render.renderSync({text: body}); result.should.eql(body); }); it('renderSync() - text (with engine)', () => { const result = hexo.render.renderSync({text: body, engine: 'yaml'}); result.should.eql(obj); }); it('renderSync() - no path and text', () => { // @ts-expect-error should.throw(() => hexo.render.renderSync(), 'No input file or string!'); }); it('renderSync() - null path and text', () => { // @ts-ignore should.throw(() => hexo.render.renderSync({text: null, engine: null}), 'No input file or string!'); }); it('renderSync() - options', () => { const result = hexo.render.renderSync({ text: [ '{{ title }}', '{{ content }}' ].join('\n'), engine: 'njk' }, { title: 'Hello world', content: 'foobar' }); result.should.eql([ 'Hello world', 'foobar' ].join('\n')); }); it('renderSync() - toString', () => { const result = hexo.render.renderSync({ text: body, engine: 'yaml', toString: true }); result.should.eql(JSON.stringify(obj)); }); it('renderSync() - custom toString method', () => { const result = hexo.render.renderSync({ text: body, engine: 'yaml', toString(data) { return JSON.stringify(data, null, ' '); } }); result.should.eql(JSON.stringify(obj, null, ' ')); }); it.skip('renderSync() - after_render filter', () => { const data = { text: ' 123456 ', engine: 'njk' }; const filter = spy(result => result.trim()); hexo.extend.filter.register('after_render:html', filter); const result = hexo.render.renderSync(data); filter.calledOnce.should.be.true; // @ts-expect-error sinonAssert.calledWith(filter, data.text, data); result.should.eql(data.text.trim()); hexo.extend.filter.unregister('after_render:html', filter); }); it('renderSync() - after_render filter: use the given output extension if not found', () => { const data = { text: 'foo', engine: 'txt' }; const filter = spy(); hexo.extend.filter.register('after_render:txt', filter); hexo.render.renderSync(data); filter.calledOnce.should.be.true; hexo.extend.filter.unregister('after_render:txt', filter); }); it('renderSync() - onRenderEnd', () => { const onRenderEnd = spy(result => result + 'bar'); const data = { text: 'foo', engine: 'txt', onRenderEnd }; const filter = spy(result => { result.should.eql('foobar'); }); hexo.extend.filter.register('after_render:txt', filter); hexo.render.renderSync(data); onRenderEnd.calledOnce.should.be.true; filter.calledOnce.should.be.true; hexo.extend.filter.unregister('after_render:txt', filter); }); }); ================================================ FILE: test/scripts/hexo/router.ts ================================================ import BluebirdPromise from 'bluebird'; import { Readable } from 'stream'; import { join } from 'path'; import crypto from 'crypto'; import { createReadStream } from 'hexo-fs'; import { spy, assert as sinonAssert } from 'sinon'; import { readStream } from '../../util'; import Router from '../../../lib/hexo/router'; import chai from 'chai'; const should = chai.should(); describe('Router', () => { const router = new Router(); function checkStream(stream, expected) { return readStream(stream).then(data => { data.should.eql(expected); }); } function checksum(stream) { return new BluebirdPromise((resolve, reject) => { const hash = crypto.createHash('sha1'); stream.on('readable', () => { let chunk; while ((chunk = stream.read()) !== null) { hash.update(chunk); } }).on('end', () => { resolve(hash.digest('hex')); }).on('error', reject); }); } it('format()', () => { router.format('foo').should.eql('foo'); // Remove prefixed slashes router.format('/foo').should.eql('foo'); router.format('///foo').should.eql('foo'); // Append `index.html` to the URL with trailing slash router.format('foo/').should.eql('foo/index.html'); // '' => `index.html router.format('').should.eql('index.html'); router.format().should.eql('index.html'); // Remove backslashes router.format('foo\\bar').should.eql('foo/bar'); router.format('foo\\bar\\').should.eql('foo/bar/index.html'); // Remove query string router.format('foo?a=1&b=2').should.eql('foo'); }); it('format() - path must be a string', () => { // @ts-expect-error should.throw(() => router.format(() => {}), 'path must be a string!'); }); it('set() - string', () => { const listener = spy(); router.once('update', listener); router.set('test', 'foo'); const data = router.get('test'); data.modified.should.be.true; listener.calledOnce.should.be.true; sinonAssert.calledWith(listener, 'test'); return checkStream(data, 'foo'); }); it('set() - function', () => { router.set('test', () => 'foo'); return checkStream(router.get('test'), 'foo'); }); it('set() - function (callback style)', () => { router.set('test', callback => { callback(null, 'foo'); }); return checkStream(router.get('test'), 'foo'); }); it('set() - readable stream', () => { // Prepare a readable stream const stream = new Readable(); stream.push('foo'); stream.push(null); router.set('test', () => stream); return checkStream(router.get('test'), 'foo'); }); it('set() - modified', () => { router.set('test', { data: '', modified: false }); router.isModified('test').should.be.false; }); it('set() - path must be a string', () => { // @ts-expect-error should.throw(() => router.set(), 'path must be a string!'); }); it('set() - data is required', () => { // @ts-expect-error should.throw(() => router.set('test'), 'data is required!'); }); it('get() - error handling', () => { router.set('test', () => { throw new Error('error test'); }); return readStream(router.get('test')).then(() => { should.fail('Return value must be rejected'); }, err => { err.should.have.property('message', 'error test'); }); }); it('get() - no data', () => { router.set('test', () => { }); return checkStream(router.get('test'), ''); }); it('get() - empty readable stream', () => { const stream = new Readable(); stream.push(null); router.set('test', () => stream); return checkStream(router.get('test'), ''); }); it('get() - large readable stream (more than 65535 bits)', () => { const path = join(__dirname, '../../fixtures/banner.jpg'); router.set('test', () => createReadStream(path)); return BluebirdPromise.all([ checksum(router.get('test')), checksum(createReadStream(path)) ]).then((data: any) => { data[0].should.eql(data[1]); }); }); it('get() - path must be a string', () => { // @ts-expect-error should.throw(() => router.get(), 'path must be a string!'); }); it('get() - export stringified JSON object', () => { const obj = {foo: 1, bar: 2}; router.set('test', () => obj); return checkStream(router.get('test'), JSON.stringify(obj)); }); it('list()', () => { const router = new Router(); router.set('foo', 'foo'); router.set('bar', 'bar'); router.set('baz', 'baz'); router.remove('bar'); router.list().should.eql(['foo', 'baz']); }); it('isModified()', () => { router.set('test', 'foo'); router.isModified('test').should.be.true; }); it('isModified() - path must be a string', () => { // @ts-expect-error should.throw(() => router.isModified(), 'path must be a string!'); }); it('remove()', () => { const listener = spy(); router.once('remove', listener); router.set('test', 'foo'); router.remove('test'); should.not.exist(router.get('test')); sinonAssert.calledWith(listener, 'test'); listener.calledOnce.should.be.true; }); it('remove() - path must be a string', () => { // @ts-expect-error should.throw(() => router.remove(), 'path must be a string!'); }); }); ================================================ FILE: test/scripts/hexo/scaffold.ts ================================================ import { join } from 'path'; import { exists, readFile, rmdir, unlink, writeFile } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Scaffold', () => { const hexo = new Hexo(__dirname); const scaffold = hexo.scaffold; const scaffoldDir = hexo.scaffold_dir; const testContent = [ '---', 'title: {{ title }}', '---', 'test scaffold' ].join('\n'); const testPath = join(scaffoldDir, 'test.md'); before(async () => { await hexo.init(); await writeFile(testPath, testContent); }); after(() => rmdir(scaffoldDir)); it('get() - file exists', async () => { const data = await scaffold.get('test'); data.should.eql(testContent); }); it('get() - normal scaffold', async () => { const data = await scaffold.get('normal'); data.should.eql(scaffold.defaults.normal); }); it('set() - file exists', async () => { await scaffold.set('test', 'foo'); const file = await readFile(testPath); const data = await scaffold.get('test'); file.should.eql('foo'); data.should.eql('foo'); await writeFile(testPath, testContent); }); it('set() - file does not exist', async () => { const testPath = join(scaffoldDir, 'foo.md'); await scaffold.set('foo', 'bar'); const file = await readFile(testPath); const data = await scaffold.get('foo'); file.should.eql('bar'); data.should.eql('bar'); await unlink(testPath); }); it('remove() - file exist', async () => { await scaffold.remove('test'); const exist = await exists(testPath); const data = await scaffold.get('test'); exist.should.be.false; should.not.exist(data); await writeFile(testPath, testContent); }); it('remove() - file does not exist', () => scaffold.remove('foo')); }); ================================================ FILE: test/scripts/hexo/update_package.ts ================================================ import { join } from 'path'; import { readFile, unlink, writeFile } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import updatePkg from '../../../lib/hexo/update_package'; describe('Update package.json', () => { const hexo = new Hexo(__dirname, {silent: true}); const packagePath = join(hexo.base_dir, 'package.json'); beforeEach(() => { hexo.env.init = false; }); it('package.json does not exist', async () => { await updatePkg(hexo); hexo.env.init.should.be.false; }); it('package.json exists, but the version doesn\'t match', async () => { const pkg = { hexo: { version: '0.0.1' } }; await writeFile(packagePath, JSON.stringify(pkg)); await updatePkg(hexo); const content = await readFile(packagePath); JSON.parse(content).hexo.version.should.eql(hexo.version); hexo.env.init.should.be.true; await unlink(packagePath); }); it('package.json exists, but don\'t have hexo data', async () => { const pkg = { name: 'hexo', version: '0.0.1' }; await writeFile(packagePath, JSON.stringify(pkg)); await updatePkg(hexo); const content = await readFile(packagePath); // Don't change the original package.json JSON.parse(content).should.eql(pkg); hexo.env.init.should.be.false; await unlink(packagePath); }); it('package.json exists and everything is ok', async () => { const pkg = { hexo: { version: hexo.version } }; await writeFile(packagePath, JSON.stringify(pkg)); await updatePkg(hexo); const content = await readFile(packagePath); JSON.parse(content).should.eql(pkg); hexo.env.init.should.be.true; await unlink(packagePath); }); }); ================================================ FILE: test/scripts/hexo/validate_config.ts ================================================ import { spy } from 'sinon'; import Hexo from '../../../lib/hexo'; import validateConfig from '../../../lib/hexo/validate_config'; import defaultConfig from '../../../lib/hexo/default_config'; import chai from 'chai'; const should = chai.should(); describe('Validate config', () => { const hexo = new Hexo(); let logSpy; beforeEach(() => { logSpy = spy(); hexo.config = JSON.parse(JSON.stringify(defaultConfig)); hexo.log.warn = logSpy; hexo.log.info = spy(); }); it('config.url - undefined', () => { delete(hexo.config as any).url; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "url" should be string, not undefined!'); } }); it('config.url - wrong type', () => { // @ts-expect-error hexo.config.url = true; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "url" should be string, not boolean!'); } }); it('config.url - empty', () => { hexo.config.url = ' '; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "url" should be a valid URL!'); } }); it('config.url - not start with xx://', () => { // @ts-ignore hexo.config.url = 'localhost:4000'; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "url" should be a valid URL!'); } }); // #4510 it('config.url - slash', () => { hexo.config.url = '/'; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "url" should be a valid URL!'); } }); it('config.root - undefined', () => { delete(hexo.config as any).root; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "root" should be string, not undefined!'); } }); it('config.root - wrong type', () => { // @ts-expect-error hexo.config.root = true; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "root" should be string, not boolean!'); } }); it('config.root - empty', () => { hexo.config.root = ' '; try { validateConfig(hexo); should.fail(); } catch (e) { e.name.should.eql('TypeError'); e.message.should.eql('Invalid config detected: "root" should not be empty!'); } }); }); ================================================ FILE: test/scripts/models/asset.ts ================================================ import { join } from 'path'; import Hexo from '../../../lib/hexo'; describe('Asset', () => { const hexo = new Hexo(); const Asset = hexo.model('Asset'); it('default values', async () => { const data = await Asset.insert({ _id: 'foo', path: 'bar' }); data.modified.should.be.true; Asset.removeById(data._id); }); it('_id - required', async () => { try { await Asset.insert({}); } catch (err) { err.message.should.eql('ID is not defined'); } }); it('path - required', async () => { try { await Asset.insert({ _id: 'foo' }); } catch (err) { err.message.should.eql('`path` is required!'); } }); it('source - virtual', async () => { const data = await Asset.insert({ _id: 'foo', path: 'bar' }); data.source.should.eql(join(hexo.base_dir, data._id)); Asset.removeById(data._id); }); }); ================================================ FILE: test/scripts/models/cache.ts ================================================ import Hexo from '../../../lib/hexo'; describe('Cache', () => { const hexo = new Hexo(); const Cache = hexo.model('Cache'); it('_id - required', async () => { try { await Cache.insert({}); } catch (err) { err.message.should.eql('ID is not defined'); } }); }); ================================================ FILE: test/scripts/models/category.ts ================================================ import { deepMerge, full_url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import defaults from '../../../lib/hexo/default_config'; describe('Category', () => { const hexo = new Hexo(); const Category = hexo.model('Category'); const Post = hexo.model('Post'); const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category; const PostCategory = hexo.model('PostCategory'); before(() => hexo.init()); beforeEach(() => { hexo.config = deepMerge({}, defaults); }); it('name - required', async () => { try { await Category.insert({}); } catch (err) { err.message.should.eql('`name` is required!'); } }); // it('parent - reference'); missing-unit-test it('slug - virtual', async () => { const data = await Category.insert({ name: 'foo' }); data.slug.should.eql('foo'); Category.removeById(data._id); }); it('slug - category_map', async () => { hexo.config.category_map = { test: 'wat' }; const data = await Category.insert({ name: 'test' }); data.slug.should.eql('wat'); Category.removeById(data._id); }); it('slug - filename_case: 0', async () => { const data = await Category.insert({ name: 'WahAHa' }); data.slug.should.eql('WahAHa'); Category.removeById(data._id); }); it('slug - filename_case: 1', async () => { hexo.config.filename_case = 1; const data = await Category.insert({ name: 'WahAHa' }); data.slug.should.eql('wahaha'); Category.removeById(data._id); }); it('slug - filename_case: 2', async () => { hexo.config.filename_case = 2; const data = await Category.insert({ name: 'WahAHa' }); data.slug.should.eql('WAHAHA'); Category.removeById(data._id); }); it('slug - parent', async () => { let cat = await Category.insert({ name: 'parent' }); cat = await Category.insert({ name: 'child', parent: cat._id }); cat.slug.should.eql('parent/child'); await Promise.all([ Category.removeById(cat._id), Category.removeById(cat.parent) ]); }); it('path - virtual', async () => { const data = await Category.insert({ name: 'foo' }); data.path.should.eql(hexo.config.category_dir + '/' + data.slug + '/'); Category.removeById(data._id); }); it('permalink - virtual', async () => { const data = await Category.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path); Category.removeById(data._id); }); it('permalink - trailing_index', async () => { hexo.config.pretty_urls.trailing_index = false; const data = await Category.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\.html$/, '')); Category.removeById(data._id); }); it('permalink - trailing_html', async () => { hexo.config.pretty_urls.trailing_html = false; const data = await Category.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\.html$/, '')); Category.removeById(data._id); }); it('permalink - should be encoded', async () => { hexo.config.url = 'http://fôo.com'; const data = await Category.insert({ name: '字' }); data.permalink.should.eql(full_url_for.call(hexo, data.path)); Category.removeById(data._id); }); it('posts - virtual', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo'}, {source: 'bar.md', slug: 'bar'}, {source: 'baz.md', slug: 'baz'} ]); await Promise.all(posts.map(post => post.setCategories(['foo']))); const cat = await Category.findOne({name: 'foo'}); function mapper(post) { return post._id; } hexo.locals.invalidate(); cat.posts.map(mapper).should.eql(posts.map(mapper)); cat.should.have.lengthOf(posts.length); await cat.remove(); await Promise.all(posts.map(post => post.remove())); }); it('posts - draft', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo', published: true}, {source: 'bar.md', slug: 'bar', published: false}, {source: 'baz.md', slug: 'baz', published: true} ]); await Promise.all(posts.map(post => post.setCategories(['foo']))); let cat = Category.findOne({name: 'foo'}); function mapper(post) { return post._id; } // draft off hexo.locals.invalidate(); cat.posts.eq(0)._id.should.eql(posts[0]._id); cat.posts.eq(1)._id.should.eql(posts[2]._id); cat.should.have.lengthOf(2); // draft on hexo.config.render_drafts = true; await Promise.all(posts.map(post => post.setCategories(['foo']))); hexo.locals.invalidate(); cat = Category.findOne({name: 'foo'}); cat.posts.map(mapper).should.eql(posts.map(mapper)); cat.should.have.lengthOf(posts.length); hexo.config.render_drafts = false; await cat.remove(); await Promise.all(posts.map(post => post.remove())); }); it('posts - future', async () => { const now = Date.now(); const posts = await Post.insert([ {source: 'foo.md', slug: 'foo', date: now - 3600}, {source: 'bar.md', slug: 'bar', date: now + 3600}, {source: 'baz.md', slug: 'baz', date: now} ]); await Promise.all(posts.map(post => post.setCategories(['foo']))); let cat = Category.findOne({name: 'foo'}); function mapper(post) { return post._id; } // future on hexo.config.future = true; hexo.locals.invalidate(); cat.posts.map(mapper).should.eql(posts.map(mapper)); cat.should.have.lengthOf(posts.length); // future off hexo.config.future = false; await Promise.all(posts.map(post => post.setCategories(['foo']))); hexo.locals.invalidate(); cat = Category.findOne({name: 'foo'}); cat.posts.eq(0)._id.should.eql(posts[0]._id); cat.posts.eq(1)._id.should.eql(posts[2]._id); cat.should.have.lengthOf(2); await cat.remove(); await Promise.all(posts.map(post => post.remove())); }); it('check whether a category exists', async () => { const data = await Category.insert({ name: 'foo' }); try { await Category.insert({ name: 'foo' }); } catch (err) { err.message.should.eql('Category `foo` has already existed!'); } Category.removeById(data._id); }); it('check whether a category exists (with parent)', async () => { let data = await Category.insert({ name: 'foo' }); data = await Category.insert({ name: 'foo', parent: data._id }); await Promise.all([ Category.removeById(data._id), Category.removeById(data.parent) ]); }); it('remove PostCategory references when a category is removed', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo'}, {source: 'bar.md', slug: 'bar'}, {source: 'baz.md', slug: 'baz'} ]); await Promise.all(posts.map(post => post.setCategories(['foo']))); const cat = Category.findOne({name: 'foo'}); await Category.removeById(cat._id!); PostCategory.find({category_id: cat._id}).should.have.lengthOf(0); ReadOnlyPostCategory.find({category_id: cat._id}).should.have.lengthOf(0); await Promise.all(posts.map(post => post.remove())); }); }); ================================================ FILE: test/scripts/models/moment.ts ================================================ import moment from 'moment-timezone'; import SchemaTypeMoment from '../../../lib/models/types/moment'; import chai from 'chai'; const should = chai.should(); describe('SchemaTypeMoment', () => { const type = new SchemaTypeMoment('test'); it('cast()', () => { type.cast(1e8).should.eql(moment(1e8)); type.cast(new Date(2014, 1, 1)).should.eql(moment(new Date(2014, 1, 1))); type.cast('2014-11-03T07:45:41.237Z').should.eql(moment('2014-11-03T07:45:41.237Z')); type.cast(moment(1e8)).valueOf().should.eql(1e8); }); it('cast() - default', () => { const type = new SchemaTypeMoment('test', {default: moment}); moment.isMoment(type.cast()).should.be.true; }); function shouldThrowError(value) { should.throw( () => type.validate(value), '`' + value + '` is not a valid date!' ); } it('validate()', () => { type.validate(moment(1e8)).valueOf().should.eql(1e8); shouldThrowError(moment.invalid()); }); it('validate() - required', () => { const type = new SchemaTypeMoment('test', {required: true}); // @ts-expect-error should.throw(() => type.validate(), '`test` is required!'); }); it('match()', () => { type.match(moment(1e8), moment(1e8)).should.be.true; type.match(moment(1e8), moment(1e8 + 1)).should.be.false; type.match(undefined, moment()).should.be.false; }); it('compare()', () => { type.compare(moment([2014, 1, 3]), moment([2014, 1, 2])).should.gt(0); type.compare(moment([2014, 1, 1]), moment([2014, 1, 2])).should.lt(0); type.compare(moment([2014, 1, 2]), moment([2014, 1, 2])).should.eql(0); type.compare(moment()).should.eql(1); type.compare(undefined, moment()).should.eql(-1); type.compare().should.eql(0); }); it('parse()', () => { type.parse('2014-11-03T07:45:41.237Z')!.should.eql(moment('2014-11-03T07:45:41.237Z')); should.not.exist(type.parse()); }); it('value()', () => { type.value(moment('2014-11-03T07:45:41.237Z')).should.eql('2014-11-03T07:45:41.237Z'); should.not.exist(type.value()); }); it('q$day()', () => { type.q$day(moment([2014, 1, 1]), 1).should.be.true; type.q$day(moment([2014, 1, 1]), 5).should.be.false; type.q$day(undefined, 1).should.be.false; }); it('q$month()', () => { type.q$month(moment([2014, 1, 1]), 1).should.be.true; type.q$month(moment([2014, 1, 1]), 5).should.be.false; type.q$month(undefined, 1).should.be.false; }); it('q$year()', () => { type.q$year(moment([2014, 1, 1]), 2014).should.be.true; type.q$year(moment([2014, 1, 1]), 1999).should.be.false; type.q$year(undefined, 1).should.be.false; }); it('u$inc()', () => { type.u$inc(moment(1e8), 1).valueOf().should.eql(1e8 + 1); // @ts-expect-error should.not.exist(undefined, 1); }); it('u$dec()', () => { type.u$dec(moment(1e8), 1).valueOf().should.eql(1e8 - 1); // @ts-expect-error should.not.exist(undefined, 1); }); }); ================================================ FILE: test/scripts/models/page.ts ================================================ import { join } from 'path'; import { deepMerge, full_url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import defaults from '../../../lib/hexo/default_config'; import chai from 'chai'; const should = chai.should(); describe('Page', () => { const hexo = new Hexo(); const Page = hexo.model('Page'); beforeEach(() => { hexo.config = deepMerge({}, defaults); }); it('default values', async () => { const now = Date.now(); const data = await Page.insert({ source: 'foo', path: 'bar' }); data.title.should.eql(''); data.date.valueOf().should.gte(now); data.comments.should.be.true; data.layout.should.eql('page'); data._content.should.eql(''); data.raw.should.eql(''); should.not.exist(data.updated); should.not.exist(data.content); should.not.exist(data.excerpt); should.not.exist(data.more); Page.removeById(data._id); }); it('source - required', async () => { try { await Page.insert({}); } catch (err) { err.message.should.eql('`source` is required!'); } }); it('path - required', async () => { try { await Page.insert({ source: 'foo' }); } catch (err) { err.message.should.eql('`path` is required!'); } }); it('permalink - virtual', async () => { const data = await Page.insert({ source: 'foo', path: 'bar' }); data.permalink.should.eql(hexo.config.url + '/' + data.path); Page.removeById(data._id); }); it('permalink - trailing_index', async () => { hexo.config.pretty_urls.trailing_index = false; const data = await Page.insert({ source: 'foo.md', path: 'bar/index.html' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\.html$/, '')); Page.removeById(data._id); }); it('permalink - trailing_html', async () => { hexo.config.pretty_urls.trailing_html = false; const data = await Page.insert({ source: 'foo.md', path: 'bar/foo.html' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\.html$/, '')); Page.removeById(data._id); }); it('permalink - trailing_html - index.html', async () => { hexo.config.pretty_urls.trailing_html = false; const data = await Page.insert({ source: 'foo.md', path: 'bar/foo/index.html' }); data.permalink.should.eql(hexo.config.url + '/' + data.path); Page.removeById(data._id); }); it('permalink - should be encoded', async () => { hexo.config.url = 'http://fôo.com'; const path = 'bár'; const data = await Page.insert({ source: 'foo', path }); data.permalink.should.eql(full_url_for.call(hexo, data.path)); Page.removeById(data._id); }); it('full_source - virtual', async () => { const data = await Page.insert({ source: 'foo', path: 'bar' }); data.full_source.should.eql(join(hexo.source_dir, data.source)); Page.removeById(data._id); }); }); ================================================ FILE: test/scripts/models/post.ts ================================================ import { join, sep } from 'path'; import BluebirdPromise from 'bluebird'; import { full_url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Post', () => { const hexo = new Hexo(); const Post = hexo.model('Post'); const Tag = hexo.model('Tag'); const Category = hexo.model('Category'); const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag; const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category; const PostTag = hexo.model('PostTag'); const PostCategory = hexo.model('PostCategory'); const Asset = hexo.model('Asset'); before(() => { hexo.config.permalink = ':title'; return hexo.init(); }); it('default values', () => { const now = Date.now(); return Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.title.should.eql(''); data.date.valueOf().should.gte(now); data.comments.should.be.true; data.layout.should.eql('post'); data._content.should.eql(''); data.raw.should.eql(''); data.published.should.be.true; should.not.exist(data.updated); should.not.exist(data.content); should.not.exist(data.excerpt); should.not.exist(data.more); return Post.removeById(data._id); }); }); it('source - required', () => { return Post.insert({}).then(() => { should.fail('Return value must be rejected'); }, err => { err.should.have.property('message', '`source` is required!'); }); }); it('slug - required', () => { return Post.insert({ source: 'foo.md' }).then(() => { should.fail('Return value must be rejected'); }, err => { err.should.have.property('message', '`slug` is required!'); }); }); it('path - virtual', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.path.should.eql(data.slug); return Post.removeById(data._id); })); it('permalink - virtual', () => { hexo.config.root = '/'; return Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path); return Post.removeById(data._id); }); }); it('permalink - should be encoded', () => { const slug = 'bár'; hexo.config.url = 'http://fôo.com'; return Post.insert({ source: 'foo.md', slug }).then(data => { data.permalink.should.eql(full_url_for.call(hexo, slug)); hexo.config.url = 'http://example.com'; return Post.removeById(data._id); }); }); it('permalink - virtual - when set relative_link', () => { hexo.config.root = '/'; hexo.config.relative_link = true; return Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path); hexo.config.relative_link = false; return Post.removeById(data._id); }); }); it('permalink_root_prefix - virtual', () => { hexo.config.url = 'http://example.com/root'; hexo.config.root = '/root/'; return Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.permalink.should.eql('http://example.com/root/' + data.path); return Post.removeById(data._id); }); }); it('permalink_root_prefix - virtual - when set relative_link', () => { hexo.config.url = 'http://example.com/root'; hexo.config.root = '/root/'; hexo.config.relative_link = true; return Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path); return Post.removeById(data._id); }); }); it('permalink - trailing_index', () => { hexo.config.pretty_urls.trailing_index = false; return Post.insert({ source: 'foo.md', slug: 'bar/index.html' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\.html$/, '')); hexo.config.pretty_urls.trailing_index = true; return Post.removeById(data._id); }); }); it('permalink - trailing_html', () => { hexo.config.pretty_urls.trailing_html = false; return Post.insert({ source: 'foo.md', slug: 'bar/foo.html' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\.html$/, '')); hexo.config.pretty_urls.trailing_html = true; return Post.removeById(data._id); }); }); it('permalink - trailing_html - index.html', () => { hexo.config.pretty_urls.trailing_html = false; return Post.insert({ source: 'foo.md', slug: 'bar/index.html' }).then(data => { data.permalink.should.eql(hexo.config.url + '/' + data.path); hexo.config.pretty_urls.trailing_html = true; return Post.removeById(data._id); }); }); it('full_source - virtual', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.full_source.should.eql(join(hexo.source_dir, data.source)); return Post.removeById(data._id); })); it('asset_dir - virtual', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(data => { data.asset_dir.should.eql(join(hexo.source_dir, 'foo') + sep); return Post.removeById(data._id); })); it('tags - virtual', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setTags(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => { post.tags.map(tag => tag.name).should.have.members(['bar', 'baz', 'foo']); return Post.removeById(post._id); })); it('categories - virtual', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setCategories(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => { const cats = post.categories; // Make sure the order of categories is correct cats.map((cat, i) => { // Make sure the parent reference is correct if (i) { cat.parent.should.eql(cats.eq(i - 1)._id); } else { should.not.exist(cat.parent); } return cat.name; }).should.eql(['foo', 'bar', 'baz']); return Post.removeById(post._id); })); it('setTags() - old tags should be removed', () => { let id; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { id = post._id; return post.setTags(['foo', 'bar']); }).then(() => { const post = Post.findById(id); return post.setTags(['bar', 'baz']); }).then(() => { const post = Post.findById(id); post.tags.map(tag => tag.name).should.eql(['bar', 'baz']); return Post.removeById(id); }); }); it('setTags() - sync problem', () => Post.insert([ {source: 'foo.md', slug: 'foo'}, {source: 'bar.md', slug: 'bar'} ]).then(posts => BluebirdPromise.all([ posts[0].setTags(['foo', 'bar']), posts[1].setTags(['bar', 'baz']) ]).thenReturn(posts)).then(posts => { Tag.map(tag => tag.name).should.have.members(['foo', 'bar', 'baz']); return posts; }).map((post: any) => Post.removeById(post._id))); it('setTags() - empty tag', () => { let id; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { id = post._id; return post.setTags(['', undefined, null, false, 0, 'normal']); }).then(() => { const post = Post.findById(id); post.tags.map(tag => tag.name).should.eql(['false', '0', 'normal']); }).finally(() => Post.removeById(id)); }); it('setCategories() - old categories should be removed', () => { let id; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { id = post._id; return post.setCategories(['foo', 'bar']); }).then(() => { const post = Post.findById(id); return post.setCategories(['foo', 'baz']); }).then(() => { const post = Post.findById(id); post.categories.map(cat => cat.name).should.eql(['foo', 'baz']); return Post.removeById(id); }); }); it('setCategories() - shared category should be same', () => { let postIdA, postIdB; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { postIdA = post._id; return post.setCategories(['foo', 'bar']); }).then(() => Post.insert({ source: 'bar.md', slug: 'bar' }).then(post => { postIdB = post._id; return post.setCategories(['foo', 'bar']); })).then(() => { const postA = Post.findById(postIdA); const postB = Post.findById(postIdB); postA.categories.map(cat => cat._id).should.eql(postB.categories.map(cat => cat._id)); return BluebirdPromise.all([ Post.removeById(postIdA), Post.removeById(postIdB) ]); }); }); it('setCategories() - category not shared should be different', () => { let postIdA, postIdB; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { postIdA = post._id; return post.setCategories(['foo', 'bar']); }).then(() => Post.insert({ source: 'bar.md', slug: 'bar' }).then(post => { postIdB = post._id; return post.setCategories(['baz', 'bar']); })).then(() => { const postA = Post.findById(postIdA); const postB = Post.findById(postIdB); const postCategoriesA = postA.categories.map(cat => cat._id); const postCategoriesB = postB.categories.map(cat => cat._id); postCategoriesA.forEach(catId => { postCategoriesB.should.not.include(catId); }); postCategoriesB.forEach(catId => { postCategoriesA.should.not.include(catId); }); return BluebirdPromise.all([ Post.removeById(postIdA), Post.removeById(postIdB) ]); }); }); it('setCategories() - empty category', () => { let id; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { id = post._id; return post.setCategories(['test', null]); }).then(() => { const post = Post.findById(id); post.categories.map(cat => cat.name).should.eql(['test']); }).finally(() => Post.removeById(id)); }); it('setCategories() - empty category in middle', () => { let id; return Post.insert({ source: 'foo.md', slug: 'foo' }).then(post => { id = post._id; return post.setCategories(['foo', null, 'bar']); }).then(() => { const post = Post.findById(id); post.categories.map(cat => cat.name).should.eql(['foo', 'bar']); }).finally(() => Post.removeById(id)); }); it('setCategories() - multiple hierarchies', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setCategories([['foo', '', 'bar'], '', 'baz']) .thenReturn(Post.findById(post._id))).then(post => { const cats = post.categories.toArray(); // There should have been 3 categories set; blanks eliminated cats.should.have.lengthOf(3); // Category 1 should be foo, no parent cats[0].name.should.eql('foo'); should.not.exist(cats[0].parent); // Category 2 should be bar, foo as parent cats[1].name.should.eql('bar'); cats[1].parent.should.eql(cats[0]._id); // Category 3 should be baz, no parent cats[2].name.should.eql('baz'); should.not.exist(cats[2].parent); return Post.removeById(post._id); })); it('setCategories() - multiple hierarchies (dedupes repeated parent)', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setCategories([['foo', 'bar'], ['foo', 'baz']]) .thenReturn(Post.findById(post._id))).then(post => { const cats = post.categories.toArray(); // There should have been 3 categories set (foo is dupe) cats.should.have.lengthOf(3); return Post.removeById(post._id); })); it('remove PostTag references when a post is removed', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setTags(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => { PostTag.find({post_id: post._id}).should.have.lengthOf(0); ReadOnlyPostTag.find({post_id: post._id}).should.have.lengthOf(0); Tag.findOne({name: 'foo'}).posts.should.have.lengthOf(0); Tag.findOne({name: 'bar'}).posts.should.have.lengthOf(0); Tag.findOne({name: 'baz'}).posts.should.have.lengthOf(0); })); it('remove PostCategory references when a post is removed', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => post.setCategories(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => { PostCategory.find({post_id: post._id}).should.have.lengthOf(0); ReadOnlyPostCategory.find({post_id: post._id}).should.have.lengthOf(0); Category.findOne({name: 'foo'}).posts.should.have.lengthOf(0); Category.findOne({name: 'bar'}).posts.should.have.lengthOf(0); Category.findOne({name: 'baz'}).posts.should.have.lengthOf(0); })); it('remove related assets when a post is removed', () => Post.insert({ source: 'foo.md', slug: 'bar' }).then(post => BluebirdPromise.all([ Asset.insert({_id: 'foo', path: 'foo'}), Asset.insert({_id: 'bar', path: 'bar'}), Asset.insert({_id: 'baz', path: 'bar'}) ]).thenReturn(post)).then(post => Post.removeById(post._id)).then(post => { Asset.find({post: post._id}).should.have.lengthOf(0); })); }); ================================================ FILE: test/scripts/models/post_asset.ts ================================================ import { join, posix } from 'path'; import Hexo from '../../../lib/hexo'; import defaults from '../../../lib/hexo/default_config'; describe('PostAsset', () => { const hexo = new Hexo(); const PostAsset = hexo.model('PostAsset'); const Post = hexo.model('Post'); let post; before(async () => { await hexo.init(); post = await Post.insert({ source: 'foo.md', slug: 'bar' }); }); beforeEach(() => { hexo.config = Object.assign({}, defaults); }); it('default values', async () => { const data = await PostAsset.insert({ _id: 'foo', slug: 'foo', post: post._id }); data.modified.should.be.true; PostAsset.removeById(data._id); }); it('_id - required', async () => { try { await PostAsset.insert({}); } catch (err) { err.message.should.eql('ID is not defined'); } }); it('slug - required', async () => { try { await PostAsset.insert({ _id: 'foo' }); } catch (err) { err.message.should.eql('`slug` is required!'); } }); it('path - virtual', async () => { const data = await PostAsset.insert({ _id: 'source/_posts/test/foo.jpg', slug: 'foo.jpg', post: post._id }); data.path.should.eql(posix.join(post.path, data.slug)); PostAsset.removeById(data._id); }); it('path - virtual - when permalink is .html', async () => { hexo.config.permalink = ':year/:month/:day/:title.html'; const data = await PostAsset.insert({ _id: 'source/_posts/test/foo.html', slug: 'foo.htm', post: post._id }); data.path.should.eql(posix.join(post.path, data.slug)); PostAsset.removeById(data._id); }); it('path - virtual - when permalink is .htm', async () => { hexo.config.permalink = ':year/:month/:day/:title.htm'; const data = await PostAsset.insert({ _id: 'source/_posts/test/foo.htm', slug: 'foo.htm', post: post._id }); data.path.should.eql(posix.join(post.path, data.slug)); PostAsset.removeById(data._id); }); it('path - virtual - when permalink contains .htm not in the end', async () => { hexo.config.permalink = ':year/:month/:day/:title/.htm-foo/'; const data = await PostAsset.insert({ _id: 'source/_posts/test/foo.html', slug: 'foo.html', post: post._id }); data.path.should.eql(posix.join(post.path + '.htm-foo/', data.slug)); PostAsset.removeById(data._id); }); it('source - virtual', async () => { const data = await PostAsset.insert({ _id: 'source/_posts/test/foo.jpg', slug: 'foo.jpg', post: post._id }); data.source.should.eql(join(hexo.base_dir, data._id)); PostAsset.removeById(data._id); }); }); ================================================ FILE: test/scripts/models/tag.ts ================================================ import { deepMerge, full_url_for } from 'hexo-util'; import Hexo from '../../../lib/hexo'; import defaults from '../../../lib/hexo/default_config'; describe('Tag', () => { const hexo = new Hexo(); const Tag = hexo.model('Tag'); const Post = hexo.model('Post'); const PostTag = hexo.model('PostTag'); const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag; before(() => hexo.init()); beforeEach(() => { hexo.config = deepMerge({}, defaults); }); it('name - required', async () => { try { await Tag.insert({}); } catch (err) { err.message.should.be.eql('`name` is required!'); } }); it('slug - virtual', async () => { const data = await Tag.insert({ name: 'foo' }); data.slug.should.eql('foo'); Tag.removeById(data._id); }); it('slug - tag_map', async () => { hexo.config.tag_map = { test: 'wat' }; const data = await Tag.insert({ name: 'test' }); data.slug.should.eql('wat'); Tag.removeById(data._id); }); it('slug - filename_case: 0', async () => { const data = await Tag.insert({ name: 'WahAHa' }); data.slug.should.eql('WahAHa'); Tag.removeById(data._id); }); it('slug - filename_case: 1', async () => { hexo.config.filename_case = 1; const data = await Tag.insert({ name: 'WahAHa' }); data.slug.should.eql('wahaha'); Tag.removeById(data._id); }); it('slug - filename_case: 2', async () => { hexo.config.filename_case = 2; const data = await Tag.insert({ name: 'WahAHa' }); data.slug.should.eql('WAHAHA'); Tag.removeById(data._id); }); it('path - virtual', async () => { const data = await Tag.insert({ name: 'foo' }); data.path.should.eql(hexo.config.tag_dir + '/' + data.slug + '/'); Tag.removeById(data._id); }); it('permalink - virtual', async () => { const data = await Tag.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path); Tag.removeById(data._id); }); it('permalink - trailing_index', async () => { hexo.config.pretty_urls.trailing_index = false; const data = await Tag.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\.html$/, '')); Tag.removeById(data._id); }); it('permalink - trailing_html', async () => { hexo.config.pretty_urls.trailing_html = false; const data = await Tag.insert({ name: 'foo' }); data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\.html$/, '')); Tag.removeById(data._id); }); it('permalink - should be encoded', async () => { hexo.config.url = 'http://fôo.com'; const data = await Tag.insert({ name: '字' }); data.permalink.should.eql(full_url_for.call(hexo, data.path)); Tag.removeById(data._id); }); it('posts - virtual', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo'}, {source: 'bar.md', slug: 'bar'}, {source: 'baz.md', slug: 'baz'} ]); await Promise.all(posts.map(post => post.setTags(['foo']))); const tag = Tag.findOne({name: 'foo'}); function mapper(post) { return post._id; } hexo.locals.invalidate(); tag.posts.map(mapper).should.eql(posts.map(mapper)); tag.should.have.lengthOf(posts.length); await tag.remove(); await Promise.all(posts.map(post => post.remove())); }); it('posts - draft', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo', published: true}, {source: 'bar.md', slug: 'bar', published: false}, {source: 'baz.md', slug: 'baz', published: true} ]); await Promise.all(posts.map(post => post.setTags(['foo']))); let tag = Tag.findOne({name: 'foo'}); function mapper(post) { return post._id; } // draft off hexo.locals.invalidate(); tag.posts.eq(0)._id.should.eql(posts[0]._id); tag.posts.eq(1)._id.should.eql(posts[2]._id); tag.should.have.lengthOf(2); // draft on hexo.config.render_drafts = true; await Promise.all(posts.map(post => post.setTags(['foo']))); tag = Tag.findOne({name: 'foo'}); hexo.locals.invalidate(); tag.posts.map(mapper).should.eql(posts.map(mapper)); tag.should.have.lengthOf(posts.length); hexo.config.render_drafts = false; await tag.remove(); await Promise.all(posts.map(post => post.remove())); }); it('posts - future', async () => { const now = Date.now(); const posts = await Post.insert([ {source: 'foo.md', slug: 'foo', date: now - 3600}, {source: 'bar.md', slug: 'bar', date: now + 3600}, {source: 'baz.md', slug: 'baz', date: now} ]); await Promise.all(posts.map(post => post.setTags(['foo']))); let tag = Tag.findOne({name: 'foo'}); function mapper(post) { return post._id; } // future on hexo.config.future = true; hexo.locals.invalidate(); tag.posts.map(mapper).should.eql(posts.map(mapper)); tag.should.have.lengthOf(posts.length); // future off hexo.config.future = false; await Promise.all(posts.map(post => post.setTags(['foo']))); hexo.locals.invalidate(); tag = Tag.findOne({name: 'foo'}); tag.posts.eq(0)._id.should.eql(posts[0]._id); tag.posts.eq(1)._id.should.eql(posts[2]._id); tag.should.have.lengthOf(2); await tag.remove(); await Promise.all(posts.map(post => post.remove())); }); it('check whether a tag exists', async () => { let data = await Tag.insert({ name: 'foo' }); try { data = await Tag.insert({ name: 'foo' }); } catch (err) { err.message.should.eql('Tag `foo` has already existed!'); } Tag.removeById(data._id); }); it('remove PostTag references when a tag is removed', async () => { const posts = await Post.insert([ {source: 'foo.md', slug: 'foo'}, {source: 'bar.md', slug: 'bar'}, {source: 'baz.md', slug: 'baz'} ]); await Promise.all(posts.map(post => post.setTags(['foo']))); const tag = Tag.findOne({name: 'foo'}); await Tag.removeById(tag._id!); PostTag.find({tag_id: tag._id}).should.have.lengthOf(0); ReadOnlyPostTag.find({tag_id: tag._id}).should.have.lengthOf(0); await Promise.all(posts.map(post => Post.removeById(post._id))); }); }); ================================================ FILE: test/scripts/processors/asset.ts ================================================ import { dirname, join } from 'path'; import { mkdirs, rmdir, stat, unlink, writeFile } from 'hexo-fs'; import { spy } from 'sinon'; import Hexo from '../../../lib/hexo'; import defaults from '../../../lib/hexo/default_config'; import assets from '../../../lib/plugins/processor/asset'; import chai from 'chai'; const should = chai.should(); const dateFormat = 'YYYY-MM-DD HH:mm:ss'; describe('asset', () => { const baseDir = join(__dirname, 'asset_test'); const hexo = new Hexo(baseDir); const asset = assets(hexo); const process = asset.process.bind(hexo); const { pattern } = asset; const { source } = hexo; const { File } = source; const Asset = hexo.model('Asset'); const Page = hexo.model('Page'); function newFile(options) { options.source = join(source.base, options.path); options.params = { renderable: options.renderable }; return new File(options); } before(async () => { await mkdirs(baseDir); await hexo.init(); }); beforeEach(() => { hexo.config = Object.assign({}, defaults); }); after(() => rmdir(baseDir)); it('pattern', () => { // Renderable files pattern.match('foo.json').should.have.property('renderable', true); // Non-renderable files pattern.match('foo.txt').should.have.property('renderable', false); // Tmp files should.not.exist(pattern.match('foo.txt~')); should.not.exist(pattern.match('foo.txt%')); // Hidden files should.not.exist(pattern.match('_foo.txt')); should.not.exist(pattern.match('test/_foo.txt')); should.not.exist(pattern.match('.foo.txt')); should.not.exist(pattern.match('test/.foo.txt')); // Include files hexo.config.include = ['fff/**']; pattern.match('fff/_foo.txt').should.exist; hexo.config.include = []; // Exclude files hexo.config.exclude = ['fff/**']; should.not.exist(pattern.match('fff/foo.txt')); hexo.config.exclude = []; // Skip render files hexo.config.skip_render = ['fff/**']; pattern.match('fff/foo.json').should.have.property('renderable', false); hexo.config.skip_render = []; }); it('asset - type: create', async () => { const file = newFile({ path: 'foo.jpg', type: 'create', renderable: false }); await writeFile(file.source, 'foo'); await process(file); const id = 'source/' + file.path; const asset = Asset.findById(id); asset._id.should.eql(id); asset.path.should.eql(file.path); asset.modified.should.be.true; asset.renderable.should.be.false; return Promise.all([ asset.remove(), unlink(file.source) ]); }); it('asset - type: create (when source path is configured to parent directory)', async () => { const file = newFile({ path: '../../source/foo.jpg', type: 'create', renderable: false }); await writeFile(file.source, 'foo'); await process(file); const id = '../source/foo.jpg'; // The id should a relative path,because the 'lib/models/assets.js' use asset path by joining base path with "_id" directly. const asset = Asset.findById(id); asset._id.should.eql(id); asset.path.should.eql(file.path); asset.modified.should.be.true; asset.renderable.should.be.false; asset.remove(); await unlink(file.source); rmdir(dirname(file.source)); }); it('asset - type: update', async () => { const file = newFile({ path: 'foo.jpg', type: 'update', renderable: false }); const id = 'source/' + file.path; await Promise.all([ writeFile(file.source, 'test'), Asset.insert({ _id: id, path: file.path, modified: false }) ]); await process(file); const asset = Asset.findById(id); asset._id.should.eql(id); asset.path.should.eql(file.path); asset.modified.should.be.true; asset.renderable.should.be.false; return Promise.all([ asset.remove(), unlink(file.source) ]); }); it('asset - type: skip', async () => { const file = newFile({ path: 'foo.jpg', type: 'skip', renderable: false }); const id = 'source/' + file.path; await Promise.all([ writeFile(file.source, 'test'), Asset.insert({ _id: id, path: file.path, modified: false }) ]); await process(file); const asset = Asset.findById(id); asset.modified.should.be.false; await Promise.all([ Asset.removeById(id), unlink(file.source) ]); }); it('asset - type: delete', async () => { const file = newFile({ path: 'foo.jpg', type: 'delete', renderable: false }); const id = 'source/' + file.path; await Asset.insert({ _id: id, path: file.path }); await process(file); should.not.exist(Asset.findById(id)); }); it('asset - type: delete - not exist', async () => { const file = newFile({ path: 'foo.jpg', type: 'delete', renderable: false }); const id = 'source/' + file.path; await process(file); should.not.exist(Asset.findById(id)); }); it('page - type: create', async () => { const body = [ 'title: "Hello world"', 'date: 2006-01-02 15:04:05', 'updated: 2014-12-13 01:02:03', '---', 'The quick brown fox jumps over the lazy dog' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.title.should.eql('Hello world'); page.date.format(dateFormat).should.eql('2006-01-02 15:04:05'); page.updated.format(dateFormat).should.eql('2014-12-13 01:02:03'); page._content.should.eql('The quick brown fox jumps over the lazy dog'); page.source.should.eql(file.path); page.raw.should.eql(body); page.path.should.eql('hello.html'); page.layout.should.eql('page'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - type: create - exist', async () => { const logSpy = spy(); hexo.log.warn = logSpy; const body = [ 'title: "Hello world"', 'date: 2006-01-02 15:04:05', 'updated: 2014-12-13 01:02:03', '---', 'The quick brown fox jumps over the lazy dog' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); await process(file); const page = Page.findOne({ source: file.path }); page.title.should.eql('Hello world'); page.date.format(dateFormat).should.eql('2006-01-02 15:04:05'); page.updated.format(dateFormat).should.eql('2014-12-13 01:02:03'); page._content.should.eql('The quick brown fox jumps over the lazy dog'); page.source.should.eql(file.path); page.raw.should.eql(body); page.path.should.eql('hello.html'); page.layout.should.eql('page'); logSpy.called.should.be.true; logSpy.args[0][0].should.contains('Trying to "create" \x1B[35mhello.njk\x1B[39m, but the file already exists!'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - type: update', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'update', renderable: true }); const doc = await Page.insert({ source: file.path, path: 'hello.html' }); await writeFile(file.source, body); const id = doc._id; await process(file); const page = Page.findOne({ source: file.path }); page._id!.should.eql(id); page.title.should.eql('Hello world'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - type: skip', async () => { const file = newFile({ path: 'hello.njk', type: 'skip', renderable: true }); await Page.insert({ source: file.path, path: 'hello.html' }); const page = Page.findOne({source: file.path}); await process(file); should.exist(page); await Promise.all([ page.remove() ]); }); it('page - type: delete', async () => { const file = newFile({ path: 'hello.njk', type: 'delete', renderable: true }); await Page.insert({ source: file.path, path: 'hello.html' }); await process(file); should.not.exist(Page.findOne({ source: file.path })); }); it('page - type: delete - not exist', async () => { const file = newFile({ path: 'hello.njk', type: 'delete', renderable: true }); await process(file); should.not.exist(Page.findOne({ source: file.path })); }); it('page - use the status of the source file if date not set', async () => { const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, ''); await process(file); const stats = await stat(file.source); const page = Page.findOne({source: file.path}); page.date.toDate().should.eql(stats.ctime); page.updated.toDate().should.eql(stats.mtime); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - use the date for updated if updated_option = date', async () => { const body = [ 'date: 2011-4-5 14:19:19', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); hexo.config.updated_option = 'date'; await writeFile(file.source, body); await process(file); const stats = await stat(file.source); const page = Page.findOne({source: file.path}); page.updated.toDate().should.eql(page.date.toDate()); page.updated.toDate().should.not.eql(stats.mtime); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - use the status if updated_option = mtime', async () => { const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); hexo.config.updated_option = 'mtime'; await writeFile(file.source, ''); await process(file); const stats = await stat(file.source); const page = Page.findOne({source: file.path}); page.date.toDate().should.eql(stats.ctime); page.updated.toDate().should.eql(stats.mtime); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - updated shouldn\'t exists if updated_option = empty', async () => { const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); hexo.config.updated_option = 'empty'; await writeFile(file.source, ''); await process(file); const stats = await stat(file.source); const page = Page.findOne({source: file.path}); page.date.toDate().should.eql(stats.ctime); should.not.exist(page.updated); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - permalink', async () => { const body = [ 'title: "Hello world"', 'permalink: foo.html', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.path.should.eql('foo.html'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - permalink (without extension name)', async () => { const body = [ 'title: "Hello world"', 'permalink: foo', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.path.should.eql('foo.html'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - permalink (with trailing slash)', async () => { const body = [ 'title: "Hello world"', 'permalink: foo/', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.path.should.eql('foo/index.html'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - set layout to false if output is not html', async () => { const body = 'foo: 1'; const file = newFile({ path: 'test.yml', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.layout.should.eql('false'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - don\'t set layout to false if layout is set but output is not html', async () => { const body = [ 'layout: something', '---', 'foo: 1' ].join('\n'); const file = newFile({ path: 'test.yml', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.layout.should.eql('something'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - parse date', async () => { const body = [ 'title: "Hello world"', 'date: Apr 24 2014', 'updated: May 5 2015', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.date.format(dateFormat).should.eql('2014-04-24 00:00:00'); page.updated.format(dateFormat).should.eql('2015-05-05 00:00:00'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - use file stats instead if date is invalid', async () => { const body = [ 'title: "Hello world"', 'date: yomama', 'updated: isfat', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const stats = await file.stat(); const page = Page.findOne({source: file.path}); page.date.toDate().should.eql(stats.ctime); page.updated.toDate().should.eql(page.date.toDate()); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - don\'t remove extension name', async () => { const body = ''; const file = newFile({ path: 'test.min.js', type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const page = Page.findOne({ source: file.path }); page.path.should.eql('test.min.js'); await Promise.all([ page.remove(), unlink(file.source) ]); }); it('page - timezone', async () => { const body = [ 'title: "Hello world"', 'date: Apr 24 2014', 'updated: May 5 2015', '---' ].join('\n'); const file = newFile({ path: 'hello.njk', type: 'create', renderable: true }); hexo.config.timezone = 'UTC'; await writeFile(file.source, body); await process(file); const page = Page.findOne({source: file.path}); page.date.utc().format(dateFormat).should.eql('2014-04-24 00:00:00'); page.updated.utc().format(dateFormat).should.eql('2015-05-05 00:00:00'); await Promise.all([ page.remove(), unlink(file.source) ]); }); }); ================================================ FILE: test/scripts/processors/common.ts ================================================ import moment from 'moment'; import { isTmpFile, isHiddenFile, toDate, adjustDateForTimezone, isMatch } from '../../../lib/plugins/processor/common'; import chai from 'chai'; const should = chai.should(); describe('common', () => { it('isTmpFile()', () => { isTmpFile('foo').should.be.false; isTmpFile('foo%').should.be.true; isTmpFile('foo~').should.be.true; }); it('isHiddenFile()', () => { isHiddenFile('foo').should.be.false; isHiddenFile('_foo').should.be.true; isHiddenFile('foo/_bar').should.be.true; isHiddenFile('.foo').should.be.true; isHiddenFile('foo/.bar').should.be.true; }); it('toDate()', () => { const m = moment(); const d = new Date(); should.not.exist(toDate()); toDate(m)!.should.eql(m); toDate(d)!.should.eql(d); toDate(1e8)!.should.eql(new Date(1e8)); toDate('2014-04-25T01:32:21.196Z')!.should.eql(new Date('2014-04-25T01:32:21.196Z')); toDate('Apr 24 2014')!.should.eql(new Date(2014, 3, 24)); should.not.exist(toDate('foo')); }); it('timezone() - date', () => { const d = new Date(Date.UTC(1972, 2, 29, 0, 0, 0)); const d_timezone_UTC = adjustDateForTimezone(d, 'UTC').getTime(); (adjustDateForTimezone(d, 'Asia/Shanghai').getTime() - d_timezone_UTC).should.eql(-8 * 3600 * 1000); (adjustDateForTimezone(d, 'Asia/Bangkok').getTime() - d_timezone_UTC).should.eql(-7 * 3600 * 1000); (adjustDateForTimezone(d, 'America/Los_Angeles').getTime() - d_timezone_UTC).should.eql(8 * 3600 * 1000); }); it('timezone() - moment', () => { const d = moment(new Date(Date.UTC(1972, 2, 29, 0, 0, 0))); const d_timezone_UTC = adjustDateForTimezone(d, 'UTC').getTime(); (adjustDateForTimezone(d, 'Europe/Moscow').getTime() - d_timezone_UTC).should.eql(-3 * 3600 * 1000); }); it('isMatch() - string', () => { // String isMatch('foo/test.html', 'foo/*.html').should.be.true; isMatch('foo/test.html', 'bar/*.html').should.be.false; // Array isMatch('foo/test.html', []).should.be.false; isMatch('foo/test.html', ['foo/*.html']).should.be.true; isMatch('foo/test.html', ['bar/*.html', 'foo/*.html']).should.be.true; isMatch('foo/test.html', ['bar/*.html', 'baz/*.html']).should.be.false; // Undefined isMatch('foo/test.html').should.be.false; }); }); ================================================ FILE: test/scripts/processors/data.ts ================================================ import BluebirdPromise from 'bluebird'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import { join } from 'path'; import Hexo from '../../../lib/hexo'; import data from '../../../lib/plugins/processor/data'; import chai from 'chai'; const should = chai.should(); describe('data', () => { const baseDir = join(__dirname, 'data_test'); const hexo = new Hexo(baseDir); const processor = data(hexo); const process = BluebirdPromise.method(processor.process).bind(hexo); const { source } = hexo; const { File } = source; const Data = hexo.model('Data'); function newFile(options) { const path = options.path; options.params = { path }; options.path = '_data/' + path; options.source = join(source.base, options.path); return new File(options); } before(async () => { await mkdirs(baseDir); hexo.init(); }); after(() => rmdir(baseDir)); it('pattern', () => { const pattern = processor.pattern; pattern.match('_data/users.json').should.eql({ 0: '_data/users.json', 1: 'users.json', path: 'users.json' }); pattern.match('_data/users.yaml').should.eql({ 0: '_data/users.yaml', 1: 'users.yaml', path: 'users.yaml' }); should.not.exist(pattern.match('users.json')); }); it('type: create - yaml', async () => { const body = 'foo: bar'; const file = newFile({ path: 'users.yml', type: 'create' }); await writeFile(file.source, body); await process(file); const data = Data.findById('users'); data.data.should.eql({foo: 'bar'}); data.remove(); unlink(file.source); }); it('type: create - json', async () => { const body = '{"foo": 1}'; const file = newFile({ path: 'users.json', type: 'create' }); await writeFile(file.source, body); await process(file); const data = Data.findById('users'); data.data.should.eql({foo: 1}); data.remove(); unlink(file.source); }); it('type: create - others', async () => { const file = newFile({ path: 'users.txt', type: 'create' }); await writeFile(file.source, 'text'); await process(file); const data = Data.findById('users'); data.data.should.eql('text'); data.remove(); unlink(file.source); }); it('type: update', async () => { const body = 'foo: bar'; const file = newFile({ path: 'users.yml', type: 'update' }); await BluebirdPromise.all([ writeFile(file.source, body), Data.insert({ _id: 'users', data: {} }) ]); await process(file); const data = Data.findById('users'); data.data.should.eql({foo: 'bar'}); data.remove(); unlink(file.source); }); it('type: skip', async () => { const file = newFile({ path: 'users.yml', type: 'skip' }); await Data.insert({ _id: 'users', data: {foo: 'bar'} }); const data = Data.findById('users'); await process(file); should.exist(data); data.remove(); }); it('type: delete', async () => { const file = newFile({ path: 'users.yml', type: 'delete' }); await Data.insert({ _id: 'users', data: {foo: 'bar'} }); await process(file); should.not.exist(Data.findById('users')); }); it('type: delete - not exist', async () => { const file = newFile({ path: 'users.yml', type: 'delete' }); await process(file); should.not.exist(Data.findById('users')); }); }); ================================================ FILE: test/scripts/processors/post.ts ================================================ import { join } from 'path'; import { exists, mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import defaultConfig from '../../../lib/hexo/default_config'; import Hexo from '../../../lib/hexo'; import posts from '../../../lib/plugins/processor/post'; import chai from 'chai'; const should = chai.should(); type PostParams = Parameters['process']> type PostReturn = ReturnType['process']> const dateFormat = 'YYYY-MM-DD HH:mm:ss'; describe('post', () => { const baseDir = join(__dirname, 'post_test'); const hexo = new Hexo(baseDir); const post = posts(hexo); const process: (...args: PostParams) => BluebirdPromise = BluebirdPromise.method(post.process.bind(hexo)); const { pattern } = post; const { source } = hexo; const { File } = source; const PostAsset = hexo.model('PostAsset'); const Post = hexo.model('Post'); function newFile(options) { const { path } = options; options.path = (options.published ? '_posts' : '_drafts') + '/' + path; options.source = join(source.base, options.path); options.params = { published: options.published, path, renderable: options.renderable }; return new File(options); } before(async () => { await mkdirs(baseDir); hexo.init(); }); beforeEach(() => { hexo.config = Object.assign({}, defaultConfig); }); after(() => rmdir(baseDir)); it('pattern', () => { // Renderable files pattern.match('_posts/foo.html').should.eql({ published: true, path: 'foo.html', renderable: true }); pattern.match('_drafts/bar.html').should.eql({ published: false, path: 'bar.html', renderable: true }); // Non-renderable files pattern.match('_posts/foo.txt').should.eql({ published: true, path: 'foo.txt', renderable: false }); pattern.match('_drafts/bar.txt').should.eql({ published: false, path: 'bar.txt', renderable: false }); // Tmp files should.not.exist(pattern.match('_posts/foo.html~')); should.not.exist(pattern.match('_posts/foo.html%')); // Hidden files should.not.exist(pattern.match('_posts/_foo.html')); should.not.exist(pattern.match('_posts/foo/_bar.html')); should.not.exist(pattern.match('_posts/.foo.html')); should.not.exist(pattern.match('_posts/foo/.bar.html')); // Outside "_posts" and "_drafts" folder should.not.exist(pattern.match('_foo/bar.html')); should.not.exist(pattern.match('baz.html')); // Skip render files hexo.config.skip_render = ['_posts/foo/**']; pattern.match('_posts/foo/bar.html').should.have.property('renderable', false); hexo.config.skip_render = []; // Skip render in the subdir assets if post_asset_folder is enabled hexo.config.post_asset_folder = true; pattern.match('_posts/foo/subdir/bar.html').should.have.property('renderable', false); pattern.match('_posts/foo/subdir/bar.css').should.have.property('renderable', false); pattern.match('_posts/foo/subdir/bar.js').should.have.property('renderable', false); hexo.config.post_asset_folder = false; // Render in the subdir assets if post_asset_folder is disabled pattern.match('_posts/foo/subdir/bar.html').should.have.property('renderable', true); pattern.match('_posts/foo/subdir/bar.css').should.have.property('renderable', true); pattern.match('_posts/foo/subdir/bar.js').should.have.property('renderable', true); }); it('asset - post_asset_folder disabled', async () => { hexo.config.post_asset_folder = false; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'create', renderable: false }); await process(file); const id = 'source/' + file.path; should.not.exist(PostAsset.findById(id)); }); it('asset - type: create', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'create', renderable: false }); const doc = await Post.insert({ source: '_posts/foo.html', slug: 'foo' }); await writeFile(file.source, 'test'); const postId = doc._id; await process(file); const id = 'source/' + file.path; const asset = PostAsset.findById(id); asset._id.should.eql(id); asset.post.should.eql(postId); asset.modified.should.be.true; asset.renderable.should.be.false; await BluebirdPromise.all([ Post.removeById(postId), unlink(file.source) ]); }); it('asset - type: update', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'update', renderable: false }); const id = 'source/' + file.path; const post = await Post.insert({ source: '_posts/foo.html', slug: 'foo' }); await writeFile(file.source, 'test'); const postId = post._id; await PostAsset.insert({ _id: id, slug: file.path, modified: false, post: postId }); await process(file); const asset = PostAsset.findById(id); asset.modified.should.be.true; await BluebirdPromise.all([ Post.removeById(postId), unlink(file.source) ]); }); it('asset - type: skip', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'skip', renderable: false }); const id = 'source/' + file.path; const post = await Post.insert({ source: '_posts/foo.html', slug: 'foo' }); await writeFile(file.source, 'test'); const postId = post._id; await PostAsset.insert({ _id: id, slug: file.path, modified: false, post: postId }); await process(file); const asset = PostAsset.findById(id); asset.modified.should.be.false; await BluebirdPromise.all([ Post.removeById(postId), unlink(file.source) ]); }); it('asset - type: delete', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'delete', renderable: false }); const id = 'source/' + file.path; const post = await Post.insert({ source: '_posts/foo.html', slug: 'foo' }); const postId = post._id; await PostAsset.insert({ _id: id, slug: file.path, modified: false, post: postId }); await process(file); should.not.exist(PostAsset.findById(id)); Post.removeById(postId); }); it('asset - type: delete - not exist', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'delete', renderable: false }); const id = 'source/' + file.path; const post = await Post.insert({ source: '_posts/foo.html', slug: 'foo' }); const postId = post._id; await process(file); should.not.exist(PostAsset.findById(id)); Post.removeById(postId); }); it('asset - skip if can\'t find a matching post', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'create', renderable: false }); const id = 'source/' + file.path; await writeFile(file.source, 'test'); await process(file); should.not.exist(PostAsset.findById(id)); unlink(file.source); }); it('asset - the related post has been deleted', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo/bar.jpg', published: true, type: 'update', renderable: false }); const id = 'source/' + file.path; await BluebirdPromise.all([ writeFile(file.source, 'test'), PostAsset.insert({ _id: id, slug: file.path }) ]); await process(file); should.not.exist(PostAsset.findById(id)); unlink(file.source); }); it('post - type: create', async () => { const body = [ 'title: "Hello world"', 'date: 2006-01-02 15:04:05', 'updated: 2014-12-13 01:02:03', '---', 'The quick brown fox jumps over the lazy dog' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.title.should.eql('Hello world'); post.date.format(dateFormat).should.eql('2006-01-02 15:04:05'); post.updated.format(dateFormat).should.eql('2014-12-13 01:02:03'); post._content.should.eql('The quick brown fox jumps over the lazy dog'); post.source.should.eql(file.path); post.raw.should.eql(body); post.slug.should.eql('foo'); post.published.should.be.true; return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - type: create - post_asset_folder enabled without asset', async () => { hexo.config.post_asset_folder = true; const fooPath = join(hexo.source_dir, '_posts', 'foo'); if (await exists(fooPath)) { await rmdir(fooPath); } const body = [ 'title: "Hello world"', 'date: 2006-01-02 15:04:05', 'updated: 2014-12-13 01:02:03', '---', 'The quick brown fox jumps over the lazy dog' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.title.should.eql('Hello world'); post.date.format(dateFormat).should.eql('2006-01-02 15:04:05'); post.updated.format(dateFormat).should.eql('2014-12-13 01:02:03'); post._content.should.eql('The quick brown fox jumps over the lazy dog'); post.source.should.eql(file.path); post.raw.should.eql(body); post.slug.should.eql('foo'); post.published.should.be.true; hexo.config.post_asset_folder = false; return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - type: update', async () => { const body = [ 'title: "New world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'update', renderable: true }); const doc = await Post.insert({ source: file.path, slug: 'foo' }); await writeFile(file.source, body); const id = doc._id; await process(file); const post = Post.findOne({ source: file.path }); post._id!.should.eql(id); post.title.should.eql('New world'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - type: skip', async () => { const file = newFile({ path: 'foo.html', published: true, type: 'skip', renderable: true }); await Post.insert({ source: file.path, slug: 'foo' }); await process(file); const post = Post.findOne({ source: file.path }); should.exist(post); post.remove(); }); it('post - type: delete - not exist', async () => { const file = newFile({ path: 'foo.html', published: true, type: 'delete', renderable: true }); await process(file); should.not.exist(Post.findOne({ source: file.path })); }); it('post - type: delete', async () => { const file = newFile({ path: 'foo.html', published: true, type: 'delete', renderable: true }); await Post.insert({ source: file.path, slug: 'foo' }); await process(file); should.not.exist(Post.findOne({ source: file.path })); }); it('post - type: delete - not exist', async () => { const file = newFile({ path: 'foo.html', published: true, type: 'delete', renderable: true }); await process(file); should.not.exist(Post.findOne({ source: file.path })); }); it('post - parse file name', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: '2006/01/02/foo.html', published: true, type: 'create', renderable: true }); hexo.config.new_post_name = ':year/:month/:day/:title'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.slug.should.eql('foo'); post.date.format('YYYY-MM-DD').should.eql('2006-01-02'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - parse unusual file name', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: '20060102.html', published: true, type: 'create', renderable: true }); hexo.config.new_post_name = ':year:month:day'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.slug.should.eql('20060102'); post.date.format('YYYY-MM-DD').should.eql('2006-01-02'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - extra data in file name', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'zh/foo.html', published: true, type: 'create', renderable: true }); hexo.config.new_post_name = ':lang/:title'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.lang.should.eql('zh'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - file name does not match to the config', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); hexo.config.new_post_name = ':year/:month/:day/:title'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.slug.should.eql('foo'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - published', async () => { const body = [ 'title: "Hello world"', 'published: false', '---' ].join('\n'); const file = newFile({ path: 'zh/foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.published.should.be.false; return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - always set published: false for drafts', async () => { const body = [ 'title: "Hello world"', 'published: true', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: false, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.published.should.be.false; return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - use the status of the source file if date not set', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); const stats = await file.stat(); await process(file); const post = Post.findOne({ source: file.path }); post.date.toDate().setMilliseconds(0).should.eql(stats.birthtime.setMilliseconds(0)); post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0)); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - use the date for updated if updated_option = date', async () => { const body = [ 'date: 2011-4-5 14:19:19', 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); hexo.config.updated_option = 'date'; await writeFile(file.source, body); const stats = await file.stat(); await process(file); const post = Post.findOne({ source: file.path }); post.updated.toDate().setMilliseconds(0).should.eql(post.date.toDate().setMilliseconds(0)); post.updated.toDate().setMilliseconds(0).should.not.eql(stats.mtime.setMilliseconds(0)); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - use the status of the source file if updated_option = mtime', async () => { const body = [ 'date: 2011-4-5 14:19:19', 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); hexo.config.updated_option = 'mtime'; await writeFile(file.source, body); const stats = await file.stat(); await process(file); const post = Post.findOne({ source: file.path }); post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0)); post.updated.toDate().setMilliseconds(0).should.not.eql(post.date.toDate().setMilliseconds(0)); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - updated shouldn\'t exists if updated_option = empty', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); hexo.config.updated_option = 'empty'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); should.not.exist(post.updated); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - photo is an alias for photos', async () => { const body = [ 'title: "Hello world"', 'photo:', '- https://hexo.io/foo.jpg', '- https://hexo.io/bar.png', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.photos.should.eql([ 'https://hexo.io/foo.jpg', 'https://hexo.io/bar.png' ]); should.not.exist(post.photo); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - photos (not array)', async () => { const body = [ 'title: "Hello world"', 'photos: https://hexo.io/foo.jpg', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.photos.should.eql([ 'https://hexo.io/foo.jpg' ]); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - without title', async () => { const body = ''; const file = newFile({ path: 'foo.md', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.title.should.eql(''); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); // use `slug` as `title` of post when `title` is not specified. // https://github.com/hexojs/hexo/issues/5372 it('post - without title - use filename', async () => { hexo.config.use_slug_as_post_title = true; const body = ''; const file = newFile({ path: 'bar.md', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.title.should.eql('bar'); return Promise.all([ post.remove(), unlink(file.source) ]); }); it('post - category is an alias for categories', async () => { const body = [ 'title: "Hello world"', 'category:', '- foo', '- bar', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); should.not.exist(post.category); post.categories.map(item => item.name).should.eql(['foo', 'bar']); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - categories (not array)', async () => { const body = [ 'title: "Hello world"', 'categories: foo', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.categories.map(item => item.name).should.eql(['foo']); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - categories (multiple hierarchies)', async () => { const body = [ 'title: "Hello world"', 'categories:', '- foo', '- [bar, baz]', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.categories.map(item => item.name).should.eql(['foo', 'bar', 'baz']); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - tag is an alias for tags', async () => { const body = [ 'title: "Hello world"', 'tag:', '- foo', '- bar', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); should.not.exist(post.tag); post.tags.map(item => item.name).should.have.members(['foo', 'bar']); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - tags (not array)', async () => { const body = [ 'title: "Hello world"', 'tags: foo', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.tags.map(item => item.name).should.eql(['foo']); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - post_asset_folder enabled', async () => { hexo.config.post_asset_folder = true; hexo.config.exclude = ['**.png']; hexo.config.include = ['**/_fizz.*']; const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); const assetFiles = [ 'bar.jpg', 'baz.png', '_fizz.jpg', '_buzz.jpg' ].map(filename => { const id = `source/_posts/foo/${filename}`; const path = join(hexo.base_dir, id); const contents = filename.replace(/\.\w+$/, ''); return { id, path, contents }; }); await BluebirdPromise.all([ writeFile(file.source, body), ...assetFiles.map(obj => writeFile(obj.path, obj.contents)) ]); await process(file); const post = Post.findOne({ source: file.path }); const assets = assetFiles.map(obj => PostAsset.findById(obj.id)); [assets[0]].should.not.eql([undefined]); assets[0]._id.should.eql(assetFiles[0].id); assets[0].post.should.eql(post._id); assets[0].modified.should.be.true; [assets[1]].should.eql([undefined]); [assets[2]].should.not.eql([undefined]); assets[2]._id.should.eql(assetFiles[2].id); assets[2].post.should.eql(post._id); assets[2].modified.should.be.true; [assets[3]].should.eql([undefined]); post.remove(); await BluebirdPromise.all([ unlink(file.source), ...assetFiles.map(obj => unlink(obj.path)) ]); }); it('post - post_asset_folder enabled with unpublished posts', async () => { hexo.config.post_asset_folder = true; const body = [ 'title: "Hello world"', 'published: false', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); const assetId = 'source/_posts/foo/bar.jpg'; const assetPath = join(hexo.base_dir, assetId); await BluebirdPromise.all([ writeFile(file.source, body), writeFile(assetPath, '') ]); // drafts disabled - no draft assets should be generated await process(file); const post = Post.findOne({ source: file.path }); post.published.should.be.false; should.not.exist(PostAsset.findById(assetId)); // drafts enabled - all assets should be generated hexo.config.render_drafts = true; await process(file); should.exist(PostAsset.findById(assetId)); hexo.config.render_drafts = false; await BluebirdPromise.all([ post.remove(), unlink(file.source), unlink(assetPath) ]); }); it('asset - post_asset_folder enabled with hot processing', async () => { hexo.config.post_asset_folder = true; const [post1, post2] = await Promise.all([ Post.insert({ source: '_posts/foo.html', slug: 'foo' }), Post.insert({ source: '_posts/bar.html', slug: 'bar' }) ]); const firstAsset = newFile({ path: 'bar/image1.jpg', published: true, type: 'create', renderable: false }); await writeFile(firstAsset.source, 'test1'); // cold processing await process(firstAsset); const firstAssetId = 'source/_posts/bar/image1.jpg'; const firstAssetRecord = PostAsset.findById(firstAssetId); firstAssetRecord._id.should.eql(firstAssetId); firstAssetRecord.post.should.eql(post2._id); firstAssetRecord.slug.should.eql('image1.jpg'); const secondAsset = newFile({ path: 'bar/image2.jpg', published: true, type: 'create', renderable: false }); await writeFile(secondAsset.source, 'test2'); // hot processing await process(secondAsset); const secondAssetId = 'source/_posts/bar/image2.jpg'; const secondAssetRecord = PostAsset.findById(secondAssetId); secondAssetRecord._id.should.eql(secondAssetId); secondAssetRecord.post.should.eql(post2._id); secondAssetRecord.slug.should.eql('image2.jpg'); secondAssetRecord.modified.should.be.true; hexo.config.post_asset_folder = false; await BluebirdPromise.all([ Post.removeById(post1._id), Post.removeById(post2._id), unlink(firstAsset.source), unlink(secondAsset.source) ]); }); it('post - delete existing draft assets if draft posts are hidden', async () => { hexo.config.post_asset_folder = true; const body = [ 'title: "Hello world"', 'published: false', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); const assetId = 'source/_posts/foo/bar.jpg'; const assetPath = join(hexo.base_dir, assetId); await BluebirdPromise.all([ writeFile(file.source, body), writeFile(assetPath, '') ]); // drafts disabled - no draft assets should be generated await process(file); const post = Post.findOne({ source: file.path }); await PostAsset.insert({ _id: 'source/_posts/foo/bar.jpg', slug: 'bar.jpg', post: post._id }); await process(file); post.published.should.be.false; should.not.exist(PostAsset.findById(assetId)); await BluebirdPromise.all([ post.remove(), unlink(file.source), unlink(assetPath) ]); }); it('post - post_asset_folder disabled', async () => { hexo.config.post_asset_folder = false; const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); const assetId = 'source/_posts/foo/bar.jpg'; const assetPath = join(hexo.base_dir, assetId); await BluebirdPromise.all([ writeFile(file.source, ''), writeFile(assetPath, '') ]); await process(file); const post = Post.findOne({ source: file.path }); should.not.exist(PostAsset.findById(assetId)); post.remove(); await BluebirdPromise.all([ unlink(file.source), unlink(assetPath) ]); }); it('post - parse date', async () => { const body = [ 'title: "Hello world"', 'date: Apr 24 2014', 'updated: May 5 2015', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.date.format(dateFormat).should.eql('2014-04-24 00:00:00'); post.updated.format(dateFormat).should.eql('2015-05-05 00:00:00'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - use file stats instead if date is invalid', async () => { const body = [ 'title: "Hello world"', 'date: yomama', 'updated: isfat', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); const stats = await file.stat(); await process(file); const post = Post.findOne({ source: file.path }); post.date.toDate().setMilliseconds(0).should.eql(stats.birthtime.setMilliseconds(0)); post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0)); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('post - timezone', async () => { const body = [ 'title: "Hello world"', 'date: 2014-04-24', 'updated: 2015-05-05', '---' ].join('\n'); const file = newFile({ path: 'foo.html', published: true, type: 'create', renderable: true }); hexo.config.timezone = 'UTC'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.date.utc().format(dateFormat).should.eql('2014-04-24 00:00:00'); post.updated.utc().format(dateFormat).should.eql('2015-05-05 00:00:00'); post.remove(); return unlink(file.source); }); it('post - new_post_name timezone', async () => { const body = [ 'title: "Hello world"', '---' ].join('\n'); const file = newFile({ path: '2006/01/02/foo.html', published: true, type: 'create', renderable: true }); hexo.config.new_post_name = ':year/:month/:day/:title'; hexo.config.timezone = 'UTC'; await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.date.utc().format(dateFormat).should.eql('2006-01-02 00:00:00'); post.remove(); unlink(file.source); }); it('post - permalink', async () => { const body = [ 'title: "Hello world"', 'permalink: foooo', '---' ].join('\n'); const file = newFile({ path: 'test.html', published: true, type: 'create', renderable: true }); await writeFile(file.source, body); await process(file); const post = Post.findOne({ source: file.path }); post.__permalink.should.eql('foooo'); return BluebirdPromise.all([ post.remove(), unlink(file.source) ]); }); it('asset - post - common render', async () => { hexo.config.post_asset_folder = true; const file = newFile({ path: 'foo.md', published: true, type: 'create', renderable: true }); const assetFile = newFile({ path: 'foo/test.yml', published: true, type: 'create' }); await BluebirdPromise.all([ writeFile(file.source, 'test'), writeFile(assetFile.source, 'test') ]); await process(file); const id = 'source/' + assetFile.path; const post = Post.findOne({ source: file.path }); PostAsset.findById(id).renderable.should.be.true; hexo.config.post_asset_folder = false; return BluebirdPromise.all([ unlink(file.source), unlink(assetFile.source), post.remove(), PostAsset.removeById(id) ]); }); it('asset - post - skip render', async () => { hexo.config.post_asset_folder = true; hexo.config.skip_render = '**.yml' as any; const file = newFile({ path: 'foo.md', published: true, type: 'create', renderable: true }); const assetFile = newFile({ path: 'foo/test.yml', published: true, type: 'create' }); await BluebirdPromise.all([ writeFile(file.source, 'test'), writeFile(assetFile.source, 'test') ]); await process(file); const id = 'source/' + assetFile.path; const post = Post.findOne({ source: file.path }); PostAsset.findById(id).renderable.should.be.false; hexo.config.post_asset_folder = false; hexo.config.skip_render = '' as any; return BluebirdPromise.all([ unlink(file.source), unlink(assetFile.source), post.remove(), PostAsset.removeById(id) ]); }); }); ================================================ FILE: test/scripts/renderers/json.ts ================================================ import r from '../../../lib/plugins/renderer/json'; describe('json', () => { it('normal', () => { const data = { foo: 1, bar: { baz: 2 } }; r({text: JSON.stringify(data)}).should.eql(data); }); }); ================================================ FILE: test/scripts/renderers/nunjucks.ts ================================================ import r from '../../../lib/plugins/renderer/nunjucks'; import { dirname, join } from 'path'; import chai from 'chai'; const _should = chai.should(); describe('nunjucks', () => { const fixturePath = join(dirname(dirname(__dirname)), 'fixtures', 'hello.njk'); it('render from string', () => { const body = [ 'Hello {{ name }}!' ].join('\n'); r({ text: body }, { name: 'world' }).should.eql('Hello world!'); }); it('render from path', () => { r({ path: fixturePath }, { name: 'world' }).should.matches(/^Hello world!\s*$/); }); it('compile from text', () => { const body = [ 'Hello {{ name }}!' ].join('\n'); const render = r.compile({ text: body }); render({ name: 'world' }).should.eql('Hello world!'); }); it('compile from an .njk file', () => { const render = r.compile({ path: fixturePath }); render({ name: 'world' }).should.eql('Hello world!\n'); }); describe('nunjucks filters', () => { const forLoop = [ '{% for x in arr | toarray %}', '{{ x }}', '{% endfor %}' ].join(''); it('toarray can iterate on Warehouse collections', () => { const data = { arr: { toArray() { return [1, 2, 3]; } } }; r({ text: forLoop }, data).should.eql('123'); }); it('toarray can iterate on plain array', () => { const data = { arr: [1, 2, 3] }; r({ text: forLoop }, data).should.eql('123'); }); it('toarray can iterate on string', () => { const data = { arr: '123' }; r({ text: forLoop }, data).should.eql('123'); }); // https://github.com/lodash/lodash/blob/master/test/toarray.test.js it('toarray can iterate on objects', () => { const data = { arr: { a: '1', b: '2', c: '3' } }; r({ text: forLoop }, data).should.eql('123'); }); it('toarray can iterate on object string', () => { const data = { arr: Object('123') }; r({ text: forLoop }, data).should.eql('123'); }); it('toarray can iterate on Map', () => { const data = { arr: new Map() }; data.arr.set('a', 1); data.arr.set('b', 2); data.arr.set('c', 3); r({ text: forLoop }, data).should.eql('123'); }); it('toarray can iterate on Set', () => { const data = { arr: new Set() }; data.arr.add(1); data.arr.add(2); data.arr.add(3); r({ text: forLoop }, data).should.eql('123'); }); it('toarray other case', () => { const data = { arr: 1 }; r({ text: forLoop }, data).should.eql(''); }); it('safedump undefined', () => { const text = [ '{{ items | safedump }}' ].join('\n'); r({ text }).should.eql('""'); }); it('safedump null', () => { const text = [ '{% set items = null %}', '{{ items | safedump }}' ].join('\n'); r({ text }).should.eql('\n""'); }); // Adapt from nunjucks test cases // https://github.com/mozilla/nunjucks/blob/9a0ce364effd28fcdb3ab922fcffa9343b7b3630/tests/filters.js#L98 it('safedump default', () => { const text = [ '{% set items = ["a", 1, { b : true}] %}', '{{ items | safedump }}' ].join('\n'); r({ text }).should.eql('\n["a",1,{"b":true}]'); }); it('safedump spacer - 2', () => { const text = [ '{% set items = ["a", 1, { b : true}] %}', '{{ items | safedump(2) }}' ].join('\n'); r({ text }).should.eql([ '', '[', ' "a",', ' 1,', ' {', ' "b": true', ' }', ']' ].join('\n')); }); it('safedump spacer - 2', () => { const text = [ '{% set items = ["a", 1, { b : true}] %}', '{{ items | safedump(2) }}' ].join('\n'); r({ text }).should.eql([ '', '[', ' "a",', ' 1,', ' {', ' "b": true', ' }', ']' ].join('\n')); }); it('safedump spacer - 4', () => { const text = [ '{% set items = ["a", 1, { b : true}] %}', '{{ items | safedump(4) }}' ].join('\n'); r({ text }).should.eql([ '', '[', ' "a",', ' 1,', ' {', ' "b": true', ' }', ']' ].join('\n')); }); it('safedump spacer - \\t', () => { const text = [ '{% set items = ["a", 1, { b : true}] %}', '{{ items | safedump(\'\t\') }}' ].join('\n'); r({ text }).should.eql([ '', '[', '\t"a",', '\t1,', '\t{', '\t\t"b": true', '\t}', ']' ].join('\n')); }); }); }); ================================================ FILE: test/scripts/renderers/plain.ts ================================================ import r from '../../../lib/plugins/renderer/plain'; describe('plain', () => { it('normal', () => { r({text: '123'}).should.eql('123'); }); }); ================================================ FILE: test/scripts/renderers/yaml.ts ================================================ import r from '../../../lib/plugins/renderer/yaml'; describe('yaml', () => { it('normal', () => { r({text: 'foo: 1'}).should.eql({foo: 1}); }); it('escape', () => { const body = [ 'foo: 1', 'bar:', '\tbaz: 3' ].join('\n'); r({text: body}).should.eql({ foo: 1, bar: { baz: 3 } }); }); }); ================================================ FILE: test/scripts/tags/asset_img.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import tagAssetImg from '../../../lib/plugins/tag/asset_img'; import chai from 'chai'; const should = chai.should(); describe('asset_img', () => { const hexo = new Hexo(__dirname); const assetImgTag = tagAssetImg(hexo); const Post = hexo.model('Post'); const PostAsset = hexo.model('PostAsset'); let post; hexo.config.permalink = ':title/'; function assetImg(args) { return assetImgTag.call(post, args.split(' ')); } before(() => hexo.init().then(() => Post.insert({ source: 'foo.md', slug: 'foo' })).then(post_ => { post = post_; return BluebirdPromise.all([ PostAsset.insert({ _id: 'bar', slug: 'bar', post: post._id }), PostAsset.insert({ _id: 'bár', slug: 'bár', post: post._id }), PostAsset.insert({ _id: 'spaced asset', slug: 'spaced asset', post: post._id }) ]); })); it('default', () => { assetImg('bar').should.eql(''); }); it('should encode path', () => { assetImg('bár').should.eql(''); }); it('default', () => { assetImg('bar "a title"').should.eql(''); }); it('with space', () => { // {% asset_img "spaced asset" "spaced title" %} assetImgTag.call(post, ['spaced asset', 'spaced title']) .should.eql(''); }); it('with alt and title', () => { assetImgTag.call(post, ['bar', '"a title"', '"an alt"']) .should.eql('an alt'); }); it('with width height alt and title', () => { assetImgTag.call(post, ['bar', '100', '200', '"a title"', '"an alt"']) .should.eql('an alt'); }); it('no slug', () => { should.not.exist(assetImg('')); }); it('asset not found', () => { should.not.exist(assetImg('boo')); }); it('with root path', () => { hexo.config.root = '/root/'; assetImg('bar').should.eql(''); }); }); ================================================ FILE: test/scripts/tags/asset_link.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import tagAssetLink from '../../../lib/plugins/tag/asset_link'; import chai from 'chai'; const should = chai.should(); describe('asset_link', () => { const hexo = new Hexo(__dirname); const assetLinkTag = tagAssetLink(hexo); const Post = hexo.model('Post'); const PostAsset = hexo.model('PostAsset'); let post; hexo.config.permalink = ':title/'; function assetLink(args) { return assetLinkTag.call(post, args.split(' ')); } before(() => hexo.init().then(() => Post.insert({ source: 'foo.md', slug: 'foo' })).then(post_ => { post = post_; return BluebirdPromise.all([ PostAsset.insert({ _id: 'bar', slug: 'bar', post: post._id }), PostAsset.insert({ _id: 'bár', slug: 'bár', post: post._id }), PostAsset.insert({ _id: 'spaced asset', slug: 'spaced asset', post: post._id }) ]); })); it('default', () => { assetLink('bar').should.eql('bar'); }); it('should encode path', () => { assetLink('bár').should.eql('bár'); }); it('title', () => { assetLink('bar Hello world').should.eql('Hello world'); }); it('should escape tag in title by default', () => { assetLink('bar "Hello" ').should.eql('"Hello" <world>'); }); it('should escape tag in title', () => { assetLink('bar "Hello" true').should.eql('"Hello" <world>'); }); it('should not escape tag in title', () => { assetLink('bar "Hello" world false').should.eql('"Hello" world'); }); it('with space', () => { // {% asset_link "spaced asset" "spaced title" %} assetLinkTag.call(post, ['spaced asset', 'spaced title']) .should.eql('spaced title'); }); it('no slug', () => { should.not.exist(assetLink('')); }); it('asset not found', () => { should.not.exist(assetLink('boo')); }); }); ================================================ FILE: test/scripts/tags/asset_path.ts ================================================ import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import tagAssetPath from '../../../lib/plugins/tag/asset_path'; import chai from 'chai'; const should = chai.should(); describe('asset_path', () => { const hexo = new Hexo(__dirname); const assetPathTag = tagAssetPath(hexo); const Post = hexo.model('Post'); const PostAsset = hexo.model('PostAsset'); let post; hexo.config.permalink = ':title/'; function assetPath(args) { return assetPathTag.call(post, args.split(' ')); } before(() => hexo.init().then(() => Post.insert({ source: 'foo.md', slug: 'foo' })).then(post_ => { post = post_; return BluebirdPromise.all([ PostAsset.insert({ _id: 'bar', slug: 'bar', post: post._id }), PostAsset.insert({ _id: 'bár', slug: 'bár', post: post._id }), PostAsset.insert({ _id: 'spaced asset', slug: 'spaced asset', post: post._id }) ]); })); it('default', () => { assetPath('bar').should.eql('/foo/bar'); }); it('should encode path', () => { assetPath('bár').should.eql('/foo/b%C3%A1r'); }); it('with space', () => { // {% asset_path "spaced asset" %} assetPathTag.call(post, ['spaced asset']) .should.eql('/foo/spaced%20asset'); }); it('no slug', () => { should.not.exist(assetPath('')); }); it('asset not found', () => { should.not.exist(assetPath('boo')); }); }); ================================================ FILE: test/scripts/tags/blockquote.ts ================================================ import Hexo from '../../../lib/hexo'; import tagBlockquote from '../../../lib/plugins/tag/blockquote'; describe('blockquote', () => { const hexo = new Hexo(__dirname); const blockquote = tagBlockquote(hexo); before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked')))); const bq = (args, content?) => blockquote(args.split(' '), content || ''); it('default', () => { const result = bq('', '123456 **bold** and *italic*'); result.should.eql('

123456 bold and italic

\n
'); }); it('author', () => { const result = bq('John Doe', ''); result.should.eql('
John Doe
'); }); it('source', () => { const result = bq('Jane Austen, Pride and Prejudice'); result.should.eql('
Jane AustenPride and Prejudice
'); }); it('link', () => { const result = bq('John Doe https://hexo.io/'); result.should.eql('
'); }); it('link title', () => { const result = bq('John Doe https://hexo.io/ Hexo'); result.should.eql('
'); }); it('titlecase', () => { hexo.config.titlecase = true; const result = bq('Jane Austen, pride and prejudice'); result.should.eql('
Jane AustenPride and Prejudice
'); hexo.config.titlecase = false; }); }); ================================================ FILE: test/scripts/tags/code.ts ================================================ import { escapeHTML, highlight as utilHighlight, prismHighlight } from 'hexo-util'; import * as cheerio from 'cheerio'; import Hexo from '../../../lib/hexo'; import tagCode from '../../../lib/plugins/tag/code'; describe('code', () => { const hexo = new Hexo(); require('../../../lib/plugins/highlight/')(hexo); const codeTag = tagCode(hexo); const fixture = [ 'if (tired && night){', ' sleep();', '}' ].join('\n'); function code(args, content) { return codeTag(args.split(' '), content); } function highlight(code, options?) { return utilHighlight(code, options || {}) .replace(/{/g, '{') .replace(/}/g, '}'); } function prism(code, options?) { return prismHighlight(code, options || {}) .replace(/{/g, '{') .replace(/}/g, '}'); } describe('highlightjs', () => { it('default', () => { const result = code('', fixture); result.should.eql(highlight(fixture)); }); it('non standard indent', () => { const nonStandardIndent = [ ' ', ' return x;', '}', '', fixture, ' ' ].join('/n'); const result = code('', nonStandardIndent); result.should.eql(highlight(nonStandardIndent)); }); it('lang', () => { const result = code('lang:js', fixture); result.should.eql(highlight(fixture, { lang: 'js' })); }); it('line_number', () => { let result = code('line_number:false', fixture); result.should.eql(highlight(fixture, { gutter: false })); result = code('line_number:true', fixture); result.should.eql(highlight(fixture, { gutter: true })); }); it('line_threshold', () => { let result = code('line_number:false line_threshold:1', fixture); result.should.eql(highlight(fixture, { gutter: false })); result = code('line_number:true line_threshold:1', fixture); result.should.eql(highlight(fixture, { gutter: true })); result = code('line_number:true line_threshold:3', fixture); result.should.eql(highlight(fixture, { gutter: false })); }); it('highlight disable', () => { const result = code('highlight:false', fixture); result.should.eql('
' + escapeHTML(fixture) + '
'); }); it('title', () => { const result = code('Hello world', fixture); result.should.eql(highlight(fixture, { caption: 'Hello world' })); }); it('uses html tag in title', () => { const result = code('Bold', fixture); result.should.eql(highlight(fixture, { caption: `${escapeHTML('Bold')}` })); }); it('link', () => { const result = code('Hello world https://hexo.io/', fixture); const expected = highlight(fixture, { caption: 'Hello worldlink' }); result.should.eql(expected); }); it('link text', () => { const result = code('Hello world https://hexo.io/ Hexo', fixture); const expected = highlight(fixture, { caption: 'Hello worldHexo' }); result.should.eql(expected); }); it('uses html tag in link text', () => { const result = code('Hello world https://hexo.io/ Bold', fixture); const expected = highlight(fixture, { caption: `Hello world${escapeHTML('Bold')}` }); result.should.eql(expected); }); it('disabled', () => { hexo.config.syntax_highlighter = ''; const result = code('', fixture); result.should.eql('
' + escapeHTML(fixture) + '
'); hexo.config.syntax_highlighter = 'highlight.js'; }); it('first_line', () => { let result = code('first_line:1234', fixture); result.should.eql(highlight(fixture, { firstLine: 1234 })); result = code('', fixture); result.should.eql(highlight(fixture, { firstLine: 1 })); }); it('mark', () => { const source = [ 'const http = require(\'http\');', '', 'const hostname = \'127.0.0.1\';', 'const port = 1337;', '', 'http.createServer((req, res) => {', ' res.writeHead(200, { \'Content-Type\': \'text/plain\' });', ' res.end(\'Hello World\n\');', '}).listen(port, hostname, () => {', ' console.log(`Server running at http://${hostname}:${port}/`);', '});' ].join('\n'); code('mark:1,7-9,11', source).should.eql(highlight(source, { mark: [1, 7, 8, 9, 11] })); code('mark:11,9-7,1', source).should.eql(highlight(source, { mark: [1, 7, 8, 9, 11] })); }); it('# lines', () => { const result = code('', fixture); const $ = cheerio.load(result); $('.gutter .line').should.have.lengthOf(3); }); it('wrap', () => { let result = code('wrap:false', fixture); result.should.eql(highlight(fixture, { wrap: false })); result = code('wrap:true', fixture); result.should.eql(highlight(fixture, { wrap: true })); }); it('language_attr', () => { const result = code('lang:js language_attr:true', fixture); result.should.eql(highlight(fixture, { lang: 'js', languageAttr: true })); }); }); describe('prismjs', () => { beforeEach(() => { hexo.config.syntax_highlighter = 'prismjs'; }); it('default', () => { const result = code('', fixture); result.should.eql(prism(fixture)); }); it('non standard indent', () => { const nonStandardIndent = [ ' ', ' return x;', '}', '', fixture, ' ' ].join('/n'); const result = code('', nonStandardIndent); result.should.eql(prism(nonStandardIndent)); }); it('lang', () => { const result = code('lang:js', fixture); result.should.eql(prism(fixture, { lang: 'js' })); }); it('line_number', () => { let result = code('line_number:false', fixture); result.should.eql(prism(fixture, { lineNumber: false })); result = code('line_number:true', fixture); result.should.eql(prism(fixture, { lineNumber: true })); }); it('line_threshold', () => { let result = code('line_number:false line_threshold:1', fixture); result.should.eql(prism(fixture, { lineNumber: false })); result = code('line_number:true line_threshold:1', fixture); result.should.eql(prism(fixture, { lineNumber: true })); result = code('line_number:true line_threshold:3', fixture); result.should.eql(prism(fixture, { lineNumber: false })); }); it('highlight disable', () => { const result = code('highlight:false', fixture); result.should.eql('
' + escapeHTML(fixture) + '
'); }); it('disabled', () => { hexo.config.syntax_highlighter = ''; const result = code('', fixture); result.should.eql('
' + escapeHTML(fixture) + '
'); hexo.config.syntax_highlighter = 'highlight.js'; }); it('first_line', () => { let result = code('first_line:1234', fixture); result.should.eql(prism(fixture, { firstLine: 1234 })); result = code('', fixture); result.should.eql(prism(fixture, { firstLine: 1 })); }); it('mark', () => { const source = [ 'const http = require(\'http\');', '', 'const hostname = \'127.0.0.1\';', 'const port = 1337;', '', 'http.createServer((req, res) => {', ' res.writeHead(200, { \'Content-Type\': \'text/plain\' });', ' res.end(\'Hello World\n\');', '}).listen(port, hostname, () => {', ' console.log(`Server running at http://${hostname}:${port}/`);', '});' ].join('\n'); code('mark:1,7-9,11', source).should.eql(prism(source, { mark: [1, 7, 8, 9, 11] })); code('mark:11,9-7,1', source).should.eql(prism(source, { mark: [1, 7, 8, 9, 11] })); }); it('title', () => { const result = code('Hello world', fixture); result.should.eql(prism(fixture, { caption: 'Hello world' })); }); }); }); ================================================ FILE: test/scripts/tags/full_url_for.ts ================================================ import * as cheerio from 'cheerio'; import tagFullUrlFor from '../../../lib/plugins/tag/full_url_for'; describe('full_url_for', () => { const ctx: any = { config: { url: 'https://example.com' } }; const fullUrlForTag = tagFullUrlFor(ctx); const fullUrlFor = args => fullUrlForTag(args.split(' ')); it('no path input', () => { const $ = cheerio.load(fullUrlFor('nopath')); $('a').attr('href')!.should.eql(ctx.config.url + '/'); $('a').html()!.should.eql('nopath'); }); it('internal url', () => { let $ = cheerio.load(fullUrlFor('index index.html')); $('a').attr('href')!.should.eql(ctx.config.url + '/index.html'); $('a').html()!.should.eql('index'); $ = cheerio.load(fullUrlFor('index /')); $('a').attr('href')!.should.eql(ctx.config.url + '/'); $('a').html()!.should.eql('index'); $ = cheerio.load(fullUrlFor('index /index.html')); $('a').attr('href')!.should.eql(ctx.config.url + '/index.html'); $('a').html()!.should.eql('index'); }); it('internal url (pretty_urls.trailing_index disabled)', () => { ctx.config.pretty_urls = { trailing_index: false }; let $ = cheerio.load(fullUrlFor('index index.html')); $('a').attr('href')!.should.eql(ctx.config.url + '/'); $('a').html()!.should.eql('index'); $ = cheerio.load(fullUrlFor('index /index.html')); $('a').attr('href')!.should.eql(ctx.config.url + '/'); $('a').html()!.should.eql('index'); }); it('external url', () => { [ 'https://hexo.io/', '//google.com/', // 'index.html' in external link should not be removed '//google.com/index.html' ].forEach(url => { const $ = cheerio.load(fullUrlFor(`external ${url}`)); $('a').attr('href')!.should.eql(url); $('a').html()!.should.eql('external'); }); }); it('only hash', () => { const $ = cheerio.load(fullUrlFor('hash #test')); $('a').attr('href')!.should.eql(ctx.config.url + '/#test'); $('a').html()!.should.eql('hash'); }); }); ================================================ FILE: test/scripts/tags/iframe.ts ================================================ import * as cheerio from 'cheerio'; import iframe from '../../../lib/plugins/tag/iframe'; describe('iframe', () => { it('url', () => { const $ = cheerio.load(iframe(['https://zespia.tw'])); $('iframe').attr('src')!.should.eql('https://zespia.tw/'); $('iframe').attr('width')!.should.eql('100%'); $('iframe').attr('height')!.should.eql('300'); $('iframe').attr('frameborder')!.should.eql('0'); $('iframe').attr('allowfullscreen')!.should.eql(''); $('iframe').attr('loading')!.should.eql('lazy'); }); it('width', () => { const $ = cheerio.load(iframe(['https://zespia.tw', '500'])); $('iframe').attr('src')!.should.eql('https://zespia.tw/'); $('iframe').attr('width')!.should.eql('500'); $('iframe').attr('height')!.should.eql('300'); $('iframe').attr('frameborder')!.should.eql('0'); $('iframe').attr('allowfullscreen')!.should.eql(''); $('iframe').attr('loading')!.should.eql('lazy'); }); it('height', () => { const $ = cheerio.load(iframe(['https://zespia.tw', '500', '600'])); $('iframe').attr('src')!.should.eql('https://zespia.tw/'); $('iframe').attr('width')!.should.eql('500'); $('iframe').attr('height')!.should.eql('600'); $('iframe').attr('frameborder')!.should.eql('0'); $('iframe').attr('allowfullscreen')!.should.eql(''); $('iframe').attr('loading')!.should.eql('lazy'); }); }); ================================================ FILE: test/scripts/tags/img.ts ================================================ import * as cheerio from 'cheerio'; import pathFn from 'path'; import Hexo from '../../../lib/hexo'; import tagImg from '../../../lib/plugins/tag/img'; describe('img', () => { const hexo = new Hexo(pathFn.join(__dirname, 'img_test')); const img = tagImg(hexo); before(() => hexo.init()); it('src', () => { const $ = cheerio.load(img(['https://placekitten.com/200/300'])); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); }); it('src //', () => { const $ = cheerio.load(img(['//placekitten.com/200/300'])); $('img').attr('src')!.should.eql('//placekitten.com/200/300'); }); it('internal src', () => { hexo.config.root = '/'; let $ = cheerio.load(img(['/images/test.jpg'])); $('img').attr('src')!.should.eql('/images/test.jpg'); hexo.config.url = 'http://example.com/root'; hexo.config.root = '/root/'; $ = cheerio.load(img(['/images/test.jpg'])); $('img').attr('src')!.should.eql('/root/images/test.jpg'); }); it('class + src', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); }); it('class + internal src', () => { hexo.config.root = '/'; let $ = cheerio.load(img('left /images/test.jpg'.split(' '))); $('img').attr('src')!.should.eql('/images/test.jpg'); $('img').attr('class')!.should.eql('left'); hexo.config.url = 'http://example.com/root'; hexo.config.root = '/root/'; $ = cheerio.load(img('left /images/test.jpg'.split(' '))); $('img').attr('src')!.should.eql('/root/images/test.jpg'); $('img').attr('class')!.should.eql('left'); }); it('multiple classes + src', () => { const $ = cheerio.load(img('left top https://placekitten.com/200/300'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left top'); }); it('multiple classes + internal src', () => { hexo.config.root = '/'; let $ = cheerio.load(img('left top /images/test.jpg'.split(' '))); $('img').attr('src')!.should.eql('/images/test.jpg'); $('img').attr('class')!.should.eql('left top'); hexo.config.url = 'http://example.com/root'; hexo.config.root = '/root/'; $ = cheerio.load(img('left top /images/test.jpg'.split(' '))); $('img').attr('src')!.should.eql('/root/images/test.jpg'); $('img').attr('class')!.should.eql('left top'); }); it('class + src + width', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); }); it('class + src + width + height', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('height')!.should.eql('300'); }); it('class + src + title', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 Place Kitten'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('title')!.should.eql('Place Kitten'); }); it('class + src + width + title', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 Place Kitten'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('title')!.should.eql('Place Kitten'); }); it('class + src + width + height + title', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 Place Kitten'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('height')!.should.eql('300'); $('img').attr('title')!.should.eql('Place Kitten'); }); it('class + src + width + height + title + alt', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" "A cute kitten"'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('height')!.should.eql('300'); $('img').attr('title')!.should.eql('Place Kitten'); $('img').attr('alt')!.should.eql('A cute kitten'); }); it('single quote in double quote', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" "A \'cute\' kitten"'.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('height')!.should.eql('300'); $('img').attr('title')!.should.eql('Place Kitten'); $('img').attr('alt')!.should.eql('A \'cute\' kitten'); }); it('double quote in single quote', () => { const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" \'A "cute" kitten\''.split(' '))); $('img').attr('src')!.should.eql('https://placekitten.com/200/300'); $('img').attr('class')!.should.eql('left'); $('img').attr('width')!.should.eql('200'); $('img').attr('height')!.should.eql('300'); $('img').attr('title')!.should.eql('Place Kitten'); $('img').attr('alt')!.should.eql('A "cute" kitten'); }); }); ================================================ FILE: test/scripts/tags/include_code.ts ================================================ import { join } from 'path'; import { rmdir, writeFile } from 'hexo-fs'; import { escapeHTML, highlight, prismHighlight } from 'hexo-util'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import tagIncludeCode from '../../../lib/plugins/tag/include_code'; import chai from 'chai'; const should = chai.should(); describe('include_code', () => { const hexo = new Hexo(join(__dirname, 'include_code_test')); require('../../../lib/plugins/highlight/')(hexo); const includeCode = BluebirdPromise.method(tagIncludeCode(hexo)) as (arg1: string[]) => BluebirdPromise; const path = join(hexo.source_dir, hexo.config.code_dir, 'test.js'); const defaultCfg = JSON.parse(JSON.stringify(hexo.config)); const fixture = [ 'if (tired && night) {', ' sleep();', '}' ].join('\n'); const code = args => includeCode(args.split(' ')); before(async () => { await writeFile(path, fixture); await hexo.init(); await hexo.load(); }); beforeEach(() => { hexo.config = JSON.parse(JSON.stringify(defaultCfg)); }); after(() => rmdir(hexo.base_dir)); describe('highlightjs', () => { it('default', async () => { hexo.config.syntax_highlighter = 'highlight.js'; const expected = highlight(fixture, { lang: 'js', caption: 'test.jsview raw' }); const result = await code('test.js'); result.should.eql(expected); }); it('title', async () => { const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world test.js'); result.should.eql(expected); }); it('uses html tag in title', async () => { const expected = highlight(fixture, { lang: 'js', caption: `${escapeHTML('Bold')}view raw` }); const result = await code('Bold test.js'); result.should.eql(expected); }); it('lang', async () => { const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js test.js'); result.should.eql(expected); }); it('language_attr', async () => { const original = hexo.config.highlight.language_attr; hexo.config.highlight.language_attr = true; const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw', languageAttr: true }); const result = await code('Hello world lang:js test.js'); result.should.eql(expected); hexo.config.highlight.language_attr = original; }); it('from', async () => { const fixture = [ '}' ].join('\n'); const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js from:3 test.js'); result.should.eql(expected); }); it('to', async () => { const fixture = [ 'if (tired && night) {', ' sleep();' ].join('\n'); const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js to:2 test.js'); result.should.eql(expected); }); it('from and to', async () => { const fixture = [ 'sleep();' ].join('\n'); const expected = highlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js from:2 to:2 test.js'); result.should.eql(expected); }); it('file not found', async () => { const result = await code('nothing'); should.not.exist(result); }); it('disabled', async () => { hexo.config.syntax_highlighter = ''; const result = await code('test.js'); result.should.eql('
' + fixture + '
'); }); }); describe('prismjs', () => { beforeEach(() => { hexo.config.syntax_highlighter = 'prismjs'; }); it('default', async () => { const expected = prismHighlight(fixture, { lang: 'js', caption: 'test.jsview raw' }); const result = await code('test.js'); result.should.eql(expected); }); it('lang', async () => { const expected = prismHighlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js test.js'); result.should.eql(expected); }); it('from', async () => { const fixture = [ '}' ].join('\n'); const expected = prismHighlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js from:3 test.js'); result.should.eql(expected); }); it('to', async () => { const fixture = [ 'if (tired && night) {', ' sleep();' ].join('\n'); const expected = prismHighlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js to:2 test.js'); result.should.eql(expected); }); it('from and to', async () => { const fixture = [ 'sleep();' ].join('\n'); const expected = prismHighlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world lang:js from:2 to:2 test.js'); result.should.eql(expected); }); it('title', async () => { const expected = prismHighlight(fixture, { lang: 'js', caption: 'Hello worldview raw' }); const result = await code('Hello world test.js'); result.should.eql(expected); }); it('uses html tag in title', async () => { const expected = prismHighlight(fixture, { lang: 'js', caption: `${escapeHTML('Bold')}view raw` }); const result = await code('Bold test.js'); result.should.eql(expected); }); it('file not found', async () => { const result = await code('nothing'); should.not.exist(result); }); it('disabled', async () => { hexo.config.syntax_highlighter = ''; const result = await code('test.js'); result.should.eql('
' + fixture + '
'); }); }); }); ================================================ FILE: test/scripts/tags/link.ts ================================================ import * as cheerio from 'cheerio'; import link from '../../../lib/plugins/tag/link'; describe('link', () => { it('text + url', () => { const $ = cheerio.load(link('Click here to Google https://google.com'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); }); it('text + url + external', () => { let $ = cheerio.load(link('Click here to Google https://google.com true'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); $('a').attr('target')!.should.eql('_blank'); $ = cheerio.load(link('Click here to Google https://google.com false'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); $('a').attr('title')!.should.eql(''); $('a').attr('target')!.should.eql(''); }); it('text + url + title', () => { const $ = cheerio.load(link('Click here to Google https://google.com Google link'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); $('a').attr('title')!.should.eql('Google link'); }); it('text + url + external + title', () => { let $ = cheerio.load(link('Click here to Google https://google.com true Google link'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); $('a').attr('target')!.should.eql('_blank'); $('a').attr('title')!.should.eql('Google link'); $ = cheerio.load(link('Click here to Google https://google.com false Google link'.split(' '))); $('a').attr('href')!.should.eql('https://google.com/'); $('a').html()!.should.eql('Click here to Google'); $('a').attr('target')!.should.eql(''); $('a').attr('title')!.should.eql('Google link'); }); }); ================================================ FILE: test/scripts/tags/post_link.ts ================================================ import Hexo from '../../../lib/hexo'; import tagPostLink from '../../../lib/plugins/tag/post_link'; import chai from 'chai'; const should = chai.should(); describe('post_link', () => { const hexo = new Hexo(__dirname); const postLink = tagPostLink(hexo); const Post = hexo.model('Post'); hexo.config.permalink = ':title/'; before(() => hexo.init().then(() => Post.insert([{ source: 'foo', slug: 'foo', title: 'Hello world' }, { source: 'title-with-tag', slug: 'title-with-tag', title: '"Hello" !' }, { source: 'fôo', slug: 'fôo', title: 'Hello world' }, { source: 'no-title', slug: 'no-title', title: '' }]))); it('default', () => { postLink(['foo']).should.eql('Hello world'); }); it('should encode path', () => { postLink(['fôo']).should.eql('Hello world'); }); it('title', () => { postLink(['foo', 'test']).should.eql('test'); }); it('no title', () => { postLink(['no-title']).should.eql('no-title'); }); it('should escape tag in title by default', () => { postLink(['title-with-tag']).should.eql('"Hello" <new world>!'); }); it('should escape tag in title', () => { postLink(['title-with-tag', 'true']).should.eql('"Hello" <new world>!'); }); it('should escape tag in custom title', () => { postLink(['title-with-tag', '', 'title', 'true']).should.eql('<test> title'); }); it('should not escape tag in title', () => { postLink(['title-with-tag', 'false']).should.eql('"Hello" !'); }); it('should not escape tag in custom title', () => { postLink(['title-with-tag', 'This is a Bold "statement"', 'false']) .should.eql('This is a Bold "statement"'); }); it('should throw if no slug', () => { should.throw(() => postLink([]), Error, /Post not found: "undefined" doesn't exist for \{% post_link %\}/); }); it('should throw if post not found', () => { should.throw(() => postLink(['bar']), Error, /Post not found: post_link bar\./); }); it('should keep hash', () => { postLink(['foo#bar']).should.eql('Hello world'); }); it('should keep subdir', () => { hexo.config.root = '/subdir/'; postLink(['foo']).should.eql('Hello world'); }); }); ================================================ FILE: test/scripts/tags/post_path.ts ================================================ import Hexo from '../../../lib/hexo'; import tagPostPath from '../../../lib/plugins/tag/post_path'; import chai from 'chai'; const should = chai.should(); describe('post_path', () => { const hexo = new Hexo(__dirname); const postPath = tagPostPath(hexo); const Post = hexo.model('Post'); hexo.config.permalink = ':title/'; before(() => hexo.init().then(() => Post.insert([{ source: 'foo', slug: 'foo' }, { source: 'fôo', slug: 'fôo' }]))); it('default', () => { postPath(['foo']).should.eql('/foo/'); }); it('should encode path', () => { postPath(['fôo']).should.eql('/f%C3%B4o/'); }); it('no slug', () => { should.not.exist(postPath([])); }); it('post not found', () => { should.not.exist(postPath(['bar'])); }); }); ================================================ FILE: test/scripts/tags/pullquote.ts ================================================ import Hexo from '../../../lib/hexo'; import tagPullquote from '../../../lib/plugins/tag/pullquote'; describe('pullquote', () => { const hexo = new Hexo(__dirname); const pullquote = tagPullquote(hexo); before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked')))); it('default', () => { const result = pullquote([], '123456 **bold** and *italic*'); result.should.eql('

123456 bold and italic

\n
'); }); it('class', () => { const result = pullquote(['foo', 'bar'], ''); result.should.eql('
'); }); }); ================================================ FILE: test/scripts/tags/url_for.ts ================================================ import * as cheerio from 'cheerio'; import tagUrlFor from '../../../lib/plugins/tag/url_for'; describe('url_for', () => { const ctx: any = { config: { url: 'https://example.com' } }; const urlForTag = tagUrlFor(ctx); const urlFor = args => urlForTag(args.split(' ')); it('should encode path', () => { ctx.config.root = '/'; let $ = cheerio.load(urlFor('foo fôo.html')); $('a').attr('href')!.should.eql('/f%C3%B4o.html'); $('a').html()!.should.eql('foo'); ctx.config.root = '/fôo/'; $ = cheerio.load(urlFor('foo bár.html')); $('a').attr('href')!.should.eql('/f%C3%B4o/b%C3%A1r.html'); $('a').html()!.should.eql('foo'); }); it('internal url (relative off)', () => { ctx.config.root = '/'; let $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('/index.html'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /')); $('a').attr('href')!.should.eql('/'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /index.html')); $('a').attr('href')!.should.eql('/index.html'); $('a').html()!.should.eql('index'); ctx.config.root = '/blog/'; $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('/blog/index.html'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /')); $('a').attr('href')!.should.eql('/blog/'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /index.html')); $('a').attr('href')!.should.eql('/blog/index.html'); $('a').html()!.should.eql('index'); }); it('internal url (relative on)', () => { ctx.config.relative_link = true; ctx.config.root = '/'; ctx.path = ''; let $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('index.html'); $('a').html()!.should.eql('index'); ctx.path = 'foo/bar/'; $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('../../index.html'); $('a').html()!.should.eql('index'); ctx.config.relative_link = false; }); it('internal url (options.relative)', () => { ctx.path = ''; let $ = cheerio.load(urlFor('index index.html true')); $('a').attr('href')!.should.eql('index.html'); $('a').html()!.should.eql('index'); ctx.config.relative_link = true; $ = cheerio.load(urlFor('index index.html false')); $('a').attr('href')!.should.eql('/index.html'); $('a').html()!.should.eql('index'); ctx.config.relative_link = false; }); it('internal url (pretty_urls.trailing_index disabled)', () => { ctx.config.pretty_urls = { trailing_index: false }; ctx.path = ''; ctx.config.root = '/'; let $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('/'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /index.html')); $('a').attr('href')!.should.eql('/'); $('a').html()!.should.eql('index'); ctx.config.root = '/blog/'; $ = cheerio.load(urlFor('index index.html')); $('a').attr('href')!.should.eql('/blog/'); $('a').html()!.should.eql('index'); $ = cheerio.load(urlFor('index /index.html')); $('a').attr('href')!.should.eql('/blog/'); $('a').html()!.should.eql('index'); }); it('external url', () => { [ 'https://hexo.io/', '//google.com/', // 'index.html' in external link should not be removed '//google.com/index.html' ].forEach(url => { const $ = cheerio.load(urlFor(`external ${url}`)); $('a').attr('href')!.should.eql(url); $('a').html()!.should.eql('external'); }); }); it('only hash', () => { const $ = cheerio.load(urlFor('hash #test')); $('a').attr('href')!.should.eql('#test'); $('a').html()!.should.eql('hash'); }); }); ================================================ FILE: test/scripts/theme/theme.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, writeFile } from 'hexo-fs'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('Theme', () => { const hexo = new Hexo(join(__dirname, 'theme_test'), {silent: true}); const themeDir = join(hexo.base_dir, 'themes', 'test'); before(async () => { await Promise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('getView()', () => { hexo.theme.setView('test.njk', ''); // With extension name hexo.theme.getView('test.njk').should.have.property('path', 'test.njk'); // Without extension name hexo.theme.getView('test').should.have.property('path', 'test.njk'); // not exist should.not.exist(hexo.theme.getView('abc.njk')); hexo.theme.removeView('test.njk'); }); it('getView() - escape backslashes', () => { hexo.theme.setView('foo/bar.njk', ''); hexo.theme.getView('foo\\bar.njk').should.have.property('path', 'foo/bar.njk'); hexo.theme.removeView('foo/bar.njk'); }); it('setView()', () => { hexo.theme.setView('test.njk', ''); const view = hexo.theme.getView('test.njk'); view.path.should.eql('test.njk'); hexo.theme.removeView('test.njk'); }); it('removeView()', () => { hexo.theme.setView('test.njk', ''); hexo.theme.removeView('test.njk'); should.not.exist(hexo.theme.getView('test.njk')); }); }); ================================================ FILE: test/scripts/theme/view.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, writeFile } from 'hexo-fs'; import moment from 'moment'; import { fake, assert as sinonAssert } from 'sinon'; import Hexo from '../../../lib/hexo'; import chai from 'chai'; const should = chai.should(); describe('View', () => { const hexo = new Hexo(join(__dirname, 'theme_test')); const themeDir = join(hexo.base_dir, 'themes', 'test'); const { compile } = Object.assign({}, hexo.extend.renderer.store.njk); hexo.env.init = true; function newView(path, data) { return new hexo.theme.View(path, data); } before(async () => { await Promise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); await hexo.init(); // Setup layout hexo.theme.setView('layout.njk', [ 'pre', '{{ body }}', 'post' ].join('\n')); }); beforeEach(() => { // Restore compile function hexo.extend.renderer.store.njk.compile = compile; }); after(() => rmdir(hexo.base_dir)); it('constructor', () => { const data = { _content: '' }; const view = newView('index.njk', data); view.path.should.eql('index.njk'); view.source.should.eql(join(themeDir, 'layout', 'index.njk')); view.data.should.eql(data); }); it('parse front-matter', () => { const body = [ 'layout: false', '---', 'content' ].join('\n'); const view = newView('index.njk', body); view.data.should.eql({ layout: false, _content: 'content' }); }); it('precompile view if possible', async () => { const body = 'Hello {{ name }}'; const view = newView('index.njk', body); view._compiledSync({ name: 'Hexo' }).should.eql('Hello Hexo'); const result = await view._compiled({ name: 'Hexo' }); result.should.eql('Hello Hexo'); }); it('generate precompiled function even if renderer does not provide compile function', async () => { // Remove compile function delete hexo.extend.renderer.store.njk.compile; const body = 'Hello {{ name }}'; const view = newView('index.njk', body); view._compiledSync({ name: 'Hexo' }).should.eql('Hello Hexo'); const result = await view._compiled({ name: 'Hexo' }); result.should.eql('Hello Hexo'); }); it('render()', async () => { const body = [ '{{ test }}' ].join('\n'); const view = newView('index.njk', body); const content = await view.render({ test: 'foo' }); content.should.eql('foo'); }); it('render() - front-matter', async () => { // The priority of front-matter is higher const body = [ 'foo: bar', '---', '{{ foo }}', '{{ test }}' ].join('\n'); const view = newView('index.njk', body); const content = await view.render({ foo: 'foo', test: 'test' }); content.should.eql('bar\ntest'); }); it('render() - helper', async () => { const body = [ '{{ date() }}' ].join('\n'); const view = newView('index.njk', body); const content = await view.render({ config: hexo.config, page: {} }); content.should.eql(moment().format(hexo.config.date_format)); }); it('render() - layout', async () => { const body = 'content'; const view = newView('index.njk', body); const content = await view.render({ layout: 'layout' }); content.should.eql('pre\n' + body + '\npost'); }); it('render() - layout not found', async () => { const body = 'content'; const view = newView('index.njk', body); const content = await view.render({ layout: 'wtf' }); content.should.eql(body); }); it('render() - callback', callback => { const body = [ '{{ test }}' ].join('\n'); const view = newView('index.njk', body); view.render({ test: 'foo' }, (err, content) => { should.not.exist(err); content.should.eql('foo'); callback(); }); }); it('render() - callback (without options)', callback => { const body = [ 'test: foo', '---', '{{ test }}' ].join('\n'); const view = newView('index.njk', body); view.render((err, content) => { should.not.exist(err); content.should.eql('foo'); callback(); }); }); it.skip('render() - execute after_render:html', async () => { const body = [ '{{ test }}' ].join('\n'); const view = newView('index.njk', body); const filter = fake.returns('bar'); hexo.extend.filter.register('after_render:html', filter); const content = await view.render({ test: 'foo' }); content.should.eql('bar'); hexo.extend.filter.unregister('after_render:html', filter); sinonAssert.alwaysCalledWith(filter, 'foo'); }); it('renderSync()', () => { const body = [ '{{ test }}' ].join('\n'); const view = newView('index.njk', body); view.renderSync({test: 'foo'}).should.eql('foo'); }); it('renderSync() - front-matter', () => { // The priority of front-matter is higher const body = [ 'foo: bar', '---', '{{ foo }}', '{{ test }}' ].join('\n'); const view = newView('index.njk', body); view.renderSync({ foo: 'foo', test: 'test' }).should.eql('bar\ntest'); }); it('renderSync() - helper', () => { const body = [ '{{ date() }}' ].join('\n'); const view = newView('index.njk', body); view.renderSync({ config: hexo.config, page: {} }).should.eql(moment().format(hexo.config.date_format)); }); it('renderSync() - layout', () => { const body = 'content'; const view = newView('index.njk', body); view.renderSync({ layout: 'layout' }).should.eql('pre\n' + body + '\npost'); }); it('renderSync() - layout not found', () => { const body = 'content'; const view = newView('index.njk', body); view.renderSync({ layout: 'wtf' }).should.eql(body); }); it.skip('renderSync() - execute after_render:html', () => { const body = [ '{{ test }}' ].join('\n'); const view = newView('index.njk', body); const filter = fake.returns('bar'); hexo.extend.filter.register('after_render:html', filter); view.renderSync({test: 'foo'}).should.eql('bar'); hexo.extend.filter.unregister('after_render:html', filter); sinonAssert.alwaysCalledWith(filter, 'foo'); }); it('_resolveLayout()', () => { const view = newView('partials/header.njk', 'header'); // Relative path view._resolveLayout('../layout').should.have.property('path', 'layout.njk'); // Absolute path view._resolveLayout('layout').should.have.property('path', 'layout.njk'); // Can't be itself should.not.exist(view._resolveLayout('header')); }); }); ================================================ FILE: test/scripts/theme_processors/config.ts ================================================ import { spy, assert as sinonAssert } from 'sinon'; import { join } from 'path'; import { mkdirs, rmdir, unlink, writeFile} from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import { config } from '../../../lib/theme/processors/config'; import chai from 'chai'; const should = chai.should(); type ConfigParams = Parameters type ConfigReturn = ReturnType describe('config', () => { const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true}); const process: (...args: ConfigParams) => BluebirdPromise = BluebirdPromise.method(config.process.bind(hexo)); const themeDir = join(hexo.base_dir, 'themes', 'test'); function newFile(options) { options.source = join(themeDir, options.path); return new hexo.theme.File(options); } before(async () => { await BluebirdPromise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); hexo.init(); }); beforeEach(() => { hexo.theme.config = {}; }); after(() => rmdir(hexo.base_dir)); it('pattern', () => { const pattern = config.pattern; pattern.match('_config.yml').should.be.ok; pattern.match('_config.json').should.be.ok; should.not.exist(pattern.match('_config/foo.yml')); should.not.exist(pattern.match('foo.yml')); }); it('type: create', async () => { const body = [ 'name:', ' first: John', ' last: Doe' ].join('\n'); const file = newFile({ path: '_config.yml', type: 'create', content: body }); await writeFile(file.source, body); await process(file); hexo.theme.config.should.eql({ name: {first: 'John', last: 'Doe'} }); unlink(file.source); }); it('type: delete', async () => { const file = newFile({ path: '_config.yml', type: 'delete' }); hexo.theme.config = {foo: 'bar'}; await process(file); hexo.theme.config.should.eql({}); }); it('load failed', () => { const file = newFile({ path: '_config.yml', type: 'create' }); const logSpy = spy(hexo.log, 'error'); return process(file).then(() => { should.fail('Return value must be rejected'); }, () => { sinonAssert.calledWith(logSpy, 'Theme config load failed.'); }).finally(() => logSpy.restore()); }); }); ================================================ FILE: test/scripts/theme_processors/i18n.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import { i18n } from '../../../lib/theme/processors/i18n'; import chai from 'chai'; const should = chai.should(); type I18nParams = Parameters type I18nReturn = ReturnType describe('i18n', () => { const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true}); const process: (...args: I18nParams) => BluebirdPromise = BluebirdPromise.method(i18n.process.bind(hexo)); const themeDir = join(hexo.base_dir, 'themes', 'test'); function newFile(options) { const { path } = options; options.params = { path }; options.path = 'languages/' + path; options.source = join(themeDir, options.path); return new hexo.theme.File(options); } before(async () => { await BluebirdPromise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('pattern', () => { const pattern = i18n.pattern; pattern.match('languages/default.yml').should.be.ok; pattern.match('languages/zh-TW.yml').should.be.ok; should.not.exist(pattern.match('default.yml')); }); it('type: create', async () => { const body = [ 'ok: OK', 'index:', ' title: Home' ].join('\n'); const file = newFile({ path: 'en.yml', type: 'create' }); await writeFile(file.source, body); await process(file); const __ = hexo.theme.i18n.__('en' as any); __('ok').should.eql('OK'); __('index.title').should.eql('Home'); unlink(file.source); }); it('type: delete', async () => { hexo.theme.i18n.set('en', { foo: 'foo', bar: 'bar' }); const file = newFile({ path: 'en.yml', type: 'delete' }); await process(file); hexo.theme.i18n.get('en').should.eql({}); }); }); ================================================ FILE: test/scripts/theme_processors/source.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import { source } from '../../../lib/theme/processors/source'; import chai from 'chai'; const should = chai.should(); type SourceParams = Parameters type SourceReturn = ReturnType describe('source', () => { const hexo = new Hexo(join(__dirname, 'source_test'), {silent: true}); const process: (...args: SourceParams) => BluebirdPromise = BluebirdPromise.method(source.process.bind(hexo)); const themeDir = join(hexo.base_dir, 'themes', 'test'); const Asset = hexo.model('Asset'); function newFile(options) { const { path } = options; options.params = {path}; options.path = 'source/' + path; options.source = join(themeDir, options.path); return new hexo.theme.File(options); } before(async () => { await BluebirdPromise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); await hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('pattern', () => { const { pattern } = source; pattern.match('source/foo.jpg').should.eql({path: 'foo.jpg'}); pattern.match('source/_foo.jpg').should.be.false; pattern.match('source/foo/_bar.jpg').should.be.false; pattern.match('source/foo.jpg~').should.be.false; pattern.match('source/foo.jpg%').should.be.false; pattern.match('layout/foo.swig').should.be.false; pattern.match('layout/foo.njk').should.be.false; pattern.match('package.json').should.be.false; pattern.match('node_modules/test/test.js').should.be.false; pattern.match('source/node_modules/test/test.js').should.be.false; }); it('type: create', async () => { const file = newFile({ path: 'style.css', type: 'create' }); const id = 'themes/test/' + file.path; await writeFile(file.source, 'test'); await process(file); const asset = Asset.findById(id); asset._id.should.eql(id); asset.path.should.eql(file.params.path); asset.modified.should.be.true; asset.remove(); unlink(file.source); }); it('type: update', async () => { const file = newFile({ path: 'style.css', type: 'update' }); const id = 'themes/test/' + file.path; await BluebirdPromise.all([ writeFile(file.source, 'test'), Asset.insert({ _id: id, path: file.params.path, modified: false }) ]); await process(file); const asset = Asset.findById(id); asset.modified.should.be.true; await BluebirdPromise.all([ unlink(file.source), Asset.removeById(id) ]); }); it('type: skip', async () => { const file = newFile({ path: 'style.css', type: 'skip' }); const id = 'themes/test/' + file.path; await BluebirdPromise.all([ writeFile(file.source, 'test'), Asset.insert({ _id: id, path: file.params.path, modified: false }) ]); await process(file); const asset = Asset.findById(id); asset.modified.should.be.false; await BluebirdPromise.all([ unlink(file.source), Asset.removeById(id) ]); }); it('type: delete', async () => { const file = newFile({ path: 'style.css', type: 'delete' }); const id = 'themes/test/' + file.path; await Asset.insert({ _id: id, path: file.params.path }); await process(file); should.not.exist(Asset.findById(id)); }); it('type: delete - not -exist', async () => { const file = newFile({ path: 'style.css', type: 'delete' }); const id = 'themes/test/' + file.path; await process(file); should.not.exist(Asset.findById(id)); }); }); ================================================ FILE: test/scripts/theme_processors/view.ts ================================================ import { join } from 'path'; import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; import BluebirdPromise from 'bluebird'; import Hexo from '../../../lib/hexo'; import { view } from '../../../lib/theme/processors/view'; import chai from 'chai'; const should = chai.should(); type ViewParams = Parameters type ViewReturn = ReturnType describe('view', () => { const hexo = new Hexo(join(__dirname, 'view_test'), {silent: true}); const process: (...args: ViewParams) => BluebirdPromise = BluebirdPromise.method(view.process.bind(hexo)); const themeDir = join(hexo.base_dir, 'themes', 'test'); hexo.env.init = true; function newFile(options) { const { path } = options; options.params = {path}; options.path = 'layout/' + path; options.source = join(themeDir, options.path); return new hexo.theme.File(options); } before(async () => { await BluebirdPromise.all([ mkdirs(themeDir), writeFile(hexo.config_path, 'theme: test') ]); await hexo.init(); }); after(() => rmdir(hexo.base_dir)); it('pattern', () => { const { pattern } = view; pattern.match('layout/index.njk').path.should.eql('index.njk'); should.not.exist(pattern.match('index.njk')); should.not.exist(pattern.match('view/index.njk')); }); it('type: create', async () => { const body = [ 'foo: bar', '---', 'test' ].join('\n'); const file = newFile({ path: 'index.njk', type: 'create' }); await writeFile(file.source, body); await process(file); const view = hexo.theme.getView('index.njk'); view.path.should.eql('index.njk'); view.source.should.eql(join(themeDir, 'layout', 'index.njk')); view.data.should.eql({ foo: 'bar', _content: 'test' }); hexo.theme.removeView('index.njk'); unlink(file.source); }); it('type: delete', async () => { const file = newFile({ path: 'index.njk', type: 'delete' }); await process(file); should.not.exist(hexo.theme.getView('index.njk')); }); }); ================================================ FILE: test/util/index.ts ================================================ export { readStream } from './stream'; ================================================ FILE: test/util/stream.ts ================================================ import Promise from 'bluebird'; export function readStream(stream): Promise { return new Promise((resolve, reject) => { let data = ''; stream.on('data', chunk => { data += chunk.toString(); }).on('end', () => { resolve(data); }).on('error', reject); }); } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es2020", "sourceMap": true, "outDir": "dist", "declaration": true, "skipLibCheck": true, "esModuleInterop": true, "types": [ "node", "mocha" ] }, "include": [ "lib/**/*.ts" ], "exclude": [ "node_modules" ] }