[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\n\n[vcbuild.bat]\nend_of_line = crlf\n\n[*.{md,markdown}]\ntrim_trailing_whitespace = false\n\n[{lib,src,test}/**.js]\nindent_style = space\nindent_size = 2\n\n[src/**.{h,cc}]\nindent_style = space\nindent_size = 2\n\n[test/*.py]\nindent_style = space\nindent_size = 2\n\n[configure]\nindent_style = space\nindent_size = 2\n\n[Makefile]\nindent_style = tab\nindent_size = 8\n\n[{deps,tools}/**]\nindent_style = ignore\nindent_size = ignore\nend_of_line = ignore\ntrim_trailing_whitespace = ignore\ncharset = ignore"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\n## Style Guide\n\nWe use [ESLint] to maintain the code style. You can install linter plugins on your editor or check the status with the following commands:\n\n``` bash\n$ npm run eslint\n\n# You can append `--fix` option to these commands to fix the code style automatically\n$ npm run eslint -- --fix\n```\n\n## Pull Requests\n\n1. Fork [hexojs/hexo](https://github.com/hexojs/hexo).\n2. Clone the repository to your computer and install dependencies.\n\n    ``` bash\n    $ git clone https://github.com/<username>/hexo.git\n    $ cd hexo\n    $ npm install\n    ```\n    \n3. Create a feature branch.\n\n    ``` bash\n    $ git checkout -b new_feature\n    ```\n    \n4. Start hacking.\n5. Push the branch.\n\n    ``` bash\n    $ git push origin new_feature\n    ```\n    \n6. Create a pull request and describe the change.\n\n## Testing\n\nBefore you submitting the pull request. Please make sure your code is coveraged and passes the tests. Otherwise your pull request won't be merged.\n\n``` bash\n$ npm test\n```\n\n## Updating Documentation\n\nThe Hexo documentation is open source and you can find the source code on [hexojs/site]. \n\n### Workflow\n\n1. Fork [hexojs/site](https://github.com/hexojs/site).\n2. Clone the repository to your computer and install dependencies.\n\n    ``` bash\n    $ git clone https://github.com/<username>/site.git\n    $ cd site\n    $ npm install\n    ```\n    \n3. Start editing the documentation. You can start the server for live previewing.\n\n    ``` bash\n    $ hexo server\n    ```\n    \n4. Push the branch.\n5. Create a pull request and describe the change.\n\n### Translating\n\n1. Add a new language folder in `source` folder. (all in lower case)\n2. Copy Markdown and template files in `source` folder to the new language folder.\n3. Add the new language to `source/_data/language.yml`.\n4. Copy `en.yml` in `themes/navy/languages` and rename to the language name (all in lower case).\n\n## Reporting Issues\n\nWhen 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.\n\n1. Represent the problem in [debug mode](https://hexo.io/docs/commands.html#Debug_mode).\n2. Run `hexo version` and check the version info.    \n3. Post both debug message and version info on GitHub.\n\n[ESLint]: https://eslint.org/"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "open_collective: hexo\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Something isn't working as expected.\n# title: \"\"\n# labels: []\n# assignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Tips\n\n        - 给简体中文用户的提示:\n\n          - 在提交 issue 时请按照下面的模板提供相关信息，这将有助于我们发现问题。\n          - 请尽量使用英语描述你遇到的问题，这可以让更多的人帮助到你。\n\n        - 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.\n\n        - 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.\n\n        - 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`.\n\n        - Please take extra precaution not to attach any secret or personal information. (likes personal privacy, password, GitHub Personal Access Token, etc.)\n\n        ------\n\n  - type: checkboxes\n    validations:\n      required: true\n    attributes:\n      label: Check List\n      description: Please check followings before submitting a new issue.\n      options:\n        - label: I have already read [Docs page](https://hexo.io/docs/) & [Troubleshooting page](https://hexo.io/docs/troubleshooting).\n        - label: I have already searched existing issues and they are not help to me.\n        - label: I examined error or warning messages and it's difficult to solve.\n        - label: I am using the [latest](https://github.com/hexojs/hexo/releases) version of Hexo. (run `hexo version` to check)\n        - label: My Node.js is matched [the required version](https://hexo.io/docs/#Required-Node-js-version).\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Expected behavior\n      # description:\n      placeholder: Descripe what you expected to happen.\n      # value:\n      # render:\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Actual behavior\n      # description:\n      placeholder: Descripe what actually happen.\n      # value:\n      # render:\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: How to reproduce?\n      description: How do you trigger this bug? Please walk us through it step by step.\n      placeholder: |\n        1. Step1\n        2. Step2\n        3. etc.\n        ...\n      # value:\n      # render:\n\n  - type: input\n    validations:\n      required: true\n    attributes:\n      label: Is the problem still there under `Safe mode`?\n      description: |\n        https://hexo.io/docs/commands#Safe-mode\n\n        \"Safe mode\" will disable all the plugins and scripts.\n        If your problem disappear under \"Safe mode\" means the problem is probably at your newly installed plugins, not at hexo.\n      # placeholder:\n      # value: |\n      # render:\n\n  - type: markdown\n    attributes:\n      value: |\n        ------\n\n        ## Environment & Settings\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Your Node.js & npm version\n      description: |\n        Please run `node -v && npm -v` \n        and paste the output here.\n      placeholder: node -v && npm -v\n      # value: |\n      render: text\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Your Hexo and Plugin version\n      description: |\n        Please run `npm ls --depth 0` \n        and paste the output here.\n      placeholder: npm ls --depth 0\n      # value:\n      render: text\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Your `package.json`\n      description: Please paste the content of `package.json` here.\n      placeholder: package.json\n      # value:\n      render: json\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Your site's `_config.yml` (Optional)\n      description: Please paste the content of your `_config.yml` here.\n      placeholder: _config.yml\n      # value: |\n      render: yaml\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Others\n      description: If you have other information. Please write here.\n      # placeholder:\n      # value:\n      # render:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a Question, Help, Discuss\n    url: https://github.com/hexojs/hexo/discussions\n    about: I have a question, help for hexo (e.g. Customize)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request-improvement.yml",
    "content": "name: Feature request / Improvement\ndescription: I have a feature request, suggestion, improvement etc...\n# title: \"\"\n# labels: []\n# assignees: []\n\nbody:\n  - type: checkboxes\n    validations:\n      required: true\n    attributes:\n      label: Check List\n      description: Please check followings before submitting a new feature request.\n      options:\n        - label: I have already read [Docs page](https://hexo.io/docs/).\n        - label: I have already searched existing issues.\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: Feature Request\n      description: Descripe the feature and why it is needed.\n      # placeholder:\n      # value:\n      # render:\n\n  - type: textarea\n    validations:\n      required: false\n    attributes:\n      label: Others\n      description: If you have other information. Please write here.\n      # placeholder:\n      # value:\n      # render:\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nThank you for creating a pull request to contribute to Hexo code! Before you open the request please answer the following questions to help it be more easily integrated. Please check the boxes \"[ ]\" with \"[x]\" when done too.\n-->\n\n## What does it do?\n\n\n\n## Screenshots\n\n\n\n## Pull request tasks\n\n- [ ] Add test cases for the changes.\n- [ ] Passed the CI test.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: npm\n  directory: \"/\"\n  schedule:\n    interval: daily\n- package-ecosystem: github-actions\n  directory: \"/\"\n  schedule:\n    interval: daily\n"
  },
  {
    "path": ".github/workflows/benchmark.yml",
    "content": "name: Benchmark\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"master\"\n    paths:\n      - \"lib/**\"\n      - \".github/workflows/benchmark.yml\"\n  pull_request:\n    branches:\n      - \"master\"\n    paths:\n      - \"lib/**\"\n      - \".github/workflows/benchmark.yml\"\n\njobs:\n  benchmark:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        node-version: [\"20\", \"22\", \"24\"]\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install dependencies\n        run: npm install --silent\n      - name: Running benchmark\n        run: node test/benchmark.js --benchmark\n\n  profiling:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        node-version: [\"20\", \"22\", \"24\"]\n      fail-fast: false\n    env:\n      comment_file: \".tmp-comment-flamegraph-node${{ matrix.node-version }}.md\"\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install dependencies\n        run: npm install --silent\n      - name: Running profiling\n        run: node test/benchmark.js --profiling\n      - name: Publish flamegraph to https://${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh/flamegraph.html\n        uses: dswistowski/surge-sh-action@v1\n        with:\n          domain: ${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh\n          project: ./.tmp-hexo-theme-unit-test/0x/\n          login: ${{ secrets.SURGE_LOGIN }}\n          token: ${{ secrets.SURGE_TOKEN }}\n\n      - name: save comment to file\n        if: ${{github.event_name == 'pull_request' }}\n        run: |\n          echo \"https://${{ github.sha }}-${{ matrix.node-version }}-hexo.surge.sh/flamegraph.html\" > ${{env.comment_file}}\n\n      - uses: actions/upload-artifact@v6\n        if: ${{github.event_name == 'pull_request' }}\n        with:\n          retention-days: 1\n          name: comment-node${{ matrix.node-version }}\n          path: ${{env.comment_file}}\n\n  number:\n    runs-on: ubuntu-latest\n    if: ${{github.event_name == 'pull_request' }}\n    env:\n      pr_number_file: .tmp-comment-pr_number\n    steps:\n      - name: save PR number to file\n        run: |\n          echo -n \"${{ github.event.number }}\" > ${{env.pr_number_file}}\n      - uses: actions/upload-artifact@v6\n        with:\n          retention-days: 1\n          name: comment-pr_number\n          path: ${{env.pr_number_file}}\n"
  },
  {
    "path": ".github/workflows/commenter.yml",
    "content": "name: Commenter\n\non:\n  pull_request_target:\n\n  workflow_run:\n    workflows: [\"Benchmark\"]\n    types:\n      - completed\n\npermissions:\n  contents: read\n\njobs:\n  comment-test:\n    name: How to test\n    permissions:\n      pull-requests: write # for marocchino/sticky-pull-request-comment to create or update PR comment\n    runs-on: ubuntu-latest\n    if: ${{github.event_name == 'pull_request_target'}}\n    steps:\n      - name: Comment PR - How to test\n        uses: marocchino/sticky-pull-request-comment@v2\n        with:\n          header: How to test\n          message: |\n            ## How to test\n\n            ```sh\n            git clone -b ${{ github.head_ref }} https://github.com/${{ github.event.pull_request.head.repo.full_name }}.git\n            cd hexo\n            npm install\n            npm test\n            ```\n\n  comment-flamegraph:\n    name: Flamegraph\n    permissions:\n      pull-requests: write # for marocchino/sticky-pull-request-comment to create or update PR comment\n      actions: read # get artifact\n    runs-on: ubuntu-latest\n    if: ${{github.event_name == 'workflow_run' && github.event.workflow_run.conclusion=='success'}}\n    env:\n      comment_result: \".tmp-comment-flamegraph.md\"\n    steps:\n      - name: download artifact\n        uses: actions/download-artifact@v7\n        with:\n          github-token: ${{secrets.GITHUB_TOKEN}}\n          run-id: ${{toJSON(github.event.workflow_run.id)}}\n          pattern: \"comment-*\"\n          merge-multiple: true\n\n      - name: get PR number\n        run: |\n          echo \"pr_number=$(cat .tmp-comment-pr_number)\" >> \"$GITHUB_ENV\"\n\n      - name: combime comment\n        if: ${{env.pr_number!=''}}\n        run: |\n          echo \"## Flamegraph\" > ${{env.comment_result}}\n          echo \"\" >> ${{env.comment_result}}\n          cat .tmp-comment-flamegraph-*.md >> ${{env.comment_result}}\n\n      - name: Comment PR - flamegraph\n        if: ${{env.pr_number!=''}}\n        uses: marocchino/sticky-pull-request-comment@v2\n        with:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n          number: ${{env.pr_number}}\n          header: Flamegraph\n          path: ${{env.comment_result}}\n"
  },
  {
    "path": ".github/workflows/dependencies-review.yml",
    "content": "name: 'Dependencies Review'\non:\n  pull_request:\n    paths:\n      - 'package.json'\n      - 'package-lock.json'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  dependency-review:\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Checkout Repository'\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #6.0.1\n      - name: 'Dependencies Review'\n        uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 #4.8.3\n        with:\n          vulnerability-check: true\n          fail-on-severity: high\n          comment-summary-in-pr: always\n"
  },
  {
    "path": ".github/workflows/linter.yml",
    "content": "name: Linter\n\non:\n  push:\n    branches:\n      - \"master\"\n    paths:\n      - \"lib/**\"\n      - \"test/**\"\n      - \".github/workflows/linter.yml\"\n  pull_request:\n    paths:\n      - \"lib/**\"\n      - \"test/**\"\n      - \".github/workflows/linter.yml\"\n\npermissions:\n  contents: read\n\njobs:\n  linter:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js 22\n        uses: actions/setup-node@v6\n        with:\n          node-version: \"22\"\n      - name: Install Dependencies\n        run: npm install\n      - name: Lint\n        run: |\n          npm run eslint\n        env:\n          CI: true\n"
  },
  {
    "path": ".github/workflows/tester.yml",
    "content": "name: Tester\n\non:\n  push:\n    branches:\n      - \"master\"\n    paths:\n      - \"lib/**\"\n      - \"test/**\"\n      - \"package.json\"\n      - \"tsconfig.json\"\n      - \".github/workflows/tester.yml\"\n  pull_request:\n    paths:\n      - \"lib/**\"\n      - \"test/**\"\n      - \"package.json\"\n      - \"tsconfig.json\"\n      - \".github/workflows/tester.yml\"\n\npermissions:\n  contents: read\n\njobs:\n  tester:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        node-version: [\"20\", \"22\", \"24\"]\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install Dependencies\n        run: npm install\n      - name: Test\n        run: npm test -- --no-parallel\n        env:\n          CI: true\n  coverage:\n    permissions:\n      checks: write # for coverallsapp/github-action to create new checks\n      contents: read # for actions/checkout to fetch code\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        node-version: [\"22\"]\n    steps:\n      - uses: actions/checkout@v6\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install Dependencies\n        run: npm install\n      - name: Coverage\n        run: npm run test-cov\n        env:\n          CI: true\n      - name: Coveralls\n        uses: coverallsapp/github-action@master\n        with:\n          github-token: ${{ secrets.github_token }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules/\ntmp/\n*.log\n.idea/\nyarn.lock\npackage-lock.json\npnpm-lock.yaml\n.nyc_output/\ncoverage/\n.tmp*\n.vscode\ndist/\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx lint-staged\n"
  },
  {
    "path": ".lintstagedrc.json",
    "content": "{\n  \"*.js\": \"eslint --fix\",\n  \"*.ts\": \"eslint --fix\"\n}\n"
  },
  {
    "path": ".mocharc.yml",
    "content": "color: true\nreporter: spec\nui: bdd\nfull-trace: true\nexit: true\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe 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.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity 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.\n\nCommunity 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.\n\n## Scope\n\nThis 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.\n\n## Enforcement\n\nInstances 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.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.\n\n**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.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**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.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**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.\n\n### 4. Permanent Ban\n\n**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.\n\n**Consequence**: A permanent ban from any sort of public interaction within the project community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,\navailable at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.\n\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2012-present Tommy Chen\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://raw.githubusercontent.com/hexojs/logo/master/hexo-logo-avatar.png\" alt=\"Hexo logo\" width=\"100\" height=\"100\" align=\"right\" />\n\n# Hexo\n\n> A fast, simple & powerful blog framework, powered by [Node.js](https://nodejs.org).\n\n[Website](https://hexo.io) |\n[Documentation](https://hexo.io/docs/) |\n[Installation Guide](https://hexo.io/docs/#Installation) |\n[Contribution Guide](https://hexo.io/docs/contributing) |\n[Code of Conduct](CODE_OF_CONDUCT.md) |\n[API](https://hexo.io/api/) |\n[Twitter](https://twitter.com/hexojs)\n\n[![NPM version](https://badge.fury.io/js/hexo.svg)](https://www.npmjs.com/package/hexo)\n![Required Node version](https://img.shields.io/node/v/hexo)\n[![Build Status](https://github.com/hexojs/hexo/workflows/Tester/badge.svg)](https://github.com/hexojs/hexo/actions?query=workflow%3ATester)\n[![dependencies Status](https://img.shields.io/librariesio/release/npm/hexo)](https://libraries.io/npm/hexo)\n[![Coverage Status](https://coveralls.io/repos/hexojs/hexo/badge.svg?branch=master)](https://coveralls.io/r/hexojs/hexo?branch=master)\n[![Gitter](https://badges.gitter.im/hexojs/hexo.svg)](https://gitter.im/hexojs/hexo)\n[![Discord Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/teM2Anj)\n[![Telegram Chat](https://img.shields.io/badge/chat-on%20telegram-32afed.svg)](https://t.me/hexojs)\n[![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)\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)\n\n## Features\n\n- Blazing fast generating\n- Support for GitHub Flavored Markdown and most Octopress plugins\n- One-command deploy to GitHub Pages, Heroku, etc.\n- Powerful API for limitless extensibility\n- Hundreds of [themes](https://hexo.io/themes/) & [plugins](https://hexo.io/plugins/)\n\n## Quick Start\n\n**Install Hexo**\n\n``` bash\n$ npm install hexo-cli -g\n```\n\nInstall with [brew](https://brew.sh/) on macOS and Linux:\n\n```bash\n$ brew install hexo\n```\n\n**Setup your blog**\n\n``` bash\n$ hexo init blog\n$ cd blog\n```\n\n**Start the server**\n\n``` bash\n$ hexo server\n```\n\n**Create a new post**\n\n``` bash\n$ hexo new \"Hello Hexo\"\n```\n\n**Generate static files**\n\n``` bash\n$ hexo generate\n```\n\n## More Information\n\n- Read the [documentation](https://hexo.io/)\n- Visit the [Awesome Hexo](https://github.com/hexojs/awesome-hexo) list\n- Find solutions in [troubleshooting](https://hexo.io/docs/troubleshooting.html)\n- 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)\n- See the [plugin list](https://hexo.io/plugins/) and the [theme list](https://hexo.io/themes/) on wiki\n- Follow [@hexojs](https://twitter.com/hexojs) for latest news\n\n## Contributing\n\nWe welcome you to join the development of Hexo. Please see [contributing document](https://hexo.io/docs/contributing). 🤗\n\nAlso, we welcome PR or issue to [official-plugins](https://github.com/hexojs).\n\n## Contributors\n\n[![](https://opencollective.com/Hexo/contributors.svg?width=890)](https://github.com/hexojs/hexo/graphs/contributors)\n\n## Backers\n\n[![Backers](https://opencollective.com/hexo/tiers/backers.svg?avatarHeight=36&width=600)](https://opencollective.com/hexo)\n\n## Sponsors\n\n[![Sponsors](https://opencollective.com/hexo/tiers/sponsors.svg?width=600)](https://opencollective.com/hexo)\n\n## License\n\n[![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)\n"
  },
  {
    "path": "bin/hexo",
    "content": "#!/usr/bin/env node\n\n'use strict';\n\nrequire('hexo-cli')();\n"
  },
  {
    "path": "eslint.config.js",
    "content": "const config = require('eslint-config-hexo/ts');\nconst testConfig = require('eslint-config-hexo/test');\n\nmodule.exports = [\n  // Configurations applied globally\n  ...config,\n  {\n    rules: {\n      '@typescript-eslint/no-explicit-any': 0,\n      '@typescript-eslint/no-var-requires': 0,\n      '@typescript-eslint/no-require-imports': 0,\n      'n/no-missing-require': 0,\n      'n/no-missing-import': 0,\n      '@typescript-eslint/no-unused-vars': [\n        'error', {\n          'argsIgnorePattern': '^_'\n        }\n      ]\n    }\n  },\n  // Configurations applied only to test files\n  {\n    files: [\n      'test/**/*.ts'\n    ],\n    languageOptions: {\n      ...testConfig.languageOptions\n    },\n    rules: {\n      ...testConfig.rules,\n      '@typescript-eslint/ban-ts-comment': 0,\n      '@typescript-eslint/no-unused-expressions': 0,\n      '@typescript-eslint/no-unused-vars': [\n        'error', {\n          'varsIgnorePattern': '^_',\n          'argsIgnorePattern': '^_',\n          'caughtErrorsIgnorePattern': '^_'\n        }\n      ]\n    }\n  }\n];\n"
  },
  {
    "path": "lib/box/file.ts",
    "content": "import type Promise from 'bluebird';\nimport { readFile, readFileSync, stat, statSync, type ReadFileOptions } from 'hexo-fs';\nimport type fs from 'fs';\n\nclass File {\n\n  /**\n   * Full path of the file\n   */\n  public source: string;\n\n  /**\n   * Relative path to the box of the file\n   */\n  public path: string;\n\n  /**\n   * The information from path matching.\n   */\n  public params: any;\n\n  /**\n   * File type. The value can be create, update, skip, delete.\n   */\n  // eslint-disable-next-line no-use-before-define\n  public type: typeof File.TYPE_CREATE | typeof File.TYPE_UPDATE | typeof File.TYPE_SKIP | typeof File.TYPE_DELETE;\n  static TYPE_CREATE: 'create';\n  static TYPE_UPDATE: 'update';\n  static TYPE_SKIP: 'skip';\n  static TYPE_DELETE: 'delete';\n\n  constructor({ source, path, params, type }: {\n    source: string;\n    path: string;\n    params: any;\n    type: typeof File.TYPE_CREATE | typeof File.TYPE_UPDATE | typeof File.TYPE_SKIP | typeof File.TYPE_DELETE;\n  }) {\n    this.source = source;\n    this.path = path;\n    this.params = params;\n    this.type = type;\n  }\n\n  read(options?: ReadFileOptions): Promise<string> {\n    return readFile(this.source, options) as Promise<string>;\n  }\n\n  readSync(options?: ReadFileOptions): string {\n    return readFileSync(this.source, options) as string;\n  }\n\n  stat(): Promise<fs.Stats> {\n    return stat(this.source);\n  }\n\n  statSync(): fs.Stats {\n    return statSync(this.source);\n  }\n}\n\nFile.TYPE_CREATE = 'create';\nFile.TYPE_UPDATE = 'update';\nFile.TYPE_SKIP = 'skip';\nFile.TYPE_DELETE = 'delete';\n\nexport = File;\n"
  },
  {
    "path": "lib/box/index.ts",
    "content": "import { join, sep } from 'path';\nimport BlueBirdPromise from 'bluebird';\nimport File from './file';\nimport { Pattern, createSha1Hash } from 'hexo-util';\nimport { createReadStream, readdir, stat, watch } from 'hexo-fs';\nimport { magenta } from 'picocolors';\nimport { EventEmitter } from 'events';\nimport { isMatch, makeRe } from 'micromatch';\nimport type Hexo from '../hexo';\nimport type { NodeJSLikeCallback } from '../types';\nimport type fs from 'fs';\n\nconst defaultPattern = new Pattern(() => ({}));\n\ninterface Processor {\n  pattern: Pattern;\n  process: (file?: File) => any;\n}\n\ninterface BoxOptions {\n  persistent: boolean;\n  awaitWriteFinish: { stabilityThreshold: number };\n  ignored: RegExp[];\n  [key: string]: any;\n}\n\nclass Box extends EventEmitter {\n  public options: BoxOptions;\n  public context: Hexo;\n  public base: string;\n  public processors: Processor[];\n  public _processingFiles: Record<string, boolean>;\n  public watcher: Awaited<ReturnType<typeof watch>> | null;\n  public Cache: any;\n  // TODO: replace runtime class _File\n  public File: any;\n  public ignore: string[];\n\n  constructor(ctx: Hexo, base: string, options?: any) {\n    super();\n\n    this.options = Object.assign({\n      persistent: true,\n      awaitWriteFinish: {\n        stabilityThreshold: 200\n      }\n    }, options);\n\n    if (!base.endsWith(sep)) {\n      base += sep;\n    }\n\n    this.context = ctx;\n    this.base = base;\n    this.processors = [];\n    this._processingFiles = {};\n    this.watcher = null;\n    this.Cache = ctx.model('Cache');\n    this.File = this._createFileClass();\n    let targets = this.options.ignored as unknown as string[] || [];\n    if (ctx.config.ignore && ctx.config.ignore.length) {\n      targets = targets.concat(ctx.config.ignore);\n    }\n    this.ignore = targets;\n    this.options.ignored = targets.map(s => toRegExp(ctx, s)).filter(x => x);\n  }\n\n  _createFileClass() {\n    const ctx = this.context;\n\n    class _File extends File {\n      public box: Box;\n\n      render(options?: any) {\n        return ctx.render.render({\n          path: this.source\n        }, options);\n      }\n\n      renderSync(options?: any) {\n        return ctx.render.renderSync({\n          path: this.source\n        }, options);\n      }\n    }\n\n    _File.prototype.box = this;\n\n    return _File;\n  }\n\n  addProcessor(pattern: (...args: any[]) => any): void;\n  addProcessor(pattern: string | RegExp | Pattern | ((str: string) => any), fn: (...args: any[]) => any): void;\n  addProcessor(pattern: string | RegExp | Pattern | ((str: string) => any), fn?: (...args: any[]) => any): void {\n    if (!fn && typeof pattern === 'function') {\n      fn = pattern;\n      pattern = defaultPattern;\n    }\n\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n    if (!(pattern instanceof Pattern)) pattern = new Pattern(pattern);\n\n    this.processors.push({\n      pattern,\n      process: fn\n    });\n  }\n\n  _readDir(base: string, prefix = ''): BlueBirdPromise<string[]> {\n    const { context: ctx } = this;\n    const results: string[] = [];\n    return readDirWalker(ctx, base, results, this.ignore, prefix)\n      .return(results)\n      .map(path => this._checkFileStatus(path))\n      .map(file => this._processFile(file.type, file.path).return(file.path));\n  }\n\n  _checkFileStatus(path: string): { type: string; path: string } {\n    const { Cache, context: ctx } = this;\n    const src = join(this.base, path);\n\n    return Cache.compareFile(\n      escapeBackslash(src.substring(ctx.base_dir.length)),\n      () => getHash(src),\n      () => stat(src)\n    ).then(result => ({\n      type: result.type,\n      path\n    }));\n  }\n\n  process(callback?: NodeJSLikeCallback<any>): BlueBirdPromise<void | (string | void)[]> {\n    const { base, Cache, context: ctx } = this;\n\n    return stat(base).then(stats => {\n      if (!stats.isDirectory()) return;\n\n      // Check existing files in cache\n      const relativeBase = escapeBackslash(base.substring(ctx.base_dir.length));\n      const cacheFiles: string[] = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length));\n\n      // Handle deleted files\n      return this._readDir(base)\n        .then(files => cacheFiles.filter(path => !files.includes(path)))\n        .map(path => this._processFile(File.TYPE_DELETE, path));\n    }).catch(err => {\n      if (err && err.code !== 'ENOENT') throw err;\n    }).asCallback(callback);\n  }\n\n  _processFile(type: string, path: string): BlueBirdPromise<void | string> {\n    if (this._processingFiles[path]) {\n      return BlueBirdPromise.resolve();\n    }\n\n    this._processingFiles[path] = true;\n\n    const { base, File, context: ctx } = this;\n\n    this.emit('processBefore', {\n      type,\n      path\n    });\n\n    return BlueBirdPromise.reduce(this.processors, (count, processor) => {\n      const params = processor.pattern.match(path);\n      if (!params) return count;\n\n      const file: File = new File({\n        // source is used for filesystem path, keep backslashes on Windows\n        source: join(base, path),\n        // path is used for URL path, replace backslashes on Windows\n        path: escapeBackslash(path),\n        params,\n        type\n      });\n\n      return Reflect.apply(BlueBirdPromise.method(processor.process), ctx, [file])\n        .thenReturn(count + 1);\n    }, 0).then(count => {\n      if (count) {\n        ctx.log.debug('Processed: %s', magenta(path));\n      }\n\n      this.emit('processAfter', {\n        type,\n        path\n      });\n    }).catch(err => {\n      ctx.log.error({ err }, 'Process failed: %s', magenta(path));\n    }).finally(() => {\n      this._processingFiles[path] = false;\n    }).thenReturn(path);\n  }\n\n  watch(callback?: NodeJSLikeCallback<never>): BlueBirdPromise<void> {\n    if (this.isWatching()) {\n      return BlueBirdPromise.reject(new Error('Watcher has already started.')).asCallback(callback);\n    }\n\n    const { base } = this;\n\n    function getPath(path) {\n      return escapeBackslash(path.substring(base.length));\n    }\n\n    return this.process().then(() => watch(base, this.options)).then(watcher => {\n      this.watcher = watcher;\n\n      watcher.on('add', path => {\n        this._processFile(File.TYPE_CREATE, getPath(path));\n      });\n\n      watcher.on('change', path => {\n        this._processFile(File.TYPE_UPDATE, getPath(path));\n      });\n\n      watcher.on('unlink', path => {\n        this._processFile(File.TYPE_DELETE, getPath(path));\n      });\n\n      watcher.on('addDir', path => {\n        let prefix = getPath(path);\n        if (prefix) prefix += '/';\n\n        this._readDir(path, prefix);\n      });\n    }).asCallback(callback);\n  }\n\n  unwatch(): void {\n    if (!this.isWatching()) return;\n\n    this.watcher.close();\n    this.watcher = null;\n  }\n\n  isWatching(): boolean {\n    return Boolean(this.watcher);\n  }\n}\n\nfunction escapeBackslash(path: string): string {\n  // Replace backslashes on Windows\n  return path.replace(/\\\\/g, '/');\n}\n\nfunction getHash(path: string): BlueBirdPromise<string> {\n  const src = createReadStream(path);\n  const hasher = createSha1Hash();\n\n  const finishedPromise = new BlueBirdPromise((resolve, reject) => {\n    src.once('error', reject);\n    src.once('end', resolve);\n  });\n\n  src.on('data', chunk => { hasher.update(chunk); });\n\n  return finishedPromise.then(() => hasher.digest('hex'));\n}\n\nfunction toRegExp(ctx: Hexo, arg: string): RegExp | null {\n  if (!arg) return null;\n  if (typeof arg !== 'string') {\n    ctx.log.warn('A value of \"ignore:\" section in \"_config.yml\" is not invalid (not a string)');\n    return null;\n  }\n  const result = makeRe(arg);\n  if (!result) {\n    ctx.log.warn('A value of \"ignore:\" section in \"_config.yml\" can not be converted to RegExp:' + arg);\n    return null;\n  }\n  return result;\n}\n\nfunction isIgnoreMatch(path: string, ignore: string | string[]): boolean {\n  return path && ignore && ignore.length && isMatch(path, ignore);\n}\n\nfunction readDirWalker(ctx: Hexo, base: string, results: string[], ignore: string | string[], prefix: string): BlueBirdPromise<any> {\n  if (isIgnoreMatch(base, ignore)) return BlueBirdPromise.resolve();\n\n  return BlueBirdPromise.map(readdir(base).catch(err => {\n    ctx.log.error({ err }, 'Failed to read directory: %s', base);\n    if (err && err.code === 'ENOENT') return [];\n    throw err;\n  }), async (path: string) => {\n    const fullPath = join(base, path);\n    const stats: fs.Stats | null = await stat(fullPath).catch(err => {\n      ctx.log.error({ err }, 'Failed to stat file: %s', fullPath);\n      if (err && err.code === 'ENOENT') return null;\n      throw err;\n    });\n    const prefixPath = `${prefix}${path}`;\n    if (stats) {\n      if (stats.isDirectory()) {\n        return readDirWalker(ctx, fullPath, results, ignore, `${prefixPath}/`);\n      }\n      if (!isIgnoreMatch(fullPath, ignore)) {\n        results.push(prefixPath);\n      }\n    }\n  });\n}\n\nexport interface _File extends File {\n  box: Box;\n  render(options?: any): any;\n  renderSync(options?: any): any;\n}\n\nexport default Box;\n"
  },
  {
    "path": "lib/extend/console.ts",
    "content": "import Promise from 'bluebird';\nimport abbrev from 'abbrev';\nimport type { NodeJSLikeCallback } from '../types';\nimport type Hexo from '../hexo';\n\ntype Option = Partial<{\n  usage: string;\n  desc: string;\n  init: boolean;\n  arguments: {\n      name: string;\n      desc: string;\n    }[];\n  options: {\n    name: string;\n    desc: string;\n  }[];\n}>\n\ninterface Args {\n  _: string[];\n  [key: string]: string | boolean | string[];\n}\ntype AnyFn = (this: Hexo, args: Args, callback?: NodeJSLikeCallback<any>) => any;\ninterface StoreFunction {\n  (this: Hexo, args: Args): Promise<any>;\n  desc?: string;\n  options?: Option;\n}\n\ninterface Store {\n  [key: string]: StoreFunction\n}\ninterface Alias {\n  [abbreviation: string]: string\n}\n\n/**\n * The console forms the bridge between Hexo and its users. It registers and describes the available console commands.\n */\nclass Console {\n  public store: Store;\n  public alias: Alias;\n\n  constructor() {\n    this.store = {};\n    this.alias = {};\n  }\n\n  /**\n   * Get a console plugin function by name\n   * @param {String} name - The name of the console plugin\n   * @returns {StoreFunction} - The console plugin function\n   */\n  get(name: string): StoreFunction {\n    name = name.toLowerCase();\n    return this.store[this.alias[name]];\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  /**\n   * Register a console plugin\n   * @param {String} name - The name of console plugin to be registered\n   * @param {String} desc - More detailed information about a console command\n   * @param {Option} options - The description of each option of a console command\n   * @param {AnyFn} fn - The console plugin to be registered\n   */\n  register(name: string, fn: AnyFn): void\n  register(name: string, desc: string, fn: AnyFn): void\n  register(name: string, options: Option, fn: AnyFn): void\n  register(name: string, desc: string, options: Option, fn: AnyFn): void\n  register(name: string, desc: string | Option | AnyFn, options?: Option | AnyFn, fn?: AnyFn): void {\n    if (!name) throw new TypeError('name is required');\n\n    if (!fn) {\n      if (options) {\n        if (typeof options === 'function') {\n          fn = options;\n\n          if (typeof desc === 'object') { // name, options, fn\n            options = desc;\n            desc = '';\n          } else { // name, desc, fn\n            options = {};\n          }\n        } else {\n          throw new TypeError('fn must be a function');\n        }\n      } else {\n        // name, fn\n        if (typeof desc === 'function') {\n          fn = desc;\n          options = {};\n          desc = '';\n        } else {\n          throw new TypeError('fn must be a function');\n        }\n      }\n    }\n\n    if (fn.length > 1) {\n      fn = Promise.promisify(fn);\n    } else {\n      fn = Promise.method(fn);\n    }\n\n    const c = fn as StoreFunction;\n    this.store[name.toLowerCase()] = c;\n    c.options = options as Option;\n    c.desc = desc as string;\n\n    this.alias = abbrev(Object.keys(this.store));\n  }\n}\n\nexport = Console;\n"
  },
  {
    "path": "lib/extend/deployer.ts",
    "content": "import Promise from 'bluebird';\nimport type { NodeJSLikeCallback } from '../types';\nimport type Hexo from '../hexo';\n\ninterface StoreFunction {\n  (this: Hexo, deployArg: { type: string; [key: string]: any }): Promise<any>;\n}\ninterface Store {\n  [key: string]: StoreFunction;\n}\n\n/**\n * A deployer helps users quickly deploy their site to a remote server without complicated commands.\n */\nclass Deployer {\n  public store: Store;\n\n  constructor() {\n    this.store = {};\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  get(name: string): StoreFunction {\n    return this.store[name];\n  }\n\n  register(\n    name: string,\n    fn: (\n      this: Hexo,\n      deployArg: {\n        type: string;\n        [key: string]: any;\n      },\n      callback?: NodeJSLikeCallback<any>\n    ) => any\n  ): void {\n    if (!name) throw new TypeError('name is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    if (fn.length > 1) {\n      fn = Promise.promisify(fn);\n    } else {\n      fn = Promise.method(fn);\n    }\n\n    this.store[name] = fn;\n  }\n}\n\nexport = Deployer;\n"
  },
  {
    "path": "lib/extend/filter.ts",
    "content": "import Promise from 'bluebird';\nimport { FilterOptions } from '../types';\n\nconst typeAlias = {\n  pre: 'before_post_render',\n  post: 'after_post_render',\n  'after_render:html': '_after_html_render'\n};\n\ninterface StoreFunction {\n  (data?: any, ...args: any[]): any;\n  priority?: number;\n}\n\ninterface Store {\n  [key: string]: StoreFunction[]\n}\n\n/**\n * 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.\n * This concept was borrowed from WordPress.\n */\nclass Filter {\n  public store: Store;\n\n  constructor() {\n    this.store = {};\n  }\n\n  list(): Store;\n  list(type: string): StoreFunction[];\n  list(type?: string) {\n    if (!type) return this.store;\n    return this.store[type] || [];\n  }\n\n  register(fn: StoreFunction): void\n  register(fn: StoreFunction, priority: number): void\n  register(type: string, fn: StoreFunction): void\n  register(type: string, fn: StoreFunction, priority: number): void\n  register(type: string | StoreFunction, fn?: StoreFunction | number, priority?: number): void {\n    if (!priority) {\n      if (typeof type === 'function') {\n        priority = fn as number;\n        fn = type;\n        type = 'after_post_render';\n      }\n    }\n\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    type = typeAlias[type as string] || type;\n    priority = priority == null ? 10 : priority;\n\n    const store = this.store[type as string] || [];\n    this.store[type as string] = store;\n\n    fn.priority = priority;\n    store.push(fn);\n\n    store.sort((a, b) => a.priority - b.priority);\n  }\n\n  unregister(type: string, fn: StoreFunction): void {\n    if (!type) throw new TypeError('type is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    type = typeAlias[type] || type;\n\n    const list = this.list(type);\n    if (!list || !list.length) return;\n\n    const index = list.indexOf(fn);\n\n    if (index !== -1) list.splice(index, 1);\n  }\n\n  exec(type: string, data: any, options: FilterOptions = {}): Promise<any> {\n    const filters = this.list(type);\n    if (filters.length === 0) return Promise.resolve(data);\n\n    const ctx = options.context;\n    const args = options.args || [];\n\n    args.unshift(data);\n\n    return Promise.each(filters, filter => Reflect.apply(Promise.method(filter), ctx, args).then(result => {\n      args[0] = result == null ? args[0] : result;\n      return args[0];\n    })).then(() => args[0]);\n  }\n\n  execSync(type: string, data: any, options: FilterOptions = {}) {\n    const filters = this.list(type);\n    const filtersLen = filters.length;\n    if (filtersLen === 0) return data;\n\n    const ctx = options.context;\n    const args = options.args || [];\n\n    args.unshift(data);\n\n    for (let i = 0, len = filtersLen; i < len; i++) {\n      const result = Reflect.apply(filters[i], ctx, args);\n      args[0] = result == null ? args[0] : result;\n    }\n\n    return args[0];\n  }\n}\n\nexport = Filter;\n"
  },
  {
    "path": "lib/extend/generator.ts",
    "content": "import Promise from 'bluebird';\nimport type { BaseGeneratorReturn, NodeJSLikeCallback, SiteLocals } from '../types';\n\ntype ReturnType = BaseGeneratorReturn | BaseGeneratorReturn[];\ntype GeneratorReturnType = ReturnType | Promise<ReturnType>;\n\ninterface GeneratorFunction {\n  (locals: SiteLocals, callback?: NodeJSLikeCallback<any>): GeneratorReturnType;\n}\n\ntype StoreFunctionReturn = Promise<ReturnType>;\n\ninterface StoreFunction {\n  (locals: SiteLocals): StoreFunctionReturn;\n}\n\ninterface Store {\n  [key: string]: StoreFunction\n}\n\n/**\n * A generator builds routes based on processed files.\n */\nclass Generator {\n  public id: number;\n  public store: Store;\n\n  constructor() {\n    this.id = 0;\n    this.store = {};\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  get(name: string): StoreFunction {\n    return this.store[name];\n  }\n\n  register(fn: GeneratorFunction): void\n  register(name: string, fn: GeneratorFunction): void\n  register(name: string | GeneratorFunction, fn?: GeneratorFunction): void {\n    if (!fn) {\n      if (typeof name === 'function') { // fn\n        fn = name;\n        name = `generator-${this.id++}`;\n      } else {\n        throw new TypeError('fn must be a function');\n      }\n    }\n\n    if (fn.length > 1) fn = Promise.promisify(fn);\n    this.store[name as string] = Promise.method(fn);\n  }\n}\n\nexport = Generator;\n"
  },
  {
    "path": "lib/extend/helper.ts",
    "content": "import Hexo from '../hexo';\nimport { PageSchema } from '../types';\nimport * as hutil from 'hexo-util';\n\ninterface HexoContext extends Hexo {\n  // get current page information\n  // https://github.com/dimaslanjaka/hexo-renderers/blob/147340f6d03a8d3103e9589ddf86778ed7f9019b/src/helper/related-posts.ts#L106-L113\n  page?: PageSchema;\n\n  // hexo-util shims\n  url_for: typeof hutil.url_for;\n  full_url_for: typeof hutil.full_url_for;\n  relative_url: typeof hutil.relative_url;\n  slugize: typeof hutil.slugize;\n  escapeDiacritic: typeof hutil.escapeDiacritic;\n  escapeHTML: typeof hutil.escapeHTML;\n  unescapeHTML: typeof hutil.unescapeHTML;\n  encodeURL: typeof hutil.encodeURL;\n  decodeURL: typeof hutil.decodeURL;\n  escapeRegExp: typeof hutil.escapeRegExp;\n  stripHTML: typeof hutil.stripHTML;\n  stripIndent: typeof hutil.stripIndent;\n  hash: typeof hutil.hash;\n  createSha1Hash: typeof hutil.createSha1Hash;\n  highlight: typeof hutil.highlight;\n  prismHighlight: typeof hutil.prismHighlight;\n  tocObj: typeof hutil.tocObj;\n  wordWrap: typeof hutil.wordWrap;\n  prettyUrls: typeof hutil.prettyUrls;\n  isExternalLink: typeof hutil.isExternalLink;\n  gravatar: typeof hutil.gravatar;\n  htmlTag: typeof hutil.htmlTag;\n  truncate: typeof hutil.truncate;\n  spawn: typeof hutil.spawn;\n  camelCaseKeys: typeof hutil.camelCaseKeys;\n  deepMerge: typeof hutil.deepMerge;\n}\n\ninterface StoreFunction {\n  (this: HexoContext, ...args: any[]): any;\n}\n\ninterface Store {\n  [key: string]: StoreFunction;\n}\n\n/**\n * 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.\n */\nclass Helper {\n  public store: Store;\n\n  constructor() {\n    this.store = {};\n  }\n\n  /**\n   * @returns {Store} - The plugin store\n   */\n  list(): Store {\n    return this.store;\n  }\n\n  /**\n   * Get helper plugin function by name\n   * @param {String} name - The name of the helper plugin\n   * @returns {StoreFunction}\n   */\n  get(name: string): StoreFunction {\n    return this.store[name];\n  }\n\n  /**\n   * Register a helper plugin\n   * @param {String} name - The name of the helper plugin\n   * @param {StoreFunction} fn - The helper plugin function\n   */\n  register(name: string, fn: StoreFunction): void {\n    if (!name) throw new TypeError('name is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    this.store[name] = fn;\n  }\n}\n\nexport = Helper;\n"
  },
  {
    "path": "lib/extend/index.ts",
    "content": "export { default as Console } from './console';\nexport { default as Deployer } from './deployer';\nexport { default as Filter } from './filter';\nexport { default as Generator } from './generator';\nexport { default as Helper } from './helper';\nexport { default as Highlight } from './syntax_highlight';\nexport { default as Injector } from './injector';\nexport { default as Migrator } from './migrator';\nexport { default as Processor } from './processor';\nexport { default as Renderer } from './renderer';\nexport { default as Tag } from './tag';\n"
  },
  {
    "path": "lib/extend/injector.ts",
    "content": "import { Cache } from 'hexo-util';\n\ntype Entry = 'head_begin' | 'head_end' | 'body_begin' | 'body_end';\n\ntype Store = {\n  [key in Entry]: {\n    [key: string]: Set<string>;\n  };\n};\n\n/**\n * An injector is used to add static code snippet to the `<head>` or/and `<body>` of generated HTML files.\n * Hexo run injector before `after_render:html` filter is executed.\n */\nclass Injector {\n  public store: Store;\n  public cache: InstanceType<typeof Cache>;\n  public page: any;\n\n  constructor() {\n    this.store = {\n      head_begin: {},\n      head_end: {},\n      body_begin: {},\n      body_end: {}\n    };\n\n    this.cache = new Cache();\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  get(entry: Entry, to = 'default'): any[] {\n    return Array.from(this.store[entry][to] || []);\n  }\n\n  getText(entry: Entry, to = 'default'): string {\n    const arr = this.get(entry, to);\n    if (!arr || !arr.length) return '';\n    return arr.join('');\n  }\n\n  getSize(entry: Entry): number {\n    return this.cache.apply(`${entry}-size`, () => Object.keys(this.store[entry]).length) as number;\n  }\n\n  register(entry: Entry, value: string | (() => string), to = 'default'): void {\n    if (!entry) throw new TypeError('entry is required');\n    if (typeof value === 'function') value = value();\n\n    const entryMap = this.store[entry] || this.store.head_end;\n    const valueSet = entryMap[to] || new Set();\n    valueSet.add(value);\n    entryMap[to] = valueSet;\n  }\n\n  _getPageType(pageLocals): string {\n    let currentType = 'default';\n    if (pageLocals.__index) currentType = 'home';\n    if (pageLocals.__post) currentType = 'post';\n    if (pageLocals.__page) currentType = 'page';\n    if (pageLocals.archive) currentType = 'archive';\n    if (pageLocals.category) currentType = 'category';\n    if (pageLocals.tag) currentType = 'tag';\n    if (pageLocals.layout) currentType = pageLocals.layout;\n\n    return currentType;\n  }\n\n  _injector(input: string, pattern: string | RegExp, flag: Entry, isBegin = true, currentType: string): string {\n    if (input.includes(`<!-- hexo injector ${flag}`)) return input;\n\n    const code = this.cache.apply(`${flag}-${currentType}-code`, () => {\n      const content = currentType === 'default' ? this.getText(flag, 'default') : this.getText(flag, currentType) + this.getText(flag, 'default');\n\n      if (!content.length) return '';\n      return '<!-- hexo injector ' + flag + ' start -->' + content + '<!-- hexo injector ' + flag + ' end -->';\n    }) as string;\n\n    // avoid unnecessary replace() for better performance\n    if (!code.length) return input;\n\n    return input.replace(pattern, str => { return isBegin ? str + code : code + str; });\n  }\n\n  exec(data: string, locals = { page: {} }): string {\n    const { page } = locals;\n    const currentType = this._getPageType(page);\n\n    if (this.getSize('head_begin') !== 0) {\n      // Inject head_begin\n      data = this._injector(data, /<head.*?>/, 'head_begin', true, currentType);\n    }\n\n    if (this.getSize('head_end') !== 0) {\n      // Inject head_end\n      data = this._injector(data, '</head>', 'head_end', false, currentType);\n    }\n\n    if (this.getSize('body_begin') !== 0) {\n      // Inject body_begin\n      data = this._injector(data, /<body.*?>/, 'body_begin', true, currentType);\n    }\n\n    if (this.getSize('body_end') !== 0) {\n      // Inject body_end\n      data = this._injector(data, '</body>', 'body_end', false, currentType);\n    }\n\n    return data;\n  }\n}\n\nexport = Injector;\n"
  },
  {
    "path": "lib/extend/migrator.ts",
    "content": "import Promise from 'bluebird';\nimport type { NodeJSLikeCallback } from '../types';\nimport type Hexo from '../hexo';\n\ninterface StoreFunction {\n  (this: Hexo, args: any): Promise<any>;\n}\n\ninterface Store {\n  [key: string]: StoreFunction\n}\n\n/**\n * A migrator helps users migrate from other systems to Hexo.\n */\nclass Migrator {\n  public store: Store;\n\n  constructor() {\n    this.store = {};\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  get(name: string): StoreFunction {\n    return this.store[name];\n  }\n\n  register(name: string, fn: (this: Hexo, args: any, callback?: NodeJSLikeCallback<any>) => any): void {\n    if (!name) throw new TypeError('name is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    if (fn.length > 1) {\n      fn = Promise.promisify(fn);\n    } else {\n      fn = Promise.method(fn);\n    }\n\n    this.store[name] = fn;\n  }\n}\n\nexport = Migrator;\n"
  },
  {
    "path": "lib/extend/processor.ts",
    "content": "import Promise from 'bluebird';\nimport { Pattern } from 'hexo-util';\nimport type File from '../box/file';\n\ninterface StoreFunction {\n  (file: File | string): any;\n}\n\ntype Store = {\n  pattern: Pattern;\n  process: StoreFunction;\n}[];\n\ntype patternType = Exclude<ConstructorParameters<typeof Pattern>[0], (str: string) => string>;\n\n/**\n * A processor is used to process source files in the `source` folder.\n */\nclass Processor {\n  public store: Store;\n\n  constructor() {\n    this.store = [];\n  }\n\n  list(): Store {\n    return this.store;\n  }\n\n  register(fn: StoreFunction): void;\n  register(pattern: patternType, fn: StoreFunction): void;\n  register(pattern: patternType | StoreFunction, fn?: StoreFunction): void {\n    if (!fn) {\n      if (typeof pattern === 'function') {\n        fn = pattern;\n        pattern = /(.*)/;\n      } else {\n        throw new TypeError('fn must be a function');\n      }\n    }\n\n    if (fn.length > 1) {\n      fn = Promise.promisify(fn);\n    } else {\n      fn = Promise.method(fn);\n    }\n\n    this.store.push({\n      pattern: new Pattern(pattern as patternType),\n      process: fn\n    });\n  }\n}\n\nexport = Processor;\n"
  },
  {
    "path": "lib/extend/renderer.ts",
    "content": "import { extname } from 'path';\nimport Promise from 'bluebird';\nimport type { NodeJSLikeCallback } from '../types';\n\nconst getExtname = (str: string) => {\n  if (typeof str !== 'string') return '';\n\n  const ext = extname(str) || str;\n  return ext.startsWith('.') ? ext.slice(1) : ext;\n};\n\nexport interface StoreFunctionData {\n  path?: any;\n  text?: string;\n  engine?: string;\n  toString?: any;\n  onRenderEnd?: (data: string) => any;\n}\n\nexport interface StoreSyncFunction {\n  (\n    data: StoreFunctionData,\n    options?: object\n  ): any;\n  output?: string;\n  compile?: (data: StoreFunctionData) => (local: any) => any;\n  disableNunjucks?: boolean;\n  [key: string]: any;\n}\n\nexport interface StoreFunction {\n  (\n    data: StoreFunctionData,\n    options?: object\n  ): Promise<any>;\n  output?: string;\n  compile?: (data: StoreFunctionData) => (local: any) => any;\n  disableNunjucks?: boolean;\n  [key: string]: any;\n}\n\ninterface StoreFunctionWithCallback {\n  (\n    data: StoreFunctionData,\n    options: object,\n    callback?: NodeJSLikeCallback<any>\n  ): Promise<any>;\n  output?: string;\n  compile?: (data: StoreFunctionData) => (local: any) => any;\n  disableNunjucks?: boolean;\n  [key: string]: any;\n}\n\ninterface SyncStore {\n  [key: string]: StoreSyncFunction;\n}\ninterface Store {\n  [key: string]: StoreFunction;\n}\n\n/**\n * A renderer is used to render content.\n */\nclass Renderer {\n  public store: Store;\n  public storeSync: SyncStore;\n\n  constructor() {\n    this.store = {};\n    this.storeSync = {};\n  }\n\n  list(sync = false): Store | SyncStore {\n    return sync ? this.storeSync : this.store;\n  }\n\n  get(name: string, sync?: boolean): StoreSyncFunction | StoreFunction {\n    const store = this[sync ? 'storeSync' : 'store'];\n\n    return store[getExtname(name)] || store[name];\n  }\n\n  isRenderable(path: string): boolean {\n    return Boolean(this.get(path));\n  }\n\n  isRenderableSync(path: string): boolean {\n    return Boolean(this.get(path, true));\n  }\n\n  getOutput(path: string): string {\n    const renderer = this.get(path);\n    return renderer ? renderer.output : '';\n  }\n\n  register(name: string, output: string, fn: StoreFunctionWithCallback): void;\n  register(name: string, output: string, fn: StoreFunctionWithCallback, sync: false): void;\n  register(name: string, output: string, fn: StoreSyncFunction, sync: true): void;\n  register(name: string, output: string, fn: StoreFunctionWithCallback | StoreSyncFunction, sync: boolean): void;\n  register(name: string, output: string, fn: StoreFunctionWithCallback | StoreSyncFunction, sync?: boolean) {\n    if (!name) throw new TypeError('name is required');\n    if (!output) throw new TypeError('output is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    name = getExtname(name);\n    output = getExtname(output);\n\n    if (sync) {\n      this.storeSync[name] = fn;\n      this.storeSync[name].output = output;\n\n      this.store[name] = Promise.method(fn);\n      this.store[name].disableNunjucks = (fn as StoreFunction).disableNunjucks;\n    } else {\n      if (fn.length > 2) fn = Promise.promisify(fn);\n      this.store[name] = fn;\n    }\n\n    this.store[name].output = output;\n    this.store[name].compile = fn.compile;\n  }\n}\n\nexport default Renderer;\n"
  },
  {
    "path": "lib/extend/syntax_highlight.ts",
    "content": "import type Hexo from '../hexo';\n\nexport interface HighlightOptions {\n  lang: string | undefined,\n  caption: string | undefined,\n  lines_length?: number | undefined,\n\n  // plugins/filter/before_post_render/backtick_code_block\n  firstLineNumber?: string | number\n\n  // plugins/tag/code.ts\n  language_attr?: boolean | undefined;\n  firstLine?: string | number;\n  line_number?: boolean | undefined;\n  line_threshold?: number | undefined;\n  mark?: number[] | string;\n  wrap?: boolean | undefined;\n\n}\n\ninterface HighlightExecArgs {\n  context?: Hexo;\n  args?: [string, HighlightOptions];\n}\n\ninterface StoreFunction {\n  (content: string, options: HighlightOptions): string;\n  priority?: number;\n}\n\ninterface Store {\n  [key: string]: StoreFunction\n}\n\nclass SyntaxHighlight {\n  public store: Store;\n\n  constructor() {\n    this.store = {};\n  }\n\n  register(name: string, fn: StoreFunction): void {\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    this.store[name] = fn;\n  }\n\n  query(name: string): StoreFunction {\n    return name && this.store[name];\n  }\n\n  exec(name: string, options: HighlightExecArgs): string {\n    const fn = this.store[name];\n\n    if (!fn) throw new TypeError(`syntax highlighter ${name} is not registered`);\n    const ctx = options.context;\n    const args = options.args || [];\n\n    return Reflect.apply(fn, ctx, args);\n  }\n}\n\nexport default SyntaxHighlight;\n"
  },
  {
    "path": "lib/extend/tag.ts",
    "content": "import { stripIndent } from 'hexo-util';\nimport { cyan, magenta, red, bold } from 'picocolors';\nimport { Environment } from 'nunjucks';\nimport Promise from 'bluebird';\nimport type { NodeJSLikeCallback } from '../types';\n\nconst rSwigRawFullBlock = /{% *raw *%}/;\nconst rCodeTag = /<code[^<>]*>[\\s\\S]+?<\\/code>/g;\nconst escapeSwigTag = (str: string) => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n\ninterface TagFunction {\n  (args: any[], content: string, callback?: NodeJSLikeCallback<any>): string | PromiseLike<string>;\n}\ninterface AsyncTagFunction {\n  (args: any[], content: string): Promise<string>;\n}\n\nclass NunjucksTag {\n  public tags: string[];\n  public fn: TagFunction | AsyncTagFunction;\n\n  constructor(name: string, fn: TagFunction | AsyncTagFunction) {\n    this.tags = [name];\n    this.fn = fn;\n  }\n\n  parse(parser, nodes, lexer) {\n    const node = this._parseArgs(parser, nodes, lexer);\n\n    return new nodes.CallExtension(this, 'run', node, []);\n  }\n\n  _parseArgs(parser, nodes, lexer) {\n    const tag = parser.nextToken();\n    const node = new nodes.NodeList(tag.lineno, tag.colno);\n    const argarray = new nodes.Array(tag.lineno, tag.colno);\n\n    let token;\n    let argitem = '';\n\n    while ((token = parser.nextToken(true))) {\n      if (token.type === lexer.TOKEN_WHITESPACE || token.type === lexer.TOKEN_BLOCK_END) {\n        if (argitem !== '') {\n          const argnode = new nodes.Literal(tag.lineno, tag.colno, argitem.trim());\n          argarray.addChild(argnode);\n          argitem = '';\n        }\n\n        if (token.type === lexer.TOKEN_BLOCK_END) {\n          break;\n        }\n      } else {\n        argitem += token.value;\n      }\n    }\n\n    node.addChild(argarray);\n\n    return node;\n  }\n\n  run(context, args, _body, _callback) {\n    return this._run(context, args, '');\n  }\n\n  _run(context, args, body): any {\n    return Reflect.apply(this.fn, context.ctx, [args, body]);\n  }\n}\n\nconst trimBody = (body: () => any) => {\n  return stripIndent(body()).replace(/^\\n?|\\n?$/g, '');\n};\n\nclass NunjucksBlock extends NunjucksTag {\n  parse(parser, nodes, lexer) {\n    const node = this._parseArgs(parser, nodes, lexer);\n    const body = this._parseBody(parser, nodes, lexer);\n\n    return new nodes.CallExtension(this, 'run', node, [body]);\n  }\n\n  _parseBody(parser, _nodes, _lexer) {\n    const body = parser.parseUntilBlocks(`end${this.tags[0]}`);\n\n    parser.advanceAfterBlockEnd();\n    return body;\n  }\n\n  run(context, args, body, _callback) {\n    return this._run(context, args, trimBody(body));\n  }\n}\n\nclass NunjucksAsyncTag extends NunjucksTag {\n  parse(parser, nodes, lexer) {\n    const node = this._parseArgs(parser, nodes, lexer);\n\n    return new nodes.CallExtensionAsync(this, 'run', node, []);\n  }\n\n  run(context, args, callback) {\n    return this._run(context, args, '').then(result => {\n      callback(null, result);\n    }, callback);\n  }\n}\n\nclass NunjucksAsyncBlock extends NunjucksBlock {\n  parse(parser, nodes, lexer) {\n    const node = this._parseArgs(parser, nodes, lexer);\n    const body = this._parseBody(parser, nodes, lexer);\n\n    return new nodes.CallExtensionAsync(this, 'run', node, [body]);\n  }\n\n  run(context, args, body, callback) {\n    // enable async tag nesting\n    body((err, result) => {\n      // wrapper for trimBody expecting\n      // body to be a function\n      body = () => result || '';\n\n      this._run(context, args, trimBody(body)).then(result => {\n        callback(err, result);\n      });\n    });\n  }\n}\n\nconst getContextLineNums = (min: number, max: number, center: number, amplitude: number) => {\n  const result = [];\n  let lbound = Math.max(min, center - amplitude);\n  const hbound = Math.min(max, center + amplitude);\n  while (lbound <= hbound) result.push(lbound++);\n  return result;\n};\n\nconst LINES_OF_CONTEXT = 5;\n\nconst getContext = (lines: string[], errLine: number, location: string, type: string) => {\n  const message = [\n    location + ' ' + red(type),\n    cyan('    =====               Context Dump               ====='),\n    cyan('    === (line number probably different from source) ===')\n  ];\n\n  message.push(\n    // get LINES_OF_CONTEXT lines surrounding `errLine`\n    ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT)\n      .map(lnNum => {\n        const line = '  ' + lnNum + ' | ' + lines[lnNum - 1];\n        if (lnNum === errLine) {\n          return cyan(bold(line));\n        }\n\n        return cyan(line);\n      })\n  );\n  message.push(cyan(\n    '    =====             Context Dump Ends            ====='));\n\n  return message;\n};\n\nclass NunjucksError extends Error {\n  line?: number;\n  location?: string;\n  type?: string;\n}\n\n/**\n * Provide context for Nunjucks error\n * @param  {Error}    err Nunjucks error\n * @param  {string}   str string input for Nunjucks\n * @return {Error}    New error object with embedded context\n */\nconst formatNunjucksError = (err: Error, input: string, source = ''): Error => {\n  err.message = err.message.replace('(unknown path)', source ? magenta(source) : '');\n\n  const match = err.message.match(/Line (\\d+), Column \\d+/);\n  if (!match) return err;\n  const errLine = parseInt(match[1], 10);\n  if (isNaN(errLine)) return err;\n\n  // trim useless info from Nunjucks Error\n  const splitted = err.message.split('\\n');\n\n  const e = new NunjucksError();\n  e.name = 'Nunjucks Error';\n  e.line = errLine;\n  e.location = splitted[0];\n  e.type = splitted[1].trim();\n  e.message = getContext(input.split(/\\r?\\n/), errLine, e.location, e.type).join('\\n');\n  return e;\n};\n\ntype RegisterOptions = {\n  async?: boolean;\n  ends?: boolean;\n}\n\n/**\n * A tag allows users to quickly and easily insert snippets into their posts.\n */\nclass Tag {\n  public env: Environment;\n  public source: string;\n\n  constructor() {\n    this.env = new Environment(null, {\n      autoescape: false\n    });\n  }\n\n  register(name: string, fn: TagFunction): void\n  register(name: string, fn: TagFunction, ends: boolean): void\n  register(name: string, fn: TagFunction, options: RegisterOptions): void\n  register(name: string, fn: TagFunction, options?: RegisterOptions | boolean):void {\n    if (!name) throw new TypeError('name is required');\n    if (typeof fn !== 'function') throw new TypeError('fn must be a function');\n\n    if (options == null || typeof options === 'boolean') {\n      options = { ends: options as boolean };\n    }\n\n    let tag: NunjucksTag;\n\n    if (options.async) {\n      let asyncFn: AsyncTagFunction;\n      if (fn.length > 2) {\n        asyncFn = Promise.promisify(fn);\n      } else {\n        asyncFn = Promise.method(fn);\n      }\n\n      if (options.ends) {\n        tag = new NunjucksAsyncBlock(name, asyncFn);\n      } else {\n        tag = new NunjucksAsyncTag(name, asyncFn);\n      }\n    } else if (options.ends) {\n      tag = new NunjucksBlock(name, fn);\n    } else {\n      tag = new NunjucksTag(name, fn);\n    }\n\n    this.env.addExtension(name, tag);\n  }\n\n  unregister(name: string): void {\n    if (!name) throw new TypeError('name is required');\n\n    const { env } = this;\n\n    if (env.hasExtension(name)) env.removeExtension(name);\n  }\n\n  render(str: string): Promise<any>;\n  render(str: string, callback: NodeJSLikeCallback<any>): Promise<any>;\n  render(str: string, options: { source?: string, [key: string]: any }, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  render(str: string, options: { source?: string, [key: string]: any } | NodeJSLikeCallback<any> = {}, callback?: NodeJSLikeCallback<any>): Promise<any> {\n    if (!callback && typeof options === 'function') {\n      callback = options;\n      options = {};\n    }\n\n    // Get path of post from source\n    const { source = '' } = options as { source?: string };\n\n    return Promise.fromCallback(cb => {\n      this.env.renderString(\n        str.replace(rCodeTag, s => {\n          // https://hexo.io/docs/tag-plugins#Raw\n          // https://mozilla.github.io/nunjucks/templating.html#raw\n          // Only escape code block when there is no raw tag included\n          return s.match(rSwigRawFullBlock) ? s : escapeSwigTag(s);\n        }),\n        options,\n        cb\n      );\n    }).catch(err => {\n      return Promise.reject(formatNunjucksError(err, str, source));\n    })\n      .asCallback(callback);\n  }\n}\n\nexport = Tag;\n"
  },
  {
    "path": "lib/hexo/default_config.ts",
    "content": "export = {\n  // Site\n  title: 'Hexo',\n  subtitle: '',\n  description: '',\n  author: 'John Doe',\n  language: 'en',\n  timezone: '',\n  // URL\n  url: 'http://example.com',\n  root: '/',\n  permalink: ':year/:month/:day/:title/',\n  permalink_defaults: {} as Record<string, string>,\n  pretty_urls: {\n    trailing_index: true,\n    trailing_html: true\n  },\n  // Directory\n  source_dir: 'source',\n  public_dir: 'public',\n  tag_dir: 'tags',\n  archive_dir: 'archives',\n  category_dir: 'categories',\n  code_dir: 'downloads/code',\n  i18n_dir: ':lang',\n  skip_render: [] as string[],\n  // Writing\n  new_post_name: ':title.md',\n  default_layout: 'post',\n  titlecase: false,\n  external_link: {\n    enable: true,\n    field: 'site',\n    exclude: ''\n  },\n  filename_case: 0,\n  render_drafts: false,\n  post_asset_folder: false,\n  relative_link: false,\n  future: true,\n  syntax_highlighter: 'highlight.js',\n  highlight: {\n    auto_detect: false,\n    line_number: true,\n    tab_replace: '',\n    wrap: true,\n    exclude_languages: [] as string[],\n    language_attr: false,\n    hljs: false,\n    line_threshold: 0,\n    first_line_number: 'always1',\n    strip_indent: true\n  },\n  prismjs: {\n    preprocess: true,\n    line_number: true,\n    tab_replace: '',\n    exclude_languages: [] as string[],\n    strip_indent: true\n  },\n  use_filename_as_post_title: false,\n\n  // Category & Tag\n  default_category: 'uncategorized',\n  category_map: {} as Record<string, string>,\n  tag_map: {} as Record<string, string>,\n  // Date / Time format\n  date_format: 'YYYY-MM-DD',\n  time_format: 'HH:mm:ss',\n  updated_option: 'mtime',\n  // * mtime: file modification date (default)\n  // * empty: no more update\n  // Pagination\n  per_page: 10,\n  pagination_dir: 'page',\n  // Extensions\n  theme: 'landscape',\n  server: {\n    cache: false\n  },\n  // Deployment\n  deploy: {} as { type: string; [keys: string]: any } | { type: string; [keys: string]: any }[],\n\n  // ignore files from processing\n  ignore: [] as string[],\n\n  // Category & Tag\n  meta_generator: true\n};\n"
  },
  {
    "path": "lib/hexo/index.ts",
    "content": "import Promise from 'bluebird';\nimport { sep, join, dirname } from 'path';\nimport tildify from 'tildify';\nimport Database from 'warehouse';\nimport { magenta, underline } from 'picocolors';\nimport { EventEmitter } from 'events';\nimport { readFile } from 'hexo-fs';\nimport Module from 'module';\nimport { runInThisContext } from 'vm';\nconst { version } = require('../../package.json');\nimport logger from 'hexo-log';\n\nimport {\n  Console,\n  Deployer,\n  Filter,\n  Generator,\n  Helper,\n  Highlight,\n  Injector,\n  Migrator,\n  Processor,\n  Renderer,\n  Tag\n} from '../extend';\n\nimport Render from './render';\nimport registerModels from './register_models';\nimport Post from './post';\nimport Scaffold from './scaffold';\nimport Source from './source';\nimport Router from './router';\nimport Theme from '../theme';\nimport Locals from './locals';\nimport defaultConfig from './default_config';\nimport loadDatabase from './load_database';\nimport multiConfigPath from './multi_config_path';\nimport { deepMerge, full_url_for } from 'hexo-util';\nimport type Box from '../box';\nimport type { BaseGeneratorReturn, FilterOptions, LocalsType, NodeJSLikeCallback, SiteLocals } from '../types';\nimport type { AddSchemaTypeOptions } from 'warehouse/dist/types';\nimport type Schema from 'warehouse/dist/schema';\nimport BinaryRelationIndex from '../models/binary_relation_index';\n\nconst libDir = dirname(__dirname);\nconst dbVersion = 1;\n\nconst stopWatcher = (box: Box) => { if (box.isWatching()) box.unwatch(); };\n\nconst routeCache = new WeakMap();\n\nconst castArray = (obj: any) => { return Array.isArray(obj) ? obj : [obj]; };\n\n// eslint-disable-next-line no-use-before-define\nconst mergeCtxThemeConfig = (ctx: Hexo) => {\n  // Merge hexo.config.theme_config into hexo.theme.config before post rendering & generating\n  // config.theme_config has \"_config.[theme].yml\" merged in load_theme_config.js\n  if (ctx.config.theme_config) {\n    ctx.theme.config = deepMerge(ctx.theme.config, ctx.config.theme_config);\n  }\n};\n\n// eslint-disable-next-line no-use-before-define\nconst createLoadThemeRoute = function(generatorResult: BaseGeneratorReturn, locals: LocalsType, ctx: Hexo) {\n  const { log, theme } = ctx;\n  const { path, cache: useCache } = locals;\n\n  const layout = [...new Set<string>(castArray(generatorResult.layout))];\n  const layoutLength = layout.length;\n\n  // always use cache in fragment_cache\n  locals.cache = true;\n  return () => {\n    if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult);\n\n    for (let i = 0; i < layoutLength; i++) {\n      const name = layout[i];\n      const view = theme.getView(name);\n\n      if (view) {\n        log.debug(`Rendering HTML ${name}: ${magenta(path)}`);\n        return view.render(locals)\n          .then(result => ctx.extend.injector.exec(result, locals))\n          .then(result => ctx.execFilter('_after_html_render', result, {\n            context: ctx,\n            args: [locals]\n          }))\n          .tap(result => {\n            if (useCache) {\n              routeCache.set(generatorResult, result);\n            }\n          }).tapCatch(err => {\n            log.error({ err }, `Render HTML failed: ${magenta(path)}`);\n          });\n      }\n    }\n\n    log.warn(`No layout: ${magenta(path)}`);\n  };\n};\n\nfunction debounce(func: () => void, wait: number): () => void {\n  let timeout: NodeJS.Timeout;\n  return function() {\n    clearTimeout(timeout);\n    timeout = setTimeout(() => {\n      func.apply(this);\n    }, wait);\n  };\n}\n\ninterface Args {\n\n  /**\n   * Enable debug mode. Display debug messages in the terminal and save debug.log in the root directory.\n   */\n  debug?: boolean;\n\n  /**\n   * Enable safe mode. Don’t load any plugins.\n   */\n  safe?: boolean;\n\n  /**\n   * Enable silent mode. Don’t display any messages in the terminal.\n   */\n  silent?: boolean;\n\n  /**\n   * Enable to add drafts to the posts list.\n   */\n  draft?: boolean;\n\n    /**\n   * Enable to add drafts to the posts list.\n   */\n  drafts?: boolean;\n  _?: string[];\n  output?: string;\n\n  /**\n   * Specify the path of the configuration file.\n   */\n  config?: string;\n  [key: string]: any;\n}\n\ninterface Query {\n  date?: any;\n  published?: boolean;\n}\n\ninterface Extend {\n  console: Console,\n  deployer: Deployer,\n  filter: Filter,\n  generator: Generator,\n  helper: Helper,\n  highlight: Highlight,\n  injector: Injector,\n  migrator: Migrator,\n  processor: Processor,\n  renderer: Renderer,\n  tag: Tag\n}\n\ninterface Env {\n  args: Args;\n  debug: boolean;\n  safe: boolean;\n  silent: boolean;\n  env: string;\n  version: string;\n  cmd: string;\n  init: boolean;\n}\n\ntype DefaultConfigType = typeof defaultConfig;\ninterface Config extends DefaultConfigType {\n  [key: string]: any;\n}\n\n// Node.js internal APIs\ndeclare module 'module' {\n  function _nodeModulePaths(path: string): string[];\n  function _resolveFilename(request: string, parent: Module, isMain?: any, options?: any): string;\n  const _extensions: NodeJS.RequireExtensions,\n    _cache: any;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\ninterface Hexo {\n\n  /**\n   * Emitted before deployment begins.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#deployBefore\n   */\n  on(event: 'deployBefore', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted after deployment begins.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#deployAfter\n   */\n  on(event: 'deployAfter', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted before Hexo exits.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#exit\n   */\n  on(event: 'exit', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted before generation begins.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#generateBefore\n   */\n  on(event: 'generateBefore', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted after generation finishes.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#generateAfter\n   */\n  on(event: 'generateAfter', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted after a new post has been created. This event returns the post data:\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#new\n   */\n  on(event: 'new', listener: (post: { path: string; content: string; }) => any): this;\n\n  /**\n   * Emitted before processing begins. This event returns a path representing the root directory of the box.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#processBefore\n   */\n  on(event: 'processBefore', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted after processing finishes. This event returns a path representing the root directory of the box.\n   * @param event\n   * @param listener\n   * @link https://hexo.io/api/events.html#processAfter\n   */\n  on(event: 'processAfter', listener: (...args: any[]) => any): this;\n\n  /**\n   * Emitted after initialization finishes.\n   * @param event\n   * @param listener\n   */\n  on(event: 'ready', listener: (...args: any[]) => any): this;\n\n  /**\n   * undescripted on emit\n   * @param event\n   * @param listener\n   */\n  on(event: string, listener: (...args: any[]) => any): any;\n  emit(event: string, ...args: any[]): any;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging\nclass Hexo extends EventEmitter {\n  public base_dir: string;\n  public public_dir: string;\n  public source_dir: string;\n  public plugin_dir: string;\n  public script_dir: string;\n  public scaffold_dir: string;\n  public theme_dir: string;\n  public theme_script_dir: string;\n  public env: Env;\n  public extend: Extend;\n  public config: Config;\n  public log: ReturnType<typeof logger>;\n  public render: Render;\n  public route: Router;\n  public post: Post;\n  public scaffold: Scaffold;\n  public _dbLoaded: boolean;\n  public _isGenerating: boolean;\n  public database: Database;\n  public config_path: string;\n  public source: Source;\n  public theme: Theme;\n  public locals: Locals;\n  public version: string;\n  public _watchBox: () => void;\n  public lib_dir: string;\n  public core_dir: string;\n  static lib_dir: string;\n  static core_dir: string;\n  static version: string;\n  public _binaryRelationIndex: {\n    post_tag: BinaryRelationIndex<'post_id', 'tag_id'>;\n    post_category: BinaryRelationIndex<'post_id', 'category_id'>;\n  };\n\n  constructor(base = process.cwd(), args: Args = {}) {\n    super();\n\n    this.base_dir = base + sep;\n    this.public_dir = join(base, 'public') + sep;\n    this.source_dir = join(base, 'source') + sep;\n    this.plugin_dir = join(base, 'node_modules') + sep;\n    this.script_dir = join(base, 'scripts') + sep;\n    this.scaffold_dir = join(base, 'scaffolds') + sep;\n    this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep;\n    this.theme_script_dir = join(this.theme_dir, 'scripts') + sep;\n\n    this.env = {\n      args,\n      debug: Boolean(args.debug),\n      safe: Boolean(args.safe),\n      silent: Boolean(args.silent),\n      env: process.env.NODE_ENV || 'development',\n      version,\n      cmd: args._ ? args._[0] : '',\n      init: false\n    };\n\n    this.extend = {\n      console: new Console(),\n      deployer: new Deployer(),\n      filter: new Filter(),\n      generator: new Generator(),\n      helper: new Helper(),\n      highlight: new Highlight(),\n      injector: new Injector(),\n      migrator: new Migrator(),\n      processor: new Processor(),\n      renderer: new Renderer(),\n      tag: new Tag()\n    };\n\n    this.config = { ...defaultConfig };\n\n    this.log = logger(this.env);\n\n    this.render = new Render(this);\n\n    this.route = new Router();\n\n    this.post = new Post(this);\n\n    this.scaffold = new Scaffold(this);\n\n    this._dbLoaded = false;\n\n    this._isGenerating = false;\n\n    // If `output` is provided, use that as the\n    // root for saving the db. Otherwise default to `base`.\n    const dbPath = args.output || base;\n\n    if (/^(init|new|g|publish|s|deploy|render|migrate)/.test(this.env.cmd)) {\n      this.log.d(`Writing database to ${join(dbPath, 'db.json')}`);\n    }\n\n    this.database = new Database({\n      version: dbVersion,\n      path: join(dbPath, 'db.json')\n    });\n\n    const mcp = multiConfigPath(this);\n\n    this.config_path = args.config ? mcp(base, args.config, args.output)\n      : join(base, '_config.yml');\n\n    registerModels(this);\n\n    this.source = new Source(this);\n    this.theme = new Theme(this);\n    this.locals = new Locals();\n    this._bindLocals();\n    this._binaryRelationIndex = {\n      post_tag: new BinaryRelationIndex<'post_id', 'tag_id'>('post_id', 'tag_id', 'PostTag', this),\n      post_category: new BinaryRelationIndex<'post_id', 'category_id'>('post_id', 'category_id', 'PostCategory', this)\n    };\n  }\n\n  _bindLocals(): void {\n    const db = this.database;\n    const { locals } = this;\n\n    locals.set('posts', () => {\n      const query: Query = {};\n\n      if (!this.config.future) {\n        query.date = { $lte: Date.now() };\n      }\n\n      if (!this._showDrafts()) {\n        query.published = true;\n      }\n\n      return db.model('Post').find(query);\n    });\n\n    locals.set('pages', () => {\n      const query: Query = {};\n\n      if (!this.config.future) {\n        query.date = { $lte: Date.now() };\n      }\n\n      return db.model('Page').find(query);\n    });\n\n    locals.set('categories', () => {\n      // Ignore categories with zero posts\n      return db.model('Category').filter(category => category.length);\n    });\n\n    locals.set('tags', () => {\n      // Ignore tags with zero posts\n      return db.model('Tag').filter(tag => tag.length);\n    });\n\n    locals.set('data', () => {\n      const obj = {};\n\n      db.model('Data').forEach(data => {\n        obj[data._id] = data.data;\n      });\n\n      return obj;\n    });\n  }\n\n  /**\n   * Load configuration and plugins.\n   * @returns {Promise}\n   * @link https://hexo.io/api#Initialize\n   */\n  init(): Promise<void> {\n    this.log.debug('Hexo version: %s', magenta(this.version));\n    this.log.debug('Working directory: %s', magenta(tildify(this.base_dir)));\n\n    // Load internal plugins\n    require('../plugins/console')(this);\n    require('../plugins/filter')(this);\n    require('../plugins/generator')(this);\n    require('../plugins/helper')(this);\n    require('../plugins/highlight')(this);\n    require('../plugins/injector')(this);\n    require('../plugins/processor')(this);\n    require('../plugins/renderer')(this);\n    require('../plugins/tag').default(this);\n\n    // Load config\n    return Promise.each([\n      'update_package', // Update package.json\n      'load_config', // Load config\n      'load_theme_config', // Load alternate theme config\n      'load_plugins' // Load external plugins & scripts\n    ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, { context: this })).then(() => {\n      // Ready to go!\n      this.emit('ready');\n    });\n  }\n\n  /**\n   * Call any console command explicitly.\n   * @param name\n   * @param args\n   * @param callback\n   * @returns {Promise}\n   * @link https://hexo.io/api#Execute-Commands\n   */\n  call(name: string, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  call(name: string, args: object, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  call(name: string, args?: object | NodeJSLikeCallback<any>, callback?: NodeJSLikeCallback<any>): Promise<any> {\n    if (!callback && typeof args === 'function') {\n      callback = args as NodeJSLikeCallback<any>;\n      args = {};\n    }\n\n    const c = this.extend.console.get(name);\n\n    if (c) return (Reflect.apply(c, this, [args]) as any).asCallback(callback);\n    return Promise.reject(new Error(`Console \\`${name}\\` has not been registered yet!`));\n  }\n\n  model(name: string, schema?: Schema | Record<string, AddSchemaTypeOptions>) {\n    return this.database.model(name, schema);\n  }\n\n  resolvePlugin(name: string, basedir: string): string {\n    try {\n      // Try to resolve the plugin with the Node.js's built-in require.resolve.\n      return require.resolve(name, { paths: [basedir] });\n    } catch {\n      // There was an error (likely the node_modules is corrupt or from early version of npm),\n      // so return a possibly non-existing path that a later part of the resolution process will check.\n      return join(basedir, 'node_modules', name);\n    }\n  }\n\n  loadPlugin(path: string, callback?: NodeJSLikeCallback<any>): Promise<any> {\n    return readFile(path).then(script => {\n      // Based on: https://github.com/nodejs/node-v0.x-archive/blob/v0.10.33/src/node.js#L516\n      const module = new Module(path);\n      module.filename = path;\n      module.paths = Module._nodeModulePaths(path);\n\n      function req(path: string) {\n        return module.require(path);\n      }\n\n      req.resolve = (request: string) => Module._resolveFilename(request, module);\n\n      req.main = require.main;\n      req.extensions = Module._extensions;\n      req.cache = Module._cache;\n\n      script = `(async function(exports, require, module, __filename, __dirname, hexo){${script}\\n});`;\n\n      const fn = runInThisContext(script, path);\n\n      return fn(module.exports, req, module, path, dirname(path), this);\n    }).asCallback(callback);\n  }\n\n  _showDrafts(): boolean {\n    const { args } = this.env;\n    return args.draft || args.drafts || this.config.render_drafts;\n  }\n\n  /**\n   * Load all files in the source folder as well as the theme data.\n   * @param callback\n   * @returns {Promise}\n   * @link https://hexo.io/api#Load-Files\n   */\n  load(callback?: NodeJSLikeCallback<any>): Promise<any> {\n    return loadDatabase(this).then(() => {\n      this._binaryRelationIndex.post_tag.load();\n      this._binaryRelationIndex.post_category.load();\n      this.log.info('Start processing');\n\n      return Promise.all([\n        this.source.process(),\n        this.theme.process()\n      ]);\n    }).then(() => {\n      mergeCtxThemeConfig(this);\n      return this._generate({ cache: false });\n    }).asCallback(callback);\n  }\n\n  /**\n   * Load all files in the source folder as well as the theme data.\n   * Start watching for file changes continuously.\n   * @param callback\n   * @returns {Promise}\n   * @link https://hexo.io/api#Load-Files\n   */\n  watch(callback?: NodeJSLikeCallback<any>): Promise<any> {\n    let useCache = false;\n    const { cache } = Object.assign({\n      cache: false\n    }, this.config.server);\n    const { alias } = this.extend.console;\n\n    if (alias[this.env.cmd] === 'server' && cache) {\n      // enable cache when run hexo server\n      useCache = true;\n    }\n    this._watchBox = debounce(() => this._generate({ cache: useCache }), 100);\n\n    return loadDatabase(this).then(() => {\n      this._binaryRelationIndex.post_tag.load();\n      this._binaryRelationIndex.post_category.load();\n      this.log.info('Start processing');\n\n      return Promise.all([\n        this.source.watch(),\n        this.theme.watch()\n      ]);\n    }).then(() => {\n      mergeCtxThemeConfig(this);\n\n      this.source.on('processAfter', this._watchBox);\n      this.theme.on('processAfter', () => {\n        this._watchBox();\n        mergeCtxThemeConfig(this);\n      });\n\n      return this._generate({ cache: useCache });\n    }).asCallback(callback);\n  }\n\n  unwatch(): void {\n    if (this._watchBox != null) {\n      this.source.removeListener('processAfter', this._watchBox);\n      this.theme.removeListener('processAfter', this._watchBox);\n\n      this._watchBox = null;\n    }\n\n    stopWatcher(this.source);\n    stopWatcher(this.theme);\n  }\n\n  _generateLocals() {\n    const { config, env, theme, theme_dir } = this;\n    const ctx = { config: { url: this.config.url } };\n    const localsObj = this.locals.toObject() as SiteLocals;\n\n    class Locals {\n      page: any;\n      path: string;\n      url: string;\n      config: Config;\n      theme: any;\n      layout: string;\n      env: Env;\n      view_dir: string;\n      site: SiteLocals;\n      cache?: boolean;\n\n      constructor(path: string, locals: any) {\n        this.page = { ...locals };\n        if (this.page.path == null) this.page.path = path;\n        this.path = path;\n        this.url = full_url_for.call(ctx, path);\n        this.config = config;\n        this.theme = theme.config;\n        this.layout = 'layout';\n        this.env = env;\n        this.view_dir = join(theme_dir, 'layout') + sep;\n        this.site = localsObj;\n      }\n    }\n\n    return Locals;\n  }\n\n  _runGenerators(): Promise<BaseGeneratorReturn[]> {\n    this.locals.invalidate();\n    const siteLocals = this.locals.toObject() as SiteLocals;\n    const generators = this.extend.generator.list();\n    const { log } = this;\n\n    // Run generators\n    return Promise.map(Object.keys(generators), key => {\n      const generator = generators[key];\n\n      log.debug('Generator: %s', magenta(key));\n      return Reflect.apply(generator, this, [siteLocals]);\n    }).reduce((result, data) => {\n      return data ? result.concat(data) : result;\n    }, []);\n  }\n\n  _routerRefresh(runningGenerators: Promise<BaseGeneratorReturn[]>, useCache: boolean): Promise<void> {\n    const { route } = this;\n    const routeList = route.list();\n    const Locals = this._generateLocals();\n    Locals.prototype.cache = useCache;\n\n    return runningGenerators.map(generatorResult => {\n      if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined;\n\n      // add Route\n      const path = route.format(generatorResult.path);\n      const { data, layout } = generatorResult;\n\n      if (!layout) {\n        route.set(path, data);\n        return path;\n      }\n\n      return this.execFilter('template_locals', new Locals(path, data), { context: this })\n        .then((locals: LocalsType) => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })\n        .thenReturn(path);\n    }).then(newRouteList => {\n      // Remove old routes\n      for (let i = 0, len = routeList.length; i < len; i++) {\n        const item = routeList[i];\n\n        if (!newRouteList.includes(item)) {\n          route.remove(item);\n        }\n      }\n    });\n  }\n\n  _generate(options: { cache?: boolean } = {}): Promise<any> {\n    if (this._isGenerating) return;\n\n    const useCache = options.cache;\n\n    this._isGenerating = true;\n\n    this.emit('generateBefore');\n\n    // Run before_generate filters\n    // https://github.com/hexojs/hexo/issues/5287\n    // locals should be invalidated before before_generate filters because tags may use locals\n    this.locals.invalidate();\n    return this.execFilter('before_generate', null, { context: this })\n      .then(() => this._routerRefresh(this._runGenerators(), useCache)).then(() => {\n        this.emit('generateAfter');\n\n        // Run after_generate filters\n        return this.execFilter('after_generate', null, { context: this });\n      }).finally(() => {\n        this._isGenerating = false;\n      });\n  }\n\n  /**\n   * Exit gracefully and finish up important things such as saving the database.\n   * @param err\n   * @returns {Promise}\n   * @link https://hexo.io/api/#Exit\n   */\n  exit(err?: any): Promise<void> {\n    if (err) {\n      this.log.fatal(\n        { err },\n        'Something\\'s wrong. Maybe you can find the solution here: %s',\n        underline('https://hexo.io/docs/troubleshooting.html')\n      );\n    }\n\n    return this.execFilter('before_exit', null, { context: this }).then(() => {\n      this.emit('exit', err);\n    });\n  }\n\n  execFilter(type: string, data: any, options?: FilterOptions) {\n    return this.extend.filter.exec(type, data, options);\n  }\n\n  execFilterSync(type: string, data: any, options?: FilterOptions) {\n    return this.extend.filter.execSync(type, data, options);\n  }\n}\n\nHexo.lib_dir = libDir + sep;\nHexo.prototype.lib_dir = Hexo.lib_dir;\n\nHexo.core_dir = dirname(libDir) + sep;\nHexo.prototype.core_dir = Hexo.core_dir;\n\nHexo.version = version;\nHexo.prototype.version = Hexo.version;\n\n// define global variable\n// this useful for plugin written in typescript\ndeclare global {\n  // eslint-disable-next-line one-var\n  const hexo: Hexo;\n}\n\nexport = Hexo;\n"
  },
  {
    "path": "lib/hexo/load_config.ts",
    "content": "import { sep, resolve, join, parse, basename, extname } from 'path';\nimport tildify from 'tildify';\nimport Theme from '../theme';\nimport Source from './source';\nimport { exists, readdir } from 'hexo-fs';\nimport { magenta } from 'picocolors';\nimport { deepMerge } from 'hexo-util';\nimport validateConfig from './validate_config';\nimport type Hexo from './index';\n\nexport = async (ctx: Hexo): Promise<void> => {\n  if (!ctx.env.init) return;\n\n  const baseDir = ctx.base_dir;\n  let configPath = ctx.config_path;\n\n  const path = await exists(configPath) ? configPath : await findConfigPath(configPath);\n  if (!path) return;\n  configPath = path;\n\n  let config = await ctx.render.render({ path });\n  if (!config || typeof config !== 'object') return;\n\n  ctx.log.debug('Config loaded: %s', magenta(tildify(configPath)));\n\n  ctx.config = deepMerge(ctx.config, config);\n  // If root is not exist, create it by config.url\n  if (!config.root) {\n    let { pathname } = new URL(ctx.config.url);\n    if (!pathname.endsWith('/')) pathname += '/';\n    ctx.config.root = pathname;\n  }\n  config = ctx.config;\n\n  validateConfig(ctx);\n\n  ctx.config_path = configPath;\n  // Trim multiple trailing '/'\n  config.root = config.root.replace(/\\/*$/, '/');\n  // Remove any trailing '/'\n  config.url = config.url.replace(/\\/+$/, '');\n\n  ctx.public_dir = resolve(baseDir, config.public_dir) + sep;\n  ctx.source_dir = resolve(baseDir, config.source_dir) + sep;\n  ctx.source = new Source(ctx);\n\n  if (!config.theme) return;\n\n  const theme = config.theme.toString();\n  config.theme = theme;\n\n  const themeDirFromThemes = join(baseDir, 'themes', theme) + sep; // base_dir/themes/[config.theme]/\n  const themeDirFromNodeModules = join(ctx.plugin_dir, 'hexo-theme-' + theme) + sep; // base_dir/node_modules/hexo-theme-[config.theme]/\n\n  // themeDirFromThemes has higher priority than themeDirFromNodeModules\n  let ignored: string[] = [];\n  if (await exists(themeDirFromThemes)) {\n    ctx.theme_dir = themeDirFromThemes;\n    ignored = ['**/themes/*/node_modules/**', '**/themes/*/.git/**'];\n  } else if (await exists(themeDirFromNodeModules)) {\n    ctx.theme_dir = themeDirFromNodeModules;\n    ignored = ['**/node_modules/hexo-theme-*/node_modules/**', '**/node_modules/hexo-theme-*/.git/**'];\n  }\n  ctx.theme_script_dir = join(ctx.theme_dir, 'scripts') + sep;\n  ctx.theme = new Theme(ctx, { ignored });\n};\n\nasync function findConfigPath(path: string): Promise<string> {\n  const { dir, name } = parse(path);\n\n  const files = await readdir(dir);\n  const item = files.find(item => basename(item, extname(item)) === name);\n  if (item != null) return join(dir, item);\n}\n"
  },
  {
    "path": "lib/hexo/load_database.ts",
    "content": "import { exists, unlink } from 'hexo-fs';\nimport Promise from 'bluebird';\nimport type Hexo from './index';\n\nexport = (ctx: Hexo): Promise<void> => {\n  if (ctx._dbLoaded) return Promise.resolve();\n\n  const db = ctx.database;\n  const { path } = db.options;\n  const { log } = ctx;\n\n  return exists(path).then(exist => {\n    if (!exist) return;\n\n    log.debug('Loading database.');\n    return db.load();\n  }).then(() => {\n    ctx._dbLoaded = true;\n  }).catch(() => {\n    log.error('Database load failed. Deleting database.');\n    return unlink(path);\n  });\n};\n"
  },
  {
    "path": "lib/hexo/load_plugins.ts",
    "content": "import { join } from 'path';\nimport { exists, readFile, listDir } from 'hexo-fs';\nimport Promise from 'bluebird';\nimport { magenta } from 'picocolors';\nimport type Hexo from './index';\n\nexport = (ctx: Hexo): Promise<void[][]> => {\n  if (!ctx.env.init || ctx.env.safe) return;\n\n  return loadModules(ctx).then(() => loadScripts(ctx));\n};\n\nfunction loadModuleList(ctx: Hexo, basedir: string): Promise<Record<string, string>> {\n  const packagePath = join(basedir, 'package.json');\n\n  // Make sure package.json exists\n  return exists(packagePath).then(exist => {\n    if (!exist) return [];\n\n    // Read package.json and find dependencies\n    return readFile(packagePath).then(content => {\n      const json = JSON.parse(content);\n      const deps = Object.keys(json.dependencies || {});\n      const devDeps = Object.keys(json.devDependencies || {});\n\n      return basedir === ctx.base_dir ? deps.concat(devDeps) : deps;\n    });\n  }).filter((name: string) => {\n    // Ignore plugins whose name is not started with \"hexo-\"\n    if (!/^hexo-|^@[^/]+\\/hexo-/.test(name)) return false;\n\n    // Ignore plugin whose name is started with \"hexo-theme\"\n    if (/^hexo-theme-|^@[^/]+\\/hexo-theme-/.test(name)) return false;\n\n    // Ignore typescript definition file that is started with \"@types/\"\n    if (name.startsWith('@types/')) return false;\n\n    // Make sure the plugin exists\n    const path = ctx.resolvePlugin(name, basedir);\n    return exists(path);\n  }).then((modules: string[]) => {\n    return Object.fromEntries(modules.map(name => [name, ctx.resolvePlugin(name, basedir)]));\n  });\n}\n\nfunction loadModules(ctx: Hexo): Promise<void[]> {\n  return Promise.map([ctx.base_dir, ctx.theme_dir], basedir => loadModuleList(ctx, basedir))\n    .then(([hexoModuleList, themeModuleList]) => {\n      return Object.entries(Object.assign(themeModuleList, hexoModuleList));\n    })\n    .map(([name, path]) => {\n      // Load plugins\n      return ctx.loadPlugin(path as string).then(() => {\n        ctx.log.debug('Plugin loaded: %s', magenta(name));\n      }).catch(err => {\n        ctx.log.error({err}, 'Plugin load failed: %s', magenta(name));\n      });\n    });\n}\n\nfunction loadScripts(ctx: Hexo): Promise<void[][]> {\n  const baseDirLength = ctx.base_dir.length;\n\n  return Promise.filter([\n    ctx.theme_script_dir,\n    ctx.script_dir\n  ], scriptDir => { // Ignore the directory if it does not exist\n    return scriptDir ? exists(scriptDir) : false;\n  }).map(scriptDir => listDir(scriptDir).map(name => {\n    const path = join(scriptDir, name);\n\n    return ctx.loadPlugin(path).then(() => {\n      ctx.log.debug('Script loaded: %s', displayPath(path, baseDirLength));\n    }).catch(err => {\n      ctx.log.error({err}, 'Script load failed: %s', displayPath(path, baseDirLength));\n    });\n  }));\n}\n\nfunction displayPath(path: string, baseDirLength: number): string {\n  return magenta(path.substring(baseDirLength));\n}\n"
  },
  {
    "path": "lib/hexo/load_theme_config.ts",
    "content": "import { join, parse, basename, extname } from 'path';\nimport tildify from 'tildify';\nimport { exists, readdir } from 'hexo-fs';\nimport { magenta } from 'picocolors';\nimport { deepMerge } from 'hexo-util';\nimport type Hexo from './index';\nimport type Promise from 'bluebird';\n\nexport = (ctx: Hexo): Promise<void> => {\n  if (!ctx.env.init) return;\n  if (!ctx.config.theme) return;\n\n  let configPath = join(ctx.base_dir, `_config.${String(ctx.config.theme)}.yml`);\n\n  return exists(configPath).then(exist => {\n    return exist ? configPath : findConfigPath(configPath);\n  }).then(path => {\n    if (!path) return;\n\n    configPath = path;\n    return ctx.render.render({ path });\n  }).then(config => {\n    if (!config || typeof config !== 'object') return;\n\n    ctx.log.debug('Second Theme Config loaded: %s', magenta(tildify(configPath)));\n\n    // ctx.config.theme_config should have highest priority\n    // If ctx.config.theme_config exists, then merge it with _config.[theme].yml\n    // If ctx.config.theme_config doesn't exist, set it to _config.[theme].yml\n    ctx.config.theme_config = ctx.config.theme_config\n      ? deepMerge(config, ctx.config.theme_config) : config;\n  });\n};\n\nfunction findConfigPath(path: string): Promise<string> {\n  const { dir, name } = parse(path);\n\n  return readdir(dir).then(files => {\n    const item = files.find(item => basename(item, extname(item)) === name);\n    if (item != null) return join(dir, item);\n  });\n}\n"
  },
  {
    "path": "lib/hexo/locals.ts",
    "content": "import { Cache } from 'hexo-util';\n\nclass Locals {\n  public cache: InstanceType<typeof Cache>;\n  public getters: Record<string, () => any>;\n\n  constructor() {\n    this.cache = new Cache();\n    this.getters = {};\n  }\n\n  get(name: string): any {\n    if (typeof name !== 'string') throw new TypeError('name must be a string!');\n\n    return this.cache.apply(name, () => {\n      const getter = this.getters[name];\n      if (!getter) return;\n\n      return getter();\n    });\n  }\n\n  set(name: string, value: any): this {\n    if (typeof name !== 'string') throw new TypeError('name must be a string!');\n    if (value == null) throw new TypeError('value is required!');\n\n    const getter = typeof value === 'function' ? value : () => value;\n\n    this.getters[name] = getter;\n    this.cache.del(name);\n\n    return this;\n  }\n\n  remove(name: string): this {\n    if (typeof name !== 'string') throw new TypeError('name must be a string!');\n\n    this.getters[name] = null;\n    this.cache.del(name);\n\n    return this;\n  }\n\n  invalidate(): this {\n    this.cache.flush();\n\n    return this;\n  }\n\n  toObject(): Record<string, any> {\n    const result = {};\n    const keys = Object.keys(this.getters);\n\n    for (let i = 0, len = keys.length; i < len; i++) {\n      const key = keys[i];\n      const item = this.get(key);\n\n      if (item != null) result[key] = item;\n    }\n\n    return result;\n  }\n}\n\nexport = Locals;\n"
  },
  {
    "path": "lib/hexo/multi_config_path.ts",
    "content": "import { isAbsolute, resolve, join, extname } from 'path';\nimport { existsSync, readFileSync, writeFileSync } from 'hexo-fs';\nimport yml from 'js-yaml';\nimport { deepMerge } from 'hexo-util';\nimport type Hexo from './index';\n\nexport = (ctx: Hexo) => function multiConfigPath(base: string, configPaths?: string, outputDir?: string): string {\n  const { log } = ctx;\n  const defaultPath = join(base, '_config.yml');\n\n  if (!configPaths) {\n    log.w('No config file entered.');\n    return join(base, '_config.yml');\n  }\n\n  let paths: string[];\n  // determine if comma or space separated\n  if (configPaths.includes(',')) {\n    paths = configPaths.replace(' ', '').split(',');\n  } else {\n    // only one config\n    let configPath = isAbsolute(configPaths) ? configPaths : resolve(base, configPaths);\n\n    if (!existsSync(configPath)) {\n      log.w(`Config file ${configPaths} not found, using default.`);\n      configPath = defaultPath;\n    }\n\n    return configPath;\n  }\n\n  const numPaths = paths.length;\n\n  // combine files\n  let combinedConfig = {};\n  let count = 0;\n  for (let i = 0; i < numPaths; i++) {\n    const configPath = isAbsolute(paths[i]) ? paths[i] : join(base, paths[i]);\n\n    if (!existsSync(configPath)) {\n      log.w(`Config file ${paths[i]} not found.`);\n      continue;\n    }\n\n    // files read synchronously to ensure proper overwrite order\n    const file = readFileSync(configPath);\n    const ext = extname(paths[i]).toLowerCase();\n\n    if (ext === '.yml') {\n      combinedConfig = deepMerge(combinedConfig, yml.load(file));\n      count++;\n    } else if (ext === '.json') {\n      combinedConfig = deepMerge(combinedConfig, yml.load(file, {json: true}));\n      count++;\n    } else {\n      log.w(`Config file ${paths[i]} not supported type.`);\n    }\n  }\n\n  if (count === 0) {\n    log.e('No config files found. Using _config.yml.');\n    return defaultPath;\n  }\n\n  log.i('Config based on', count.toString(), 'files');\n\n  const multiconfigRoot = outputDir || base;\n  const outputPath = join(multiconfigRoot, '_multiconfig.yml');\n\n  log.d(`Writing _multiconfig.yml to ${outputPath}`);\n\n  writeFileSync(outputPath, yml.dump(combinedConfig));\n\n  // write file and return path\n  return outputPath;\n};\n"
  },
  {
    "path": "lib/hexo/post.ts",
    "content": "import assert from 'assert';\nimport moment from 'moment';\nimport Promise from 'bluebird';\nimport { join, extname, basename } from 'path';\nimport { magenta } from 'picocolors';\nimport { load } from 'js-yaml';\nimport { slugize, escapeRegExp, deepMerge} from 'hexo-util';\nimport { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs';\nimport { parse as yfmParse, split as yfmSplit, stringify as yfmStringify } from 'hexo-front-matter';\nimport type Hexo from './index';\nimport type { NodeJSLikeCallback, RenderData } from '../types';\n\nconst preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];\n\nconst rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\\s\\S]+?)<\\/hexoPostRenderCodeBlock>/g;\nconst rCommentEscape = /(<!--[\\s\\S]*?-->)/g;\nconst rSwigTag = /(\\{\\{.+?\\}\\})|(\\{#.+?#\\})|(\\{%.+?%\\})/s;\n\nconst rSwigPlaceHolder = /(?:<|&lt;)!--swig\\uFFFC(\\d+)--(?:>|&gt;)/g;\nconst rCodeBlockPlaceHolder = /(?:<|&lt;)!--code\\uFFFC(\\d+)--(?:>|&gt;)/g;\nconst rCommentHolder = /(?:<|&lt;)!--comment\\uFFFC(\\d+)--(?:>|&gt;)/g;\n\nconst STATE_PLAINTEXT = 0;\nconst STATE_SWIG_VAR = 1;\nconst STATE_SWIG_COMMENT = 2;\nconst STATE_SWIG_TAG = 3;\nconst STATE_SWIG_FULL_TAG = 4;\nconst STATE_PLAINTEXT_COMMENT = 5;\n\nconst isNonWhiteSpaceChar = (char: string) => char !== '\\r'\n  && char !== '\\n'\n  && char !== '\\t'\n  && char !== '\\f'\n  && char !== '\\v'\n  && char !== ' ';\n\nclass PostRenderEscape {\n  public stored: string[];\n  public length: number;\n\n  constructor() {\n    this.stored = [];\n  }\n\n  static escapeContent(cache: string[], flag: string, str: string) {\n    return `<!--${flag}\\uFFFC${cache.push(str) - 1}-->`;\n  }\n\n  static restoreContent(cache: string[]) {\n    return (_: string, index: number) => {\n      assert(cache[index]);\n      const value = cache[index];\n      cache[index] = null;\n      return value;\n    };\n  }\n\n  restoreAllSwigTags(str: string) {\n    const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored));\n    return restored;\n  }\n\n  restoreCodeBlocks(str: string) {\n    return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored));\n  }\n\n  restoreComments(str: string) {\n    return str.replace(rCommentHolder, PostRenderEscape.restoreContent(this.stored));\n  }\n\n  escapeComments(str: string) {\n    return str.replace(rCommentEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'comment', content));\n  }\n\n  escapeCodeBlocks(str: string) {\n    return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content));\n  }\n\n  /**\n   * @param {string} str\n   * @returns string\n   */\n  escapeAllSwigTags(str: string) {\n    let state = STATE_PLAINTEXT;\n    let buffer_start = -1;\n    let plaintext_comment_start = -1;\n    let plain_text_start = 0;\n    let output = '';\n\n    let swig_tag_name_begin = false;\n    let swig_tag_name_end = false;\n    let swig_tag_name = '';\n\n    let swig_full_tag_start_start = -1;\n    let swig_full_tag_start_end = -1;\n    // current we just consider one level of string quote\n    let swig_string_quote = '';\n\n    const { length } = str;\n\n    let idx = 0;\n\n    // for backtracking\n    const swig_start_idx = [0, 0, 0, 0, 0];\n\n    const flushPlainText = (end: number) => {\n      if (plain_text_start !== -1 && end > plain_text_start) {\n        output += str.slice(plain_text_start, end);\n      }\n      plain_text_start = -1;\n    };\n\n    const ensurePlainTextStart = (position: number) => {\n      if (plain_text_start === -1) {\n        plain_text_start = position;\n      }\n    };\n\n    const pushAndReset = (value: string) => {\n      output += value;\n      plain_text_start = -1;\n    };\n\n    while (idx < length) {\n      while (idx < length) {\n        const char = str[idx];\n        const next_char = str[idx + 1];\n\n        if (state === STATE_PLAINTEXT) { // From plain text to swig\n          ensurePlainTextStart(idx);\n          if (char === '{') {\n            // check if it is a complete tag {{ }}\n            if (next_char === '{') {\n              flushPlainText(idx);\n              state = STATE_SWIG_VAR;\n              idx++;\n              buffer_start = idx + 1;\n              swig_start_idx[state] = idx;\n            } else if (next_char === '#') {\n              flushPlainText(idx);\n              state = STATE_SWIG_COMMENT;\n              idx++;\n              buffer_start = idx + 1;\n              swig_start_idx[state] = idx;\n            } else if (next_char === '%') {\n              flushPlainText(idx);\n              state = STATE_SWIG_TAG;\n              idx++;\n              buffer_start = idx + 1;\n              swig_full_tag_start_start = idx + 1;\n              swig_full_tag_start_end = idx + 1;\n              swig_tag_name = '';\n              swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag\n              swig_tag_name_end = false;\n              swig_start_idx[state] = idx;\n            }\n          }\n          if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') {\n            flushPlainText(idx);\n            state = STATE_PLAINTEXT_COMMENT;\n            plaintext_comment_start = idx;\n            idx += 3;\n          }\n        } else if (state === STATE_SWIG_TAG) {\n          if (char === '\"' || char === '\\'') {\n            if (swig_string_quote === '') {\n              swig_string_quote = char;\n            } else if (swig_string_quote === char) {\n              swig_string_quote = '';\n            }\n          }\n          if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text\n            idx++;\n            if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) {\n              state = STATE_SWIG_FULL_TAG;\n              buffer_start = idx + 1;\n              // since we have already move idx to next char of '}', so here is idx -1\n              swig_full_tag_start_end = idx - 1;\n              swig_start_idx[state] = idx;\n            } else {\n              swig_tag_name = '';\n              state = STATE_PLAINTEXT;\n              // since we have already move idx to next char of '}', so here is idx -1\n              pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(buffer_start, idx - 1)}%}`));\n            }\n\n          } else {\n            if (isNonWhiteSpaceChar(char)) {\n              if (!swig_tag_name_begin && !swig_tag_name_end) {\n                swig_tag_name_begin = true;\n              }\n\n              if (swig_tag_name_begin) {\n                swig_tag_name += char;\n              }\n            } else {\n              if (swig_tag_name_begin === true) {\n                swig_tag_name_begin = false;\n                swig_tag_name_end = true;\n              }\n            }\n          }\n        } else if (state === STATE_SWIG_VAR) {\n          if (char === '\"' || char === '\\'') {\n            if (swig_string_quote === '') {\n              swig_string_quote = char;\n            } else if (swig_string_quote === char) {\n              swig_string_quote = '';\n            }\n          }\n          // {{ }\n          if (char === '}' && next_char !== '}' && swig_string_quote === '') {\n            // From swig back to plain text\n            state = STATE_PLAINTEXT;\n            pushAndReset(`{{${str.slice(buffer_start, idx)}${char}`);\n          } else if (char === '}' && next_char === '}' && swig_string_quote === '') {\n            pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{{${str.slice(buffer_start, idx)}}}`));\n            idx++;\n            state = STATE_PLAINTEXT;\n          }\n        } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text\n          if (char === '#' && next_char === '}') {\n            idx++;\n            state = STATE_PLAINTEXT;\n            plain_text_start = -1;\n          }\n        } else if (state === STATE_SWIG_FULL_TAG) {\n          if (char === '{' && next_char === '%') {\n            let swig_full_tag_end_buffer = '';\n            let swig_full_tag_found = false;\n\n            let _idx = idx + 2;\n            for (; _idx < length; _idx++) {\n              const _char = str[_idx];\n              const _next_char = str[_idx + 1];\n\n              if (_char === '%' && _next_char === '}') {\n                _idx++;\n                swig_full_tag_found = true;\n                break;\n              }\n\n              swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;\n            }\n\n            if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {\n              state = STATE_PLAINTEXT;\n              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}%}`));\n              idx = _idx;\n              swig_full_tag_end_buffer = '';\n            }\n          }\n        } else if (state === STATE_PLAINTEXT_COMMENT) {\n          if (char === '-' && next_char === '-' && str[idx + 2] === '>') {\n            state = STATE_PLAINTEXT;\n            const comment = str.slice(plaintext_comment_start, idx + 3);\n            pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));\n            idx += 2;\n          }\n        }\n        idx++;\n      }\n      if (state === STATE_PLAINTEXT) {\n        break;\n      }\n      if (state === STATE_PLAINTEXT_COMMENT) {\n        // Unterminated comment, just push the rest as comment\n        const comment = str.slice(plaintext_comment_start, length);\n        pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));\n        break;\n      }\n      // If the swig tag is not closed, then it is a plain text, we need to backtrack\n      if (state === STATE_SWIG_FULL_TAG) {\n        pushAndReset(`{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%`);\n      } else {\n        pushAndReset('{');\n      }\n      idx = swig_start_idx[state];\n      swig_string_quote = '';\n      state = STATE_PLAINTEXT;\n    }\n\n    if (plain_text_start !== -1 && plain_text_start < length) {\n      output += str.slice(plain_text_start);\n    }\n\n    return output;\n  }\n}\n\nconst prepareFrontMatter = (data: any, jsonMode: boolean): Record<string, string> => {\n  for (const [key, item] of Object.entries(data)) {\n    if (moment.isMoment(item)) {\n      data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss');\n    } else if (moment.isDate(item)) {\n      data[key] = moment.utc(item).format('YYYY-MM-DD HH:mm:ss');\n    } else if (typeof item === 'string') {\n      if (jsonMode || item.includes(':') || item.startsWith('#') || item.startsWith('!!')\n      || item.includes('{') || item.includes('}') || item.includes('[') || item.includes(']')\n      || item.includes('\\'') || item.includes('\"')) data[key] = `\"${item.replace(/\"/g, '\\\\\"')}\"`;\n    }\n  }\n\n  return data;\n};\n\n\nconst removeExtname = (str: string) => {\n  return str.substring(0, str.length - extname(str).length);\n};\n\nconst createAssetFolder = (path: string, assetFolder: boolean) => {\n  if (!assetFolder) return Promise.resolve();\n\n  const target = removeExtname(path);\n\n  if (basename(target) === 'index') return Promise.resolve();\n\n  return exists(target).then(exist => {\n    if (!exist) return mkdirs(target);\n  });\n};\n\ninterface Result {\n  path: string;\n  content: string;\n}\n\ninterface PostData {\n  title?: string | number;\n  layout?: string;\n  slug?: string | number;\n  path?: string;\n  date?: moment.Moment;\n  [prop: string]: any;\n}\n\nclass Post {\n  public context: Hexo;\n\n  constructor(context: Hexo) {\n    this.context = context;\n  }\n\n  create(data: PostData, callback?: NodeJSLikeCallback<any>): Promise<Result>;\n  create(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<any>): Promise<Result>;\n  create(data: PostData, replace: boolean | (NodeJSLikeCallback<any>), callback?: NodeJSLikeCallback<any>): Promise<Result> {\n    if (!callback && typeof replace === 'function') {\n      callback = replace;\n      replace = false;\n    }\n\n    const ctx = this.context;\n    const { config } = ctx;\n\n    data.slug = slugize((data.slug || data.title).toString(), { transform: config.filename_case });\n    data.layout = (data.layout || config.default_layout).toLowerCase();\n    data.date = data.date ? moment(data.date) : moment();\n\n    return Promise.all([\n      // Get the post path\n      ctx.execFilter('new_post_path', data, {\n        args: [replace],\n        context: ctx\n      }),\n      this._renderScaffold(data)\n    ]).spread((path: string, content: string) => {\n      const result = { path, content };\n\n      return Promise.all<void, void | string>([\n        // Write content to file\n        writeFile(path, content),\n        // Create asset folder\n        createAssetFolder(path, config.post_asset_folder)\n      ]).then(() => {\n        ctx.emit('new', result);\n        return result;\n      });\n    }).asCallback(callback);\n  }\n\n  _getScaffold(layout: string) {\n    const ctx = this.context;\n\n    return ctx.scaffold.get(layout).then(result => {\n      if (result != null) return result;\n      return ctx.scaffold.get('normal');\n    });\n  }\n\n  _renderScaffold(data: PostData) {\n    const { tag } = this.context.extend;\n    let splitted: ReturnType<typeof yfmSplit>;\n\n    return this._getScaffold(data.layout).then(scaffold => {\n      splitted = yfmSplit(scaffold);\n      const jsonMode = splitted.separator.startsWith(';');\n      const frontMatter = prepareFrontMatter({ ...data }, jsonMode);\n\n      return tag.render(splitted.data, frontMatter);\n    }).then(frontMatter => {\n      const { separator } = splitted;\n      const jsonMode = separator.startsWith(';');\n\n      // Parse front-matter\n      let obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter);\n\n      obj = deepMerge(obj, Object.fromEntries(Object.entries(data).filter(([key, value]) => !preservedKeys.includes(key) && value != null)));\n\n      let content = '';\n      // Prepend the separator\n      if (splitted.prefixSeparator) content += `${separator}\\n`;\n\n      content += yfmStringify(obj, {\n        mode: jsonMode ? 'json' : ''\n      });\n\n      // Concat content\n      content += splitted.content;\n\n      if (data.content) {\n        content += `\\n${data.content}`;\n      }\n\n      return content;\n    });\n  }\n\n  publish(data: PostData, replace?: boolean): Promise<Result>;\n  publish(data: PostData, callback?: NodeJSLikeCallback<Result>): Promise<Result>;\n  publish(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<Result>): Promise<Result>;\n  publish(data: PostData, replace?: boolean | NodeJSLikeCallback<Result>, callback?: NodeJSLikeCallback<Result>): Promise<Result> {\n    if (!callback && typeof replace === 'function') {\n      callback = replace;\n      replace = false;\n    }\n\n    if (data.layout === 'draft') data.layout = 'post';\n\n    const ctx = this.context;\n    const { config } = ctx;\n    const draftDir = join(ctx.source_dir, '_drafts');\n    const slug = slugize(data.slug.toString(), { transform: config.filename_case });\n    data.slug = slug;\n    const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\\\/\\\\\\\\]+)`);\n    let src = '';\n    const result: Result = {} as any;\n\n    data.layout = (data.layout || config.default_layout).toLowerCase();\n\n    // Find the draft\n    return listDir(draftDir).then(list => {\n      const item = list.find(item => regex.test(item));\n      if (!item) throw new Error(`Draft \"${slug}\" does not exist.`);\n\n      // Read the content\n      src = join(draftDir, item);\n      return readFile(src);\n    }).then(content => {\n      // Create post\n      Object.assign(data, yfmParse(content));\n      data.content = data._content;\n      data._content = undefined;\n\n      return this.create(data, replace as boolean);\n    }).then(post => {\n      result.path = post.path;\n      result.content = post.content;\n      return unlink(src);\n    }).then(() => { // Remove the original draft file\n      if (!config.post_asset_folder) return;\n\n      // Copy assets\n      const assetSrc = removeExtname(src);\n      const assetDest = removeExtname(result.path);\n\n      return exists(assetSrc).then(exist => {\n        if (!exist) return;\n\n        return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc));\n      });\n    }).thenReturn(result).asCallback(callback);\n  }\n\n  render(source: string, data: RenderData = {}, callback?: NodeJSLikeCallback<never>) {\n    const ctx = this.context;\n    const { config } = ctx;\n    const { tag } = ctx.extend;\n    const ext = data.engine || (source ? extname(source) : '');\n\n    let promise;\n\n    if (data.content != null) {\n      promise = Promise.resolve(data.content);\n    } else if (source) {\n      // Read content from files\n      promise = readFile(source);\n    } else {\n      return Promise.reject(new Error('No input file or string!')).asCallback(callback);\n    }\n\n    // Files like js and css are also processed by this function, but they do not require preprocessing like markdown\n    // data.source does not exist when tag plugins call the markdown renderer\n    const isPost = !data.source || ['html', 'htm'].includes(ctx.render.getOutput(data.source));\n\n    if (!isPost) {\n      return promise.then(content => {\n        data.content = content;\n        ctx.log.debug('Rendering file: %s', magenta(source));\n\n        return ctx.render.render({\n          text: data.content,\n          path: source,\n          engine: data.engine,\n          toString: true\n        });\n      }).then(content => {\n        data.content = content;\n        return data;\n      }).asCallback(callback);\n    }\n\n    // disable Nunjucks when the renderer specify that.\n    let disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks;\n\n    // front-matter overrides renderer's option\n    if (typeof data.disableNunjucks === 'boolean') disableNunjucks = data.disableNunjucks;\n\n    const cacheObj = new PostRenderEscape();\n\n    return promise.then(content => {\n      data.content = content;\n      // Run \"before_post_render\" filters\n      return ctx.execFilter('before_post_render', data, { context: ctx });\n    }).then(() => {\n      // Escape all comments to avoid conflict with Nunjucks and code block\n      data.content = cacheObj.escapeCodeBlocks(data.content);\n      // Escape all Nunjucks/Swig tags\n      let hasSwigTag = true;\n      if (disableNunjucks === false) {\n        hasSwigTag = rSwigTag.test(data.content);\n        if (hasSwigTag) {\n          data.content = cacheObj.escapeAllSwigTags(data.content);\n        }\n      }\n\n      const options: { highlight?: boolean; } = data.markdown || {};\n      if (!config.syntax_highlighter) options.highlight = null;\n\n      ctx.log.debug('Rendering post: %s', magenta(source));\n      // Render with markdown or other renderer\n      return ctx.render.render({\n        text: data.content,\n        path: source,\n        engine: data.engine,\n        toString: true,\n        onRenderEnd(content) {\n          // Replace cache data with real contents\n          data.content = cacheObj.restoreAllSwigTags(content);\n\n          // Return content after replace the placeholders\n          if (disableNunjucks || !hasSwigTag) return data.content;\n\n          // Render with Nunjucks if there are Swig tags\n          return tag.render(data.content, data);\n        }\n      }, options);\n    }).then(content => {\n      data.content = cacheObj.restoreComments(content);\n      data.content = cacheObj.restoreCodeBlocks(data.content);\n\n      // Run \"after_post_render\" filters\n      return ctx.execFilter('after_post_render', data, { context: ctx });\n    }).asCallback(callback);\n  }\n}\n\nexport = Post;\n"
  },
  {
    "path": "lib/hexo/register_models.ts",
    "content": "import * as models from '../models';\nimport type Hexo from './index';\n\nexport = (ctx: Hexo): void => {\n  const db = ctx.database;\n\n  const keys = Object.keys(models);\n\n  for (let i = 0, len = keys.length; i < len; i++) {\n    const key = keys[i];\n    db.model(key, models[key](ctx));\n  }\n};\n"
  },
  {
    "path": "lib/hexo/render.ts",
    "content": "import { extname } from 'path';\nimport Promise from 'bluebird';\nimport { readFile, readFileSync } from 'hexo-fs';\nimport type Hexo from './index';\nimport type { Renderer } from '../extend';\nimport type { StoreFunction, StoreFunctionData, StoreSyncFunction } from '../extend/renderer';\nimport { NodeJSLikeCallback } from '../types';\n\nconst getExtname = (str: string): string => {\n  if (typeof str !== 'string') return '';\n\n  const ext = extname(str);\n  return ext.startsWith('.') ? ext.slice(1) : ext;\n};\n\nconst toString = (result: any, options: StoreFunctionData): string => {\n  if (!Object.prototype.hasOwnProperty.call(options, 'toString') || typeof result === 'string') return result;\n\n  if (typeof options.toString === 'function') {\n    return options.toString(result);\n  } else if (typeof result === 'object') {\n    return JSON.stringify(result);\n  } else if (result.toString) {\n    return result.toString();\n  }\n\n  return result;\n};\n\nclass Render {\n  public context: Hexo;\n  public renderer: Renderer;\n\n  constructor(ctx: Hexo) {\n    this.context = ctx;\n    this.renderer = ctx.extend.renderer;\n  }\n\n  isRenderable(path: string): boolean {\n    return this.renderer.isRenderable(path);\n  }\n\n  isRenderableSync(path: string): boolean {\n    return this.renderer.isRenderableSync(path);\n  }\n\n  getOutput(path: string): string {\n    return this.renderer.getOutput(path);\n  }\n\n  getRenderer(ext: string, sync?: boolean): StoreSyncFunction | StoreFunction {\n    return this.renderer.get(ext, sync);\n  }\n\n  getRendererSync(ext: string): StoreSyncFunction | StoreFunction {\n    return this.getRenderer(ext, true);\n  }\n\n  render(data: StoreFunctionData, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  render(data: StoreFunctionData, options: any, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  render(data: StoreFunctionData, options?: any | NodeJSLikeCallback<any>, callback?: NodeJSLikeCallback<any>): Promise<any> {\n    if (!callback && typeof options === 'function') {\n      callback = options;\n      options = {};\n    }\n\n    const ctx = this.context;\n    let ext = '';\n\n    let promise: Promise<string>;\n\n    if (!data) return Promise.reject(new TypeError('No input file or string!'));\n\n    if (data.text != null) {\n      promise = Promise.resolve(data.text);\n    } else if (!data.path) {\n      return Promise.reject(new TypeError('No input file or string!'));\n    } else {\n      promise = readFile(data.path);\n    }\n\n    return promise.then(text => {\n      data.text = text;\n      ext = data.engine || getExtname(data.path);\n      if (!ext || !this.isRenderable(ext)) return text;\n\n      const renderer = this.getRenderer(ext);\n      return Reflect.apply(renderer, ctx, [data, options]);\n    }).then(result => {\n      result = toString(result, data);\n      if (data.onRenderEnd) {\n        return data.onRenderEnd(result);\n      }\n\n      return result;\n    }).then(result => {\n      const output = this.getOutput(ext) || ext;\n      return ctx.execFilter(`after_render:${output}`, result, {\n        context: ctx,\n        args: [data]\n      });\n    }).asCallback(callback);\n  }\n\n  renderSync(data: StoreFunctionData, options = {}): any {\n    if (!data) throw new TypeError('No input file or string!');\n\n    const ctx = this.context;\n\n    if (data.text == null) {\n      if (!data.path) throw new TypeError('No input file or string!');\n      data.text = readFileSync(data.path);\n    }\n\n    if (data.text == null) throw new TypeError('No input file or string!');\n\n    const ext = data.engine || getExtname(data.path);\n    let result;\n\n    if (ext && this.isRenderableSync(ext)) {\n      const renderer = this.getRendererSync(ext);\n      result = Reflect.apply(renderer, ctx, [data, options]);\n    } else {\n      result = data.text;\n    }\n\n    const output = this.getOutput(ext) || ext;\n    result = toString(result, data);\n\n    if (data.onRenderEnd) {\n      result = data.onRenderEnd(result);\n    }\n\n    return ctx.execFilterSync(`after_render:${output}`, result, {\n      context: ctx,\n      args: [data]\n    });\n  }\n}\n\nexport = Render;\n"
  },
  {
    "path": "lib/hexo/router.ts",
    "content": "import { EventEmitter } from 'events';\nimport Promise from 'bluebird';\nimport Stream from 'stream';\nconst { Readable } = Stream;\n\ninterface Data {\n  data: any;\n  modified: boolean;\n}\n\nclass RouteStream extends Readable {\n  public _data: any;\n  public _ended: boolean;\n  public modified: boolean;\n\n  constructor(data: Data) {\n    super({ objectMode: true });\n\n    this._data = data.data;\n    this._ended = false;\n    this.modified = data.modified;\n  }\n\n  // Assume we only accept Buffer, plain object, or string\n  _toBuffer(data: Buffer | object | string): Buffer | null {\n    if (data instanceof Buffer) {\n      return data;\n    }\n    if (typeof data === 'object') {\n      data = JSON.stringify(data);\n    }\n    if (typeof data === 'string') {\n      return Buffer.from(data); // Assume string is UTF-8 encoded string\n    }\n    return null;\n  }\n\n  _read(): boolean {\n    const data = this._data;\n\n    if (typeof data !== 'function') {\n      const bufferData = this._toBuffer(data);\n      if (bufferData) {\n        this.push(bufferData);\n      }\n      this.push(null);\n      return;\n    }\n\n    // Don't read it twice!\n    if (this._ended) return false;\n    this._ended = true;\n\n    data().then(data => {\n      if (data instanceof Stream && (data as Stream.Readable).readable) {\n        data.on('data', d => {\n          this.push(d);\n        });\n\n        data.on('end', () => {\n          this.push(null);\n        });\n\n        data.on('error', err => {\n          this.emit('error', err);\n        });\n      } else {\n        const bufferData = this._toBuffer(data);\n        if (bufferData) {\n          this.push(bufferData);\n        }\n        this.push(null);\n      }\n    }).catch(err => {\n      this.emit('error', err);\n      this.push(null);\n    });\n  }\n}\n\nconst _format = (path?: string): string => {\n  path = path || '';\n  if (typeof path !== 'string') throw new TypeError('path must be a string!');\n\n  path = path\n    .replace(/^\\/+/, '') // Remove prefixed slashes\n    .replace(/\\\\/g, '/') // Replaces all backslashes\n    .replace(/\\?.*$/, ''); // Remove query string\n\n  // Appends `index.html` to the path with trailing slash\n  if (!path || path.endsWith('/')) {\n    path += 'index.html';\n  }\n\n  return path;\n};\n\nclass Router extends EventEmitter {\n  public routes: {\n    [key: string]: Data | null;\n  };\n\n  constructor() {\n    super();\n\n    this.routes = {};\n  }\n\n  list(): string[] {\n    const { routes } = this;\n    return Object.keys(routes).filter(key => routes[key]);\n  }\n\n  format(path?: string): string {\n    return _format(path);\n  }\n\n  get(path: string): RouteStream {\n    if (typeof path !== 'string') throw new TypeError('path must be a string!');\n\n    const data = this.routes[this.format(path)];\n    if (data == null) return;\n\n    return new RouteStream(data);\n  }\n\n  isModified(path: string): boolean {\n    if (typeof path !== 'string') throw new TypeError('path must be a string!');\n\n    const data = this.routes[this.format(path)];\n    return data ? data.modified : false;\n  }\n\n  set(path: string, data: any): this {\n    if (typeof path !== 'string') throw new TypeError('path must be a string!');\n    if (data == null) throw new TypeError('data is required!');\n\n    let obj: Data;\n\n    if (typeof data === 'object' && data.data != null) {\n      obj = data;\n    } else {\n      obj = {\n        data,\n        modified: true\n      };\n    }\n\n    if (typeof obj.data === 'function') {\n      if (obj.data.length) {\n        obj.data = Promise.promisify(obj.data);\n      } else {\n        obj.data = Promise.method(obj.data);\n      }\n    }\n\n    path = this.format(path);\n\n    this.routes[path] = {\n      data: obj.data,\n      modified: obj.modified == null ? true : obj.modified\n    };\n\n    this.emit('update', path);\n\n    return this;\n  }\n\n  remove(path: string): this {\n    if (typeof path !== 'string') throw new TypeError('path must be a string!');\n    path = this.format(path);\n\n    this.routes[path] = null;\n    this.emit('remove', path);\n\n    return this;\n  }\n}\n\nexport = Router;\n"
  },
  {
    "path": "lib/hexo/scaffold.ts",
    "content": "import { extname, join } from 'path';\nimport { exists, listDir, readFile, unlink, writeFile } from 'hexo-fs';\nimport type Hexo from './index';\nimport type { NodeJSLikeCallback } from '../types';\nimport type Promise from 'bluebird';\n\nclass Scaffold {\n  public context: Hexo;\n  public scaffoldDir: string;\n  public defaults: {\n    normal: string\n  };\n\n  constructor(context: Hexo) {\n    this.context = context;\n    this.scaffoldDir = context.scaffold_dir;\n    this.defaults = {\n      normal: [\n        '---',\n        'layout: {{ layout }}',\n        'title: {{ title }}',\n        'date: {{ date }}',\n        'tags:',\n        '---'\n      ].join('\\n')\n    };\n  }\n\n  _listDir(): Promise<{\n    name: string;\n    path: string;\n  }[]> {\n    const { scaffoldDir } = this;\n\n    return exists(scaffoldDir).then(exist => {\n      if (!exist) return [];\n\n      return listDir(scaffoldDir, {\n        ignorePattern: /^_|\\/_/\n      });\n    }).map(item => ({\n      name: item.substring(0, item.length - extname(item).length),\n      path: join(scaffoldDir, item)\n    }));\n  }\n\n  _getScaffold(name: string): Promise<{\n    name: string;\n    path: string;\n  }> {\n    return this._listDir().then(list => list.find(item => item.name === name));\n  }\n\n  get(name: string, callback?: NodeJSLikeCallback<any>): Promise<string> {\n    return this._getScaffold(name).then(item => {\n      if (item) {\n        return readFile(item.path);\n      }\n\n      return this.defaults[name];\n    }).asCallback(callback);\n  }\n\n  set(name: string, content: any, callback?: NodeJSLikeCallback<void>): Promise<void> {\n    const { scaffoldDir } = this;\n\n    return this._getScaffold(name).then(item => {\n      let path = item ? item.path : join(scaffoldDir, name);\n      if (!extname(path)) path += '.md';\n\n      return writeFile(path, content);\n    }).asCallback(callback);\n  }\n\n  remove(name: string, callback?: NodeJSLikeCallback<void>): Promise<void> {\n    return this._getScaffold(name).then(item => {\n      if (!item) return;\n\n      return unlink(item.path);\n    }).asCallback(callback);\n  }\n}\n\nexport = Scaffold;\n"
  },
  {
    "path": "lib/hexo/source.ts",
    "content": "import Box from '../box';\nimport type Hexo from './index';\n\nclass Source extends Box {\n  constructor(ctx: Hexo) {\n    super(ctx, ctx.source_dir);\n\n    this.processors = ctx.extend.processor.list();\n  }\n}\n\nexport = Source;\n"
  },
  {
    "path": "lib/hexo/update_package.ts",
    "content": "import { join } from 'path';\nimport { writeFile, exists, readFile } from 'hexo-fs';\nimport type Hexo from './index';\nimport type Promise from 'bluebird';\n\nexport = (ctx: Hexo): Promise<void> => {\n  const pkgPath = join(ctx.base_dir, 'package.json');\n\n  return readPkg(pkgPath).then(pkg => {\n    if (!pkg) return;\n\n    ctx.env.init = true;\n\n    if (pkg.hexo.version === ctx.version) return;\n\n    pkg.hexo.version = ctx.version;\n\n    ctx.log.debug('Updating package.json');\n    return writeFile(pkgPath, JSON.stringify(pkg, null, '  '));\n  });\n};\n\nfunction readPkg(path: string): Promise<any> {\n  return exists(path).then(exist => {\n    if (!exist) return;\n\n    return readFile(path).then(content => {\n      const pkg = JSON.parse(content);\n      if (typeof pkg.hexo !== 'object') return;\n\n      return pkg;\n    });\n  });\n}\n"
  },
  {
    "path": "lib/hexo/validate_config.ts",
    "content": "import assert from 'assert';\nimport type Hexo from './index';\n\nexport = (ctx: Hexo): void => {\n  const { config, log } = ctx;\n\n  log.info('Validating config');\n\n  // Validation for config.url && config.root\n  if (typeof config.url !== 'string') {\n    throw new TypeError(`Invalid config detected: \"url\" should be string, not ${typeof config.url}!`);\n  }\n  try {\n    // eslint-disable-next-line no-new\n    new URL(config.url);\n    assert(new URL(config.url).protocol.startsWith('http'));\n  } catch {\n    throw new TypeError('Invalid config detected: \"url\" should be a valid URL!');\n  }\n\n  if (typeof config.root !== 'string') {\n    throw new TypeError(`Invalid config detected: \"root\" should be string, not ${typeof config.root}!`);\n  }\n  if (config.root.trim().length <= 0) {\n    throw new TypeError('Invalid config detected: \"root\" should not be empty!');\n  }\n};\n\n"
  },
  {
    "path": "lib/models/asset.ts",
    "content": "import warehouse from 'warehouse';\nimport { join } from 'path';\nimport type Hexo from '../hexo';\nimport type { AssetSchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const Asset = new warehouse.Schema<AssetSchema>({\n    _id: {type: String, required: true},\n    path: {type: String, required: true},\n    modified: {type: Boolean, default: true},\n    renderable: {type: Boolean, default: true}\n  });\n\n  Asset.virtual('source').get(function() {\n    return join(ctx.base_dir, this._id);\n  });\n\n  return Asset;\n};\n"
  },
  {
    "path": "lib/models/binary_relation_index.ts",
    "content": "import type Hexo from '../hexo';\n\ntype BinaryRelationType<K extends PropertyKey, V extends PropertyKey> = {\n  [key in K]: PropertyKey;\n} & {\n  [key in V]: PropertyKey;\n};\n\nclass BinaryRelationIndex<K extends PropertyKey, V extends PropertyKey> {\n  keyIndex: Map<PropertyKey, Set<PropertyKey>> = new Map();\n  valueIndex: Map<PropertyKey, Set<PropertyKey>> = new Map();\n  key: K;\n  value: V;\n  ctx: Hexo;\n  schemaName: string;\n\n  constructor(key: K, value: V, schemaName: string, ctx: Hexo) {\n    this.key = key;\n    this.value = value;\n    this.schemaName = schemaName;\n    this.ctx = ctx;\n  }\n\n  load() {\n    this.keyIndex.clear();\n    this.valueIndex.clear();\n    const raw = this.ctx.model(this.schemaName).data;\n    for (const _id in raw) {\n      this.saveHook(raw[_id]);\n    }\n  }\n\n  saveHook(data: BinaryRelationType<K, V> & { _id: PropertyKey }) {\n    if (!data) return;\n    const _id = data._id;\n    const key = data[this.key];\n    const value = data[this.value];\n    if (!this.keyIndex.has(key)) {\n      this.keyIndex.set(key, new Set());\n    }\n    this.keyIndex.get(key).add(_id);\n\n    if (!this.valueIndex.has(value)) {\n      this.valueIndex.set(value, new Set());\n    }\n    this.valueIndex.get(value).add(_id);\n  }\n\n  removeHook(data: BinaryRelationType<K, V> & { _id: PropertyKey }) {\n    const _id = data._id;\n    const key = data[this.key];\n    const value = data[this.value];\n    this.keyIndex.get(key)?.delete(_id);\n    if (this.keyIndex.get(key)?.size === 0) {\n      this.keyIndex.delete(key);\n    }\n    this.valueIndex.get(value)?.delete(_id);\n    if (this.valueIndex.get(value)?.size === 0) {\n      this.valueIndex.delete(value);\n    }\n  }\n\n  findById(_id: PropertyKey) {\n    const raw = this.ctx.model(this.schemaName).findById(_id, { lean: true });\n    if (!raw) return;\n    return { ...raw };\n  }\n\n  find(query: Partial<BinaryRelationType<K, V>>) {\n    const key = query[this.key];\n    const value = query[this.value];\n\n    if (key && value) {\n      const ids = this.keyIndex.get(key);\n      if (!ids) return [];\n      return Array.from(ids)\n        .map(_id => this.findById(_id))\n        .filter(record => record?.[this.value] === value);\n    }\n\n    if (key) {\n      const ids = this.keyIndex.get(key);\n      if (!ids) return [];\n      return Array.from(ids).map(_id => this.findById(_id));\n    }\n\n    if (value) {\n      const ids = this.valueIndex.get(value);\n      if (!ids) return [];\n      return Array.from(ids).map(_id => this.findById(_id));\n    }\n\n    return [];\n  }\n\n  findOne(query: Partial<BinaryRelationType<K, V>>) {\n    return this.find(query)[0];\n  }\n}\n\nexport default BinaryRelationIndex;\n"
  },
  {
    "path": "lib/models/cache.ts",
    "content": "import warehouse from 'warehouse';\nimport Promise from 'bluebird';\nimport type Hexo from '../hexo';\nimport type fs from 'fs';\nimport type Document from 'warehouse/dist/document';\nimport type { CacheSchema } from '../types';\n\nexport = (_ctx: Hexo) => {\n  const Cache = new warehouse.Schema<CacheSchema>({\n    _id: {type: String, required: true},\n    hash: {type: String, default: ''},\n    modified: {type: Number, default: Date.now() } // UnixTime\n  });\n\n  Cache.static('compareFile', function(id: string,\n    hashFn: (id: string) => Promise<string>,\n    statFn: (id: string) => Promise<fs.Stats>): Promise<{ type: string }> {\n    const cache = this.findById(id) as Document<CacheSchema>;\n\n    // If cache does not exist, then it must be a new file. We have to get both\n    // file hash and stats.\n    if (!cache) {\n      return Promise.all([hashFn(id), statFn(id)]).spread((hash: string, stats: fs.Stats) => this.insert({\n        _id: id,\n        hash,\n        modified: stats.mtime.getTime()\n      })).thenReturn({\n        type: 'create'\n      });\n    }\n\n    let mtime: number;\n\n    // Get file stats\n    return statFn(id).then<any>(stats => {\n      mtime = stats.mtime.getTime();\n\n      // Skip the file if the modified time is unchanged\n      if (cache.modified === mtime) {\n        return {\n          type: 'skip'\n        };\n      }\n\n      // Get file hash\n      return hashFn(id);\n    }).then((result: string | { type: string }) => {\n      // If the result is an object, skip the following steps because it's an\n      // unchanged file\n      if (typeof result === 'object') return result;\n\n      const hash = result;\n\n      // Skip the file if the hash is unchanged\n      if (cache.hash === hash) {\n        return {\n          type: 'skip'\n        };\n      }\n\n      // Update cache info\n      cache.hash = hash;\n      cache.modified = mtime;\n\n      return cache.save().thenReturn({\n        type: 'update'\n      });\n    });\n  });\n\n  return Cache;\n};\n"
  },
  {
    "path": "lib/models/category.ts",
    "content": "import warehouse from 'warehouse';\nimport { slugize, full_url_for } from 'hexo-util';\nimport type Hexo from '../hexo';\nimport type { CategorySchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const Category = new warehouse.Schema<CategorySchema>({\n    name: {type: String, required: true},\n    parent: { type: warehouse.Schema.Types.CUID, ref: 'Category'}\n  });\n\n  Category.virtual('slug').get(function() {\n    let name = this.name;\n\n    if (!name) return;\n\n    let str = '';\n\n    if (this.parent) {\n      const parent = ctx.model('Category').findById(this.parent);\n      str += `${parent.slug}/`;\n    }\n\n    const map = ctx.config.category_map || {};\n\n    name = map[name] || name;\n    str += slugize(name, {transform: ctx.config.filename_case});\n\n    return str;\n  });\n\n  Category.virtual('path').get(function() {\n    let catDir = ctx.config.category_dir;\n    if (catDir === '/') catDir = '';\n    if (!catDir.endsWith('/')) catDir += '/';\n\n    return `${catDir + this.slug}/`;\n  });\n\n  Category.virtual('permalink').get(function() {\n    return full_url_for.call(ctx, this.path);\n  });\n\n  Category.virtual('posts').get(function() {\n    const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category;\n\n    const ids = ReadOnlyPostCategory.find({category_id: this._id}).map(item => item.post_id);\n\n    return ctx.locals.get('posts').find({\n      _id: {$in: ids}\n    });\n  });\n\n  Category.virtual('length').get(function() {\n    const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category;\n\n    return ReadOnlyPostCategory.find({category_id: this._id}).length;\n  });\n\n  // Check whether a category exists\n  Category.pre('save', (data: CategorySchema) => {\n    const { name, parent } = data;\n    if (!name) return;\n\n    const Category = ctx.model('Category');\n    const cat = Category.findOne({\n      name,\n      parent: parent || {$exists: false}\n    }, {lean: true});\n\n    if (cat) {\n      throw new Error(`Category \\`${name}\\` has already existed!`);\n    }\n  });\n\n  // Remove PostCategory references\n  Category.pre('remove', (data: CategorySchema) => {\n    const PostCategory = ctx.model('PostCategory');\n    return PostCategory.remove({category_id: data._id});\n  });\n\n  return Category;\n};\n"
  },
  {
    "path": "lib/models/data.ts",
    "content": "import warehouse from 'warehouse';\nimport type Hexo from '../hexo';\nimport { DataSchema } from '../types';\n\nexport = (_ctx: Hexo) => {\n  const Data = new warehouse.Schema<DataSchema>({\n    _id: {type: String, required: true},\n    data: Object\n  });\n\n  return Data;\n};\n"
  },
  {
    "path": "lib/models/index.ts",
    "content": "export { default as Asset } from './asset';\nexport { default as Cache } from './cache';\nexport { default as Category } from './category';\nexport { default as Data } from './data';\nexport { default as Page } from './page';\nexport { default as Post } from './post';\nexport { default as PostAsset } from './post_asset';\nexport { default as PostCategory } from './post_category';\nexport { default as PostTag } from './post_tag';\nexport { default as Tag } from './tag';\n"
  },
  {
    "path": "lib/models/page.ts",
    "content": "import warehouse from 'warehouse';\nimport { join } from 'path';\nimport Moment from './types/moment';\nimport moment from 'moment';\nimport { full_url_for } from 'hexo-util';\nimport type Hexo from '../hexo';\nimport type { PageSchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const Page = new warehouse.Schema<PageSchema>({\n    title: {type: String, default: ''},\n    date: {\n      type: Moment,\n      default: moment\n    },\n    updated: {\n      type: Moment\n    },\n    comments: {type: Boolean, default: true},\n    layout: {type: String, default: 'page'},\n    _content: {type: String, default: ''},\n    source: {type: String, required: true},\n    path: {type: String, required: true},\n    raw: {type: String, default: ''},\n    content: {type: String},\n    excerpt: {type: String},\n    more: {type: String}\n  });\n\n  Page.virtual('permalink').get(function() {\n    return full_url_for.call(ctx, this.path);\n  });\n\n  Page.virtual('full_source').get(function() {\n    return join(ctx.source_dir, this.source || '');\n  });\n\n  return Page;\n};\n"
  },
  {
    "path": "lib/models/post.ts",
    "content": "import warehouse from 'warehouse';\nimport moment from 'moment';\nimport { extname, join, sep } from 'path';\nimport Promise from 'bluebird';\nimport Moment from './types/moment';\nimport { full_url_for, Cache } from 'hexo-util';\nimport type Hexo from '../hexo';\nimport type { CategorySchema, PostCategorySchema, PostSchema } from '../types';\n\nfunction pickID(data: PostSchema | PostCategorySchema) {\n  return data._id;\n}\n\nfunction removeEmptyTag(tags: string[]) {\n  return tags.filter(tag => tag != null && tag !== '').map(tag => `${tag}`);\n}\n\nconst tagsGetterCache = new Cache();\n\nexport = (ctx: Hexo) => {\n  const Post = new warehouse.Schema<PostSchema>({\n    id: String,\n    title: {type: String, default: ''},\n    date: {\n      type: Moment,\n      default: moment\n    },\n    updated: {\n      type: Moment\n    },\n    comments: {type: Boolean, default: true},\n    layout: {type: String, default: 'post'},\n    _content: {type: String, default: ''},\n    source: {type: String, required: true},\n    slug: {type: String, required: true},\n    photos: [String],\n    raw: {type: String, default: ''},\n    published: {type: Boolean, default: true},\n    content: {type: String},\n    excerpt: {type: String},\n    more: {type: String}\n  });\n\n  Post.virtual('path').get(function() {\n    const path = ctx.execFilterSync('post_permalink', this, {context: ctx});\n    return typeof path === 'string' ? path : '';\n  });\n\n  Post.virtual('permalink').get(function() {\n    return full_url_for.call(ctx, this.path);\n  });\n\n  Post.virtual('full_source').get(function() {\n    return join(ctx.source_dir, this.source || '');\n  });\n\n  Post.virtual('asset_dir').get(function() {\n    const src = this.full_source;\n    return src.substring(0, src.length - extname(src).length) + sep;\n  });\n\n  Post.virtual('tags').get(function() {\n    return tagsGetterCache.apply(this._id, () => {\n      const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag;\n      const Tag = ctx.model('Tag');\n\n      const ids = ReadOnlyPostTag.find({post_id: this._id}).map(item => item.tag_id);\n\n      return Tag.find({_id: {$in: ids}});\n    });\n  });\n\n  Post.method('notPublished', function() {\n    // The same condition as ctx._bindLocals\n    return (!ctx.config.future && this.date.valueOf() > Date.now()) || (!ctx._showDrafts() && this.published === false);\n  });\n\n  Post.method('setTags', function(tags: string[]) {\n    if (this.notPublished()) {\n      // Ignore tags of draft posts\n      // If the post is unpublished then the tag needs to be removed, thus the function cannot be returned early here\n      tags = [];\n    }\n    tagsGetterCache.flush();\n    tags = removeEmptyTag(tags);\n\n    const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag;\n    const PostTag = ctx.model('PostTag');\n    const Tag = ctx.model('Tag');\n    const id = this._id;\n    const existed = ReadOnlyPostTag.find({post_id: id}).map(pickID);\n\n    return Promise.map(tags, tag => {\n      // Find the tag by name\n      const data = Tag.findOne({name: tag}, {lean: true});\n      if (data) return data;\n\n      // Insert the tag if not exist\n      return Tag.insert({name: tag}).catch(err => {\n        // Try to find the tag again. Throw the error if not found\n        const data = Tag.findOne({name: tag}, {lean: true});\n\n        if (data) return data;\n        throw err;\n      });\n    }).map(tag => {\n      // Find the reference\n      const ref = ReadOnlyPostTag.findOne({post_id: id, tag_id: tag._id});\n      if (ref) return ref;\n\n      // Insert the reference if not exist\n      return PostTag.insert({\n        post_id: id,\n        tag_id: tag._id\n      });\n    }).then(tags => {\n      // Remove old tags\n      const deleted = existed.filter(item => !tags.map(pickID).includes(item));\n      return deleted;\n    }).map(tag => PostTag.removeById(tag));\n  });\n\n  Post.virtual('categories').get(function() {\n    const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category;\n    const Category = ctx.model('Category');\n\n    const ids = ReadOnlyPostCategory.find({post_id: this._id}).map(item => item.category_id);\n\n    return Category.find({_id: {$in: ids}});\n  });\n\n  Post.method('setCategories', function(cats: (string | string[])[]) {\n    if (this.notPublished()) {\n      cats = [];\n    }\n    // Remove empty categories, preserving hierarchies\n    cats = cats.filter(cat => {\n      return Array.isArray(cat) || (cat != null && cat !== '');\n    }).map(cat => {\n      return Array.isArray(cat) ? removeEmptyTag(cat) : `${cat}`;\n    });\n\n    const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category;\n    const PostCategory = ctx.model('PostCategory');\n    const Category = ctx.model('Category');\n    const id = this._id;\n    const allIds: string[] = [];\n    const existed = ReadOnlyPostCategory.find({post_id: id}).map(pickID);\n    const hasHierarchy = cats.filter(Array.isArray).length > 0;\n\n    // Add a hierarchy of categories\n    const addHierarchy = (catHierarchy: string | string[]) => {\n      const parentIds = [];\n      if (!Array.isArray(catHierarchy)) catHierarchy = [catHierarchy];\n      // Don't use \"Promise.map\". It doesn't run in series.\n      // MUST USE \"Promise.each\".\n      return Promise.each(catHierarchy, (cat, i) => {\n        // Find the category by name\n        const data: CategorySchema = Category.findOne({\n          name: cat,\n          parent: i ? parentIds[i - 1] : {$exists: false}\n        }, {lean: true});\n\n        if (data) {\n          allIds.push(data._id);\n          parentIds.push(data._id);\n          return data;\n        }\n\n        // Insert the category if not exist\n        const obj: {name: string, parent?: string} = {name: cat};\n        if (i) obj.parent = parentIds[i - 1];\n\n        return Category.insert(obj).catch(err => {\n          // Try to find the category again. Throw the error if not found\n          const data: CategorySchema = Category.findOne({\n            name: cat,\n            parent: i ? parentIds[i - 1] : {$exists: false}\n          }, {lean: true});\n\n          if (data) return data;\n          throw err;\n        }).then((data: CategorySchema) => {\n          allIds.push(data._id);\n          parentIds.push(data._id);\n          return data;\n        });\n      });\n    };\n\n    return (hasHierarchy ? Promise.each(cats, addHierarchy) : Promise.resolve(addHierarchy(cats as string[]))\n    ).then(() => allIds).map(catId => {\n      // Find the reference\n      const ref: PostCategorySchema = ReadOnlyPostCategory.findOne({post_id: id, category_id: catId});\n      if (ref) return ref;\n\n      // Insert the reference if not exist\n      return PostCategory.insert({\n        post_id: id,\n        category_id: catId\n      });\n    }).then((postCats: PostCategorySchema[]) => // Remove old categories\n      existed.filter(item => !postCats.map(pickID).includes(item))).map(cat => PostCategory.removeById(cat));\n  });\n\n  // Remove PostTag references\n  Post.pre('remove', (data: PostSchema) => {\n    const PostTag = ctx.model('PostTag');\n    return PostTag.remove({post_id: data._id});\n  });\n\n  // Remove PostCategory references\n  Post.pre('remove', (data: PostSchema) => {\n    const PostCategory = ctx.model('PostCategory');\n    return PostCategory.remove({post_id: data._id});\n  });\n\n  // Remove assets\n  Post.pre('remove', (data: PostSchema) => {\n    const PostAsset = ctx.model('PostAsset');\n    return PostAsset.remove({post: data._id});\n  });\n\n  return Post;\n};\n"
  },
  {
    "path": "lib/models/post_asset.ts",
    "content": "import warehouse from 'warehouse';\nimport { join, posix } from 'path';\nimport type Hexo from '../hexo';\nimport type { PostAssetSchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const PostAsset = new warehouse.Schema<PostAssetSchema>({\n    _id: {type: String, required: true},\n    slug: {type: String, required: true},\n    modified: {type: Boolean, default: true},\n    post: {type: warehouse.Schema.Types.CUID, ref: 'Post'},\n    renderable: {type: Boolean, default: true}\n  });\n\n  PostAsset.virtual('path').get(function() {\n    const Post = ctx.model('Post');\n    const post = Post.findById(this.post);\n    if (!post) return;\n\n    // PostAsset.path is file path relative to `public_dir`\n    // no need to urlescape, #1562\n    // strip /\\.html?$/ extensions on permalink, #2134\n    // Use path.posix.join to avoid path.join introducing unwanted backslashes on Windows.\n    return posix.join(post.path.replace(/\\.html?$/, ''), this.slug);\n  });\n\n  PostAsset.virtual('source').get(function() {\n    return join(ctx.base_dir, this._id);\n  });\n\n  return PostAsset;\n};\n"
  },
  {
    "path": "lib/models/post_category.ts",
    "content": "import warehouse from 'warehouse';\nimport type Hexo from '../hexo';\nimport { PostCategorySchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const PostCategory = new warehouse.Schema<PostCategorySchema>({\n    post_id: {type: warehouse.Schema.Types.CUID, ref: 'Post'},\n    category_id: {type: warehouse.Schema.Types.CUID, ref: 'Category'}\n  });\n\n  PostCategory.pre('save', data => {\n    ctx._binaryRelationIndex.post_category.removeHook(data);\n    return data;\n  });\n\n  PostCategory.post('save', data => {\n    ctx._binaryRelationIndex.post_category.saveHook(data);\n    return data;\n  });\n\n  PostCategory.pre('remove', data => {\n    ctx._binaryRelationIndex.post_category.removeHook(data);\n    return data;\n  });\n\n  return PostCategory;\n};\n"
  },
  {
    "path": "lib/models/post_tag.ts",
    "content": "import warehouse from 'warehouse';\nimport type Hexo from '../hexo';\nimport { PostTagSchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const PostTag = new warehouse.Schema<PostTagSchema>({\n    post_id: {type: warehouse.Schema.Types.CUID, ref: 'Post'},\n    tag_id: {type: warehouse.Schema.Types.CUID, ref: 'Tag'}\n  });\n\n  PostTag.pre('save', data => {\n    ctx._binaryRelationIndex.post_tag.removeHook(data);\n    return data;\n  });\n\n  PostTag.post('save', data => {\n    ctx._binaryRelationIndex.post_tag.saveHook(data);\n    return data;\n  });\n\n  PostTag.pre('remove', data => {\n    ctx._binaryRelationIndex.post_tag.removeHook(data);\n    return data;\n  });\n\n  return PostTag;\n};\n"
  },
  {
    "path": "lib/models/tag.ts",
    "content": "import warehouse from 'warehouse';\nimport { slugize, full_url_for } from 'hexo-util';\nconst { hasOwnProperty: hasOwn } = Object.prototype;\nimport type Hexo from '../hexo';\nimport type { TagSchema } from '../types';\n\nexport = (ctx: Hexo) => {\n  const Tag = new warehouse.Schema<TagSchema>({\n    name: {type: String, required: true}\n  });\n\n  Tag.virtual('slug').get(function() {\n    const map = ctx.config.tag_map || {};\n    let name = this.name;\n    if (!name) return;\n\n    if (Reflect.apply(hasOwn, map, [name])) {\n      name = map[name] || name;\n    }\n\n    return slugize(name, {transform: ctx.config.filename_case});\n  });\n\n  Tag.virtual('path').get(function() {\n    let tagDir = ctx.config.tag_dir;\n    if (!tagDir.endsWith('/')) tagDir += '/';\n\n    return `${tagDir + this.slug}/`;\n  });\n\n  Tag.virtual('permalink').get(function() {\n    return full_url_for.call(ctx, this.path);\n  });\n\n  Tag.virtual('posts').get(function() {\n    const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag;\n\n    const ids = ReadOnlyPostTag.find({tag_id: this._id}).map(item => item.post_id);\n\n    return ctx.locals.get('posts').find({\n      _id: {$in: ids}\n    });\n  });\n\n  Tag.virtual('length').get(function() {\n    // Note: this.posts.length is also working\n    // But it's slow because `find` has to iterate over all posts\n    const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag;\n\n    return ReadOnlyPostTag.find({tag_id: this._id}).length;\n  });\n\n  // Check whether a tag exists\n  Tag.pre('save', (data: TagSchema) => {\n    const { name } = data;\n    if (!name) return;\n\n    const Tag = ctx.model('Tag');\n    const tag = Tag.findOne({name}, {lean: true});\n\n    if (tag) {\n      throw new Error(`Tag \\`${name}\\` has already existed!`);\n    }\n  });\n\n  // Remove PostTag references\n  Tag.pre('remove', (data: TagSchema) => {\n    const PostTag = ctx.model('PostTag');\n    return PostTag.remove({tag_id: data._id});\n  });\n\n  return Tag;\n};\n"
  },
  {
    "path": "lib/models/types/moment.ts",
    "content": "import warehouse from 'warehouse';\nimport { moment } from '../../plugins/helper/date';\n\n// It'll pollute the moment module.\n// declare module 'moment' {\n//   export default interface Moment extends moment.Moment {\n//     _d: Date;\n//   // eslint-disable-next-line semi\n//   }\n// }\n\nclass SchemaTypeMoment extends warehouse.SchemaType<moment.Moment> {\n  public options: any;\n\n  constructor(name, options = {}) {\n    super(name, options);\n  }\n\n  cast(value?, data?) {\n    value = super.cast(value, data);\n    if (value == null) return value;\n\n    return toMoment(value);\n  }\n\n  validate(value, data?) {\n    value = super.validate(value, data);\n    if (value == null) return value;\n\n    value = toMoment(value);\n\n    if (!value.isValid()) {\n      throw new Error('`' + value + '` is not a valid date!');\n    }\n\n    return value;\n  }\n\n  match(value, query, _data?) {\n    return value ? value.valueOf() === query.valueOf() : false;\n  }\n\n  compare(a?, b?) {\n    if (a) {\n      if (b) return a - b;\n      return 1;\n    }\n\n    if (b) return -1;\n    return 0;\n  }\n\n  parse(value?) {\n    if (value) return toMoment(value);\n  }\n\n  value(value?, _data?) {\n    // FIXME: Same as above. Also a dirty hack.\n    return value ? value._d.toISOString() : value;\n  }\n\n  q$day(value, query, _data?) {\n    return value ? value.date() === query : false;\n  }\n\n  q$month(value, query, _data?) {\n    return value ? value.month() === query : false;\n  }\n\n  q$year(value, query, _data?) {\n    return value ? value.year() === query : false;\n  }\n\n  u$inc(value, update, _data?) {\n    if (!value) return value;\n    return value.add(update);\n  }\n\n  u$dec(value, update, _data?) {\n    if (!value) return value;\n    return value.subtract(update);\n  }\n}\n\nfunction toMoment(value) {\n  // FIXME: Something is wrong when using a moment instance. I try to get the\n  // original date object and create a new moment object again.\n  if (moment.isMoment(value)) return moment((value as any)._d);\n  return moment(value);\n}\n\nexport = SchemaTypeMoment;\n"
  },
  {
    "path": "lib/plugins/console/clean.ts",
    "content": "import Promise from 'bluebird';\nimport { exists, unlink, rmdir } from 'hexo-fs';\nimport type Hexo from '../../hexo';\n\nfunction cleanConsole(this: Hexo): Promise<[void, void, any]> {\n  return Promise.all([\n    deleteDatabase(this),\n    deletePublicDir(this),\n    this.execFilter('after_clean', null, {context: this})\n  ]);\n}\n\nfunction deleteDatabase(ctx: Hexo): Promise<void> {\n  const dbPath = ctx.database.options.path;\n\n  return exists(dbPath).then(exist => {\n    if (!exist) return;\n\n    return unlink(dbPath).then(() => {\n      ctx.log.info('Deleted database.');\n    });\n  });\n}\n\nfunction deletePublicDir(ctx: Hexo): Promise<void> {\n  const publicDir = ctx.public_dir;\n\n  return exists(publicDir).then(exist => {\n    if (!exist) return;\n\n    return rmdir(publicDir).then(() => {\n      ctx.log.info('Deleted public folder.');\n    });\n  });\n}\n\nexport = cleanConsole;\n"
  },
  {
    "path": "lib/plugins/console/config.ts",
    "content": "import yaml from 'js-yaml';\nimport { exists, writeFile } from 'hexo-fs';\nimport { extname } from 'path';\nimport Promise from 'bluebird';\nimport type Hexo from '../../hexo';\n\ninterface ConfigArgs {\n  _: string[]\n  [key: string]: any\n}\n\nfunction configConsole(this: Hexo, args: ConfigArgs): Promise<void> {\n  const key = args._[0];\n  let value = args._[1];\n\n  if (!key) {\n    console.log(this.config);\n    return Promise.resolve();\n  }\n\n  if (!value) {\n    value = getProperty(this.config, key);\n    if (value) console.log(value);\n    return Promise.resolve();\n  }\n\n  const configPath = this.config_path;\n  const ext = extname(configPath);\n\n  return exists(configPath).then(exist => {\n    if (!exist) return {};\n    return this.render.render({path: configPath});\n  }).then(config => {\n    if (!config) config = {};\n\n    setProperty(config, key, castValue(value));\n\n    const result = ext === '.json' ? JSON.stringify(config) : yaml.dump(config);\n\n    return writeFile(configPath, result);\n  });\n}\n\nfunction getProperty(obj: object, key: string): any {\n  const split = key.split('.');\n  let result = obj[split[0]];\n\n  for (let i = 1, len = split.length; i < len; i++) {\n    result = result[split[i]];\n  }\n\n  return result;\n}\n\nfunction setProperty(obj: object, key: string, value: any): void {\n  const split = key.split('.');\n  let cursor = obj;\n  const lastKey = split.pop();\n\n  for (let i = 0, len = split.length; i < len; i++) {\n    const name = split[i];\n    cursor[name] = cursor[name] || {};\n    cursor = cursor[name];\n  }\n\n  cursor[lastKey] = value;\n}\n\nfunction castValue(value: string): any {\n  switch (value) {\n    case 'true':\n      return true;\n\n    case 'false':\n      return false;\n\n    case 'null':\n      return null;\n\n    case 'undefined':\n      return undefined;\n  }\n\n  const num = Number(value);\n  if (!isNaN(num)) return num;\n\n  return value;\n}\n\nexport = configConsole;\n"
  },
  {
    "path": "lib/plugins/console/deploy.ts",
    "content": "import { exists } from 'hexo-fs';\nimport { underline, magenta } from 'picocolors';\nimport type Hexo from '../../hexo';\nimport type Promise from 'bluebird';\n\ninterface DeployArgs {\n  _?: string[]\n  g?: boolean\n  generate?: boolean\n  [key: string]: any\n}\n\nfunction deployConsole(this: Hexo, args: DeployArgs): Promise<any> {\n  let config = this.config.deploy;\n  const deployers = this.extend.deployer.list();\n\n  if (!config) {\n    let help = '';\n\n    help += 'You should configure deployment settings in _config.yml first!\\n\\n';\n    help += 'Available deployer plugins:\\n';\n    help += `  ${Object.keys(deployers).join(', ')}\\n\\n`;\n    help += `For more help, you can check the online docs: ${underline('https://hexo.io/')}`;\n\n    console.log(help);\n    return;\n  }\n\n  let promise: Promise<void>;\n\n  if (args.g || args.generate) {\n    promise = this.call('generate', args);\n  } else {\n    promise = exists(this.public_dir).then(exist => {\n      if (!exist) return this.call('generate', args);\n    });\n  }\n\n  return promise.then(() => {\n    this.emit('deployBefore');\n\n    if (!Array.isArray(config)) config = [config];\n    return config;\n  }).each(item => {\n    if (!item.type) return;\n\n    const { type } = item;\n\n    if (!deployers[type]) {\n      this.log.error('Deployer not found: %s', magenta(type));\n      return;\n    }\n\n    this.log.info('Deploying: %s', magenta(type));\n\n    return (Reflect.apply(deployers[type], this, [{ ...item, ...args }]) as any).then(() => {\n      this.log.info('Deploy done: %s', magenta(type));\n    });\n  }).then(() => {\n    this.emit('deployAfter');\n  });\n}\n\nexport = deployConsole;\n"
  },
  {
    "path": "lib/plugins/console/generate.ts",
    "content": "import { exists, writeFile, unlink, stat, mkdirs } from 'hexo-fs';\nimport { join } from 'path';\nimport Promise from 'bluebird';\nimport prettyHrtime from 'pretty-hrtime';\nimport { cyan, magenta } from 'picocolors';\nimport tildify from 'tildify';\nimport { PassThrough, type Readable } from 'stream';\nimport { createSha1Hash } from 'hexo-util';\nimport type Hexo from '../../hexo';\nimport type Router from '../../hexo/router';\n\ninterface GenerateArgs {\n  f?: boolean\n  force?: boolean\n  b?: boolean\n  bail?: boolean\n  c?: string\n  concurrency?: string\n  w?: boolean\n  watch?: boolean\n  d?: boolean\n  deploy?: boolean\n  [key: string]: any\n}\n\nclass Generator {\n  public context: Hexo;\n  public force: boolean;\n  public bail: boolean;\n  public concurrency: string;\n  public watch: boolean;\n  public deploy: boolean;\n  public generatingFiles: Set<string>;\n  public start: [number, number];\n  public args: GenerateArgs;\n\n  constructor(ctx: Hexo, args: GenerateArgs) {\n    this.context = ctx;\n    this.force = args.f || args.force;\n    this.bail = args.b || args.bail;\n    this.concurrency = args.c || args.concurrency;\n    this.watch = args.w || args.watch;\n    this.deploy = args.d || args.deploy;\n    this.generatingFiles = new Set();\n    this.start = process.hrtime();\n    this.args = args;\n  }\n  generateFile(path: string): Promise<void | boolean> {\n    const publicDir = this.context.public_dir;\n    const { generatingFiles } = this;\n    const { route } = this.context;\n    // Skip if the file is generating\n    if (generatingFiles.has(path)) return Promise.resolve();\n\n    // Lock the file\n    generatingFiles.add(path);\n\n    let promise: Promise<boolean>;\n\n    if (this.force) {\n      promise = this.writeFile(path, true);\n    } else {\n      const dest = join(publicDir, path);\n      promise = exists(dest).then(exist => {\n        if (!exist) return this.writeFile(path, true);\n        if (route.isModified(path)) return this.writeFile(path);\n      });\n    }\n\n    return promise.finally(() => {\n      // Unlock the file\n      generatingFiles.delete(path);\n    });\n  }\n  writeFile(path: string, force?: boolean): Promise<boolean> {\n    const { route, log } = this.context;\n    const publicDir = this.context.public_dir;\n    const Cache = this.context.model('Cache');\n    const dataStream = this.wrapDataStream(route.get(path));\n    const buffers = [];\n    const hasher = createSha1Hash();\n\n    const finishedPromise = new Promise<void>((resolve, reject) => {\n      dataStream.once('error', reject);\n      dataStream.once('end', resolve);\n    });\n\n    // Get data => Cache data => Calculate hash\n    dataStream.on('data', chunk => {\n      buffers.push(chunk);\n      hasher.update(chunk);\n    });\n\n    return finishedPromise.then(() => {\n      const dest = join(publicDir, path);\n      const cacheId = `public/${path}`;\n      const cache = Cache.findById(cacheId);\n      const hash = hasher.digest('hex');\n\n      // Skip generating if hash is unchanged\n      if (!force && cache && cache.hash === hash) {\n        return;\n      }\n\n      // Save new hash to cache\n      return Cache.save({\n        _id: cacheId,\n        hash\n      }).then(() => // Write cache data to public folder\n        writeFile(dest, Buffer.concat(buffers))).then(() => {\n        log.info('Generated: %s', magenta(path));\n        return true;\n      });\n    });\n  }\n  deleteFile(path: string): Promise<void> {\n    const { log } = this.context;\n    const publicDir = this.context.public_dir;\n    const dest = join(publicDir, path);\n\n    return unlink(dest).then(() => {\n      log.info('Deleted: %s', magenta(path));\n    }, err => {\n      // Skip ENOENT errors (file was deleted)\n      if (err && err.code === 'ENOENT') return;\n      throw err;\n    });\n  }\n  wrapDataStream(dataStream: ReturnType<Router['get']>): Readable {\n    const { log } = this.context;\n    // Pass original stream with all data and errors\n    if (this.bail) {\n      return dataStream;\n    }\n\n    // Pass all data, but don't populate errors\n    dataStream.on('error', err => {\n      log.error(err);\n    });\n\n    return dataStream.pipe(new PassThrough());\n  }\n  firstGenerate(): Promise<void> {\n    const { concurrency } = this;\n    const { route, log } = this.context;\n    const publicDir = this.context.public_dir;\n    const Cache = this.context.model('Cache');\n\n    // Show the loading time\n    const interval = prettyHrtime(process.hrtime(this.start));\n    log.info('Files loaded in %s', cyan(interval));\n\n    // Reset the timer for later usage\n    this.start = process.hrtime();\n\n\n    // Check the public folder\n    return stat(publicDir).then(stats => {\n      if (!stats.isDirectory()) {\n        throw new Error(`${magenta(tildify(publicDir))} is not a directory`);\n      }\n    }).catch(err => {\n      // Create public folder if not exists\n      if (err && err.code === 'ENOENT') {\n        return mkdirs(publicDir);\n      }\n\n      throw err;\n    }).then(() => {\n      const task = (fn, path) => () => fn.call(this, path);\n      const doTask = fn => fn();\n      const routeList = route.list();\n      const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7));\n      const tasks = publicFiles.filter(path => !routeList.includes(path))\n        // Clean files\n        .map(path => task(this.deleteFile, path))\n        // Generate files\n        .concat(routeList.map(path => task(this.generateFile, path)));\n\n      return Promise.all(Promise.map(tasks, doTask, { concurrency: parseFloat(concurrency || 'Infinity') }));\n    }).then(result => {\n      const interval = prettyHrtime(process.hrtime(this.start));\n      const count = result.filter(Boolean).length;\n\n      log.info('%d files generated in %s', count.toString(), cyan(interval));\n    });\n  }\n  execWatch(): Promise<void> {\n    const { route, log } = this.context;\n    return this.context.watch().then(() => this.firstGenerate()).then(() => {\n      log.info('Hexo is watching for file changes. Press Ctrl+C to exit.');\n\n      // Watch changes of the route\n      route.on('update', path => {\n        const modified = route.isModified(path);\n        if (!modified) return;\n\n        this.generateFile(path);\n      }).on('remove', path => {\n        this.deleteFile(path);\n      });\n    });\n  }\n  execDeploy() {\n    return this.context.call('deploy', this.args);\n  }\n}\n\nfunction generateConsole(this: Hexo, args: GenerateArgs = {}): Promise<any> {\n  const generator = new Generator(this, args);\n\n  if (generator.watch) {\n    return generator.execWatch();\n  }\n\n  return this.load().then(() => generator.firstGenerate()).then(() => {\n    if (generator.deploy) {\n      return generator.execDeploy();\n    }\n  });\n}\n\nexport = generateConsole;\n"
  },
  {
    "path": "lib/plugins/console/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = function(ctx: Hexo) {\n  const { console } = ctx.extend;\n\n  console.register('clean', 'Remove generated files and cache.', require('./clean'));\n\n  console.register('config', 'Get or set configurations.', {\n    usage: '[name] [value]',\n    arguments: [\n      {name: 'name', desc: 'Setting name. Leave it blank if you want to show all configurations.'},\n      {name: 'value', desc: 'New value of a setting. Leave it blank if you just want to show a single configuration.'}\n    ]\n  }, require('./config'));\n\n  console.register('deploy', 'Deploy your website.', {\n    options: [\n      {name: '--setup', desc: 'Setup without deployment'},\n      {name: '-g, --generate', desc: 'Generate before deployment'}\n    ]\n  }, require('./deploy'));\n\n  console.register('generate', 'Generate static files.', {\n    options: [\n      {name: '-d, --deploy', desc: 'Deploy after generated'},\n      {name: '-f, --force', desc: 'Force regenerate'},\n      {name: '-w, --watch', desc: 'Watch file changes'},\n      {name: '-b, --bail', desc: 'Raise an error if any unhandled exception is thrown during generation'},\n      {name: '-c, --concurrency', desc: 'Maximum number of files to be generated in parallel. Default is infinity'}\n    ]\n  }, require('./generate'));\n\n  console.register('list', 'List the information of the site', {\n    desc: 'List the information of the site.',\n    usage: '<type>',\n    arguments: [\n      {name: 'type', desc: 'Available types: page, post, route, tag, category'}\n    ]\n  }, require('./list'));\n\n  console.register('migrate', 'Migrate your site from other system to Hexo.', {\n    init: true,\n    usage: '<type>',\n    arguments: [\n      {name: 'type', desc: 'Migrator type.'}\n    ]\n  }, require('./migrate'));\n\n  console.register('new', 'Create a new post.', {\n    usage: '[layout] <title>',\n    arguments: [\n      {name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'},\n      {name: 'title', desc: 'Post title. Wrap it with quotations to escape.'}\n    ],\n    options: [\n      {name: '-r, --replace', desc: 'Replace the current post if existed.'},\n      {name: '-s, --slug', desc: 'Post slug. Customize the URL of the post.'},\n      {name: '-p, --path', desc: 'Post path. Customize the path of the post.'}\n    ]\n  }, require('./new'));\n\n  console.register('publish', 'Moves a draft post from _drafts to _posts folder.', {\n    usage: '[layout] <filename>',\n    arguments: [\n      {name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'},\n      {name: 'filename', desc: 'Draft filename. \"hello-world\" for example.'}\n    ]\n  }, require('./publish'));\n\n  console.register('render', 'Render files with renderer plugins.', {\n    init: true,\n    desc: 'Render files with renderer plugins (e.g. Markdown) and save them at the specified path.',\n    usage: '<file1> [file2] ...',\n    options: [\n      {name: '--output', desc: 'Output destination. Result will be printed in the terminal if the output destination is not set.'},\n      {name: '--engine', desc: 'Specify render engine'},\n      {name: '--pretty', desc: 'Prettify JSON output'}\n    ]\n  }, require('./render'));\n}\n"
  },
  {
    "path": "lib/plugins/console/list/category.ts",
    "content": "import { underline } from 'picocolors';\nimport table from 'fast-text-table';\nimport { stringLength } from './common';\nimport type Hexo from '../../../hexo';\nimport type { CategorySchema } from '../../../types';\nimport type Model from 'warehouse/dist/model';\nimport type Document from 'warehouse/dist/document';\n\nfunction listCategory(this: Hexo): void {\n  const categories: Model<CategorySchema> = this.model('Category');\n\n  const data = categories.sort({name: 1}).map((cate: Document<CategorySchema> & CategorySchema) => [cate.name, String(cate.length)]);\n\n  // Table header\n  const header = ['Name', 'Posts'].map(str => underline(str));\n\n  data.unshift(header);\n\n  const t = table(data, {\n    align: ['l', 'r'],\n    stringLength\n  });\n\n  console.log(t);\n  if (data.length === 1) console.log('No categories.');\n}\n\nexport = listCategory;\n"
  },
  {
    "path": "lib/plugins/console/list/common.ts",
    "content": "import strip from 'strip-ansi';\n\nexport function stringLength(str: string): number {\n  str = strip(str);\n\n  const len = str.length;\n  let result = len;\n\n  // Detect double-byte characters\n  for (let i = 0; i < len; i++) {\n    if (str.charCodeAt(i) > 255) {\n      result++;\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "lib/plugins/console/list/index.ts",
    "content": "import abbrev from 'abbrev';\nimport page from './page';\nimport post from './post';\nimport route from './route';\nimport tag from './tag';\nimport category from './category';\nimport type Hexo from '../../../hexo';\nimport type Promise from 'bluebird';\n\ninterface ListArgs {\n  _: string[]\n}\n\nconst store = {\n  page, post, route, tag, category\n};\n\nconst alias = abbrev(Object.keys(store));\n\nfunction listConsole(this: Hexo, args: ListArgs): Promise<void> {\n  const type = args._.shift();\n\n  // Display help message if user didn't input any arguments\n  if (!type || !alias[type]) {\n    return this.call('help', {_: ['list']});\n  }\n\n  return this.load().then(() => Reflect.apply(store[alias[type]], this, [args]));\n}\n\nexport = listConsole;\n"
  },
  {
    "path": "lib/plugins/console/list/page.ts",
    "content": "import { magenta, underline, gray } from 'picocolors';\nimport table from 'fast-text-table';\nimport { stringLength } from './common';\nimport type Hexo from '../../../hexo';\nimport type { PageSchema } from '../../../types';\nimport type Model from 'warehouse/dist/model';\nimport type Document from 'warehouse/dist/document';\n\nfunction listPage(this: Hexo): void {\n  const Page: Model<PageSchema> = this.model('Page');\n\n  const data = Page.sort({date: 1}).map((page: Document<PageSchema> & PageSchema) => {\n    const date = page.date.format('YYYY-MM-DD');\n    return [gray(date), page.title, magenta(page.source)];\n  });\n\n  // Table header\n  const header = ['Date', 'Title', 'Path'].map(str => underline(str));\n\n  data.unshift(header);\n\n  const t = table(data, {\n    stringLength\n  });\n\n  console.log(t);\n  if (data.length === 1) console.log('No pages.');\n}\n\nexport = listPage;\n"
  },
  {
    "path": "lib/plugins/console/list/post.ts",
    "content": "import { gray, magenta, underline } from 'picocolors';\nimport table from 'fast-text-table';\nimport { stringLength } from './common';\nimport type Hexo from '../../../hexo';\nimport type { PostSchema } from '../../../types';\nimport type Model from 'warehouse/dist/model';\nimport type Document from 'warehouse/dist/document';\n\nfunction mapName(item: any): string {\n  return item.name;\n}\n\nfunction listPost(this: Hexo): void {\n  const Post: Model<PostSchema> = this.model('Post');\n\n  const data = Post.sort({published: -1, date: 1}).map((post: Document<PostSchema> & PostSchema) => {\n    const date = post.published ? post.date.format('YYYY-MM-DD') : 'Draft';\n    const tags = post.tags.map(mapName);\n    const categories = post.categories.map(mapName);\n\n    return [\n      gray(date),\n      post.title,\n      magenta(post.source),\n      categories.join(', '),\n      tags.join(', ')\n    ];\n  });\n\n  // Table header\n  const header = ['Date', 'Title', 'Path', 'Category', 'Tags'].map(str => underline(str));\n\n  data.unshift(header);\n\n  const t = table(data, {\n    stringLength\n  });\n\n  console.log(t);\n  if (data.length === 1) console.log('No posts.');\n}\n\nexport = listPost;\n"
  },
  {
    "path": "lib/plugins/console/list/route.ts",
    "content": "import archy from 'fast-archy';\nimport type Hexo from '../../../hexo';\n\nfunction listRoute(this: Hexo): void {\n  const routes = this.route.list().sort();\n  const tree = buildTree(routes);\n  const nodes = buildNodes(tree);\n\n  const s = archy({\n    label: `Total: ${routes.length}`,\n    nodes\n  });\n\n  console.log(s);\n}\n\nfunction buildTree(routes: string[]) {\n  const obj: Record<string, any> = {};\n  let cursor: typeof obj;\n\n  for (let i = 0, len = routes.length; i < len; i++) {\n    const item = routes[i].split('/');\n    cursor = obj;\n\n    for (let j = 0, lenj = item.length; j < lenj; j++) {\n      const seg = item[j];\n      cursor[seg] = cursor[seg] || {};\n      cursor = cursor[seg];\n    }\n  }\n\n  return obj;\n}\n\nfunction buildNodes(tree: Record<string, any>) {\n  const nodes = [];\n\n  for (const [key, item] of Object.entries(tree)) {\n    if (Object.keys(item).length) {\n      nodes.push({\n        label: key,\n        nodes: buildNodes(item)\n      });\n    } else {\n      nodes.push(key);\n    }\n  }\n\n  return nodes;\n}\n\nexport = listRoute;\n"
  },
  {
    "path": "lib/plugins/console/list/tag.ts",
    "content": "import { magenta, underline } from 'picocolors';\nimport table from 'fast-text-table';\nimport { stringLength } from './common';\nimport type Hexo from '../../../hexo';\nimport type { TagSchema } from '../../../types';\nimport type Model from 'warehouse/dist/model';\nimport type Document from 'warehouse/dist/document';\n\nfunction listTag(this: Hexo): void {\n  const Tag: Model<TagSchema> = this.model('Tag');\n\n  const data = Tag.sort({name: 1}).map((tag: Document<TagSchema> & TagSchema) => [tag.name, String(tag.length), magenta(tag.path)]);\n\n  // Table header\n  const header = ['Name', 'Posts', 'Path'].map(str => underline(str));\n\n  data.unshift(header);\n\n  const t = table(data, {\n    align: ['l', 'r', 'l'],\n    stringLength\n  });\n\n  console.log(t);\n  if (data.length === 1) console.log('No tags.');\n}\n\nexport = listTag;\n"
  },
  {
    "path": "lib/plugins/console/migrate.ts",
    "content": "import { underline, magenta } from 'picocolors';\nimport type Hexo from '../../hexo';\n\ninterface MigrateArgs {\n  _: string[]\n  [key: string]: any\n}\n\nfunction migrateConsole(this: Hexo, args: MigrateArgs): Promise<any> {\n  // Display help message if user didn't input any arguments\n  if (!args._.length) {\n    return this.call('help', {_: ['migrate']});\n  }\n\n  const type = args._.shift();\n  const migrators = this.extend.migrator.list();\n\n  if (!migrators[type]) {\n    let help = '';\n\n    help += `${magenta(type)} migrator plugin is not installed.\\n\\n`;\n    help += 'Installed migrator plugins:\\n';\n    help += `  ${Object.keys(migrators).join(', ')}\\n\\n`;\n    help += `For more help, you can check the online docs: ${underline('https://hexo.io/')}`;\n\n    console.log(help);\n    return;\n  }\n\n  return Reflect.apply(migrators[type], this, [args]);\n}\n\nexport = migrateConsole;\n"
  },
  {
    "path": "lib/plugins/console/new.ts",
    "content": "import tildify from 'tildify';\nimport { magenta } from 'picocolors';\nimport { basename } from 'path';\nimport Hexo from '../../hexo';\nimport type Promise from 'bluebird';\n\nconst reservedKeys = {\n  _: true,\n  title: true,\n  layout: true,\n  slug: true,\n  s: true,\n  path: true,\n  p: true,\n  replace: true,\n  r: true,\n  // Global options\n  config: true,\n  debug: true,\n  safe: true,\n  silent: true\n};\n\ninterface NewArgs {\n  _?: string[]\n  p?: string\n  path?: string\n  s?: string\n  slug?: string\n  r?: boolean\n  replace?: boolean\n  [key: string]: any\n}\n\nfunction newConsole(this: Hexo, args: NewArgs): Promise<void> {\n  const path = args.p || args.path;\n  let title: string;\n  if (args._.length) {\n    title = args._.pop();\n  } else if (path) {\n    // Default title\n    title = basename(path);\n  } else {\n    // Display help message if user didn't input any arguments\n    return this.call('help', { _: ['new'] });\n  }\n\n  const data = {\n    title,\n    layout: args._.length ? args._[0] : this.config.default_layout,\n    slug: args.s || args.slug,\n    path\n  };\n\n  const keys = Object.keys(args);\n\n  for (let i = 0, len = keys.length; i < len; i++) {\n    const key = keys[i];\n    if (!reservedKeys[key]) data[key] = args[key];\n  }\n\n  return this.post.create(data, args.r || args.replace).then(post => {\n    this.log.info('Created: %s', magenta(tildify(post.path)));\n  });\n}\n\nexport = newConsole;\n"
  },
  {
    "path": "lib/plugins/console/publish.ts",
    "content": "import tildify from 'tildify';\nimport { magenta } from 'picocolors';\nimport type Hexo from '../../hexo';\nimport type Promise from 'bluebird';\n\ninterface PublishArgs {\n  _: string[]\n  r?: boolean\n  replace?: boolean\n  [key: string]: any\n}\n\nfunction publishConsole(this: Hexo, args: PublishArgs): Promise<void> {\n  // Display help message if user didn't input any arguments\n  if (!args._.length) {\n    return this.call('help', {_: ['publish']});\n  }\n\n  return this.post.publish({\n    slug: args._.pop(),\n    layout: args._.length ? args._[0] : this.config.default_layout\n  }, args.r || args.replace).then(post => {\n    this.log.info('Published: %s', magenta(tildify(post.path)));\n  });\n}\n\nexport = publishConsole;\n"
  },
  {
    "path": "lib/plugins/console/render.ts",
    "content": "import { resolve } from 'path';\nimport tildify from 'tildify';\nimport prettyHrtime from 'pretty-hrtime';\nimport { writeFile } from 'hexo-fs';\nimport { cyan, magenta } from 'picocolors';\nimport type Hexo from '../../hexo';\nimport type Promise from 'bluebird';\n\ninterface RenderArgs {\n  _: string[]\n  o?: string\n  output?: string\n  pretty?: boolean\n  engine?: string\n  [key: string]: any\n}\n\nfunction renderConsole(this: Hexo, args: RenderArgs): Promise<void> {\n  // Display help message if user didn't input any arguments\n  if (!args._.length) {\n    return this.call('help', {_: 'render'});\n  }\n\n  const baseDir = this.base_dir;\n  const src = resolve(baseDir, args._[0]);\n  const output = args.o || args.output;\n  const start = process.hrtime();\n  const { log } = this;\n\n  return this.render.render({\n    path: src,\n    engine: args.engine\n  }).then(result => {\n    if (typeof result === 'object') {\n      if (args.pretty) {\n        result = JSON.stringify(result, null, '  ');\n      } else {\n        result = JSON.stringify(result);\n      }\n    }\n\n    if (!output) return console.log(result);\n\n    const dest = resolve(baseDir, output);\n    const interval = prettyHrtime(process.hrtime(start));\n\n    log.info('Rendered in %s: %s -> %s', cyan(interval), magenta(tildify(src)), magenta(tildify(dest)));\n    return writeFile(dest, result);\n  });\n}\n\nexport = renderConsole;\n"
  },
  {
    "path": "lib/plugins/filter/after_post_render/excerpt.ts",
    "content": "import type { RenderData } from '../../../types';\n\nconst rExcerpt = /<!-- ?more ?-->/i;\n\nfunction excerptFilter(data: RenderData): void {\n  const { content } = data;\n\n  if (typeof data.excerpt !== 'undefined') {\n    data.more = content;\n  } else if (rExcerpt.test(content)) {\n    data.content = content.replace(rExcerpt, (match, index) => {\n      data.excerpt = content.substring(0, index).trim();\n      data.more = content.substring(index + match.length).trim();\n\n      return '<span id=\"more\"></span>';\n    });\n  } else {\n    data.excerpt = '';\n    data.more = content;\n  }\n}\n\nexport = excerptFilter;\n"
  },
  {
    "path": "lib/plugins/filter/after_post_render/external_link.ts",
    "content": "import { isExternalLink } from 'hexo-util';\nimport type Hexo from '../../../hexo';\nimport type { RenderData } from '../../../types';\n\nlet EXTERNAL_LINK_POST_ENABLED = true;\nconst rATag = /<a(?:\\s+?|\\s+?[^<>]+?\\s+?)href=[\"']((?:https?:|\\/\\/)[^<>\"']+)[\"'][^<>]*>/gi;\nconst rTargetAttr = /target=/i;\nconst rRelAttr = /rel=/i;\nconst rRelStrAttr = /rel=[\"']([^<>\"']*)[\"']/i;\n\nfunction externalLinkFilter(this: Hexo, data: RenderData): void {\n  if (!EXTERNAL_LINK_POST_ENABLED) return;\n\n  const { external_link, url } = this.config;\n\n  if (!external_link.enable || external_link.field !== 'post') {\n    EXTERNAL_LINK_POST_ENABLED = false;\n    return;\n  }\n\n  data.content = data.content.replace(rATag, (str, href) => {\n    if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) return str;\n\n    if (rRelAttr.test(str)) {\n      str = str.replace(rRelStrAttr, (relStr, rel) => {\n        return rel.includes('noopener') ? relStr : `rel=\"${rel} noopener\"`;\n      });\n      return str.replace('href=', 'target=\"_blank\" href=');\n    }\n\n    return str.replace('href=', 'target=\"_blank\" rel=\"noopener\" href=');\n  });\n}\n\nexport = externalLinkFilter;\n"
  },
  {
    "path": "lib/plugins/filter/after_post_render/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('after_post_render', require('./external_link'));\n  filter.register('after_post_render', require('./excerpt'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/after_render/external_link.ts",
    "content": "import { isExternalLink } from 'hexo-util';\nimport type Hexo from '../../../hexo';\n\nlet EXTERNAL_LINK_SITE_ENABLED = true;\nconst rATag = /<a(?:\\s+?|\\s+?[^<>]+?\\s+?)href=[\"']((?:https?:|\\/\\/)[^<>\"']+)[\"'][^<>]*>/gi;\nconst rTargetAttr = /target=/i;\nconst rRelAttr = /rel=/i;\nconst rRelStrAttr = /rel=[\"']([^<>\"']*)[\"']/i;\n\nconst addNoopener = (relStr: string, rel: string) => {\n  return rel.includes('noopener') ? relStr : `rel=\"${rel} noopener\"`;\n};\n\nfunction externalLinkFilter(this: Hexo, data: string): string {\n  if (!EXTERNAL_LINK_SITE_ENABLED) return;\n\n  const { external_link, url } = this.config;\n\n  if (!external_link.enable || external_link.field !== 'site') {\n    EXTERNAL_LINK_SITE_ENABLED = false;\n    return;\n  }\n\n  let result = '';\n  let lastIndex = 0;\n  let match;\n\n  while ((match = rATag.exec(data)) !== null) {\n    result += data.slice(lastIndex, match.index);\n\n    const str = match[0];\n    const href = match[1];\n\n    if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) {\n      result += str;\n    } else {\n      if (rRelAttr.test(str)) {\n        result += str.replace(rRelStrAttr, addNoopener).replace('href=', 'target=\"_blank\" href=');\n      } else {\n        result += str.replace('href=', 'target=\"_blank\" rel=\"noopener\" href=');\n      }\n    }\n    lastIndex = rATag.lastIndex;\n  }\n  result += data.slice(lastIndex);\n\n  return result;\n}\n\nexport = externalLinkFilter;\n"
  },
  {
    "path": "lib/plugins/filter/after_render/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('after_render:html', require('./external_link'));\n  filter.register('after_render:html', require('./meta_generator'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/after_render/meta_generator.ts",
    "content": "import type Hexo from '../../../hexo';\n\nlet NEED_INJECT = true;\nlet HAS_CHECKED = false;\nlet META_GENERATOR_TAG;\n\nfunction hexoMetaGeneratorInject(this: Hexo, data: string): string {\n  if (!NEED_INJECT) return;\n\n  if (!HAS_CHECKED) {\n    HAS_CHECKED = true;\n    if (!this.config.meta_generator\n    || data.match(/<meta\\s+(?:[^<>/]+\\s)?name=['\"]generator['\"]/i)) {\n      NEED_INJECT = false;\n      return;\n    }\n  }\n\n  META_GENERATOR_TAG = META_GENERATOR_TAG || `<meta name=\"generator\" content=\"Hexo ${this.version}\">`;\n\n  return data.replace('</head>', `${META_GENERATOR_TAG}</head>`);\n}\n\nexport = hexoMetaGeneratorInject;\n"
  },
  {
    "path": "lib/plugins/filter/before_exit/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('before_exit', require('./save_database'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/before_exit/save_database.ts",
    "content": "import type Hexo from '../../../hexo';\n\nfunction saveDatabaseFilter(this: Hexo): Promise<void> {\n  if (!this.env.init || !this._dbLoaded) return;\n\n  return this.database.save().then(() => {\n    this.log.debug('Database saved');\n  });\n}\n\nexport = saveDatabaseFilter;\n"
  },
  {
    "path": "lib/plugins/filter/before_generate/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('before_generate', require('./render_post'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/before_generate/render_post.ts",
    "content": "import Promise from 'bluebird';\nimport type Hexo from '../../../hexo';\nimport type Model from 'warehouse/dist/model';\n\nfunction renderPostFilter(this: Hexo): Promise<[any[], any[]]> {\n  const renderPosts = (model: Model<any>) => {\n    const posts = model.toArray().filter(post => post.content == null);\n\n    return Promise.map(posts, (post: any) => {\n      post.content = post._content;\n\n      return this.post.render(post.full_source, post).then(() => post.save());\n    });\n  };\n\n  return Promise.all([\n    renderPosts(this.model('Post')),\n    renderPosts(this.model('Page'))\n  ]);\n}\n\nexport = renderPostFilter;\n"
  },
  {
    "path": "lib/plugins/filter/before_post_render/backtick_code_block.ts",
    "content": "import type { HighlightOptions } from '../../../extend/syntax_highlight';\nimport type Hexo from '../../../hexo';\nimport type { RenderData } from '../../../types';\n\nconst 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;\nconst rAllOptions = /([^\\s]+)\\s+(.+?)\\s+(https?:\\/\\/\\S+|\\/\\S+)\\s*(.+)?/;\nconst rLangCaption = /([^\\s]+)\\s*(.+)?/;\nconst rCommentEscape = /(<!--[\\s\\S]*?-->)/g;\nconst rAdditionalOptions = /\\s((?:line_number|line_threshold|first_line|wrap|mark|language_attr|highlight):\\S+)/g;\n\nconst escapeSwigTag = (str: string) => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n\nfunction parseArgs(args: string) {\n  const matches = [];\n\n  let match: RegExpExecArray | null, language_attr: boolean,\n    line_number: boolean, line_threshold: number, wrap: boolean;\n  let enableHighlight = true;\n  while ((match = rAdditionalOptions.exec(args)) !== null) {\n    matches.push(match[1]);\n  }\n\n  const len = matches.length;\n  const mark: number[] = [];\n  let firstLine = 1;\n  for (let i = 0; i < len; i++) {\n    const [key, value] = matches[i].split(':');\n\n    switch (key) {\n      case 'highlight':\n        enableHighlight = value === 'true';\n        break;\n      case 'line_number':\n        line_number = value === 'true';\n        break;\n      case 'line_threshold':\n        if (!isNaN(Number(value))) line_threshold = +value;\n        break;\n      case 'first_line':\n        if (!isNaN(Number(value))) firstLine = +value;\n        break;\n      case 'wrap':\n        wrap = value === 'true';\n        break;\n      case 'mark': {\n        for (const cur of value.split(',')) {\n          const hyphen = cur.indexOf('-');\n          if (hyphen !== -1) {\n            let a = +cur.slice(0, hyphen);\n            let b = +cur.slice(hyphen + 1);\n            if (Number.isNaN(a) || Number.isNaN(b)) continue;\n            if (b < a) { // switch a & b\n              [a, b] = [b, a];\n            }\n\n            for (; a <= b; a++) {\n              mark.push(a);\n            }\n          }\n          if (!isNaN(Number(cur))) mark.push(+cur);\n        }\n        break;\n      }\n      case 'language_attr': {\n        language_attr = value === 'true';\n        break;\n      }\n    }\n  }\n  return {\n    options: {\n      language_attr,\n      firstLine,\n      line_number,\n      line_threshold,\n      mark,\n      wrap\n    },\n    enableHighlight,\n    _args: args.replace(rAdditionalOptions, '')\n  };\n}\n\nexport = (ctx: Hexo): (data: RenderData) => void => {\n  return function backtickCodeBlock(data: RenderData): void {\n    const dataContent = data.content;\n\n    if ((!dataContent.includes('```') && !dataContent.includes('~~~')) || !ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return;\n    // get all comment starts and ends\n    const commentStarts = [];\n    const commentEnds = [];\n    let match: RegExpExecArray | null;\n    rCommentEscape.lastIndex = 0;\n    while ((match = rCommentEscape.exec(dataContent)) !== null) {\n      commentStarts.push(match.index);\n      commentEnds.push(match.index + match[0].length);\n    }\n    // notice that commentStarts and commentEnds are sorted, and commentStarts[i] < commentEnds[i], commentEnds[i] <= commentStarts[i+1]\n    let commentIndex = 0;\n    data.content = data.content.replace(rBacktick, ($0, start, $2, _args, _content, end, matchIndex) => {\n      // get the start and end of the code block\n      const codeBlockStart = matchIndex;\n      const codeBlockEnd = matchIndex + $0.length;\n      // check if the code block is nested in a comment\n      while (commentIndex < commentStarts.length && commentEnds[commentIndex] <= codeBlockStart) {\n        commentIndex++;\n      }\n      if (commentIndex < commentStarts.length && commentStarts[commentIndex] < codeBlockStart && commentEnds[commentIndex] > codeBlockEnd) {\n        // the code block is nested in a comment, return escaped content directly\n        return escapeSwigTag($0);\n      }\n      let content = _content.replace(/\\n$/, '');\n\n      // neither highlight or prismjs is enabled, return escaped content directly.\n      if (!ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return escapeSwigTag($0);\n\n      const parsedArgs = parseArgs(_args);\n      if (!parsedArgs.enableHighlight) return escapeSwigTag($0);\n      _args = parsedArgs._args;\n\n      // Extract language and caption of code blocks\n      const args = _args.split('=').shift();\n      let lang: string, caption: string;\n\n      if (args) {\n        const match = rAllOptions.exec(args) || rLangCaption.exec(args);\n\n        if (match) {\n          lang = match[1];\n\n          if (match[2]) {\n            caption = `<span>${match[2]}</span>`;\n\n            if (match[3]) {\n              caption += `<a href=\"${match[3]}\">${match[4] ? match[4] : 'link'}</a>`;\n            }\n          }\n        }\n      }\n\n      // PR #3765\n      if (start.includes('>')) {\n        // heading of last line is already removed by the top RegExp \"rBacktick\"\n        const depth = start.split('>').length - 1;\n        const regexp = new RegExp(`^([^\\\\S\\\\r\\\\n]*>){0,${depth}}([^\\\\S\\\\r\\\\n]|$)`, 'mg');\n        content = content.replace(regexp, '');\n      }\n\n      const options: HighlightOptions = {\n        lang,\n        caption,\n        lines_length: content.split('\\n').length,\n        ...parsedArgs.options\n      };\n      // setup line number by inline\n      _args = _args.replace('=+', '=');\n\n      // setup firstLineNumber;\n      if (_args.includes('=')) {\n        options.firstLineNumber = _args.split('=')[1] || 1;\n      }\n      content = ctx.extend.highlight.exec(ctx.config.syntax_highlighter, {\n        context: ctx,\n        args: [content, options]\n      });\n\n      return start\n        + '<hexoPostRenderCodeBlock>'\n        + escapeSwigTag(content)\n        + '</hexoPostRenderCodeBlock>'\n        + end;\n    });\n  };\n};\n"
  },
  {
    "path": "lib/plugins/filter/before_post_render/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('before_post_render', require('./backtick_code_block')(ctx));\n  filter.register('before_post_render', require('./titlecase'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/before_post_render/titlecase.ts",
    "content": "import type { RenderData } from '../../../types';\n\nlet titlecase;\n\nfunction titlecaseFilter(data: RenderData): void {\n  if (!(typeof data.titlecase !== 'undefined' ? data.titlecase : this.config.titlecase) || !data.title) return;\n\n  if (!titlecase) titlecase = require('titlecase');\n  data.title = titlecase(data.title);\n}\n\nexport = titlecaseFilter;\n"
  },
  {
    "path": "lib/plugins/filter/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  require('./after_render')(ctx);\n  require('./after_post_render')(ctx);\n  require('./before_post_render')(ctx);\n  require('./before_exit')(ctx);\n  require('./before_generate')(ctx);\n  require('./template_locals')(ctx);\n\n  filter.register('new_post_path', require('./new_post_path'));\n  filter.register('post_permalink', require('./post_permalink'));\n};\n"
  },
  {
    "path": "lib/plugins/filter/new_post_path.ts",
    "content": "import { join, extname } from 'path';\nimport moment from 'moment';\nimport Promise from 'bluebird';\nimport { createSha1Hash, Permalink } from 'hexo-util';\nimport { ensurePath } from 'hexo-fs';\nimport type Hexo from '../../hexo';\nimport type { PostSchema } from '../../types';\n\nlet permalink: Permalink;\n\nconst reservedKeys = {\n  year: true,\n  month: true,\n  i_month: true,\n  day: true,\n  i_day: true,\n  title: true,\n  hash: true\n};\n\nfunction newPostPathFilter(this: Hexo, data: Partial<PostSchema> = {}, replace?: boolean): Promise<string> {\n  const sourceDir = this.source_dir;\n  const draftDir = join(sourceDir, '_drafts');\n  const postDir = join(sourceDir, '_posts');\n  const { config } = this;\n  const newPostName = config.new_post_name;\n  const permalinkDefaults = config.permalink_defaults;\n  const { path, layout, slug } = data;\n\n  if (!permalink || permalink.rule !== newPostName) {\n    permalink = new Permalink(newPostName, {});\n  }\n\n  let target = '';\n\n  if (path) {\n    switch (layout) {\n      case 'page':\n        target = join(sourceDir, path);\n        break;\n\n      case 'draft':\n        target = join(draftDir, path);\n        break;\n\n      default:\n        target = join(postDir, path);\n    }\n  } else if (slug) {\n    switch (layout) {\n      case 'page':\n        target = join(sourceDir, slug, 'index');\n        break;\n\n      case 'draft':\n        target = join(draftDir, slug);\n        break;\n\n      default: {\n        const date = moment(data.date || Date.now());\n        const keys = Object.keys(data);\n        const hash = createSha1Hash().update(slug + date.unix().toString())\n          .digest('hex').slice(0, 12);\n\n        const filenameData = {\n          year: date.format('YYYY'),\n          month: date.format('MM'),\n          i_month: date.format('M'),\n          day: date.format('DD'),\n          i_day: date.format('D'),\n          title: slug,\n          hash\n        };\n\n        for (let i = 0, len = keys.length; i < len; i++) {\n          const key = keys[i];\n          if (!reservedKeys[key]) filenameData[key] = data[key];\n        }\n\n        target = join(postDir, permalink.stringify({\n          ...permalinkDefaults,\n          ...filenameData\n        }));\n      }\n    }\n  } else {\n    return Promise.reject(new TypeError('Either data.path or data.slug is required!'));\n  }\n\n  if (!extname(target)) {\n    target += extname(newPostName) || '.md';\n  }\n\n  if (replace) {\n    return Promise.resolve(target);\n  }\n\n  return ensurePath(target);\n}\n\nexport = newPostPathFilter;\n"
  },
  {
    "path": "lib/plugins/filter/post_permalink.ts",
    "content": "import { createSha1Hash, Permalink, slugize } from 'hexo-util';\nimport { basename } from 'path';\nimport type Hexo from '../../hexo';\nimport type { PostSchema } from '../../types';\n\nlet permalink: Permalink;\n\nfunction postPermalinkFilter(this: Hexo, data: PostSchema): string {\n  const { config } = this;\n  const { id, _id, slug, title, date } = data;\n  let { __permalink } = data;\n  const { post_asset_folder } = config;\n\n  if (__permalink) {\n    if (post_asset_folder && !__permalink.endsWith('/') && !__permalink.endsWith('.html')) {\n      __permalink += '/';\n    }\n    if (!__permalink.startsWith('/')) return `/${__permalink}`;\n    return __permalink;\n  }\n\n  const hash = slug && date\n    ? createSha1Hash().update(slug + date.unix().toString()).digest('hex').slice(0, 12)\n    : null;\n  const meta = {\n    id: id || _id,\n    title: slug,\n    name: typeof slug === 'string' ? basename(slug) : '',\n    post_title: slugize(title, {transform: 1}),\n    year: date.format('YYYY'),\n    month: date.format('MM'),\n    day: date.format('DD'),\n    hour: date.format('HH'),\n    minute: date.format('mm'),\n    second: date.format('ss'),\n    i_month: date.format('M'),\n    i_day: date.format('D'),\n    timestamp: date.format('X'),\n    hash,\n    category: config.default_category\n  };\n\n  if (!permalink || permalink.rule !== config.permalink) {\n    permalink = new Permalink(config.permalink, {});\n  }\n\n  const { categories } = data;\n\n  if (categories.length) {\n    meta.category = categories.last().slug;\n  }\n\n  const keys = Object.keys(data);\n\n  for (const key of keys) {\n    if (Object.prototype.hasOwnProperty.call(meta, key)) continue;\n\n    // Use Object.getOwnPropertyDescriptor to copy getters to avoid \"Maximum call\n    // stack size exceeded\" error\n    Object.defineProperty(meta, key, Object.getOwnPropertyDescriptor(data, key));\n  }\n\n  if (config.permalink_defaults) {\n    const keys2 = Object.keys(config.permalink_defaults);\n\n    for (const key of keys2) {\n      if (Object.prototype.hasOwnProperty.call(meta, key)) continue;\n\n      meta[key] = config.permalink_defaults[key];\n    }\n  }\n\n  const permalink_stringify = permalink.stringify(meta);\n  if (post_asset_folder && !permalink_stringify.endsWith('/') && !permalink_stringify.endsWith('.html')) {\n    return `${permalink_stringify}/`;\n  }\n  return permalink_stringify;\n}\n\nexport = postPermalinkFilter;\n"
  },
  {
    "path": "lib/plugins/filter/template_locals/i18n.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport type Hexo from '../../../hexo';\nimport type { LocalsType } from '../../../types';\n\nfunction i18nLocalsFilter(this: Hexo, locals: LocalsType): void {\n  const { i18n } = this.theme;\n  const { config } = this;\n  const i18nDir = config.i18n_dir;\n  const { page } = locals;\n  let lang = page.lang || page.language;\n  const i18nLanguages = i18n.list();\n  const i18nConfigLanguages = i18n.languages;\n\n  if (!lang) {\n    const pattern = new Pattern(`${i18nDir}/*path`);\n    const data = pattern.match(locals.path);\n\n    if (data && 'lang' in data && i18nLanguages.includes(data.lang)) {\n      lang = data.lang;\n      page.canonical_path = data.path;\n    } else {\n      // i18n.languages is always an array with at least one argument ('default')\n      lang = i18nConfigLanguages[0];\n    }\n  }\n\n  page.lang = lang;\n  page.canonical_path = page.canonical_path || locals.path;\n\n  const languages = [...new Set<string>([].concat(lang, i18nConfigLanguages, i18nLanguages).filter(Boolean))];\n\n  locals.__ = i18n.__(languages);\n  locals._p = i18n._p(languages);\n}\n\nexport = i18nLocalsFilter;\n"
  },
  {
    "path": "lib/plugins/filter/template_locals/index.ts",
    "content": "import type Hexo from '../../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { filter } = ctx.extend;\n\n  filter.register('template_locals', require('./i18n'));\n};\n"
  },
  {
    "path": "lib/plugins/generator/asset.ts",
    "content": "import { exists, createReadStream } from 'hexo-fs';\nimport Promise from 'bluebird';\nimport { extname } from 'path';\nimport { magenta } from 'picocolors';\nimport type Hexo from '../../hexo';\nimport type { AssetSchema, BaseGeneratorReturn } from '../../types';\nimport type Document from 'warehouse/dist/document';\n\ninterface AssetData {\n  modified: boolean;\n  data?: () => any;\n}\n\ninterface AssetGenerator extends BaseGeneratorReturn {\n  data: {\n    modified: boolean;\n    data?: () => any;\n  }\n}\n\nconst process = (name: string, ctx: Hexo) => {\n  return Promise.filter(ctx.model(name).toArray(), (asset: Document<AssetSchema>) => exists(asset.source).tap(exist => {\n    if (!exist) return asset.remove();\n  })).map((asset: Document<AssetSchema>) => {\n    const { source } = asset;\n    let { path } = asset;\n    const data: AssetData = {\n      modified: asset.modified\n    };\n\n    if (asset.renderable && ctx.render.isRenderable(path)) {\n      // Replace extension name if the asset is renderable\n      const filename = path.substring(0, path.length - extname(path).length);\n\n      path = `${filename}.${ctx.render.getOutput(path)}`;\n\n      data.data = () => ctx.render.render({\n        path: source,\n        toString: true\n      }).catch((err: Error) => {\n        ctx.log.error({err}, 'Asset render failed: %s', magenta(path));\n      });\n    } else {\n      data.data = () => createReadStream(source);\n    }\n\n    return { path, data };\n  });\n};\n\nfunction assetGenerator(this: Hexo): Promise<AssetGenerator[]> {\n  return Promise.all([\n    process('Asset', this),\n    process('PostAsset', this)\n  ]).then(data => [].concat(...data));\n}\n\nexport = assetGenerator;\n"
  },
  {
    "path": "lib/plugins/generator/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { generator } = ctx.extend;\n\n  generator.register('asset', require('./asset'));\n  generator.register('page', require('./page'));\n  generator.register('post', require('./post'));\n};\n"
  },
  {
    "path": "lib/plugins/generator/page.ts",
    "content": "import type { BaseGeneratorReturn, PageSchema, SiteLocals } from '../../types';\nimport type Document from 'warehouse/dist/document';\n\ntype SimplePageGenerator = Omit<BaseGeneratorReturn, 'layout'> & { data: string };\ninterface NormalPageGenerator extends BaseGeneratorReturn {\n  layout: string[];\n  data: PageSchema;\n}\ntype PageGenerator = SimplePageGenerator | NormalPageGenerator;\n\nfunction pageGenerator(locals: SiteLocals): PageGenerator[] {\n  return locals.pages.map((page: Document<PageSchema> & PageSchema) => {\n    const { path, layout } = page;\n\n    if (!layout || layout === 'false' || layout === 'off') {\n      return {\n        path,\n        data: page.content\n      };\n    }\n\n    const layouts = ['page', 'post', 'index'];\n    if (layout !== 'page') layouts.unshift(layout);\n\n    page.__page = true;\n\n    return {\n      path,\n      layout: layouts,\n      data: page\n    };\n  });\n}\n\nexport = pageGenerator;\n"
  },
  {
    "path": "lib/plugins/generator/post.ts",
    "content": "import type { BaseGeneratorReturn, PostSchema, SiteLocals } from '../../types';\nimport type Document from 'warehouse/dist/document';\n\ntype SimplePostGenerator = Omit<BaseGeneratorReturn, 'layout'> & { data: string };\ninterface NormalPostGenerator extends BaseGeneratorReturn {\n  data: PostSchema | Document<PostSchema>;\n  layout: string[];\n}\ntype PostGenerator = SimplePostGenerator | NormalPostGenerator;\n\nfunction postGenerator(locals: SiteLocals): PostGenerator[] {\n  const posts = locals.posts.sort('-date').toArray();\n  const { length } = posts;\n\n  return posts.map((post: Document<PostSchema>, i: number) => {\n    const { path, layout } = post;\n\n    if (!layout || layout === 'false') {\n      return {\n        path,\n        data: post.content\n      };\n    }\n\n    if (i) post.prev = posts[i - 1];\n    if (i < length - 1) post.next = posts[i + 1];\n\n    const layouts = ['post', 'page', 'index'];\n    if (layout !== 'post') layouts.unshift(layout);\n\n    post.__post = true;\n\n    return {\n      path,\n      layout: layouts,\n      data: post\n    };\n  });\n}\n\nexport = postGenerator;\n"
  },
  {
    "path": "lib/plugins/helper/css.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType } from '../../types';\n\nlet relative_link = true;\nfunction cssHelper(this: LocalsType, ...args: any[]) {\n  let result = '\\n';\n\n  relative_link = this.config.relative_link;\n\n  args.flat(Infinity).forEach(item => {\n    if (typeof item === 'string' || item instanceof String) {\n      let path = item;\n      if (!path.endsWith('.css')) {\n        path += '.css';\n      }\n      result += `<link rel=\"stylesheet\" href=\"${url_for.call(this, path)}\">\\n`;\n    } else {\n      const newItem = {\n        rel: 'stylesheet',\n        ...item\n      };\n      // Custom attributes\n      newItem.href = url_for.call(this, newItem.href);\n      if (!newItem.href.endsWith('.css')) newItem.href += '.css';\n      result += htmlTag('link', newItem) + '\\n';\n    }\n  });\n  return result;\n}\n\nexport = moize(cssHelper, {\n  maxSize: 10,\n  isDeepEqual: true,\n  updateCacheForKey() {\n    return relative_link;\n  }\n});\n"
  },
  {
    "path": "lib/plugins/helper/date.ts",
    "content": "import moment from 'moment-timezone';\nconst { isMoment } = moment;\nimport moize from 'moize';\nimport type { LocalsType } from '../../types';\n\nconst isDate = (value: moment.MomentInput | moment.Moment): boolean =>\n  typeof value === 'object' && value instanceof Date && !isNaN(value.getTime());\n\nfunction getMoment(date: moment.MomentInput | moment.Moment, lang: string, timezone: string): moment.Moment {\n  if (date == null) date = moment();\n  if (!isMoment(date)) date = moment(isDate(date) ? <Date>date : new Date(<string | number>date));\n  lang = _toMomentLocale(lang);\n\n  if (lang) date = date.locale(lang);\n  if (timezone) date = date.tz(timezone);\n\n  return date;\n}\n\nfunction toISOString(date?: string | number | Date | moment.Moment) {\n  if (date == null) {\n    return new Date().toISOString();\n  }\n\n  if (date instanceof Date || isMoment(date)) {\n    return date.toISOString();\n  }\n\n  return new Date(date as (string | number)).toISOString();\n}\n\nfunction dateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) {\n  const { config } = this;\n  const moment = getMoment(date, getLanguage(this), config.timezone);\n  return moment.format(format || config.date_format);\n}\n\nfunction timeHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) {\n  const { config } = this;\n  const moment = getMoment(date, getLanguage(this), config.timezone);\n  return moment.format(format || config.time_format);\n}\n\nfunction fullDateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput, format?: string) {\n  if (format) {\n    const moment = getMoment(date, getLanguage(this), this.config.timezone);\n    return moment.format(format);\n  }\n\n  return `${this.date(date)} ${this.time(date)}`;\n}\n\nfunction relativeDateHelper(this: LocalsType, date?: moment.Moment | moment.MomentInput) {\n  const { config } = this;\n  const moment = getMoment(date, getLanguage(this), config.timezone);\n  return moment.fromNow();\n}\n\nfunction timeTagHelper(this: LocalsType, date?: string | number | Date | moment.Moment, format?: string) {\n  return `<time datetime=\"${toISOString(date)}\">${this.date(date, format)}</time>`;\n}\n\nfunction getLanguage(ctx: LocalsType) {\n  return ctx.page.lang || ctx.page.language || ctx.config.language;\n}\n\n/**\n * Convert Hexo language code to Moment locale code.\n * examples:\n *   default => en\n *   zh-CN => zh-cn\n *\n * Moment defined locales: https://github.com/moment/moment/tree/master/locale\n */\nfunction _toMomentLocale(lang?: string) {\n  if (lang === undefined) {\n    return undefined;\n  }\n\n  // moment.locale('') equals moment.locale('en')\n  // moment.locale(null) equals moment.locale('en')\n  if (!lang || lang === 'en' || lang === 'default') {\n    return 'en';\n  }\n  return lang.toLowerCase().replace('_', '-');\n}\n\nexport {dateHelper as date};\nexport {toISOString as date_xml};\nexport {timeHelper as time};\nexport {fullDateHelper as full_date};\nexport {relativeDateHelper as relative_date};\nexport {timeTagHelper as time_tag};\nexport {moment};\nexport const toMomentLocale = moize.shallow(_toMomentLocale);\n"
  },
  {
    "path": "lib/plugins/helper/debug.ts",
    "content": "import { inspect } from 'util';\n\n// this format object as string, resolves circular reference\nfunction inspectObject(object: any, options?: any) {\n  return inspect(object, options);\n}\n\n// wrapper to log to console\nfunction log(...args: any[]) {\n  return Reflect.apply(console.log, null, args);\n}\n\nexport {inspectObject};\nexport {log};\n"
  },
  {
    "path": "lib/plugins/helper/favicon_tag.ts",
    "content": "import { url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\nfunction faviconTagHelper(this: LocalsType, path: string) {\n  return `<link rel=\"shortcut icon\" href=\"${url_for.call(this, path)}\">`;\n}\n\nexport = faviconTagHelper;\n"
  },
  {
    "path": "lib/plugins/helper/feed_tag.ts",
    "content": "import { url_for } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType } from '../../types';\n\nconst feedFn = (str = '') => {\n  if (str) return str.replace(/2$/, '');\n  return str;\n};\n\ninterface Options {\n  title?: string;\n  type?: string | null;\n}\n\nfunction makeFeedTag(this: LocalsType, path?: string, options: Options = {}, configFeed?: any, configTitle?: string) {\n  const title = options.title || configTitle;\n\n  if (path) {\n    if (typeof path !== 'string') throw new TypeError('path must be a string!');\n\n    let type = feedFn(options.type);\n\n    if (!type) {\n      if (path.includes('atom')) type = 'atom';\n      else if (path.includes('rss')) type = 'rss';\n    }\n\n    const typeAttr = type ? `type=\"application/${type}+xml\"` : '';\n\n    return `<link rel=\"alternate\" href=\"${url_for.call(this, path)}\" title=\"${title}\" ${typeAttr}>`;\n  }\n\n  if (configFeed) {\n    if (configFeed.type && configFeed.path) {\n      if (typeof configFeed.type === 'string') {\n        return `<link rel=\"alternate\" href=\"${url_for.call(this, configFeed.path)}\" title=\"${title}\" type=\"application/${feedFn(configFeed.type)}+xml\">`;\n      }\n\n      let result = '';\n      for (const i in configFeed.type) {\n        result += `<link rel=\"alternate\" href=\"${url_for.call(this, configFeed.path[i])}\" title=\"${title}\" type=\"application/${feedFn(configFeed.type[i])}+xml\">`;\n      }\n      return result;\n    }\n  }\n\n  return '';\n}\n\nfunction feedTagHelper(this: LocalsType, path?: string, options: Options = {}) {\n  const { config } = this;\n  return moize.deep(makeFeedTag.bind(this))(path, options, (config as any).feed, config.title);\n}\n\nexport = feedTagHelper;\n"
  },
  {
    "path": "lib/plugins/helper/format.ts",
    "content": "import { stripHTML, wordWrap, truncate, escapeHTML } from 'hexo-util';\nimport titlecase from 'titlecase';\nexport {stripHTML as strip_html};\nexport {stripHTML};\n\nexport function trim(str: string) {\n  return str.trim();\n}\n\nexport {titlecase};\nexport {wordWrap as word_wrap};\nexport {wordWrap};\nexport {truncate};\nexport {escapeHTML as escape_html};\nexport {escapeHTML};\n"
  },
  {
    "path": "lib/plugins/helper/fragment_cache.ts",
    "content": "import { Cache } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const cache = new Cache();\n\n  // reset cache for watch mode\n  ctx.on('generateBefore', () => { cache.flush(); });\n\n  return function fragmentCache(id: string, fn: () => any) {\n    if (this.cache) return cache.apply(id, fn);\n\n    const result = fn();\n\n    cache.set(id, result);\n    return result;\n  };\n};\n"
  },
  {
    "path": "lib/plugins/helper/full_url_for.ts",
    "content": "\nimport { full_url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\nexport = function(this: LocalsType, path?: string) {\n  return full_url_for.call(this, path);\n}\n"
  },
  {
    "path": "lib/plugins/helper/gravatar.ts",
    "content": "import { gravatar } from 'hexo-util';\nexport = gravatar;\n"
  },
  {
    "path": "lib/plugins/helper/image_tag.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  src?: string;\n  alt?: string;\n  class?: string | string[];\n}\n\ninterface Attrs {\n  src?: string;\n  class?: string;\n  [key: string]: string | undefined;\n}\n\nfunction imageTagHelper(this: LocalsType, path: string, options: Options = {}) {\n  const attrs = Object.assign({\n    src: url_for.call(this, path) as string\n  }, options);\n\n  if (attrs.class && Array.isArray(attrs.class)) {\n    attrs.class = attrs.class.join(' ');\n  }\n\n  return htmlTag('img', attrs as Attrs);\n}\n\nexport = imageTagHelper;\n"
  },
  {
    "path": "lib/plugins/helper/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { helper } = ctx.extend;\n\n  const date = require('./date');\n\n  helper.register('date', date.date);\n  helper.register('date_xml', date.date_xml);\n  helper.register('time', date.time);\n  helper.register('full_date', date.full_date);\n  helper.register('relative_date', date.relative_date);\n  helper.register('time_tag', date.time_tag);\n  helper.register('moment', date.moment);\n\n  helper.register('search_form', require('./search_form'));\n\n  const { strip_html, trim, titlecase, word_wrap, truncate, escape_html } = require('./format');\n\n  helper.register('strip_html', strip_html);\n  helper.register('trim', trim);\n  helper.register('titlecase', titlecase);\n  helper.register('word_wrap', word_wrap);\n  helper.register('truncate', truncate);\n  helper.register('escape_html', escape_html);\n\n  helper.register('fragment_cache', require('./fragment_cache')(ctx));\n\n  helper.register('gravatar', require('./gravatar'));\n\n  const is = require('./is');\n  helper.register('is_current', is.current);\n  helper.register('is_home', is.home);\n  helper.register('is_home_first_page', is.home_first_page);\n  helper.register('is_post', is.post);\n  helper.register('is_page', is.page);\n  helper.register('is_archive', is.archive);\n  helper.register('is_year', is.year);\n  helper.register('is_month', is.month);\n  helper.register('is_category', is.category);\n  helper.register('is_tag', is.tag);\n\n  helper.register('list_archives', require('./list_archives'));\n  helper.register('list_categories', require('./list_categories'));\n  helper.register('list_tags', require('./list_tags'));\n  helper.register('list_posts', require('./list_posts'));\n\n  helper.register('meta_generator', require('./meta_generator'));\n\n  helper.register('open_graph', require('./open_graph'));\n\n  helper.register('number_format', require('./number_format'));\n\n  helper.register('paginator', require('./paginator'));\n\n  helper.register('partial', require('./partial')(ctx));\n\n  helper.register('markdown', require('./markdown'));\n  helper.register('render', require('./render')(ctx));\n\n  helper.register('css', require('./css'));\n  helper.register('js', require('./js'));\n  helper.register('link_to', require('./link_to'));\n  helper.register('mail_to', require('./mail_to'));\n  helper.register('image_tag', require('./image_tag'));\n  helper.register('favicon_tag', require('./favicon_tag'));\n  helper.register('feed_tag', require('./feed_tag'));\n\n  const tagcloud = require('./tagcloud');\n  helper.register('tagcloud', tagcloud);\n  helper.register('tag_cloud', tagcloud);\n\n  helper.register('toc', require('./toc'));\n\n  helper.register('relative_url', require('./relative_url'));\n  helper.register('url_for', require('./url_for'));\n  helper.register('full_url_for', require('./full_url_for'));\n\n  const debug = require('./debug');\n  helper.register('inspect', debug.inspectObject);\n  helper.register('log', debug.log);\n};\n"
  },
  {
    "path": "lib/plugins/helper/is.ts",
    "content": "import type { LocalsType } from '../../types';\n\nfunction isCurrentHelper(this: LocalsType, path = '/', strict: boolean) {\n  const currentPath = this.path.replace(/^[^/].*/, '/$&');\n\n  if (strict) {\n    if (path.endsWith('/')) path += 'index.html';\n    path = path.replace(/^[^/].*/, '/$&');\n\n    return currentPath === path;\n  }\n\n  path = path.replace(/\\/index\\.html$/, '/');\n\n  if (path === '/') return currentPath === '/index.html';\n\n  path = path.replace(/^[^/].*/, '/$&');\n\n  return currentPath.startsWith(path);\n}\n\nfunction isHomeHelper() {\n  return Boolean(this.page.__index);\n}\n\nfunction isHomeFirstPageHelper() {\n  return Boolean(this.page.__index) && this.page.current === 1;\n}\n\nfunction isPostHelper() {\n  return Boolean(this.page.__post);\n}\n\nfunction isPageHelper() {\n  return Boolean(this.page.__page);\n}\n\nfunction isArchiveHelper() {\n  return Boolean(this.page.archive);\n}\n\nfunction isYearHelper(year?) {\n  const { page } = this;\n  if (!page.archive) return false;\n\n  if (year) {\n    return page.year === year;\n  }\n\n  return Boolean(page.year);\n}\n\nfunction isMonthHelper(year?, month?) {\n  const { page } = this;\n  if (!page.archive) return false;\n\n  if (year) {\n    if (month) {\n      return page.year === year && page.month === month;\n    }\n\n    return page.month === year;\n  }\n\n  return Boolean(page.year && page.month);\n}\n\nfunction isCategoryHelper(category?) {\n  if (category) {\n    return this.page.category === category;\n  }\n\n  return Boolean(this.page.category);\n}\n\nfunction isTagHelper(tag?) {\n  if (tag) {\n    return this.page.tag === tag;\n  }\n\n  return Boolean(this.page.tag);\n}\n\nexport {isCurrentHelper as current};\nexport {isHomeHelper as home};\nexport {isHomeFirstPageHelper as home_first_page};\nexport {isPostHelper as post};\nexport {isPageHelper as page};\nexport {isArchiveHelper as archive};\nexport {isYearHelper as year};\nexport {isMonthHelper as month};\nexport {isCategoryHelper as category};\nexport {isTagHelper as tag};\n"
  },
  {
    "path": "lib/plugins/helper/js.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType } from '../../types';\n\nlet relative_link = true;\nfunction jsHelper(this: LocalsType, ...args: any[]) {\n  let result = '\\n';\n\n  relative_link = this.config.relative_link;\n\n  args.flat(Infinity).forEach(item => {\n    if (typeof item === 'string' || item instanceof String) {\n      let path = item;\n      if (!path.endsWith('.js')) {\n        path += '.js';\n      }\n      result += `<script src=\"${url_for.call(this, path)}\"></script>\\n`;\n    } else {\n      const newItem = { ...item };\n      // Custom attributes\n      newItem.src = url_for.call(this, newItem.src);\n      if (!newItem.src.endsWith('.js')) newItem.src += '.js';\n      result += htmlTag('script', newItem, '') + '\\n';\n    }\n  });\n  return result;\n}\n\nexport = moize(jsHelper, {\n  maxSize: 10,\n  isDeepEqual: true,\n  updateCacheForKey() {\n    return relative_link;\n  }\n});\n"
  },
  {
    "path": "lib/plugins/helper/link_to.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  id?: string;\n  href?: string;\n  title?: string;\n  external?: boolean | null;\n  class?: string | string[];\n  target?: string;\n  rel?: string;\n}\n\ninterface Attrs {\n  href: string;\n  title: string;\n  external?: boolean | null;\n  class?: string;\n  target?: string;\n  rel?: string;\n  [key: string]: string | boolean | null | undefined;\n}\n\nfunction linkToHelper(this: LocalsType, path: string, text?: string, options: Options | boolean = {}) {\n  if (typeof options === 'boolean') options = {external: options};\n\n  if (!text) text = path.replace(/^https?:\\/\\/|\\/$/g, '');\n\n  const attrs = Object.assign({\n    href: url_for.call(this, path) as string,\n    title: text\n  }, options);\n\n  if (attrs.external) {\n    attrs.target = '_blank';\n    attrs.rel = 'noopener';\n    attrs.external = null;\n  }\n\n  if (attrs.class && Array.isArray(attrs.class)) {\n    attrs.class = attrs.class.join(' ');\n  }\n\n  return htmlTag('a', attrs as Attrs, text);\n}\n\nexport = linkToHelper;\n"
  },
  {
    "path": "lib/plugins/helper/list_archives.ts",
    "content": "import type Query from 'warehouse/dist/query';\nimport type { LocalsType, PostSchema } from '../../types';\nimport { toMomentLocale } from './date';\nimport { url_for, Cache } from 'hexo-util';\n\ninterface Options {\n  format?: string;\n  type?: string;\n  style?: string | false;\n  transform?: (name: string) => string;\n  separator?: string;\n  show_count?: boolean;\n  class?: string;\n  order?: number;\n}\n\ninterface Data {\n  name: string;\n  year: number;\n  month: number;\n  count: number;\n}\n\nconst postsCache = new Cache();\n\nfunction listArchivesHelper(this: LocalsType, options: Options = {}) {\n  const { config } = this;\n  const archiveDir = config.archive_dir;\n  const { timezone } = config;\n  const lang = toMomentLocale(this.page.lang || this.page.language || config.language);\n  let { format } = options;\n  const type = options.type || 'monthly';\n  const { style = 'list', transform, separator = ', ' } = options;\n  const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true;\n  const className = options.class || 'archive';\n  const order = options.order || -1;\n  const compareFunc = type === 'monthly'\n    ? (yearA, monthA, yearB, monthB) => yearA === yearB && monthA === monthB\n    : (yearA, _monthA, yearB, _monthB) => yearA === yearB;\n\n  let result = '';\n\n  if (!format) {\n    format = type === 'monthly' ? 'MMMM YYYY' : 'YYYY';\n  }\n\n  const posts = config.relative_link ? postsCache.apply(`date-${order}`, () => this.site.posts.sort('date', order)) as Query<PostSchema> : this.site.posts.sort('date', order);\n  if (!posts.length) return result;\n\n  const data: Data[] = [];\n  let length = 0;\n\n  posts.forEach(post => {\n    // Clone the date object to avoid pollution\n    let date = post.date.clone();\n\n    if (timezone) date = date.tz(timezone);\n\n    const year = date.year();\n    const month = date.month() + 1;\n    const lastData = data[length - 1];\n\n    if (!lastData || !compareFunc(lastData.year, lastData.month, year, month)) {\n      if (lang) date = date.locale(lang);\n      const name = date.format(format);\n      length = data.push({\n        name,\n        year,\n        month,\n        count: 1\n      });\n    } else {\n      lastData.count++;\n    }\n  });\n\n  const link = item => {\n    let url = `${archiveDir}/${item.year}/`;\n\n    if (type === 'monthly') {\n      if (item.month < 10) url += '0';\n      url += `${item.month}/`;\n    }\n\n    return url_for.call(this, url);\n  };\n\n  if (style === 'list') {\n    result += `<ul class=\"${className}-list\">`;\n\n    for (let i = 0, len = data.length; i < len; i++) {\n      const item = data[i];\n\n      result += `<li class=\"${className}-list-item\">`;\n\n      result += `<a class=\"${className}-list-link\" href=\"${link(item)}\">`;\n      result += transform ? transform(item.name) : item.name;\n      result += '</a>';\n\n      if (showCount) {\n        result += `<span class=\"${className}-list-count\">${item.count}</span>`;\n      }\n\n      result += '</li>';\n    }\n\n    result += '</ul>';\n  } else {\n    for (let i = 0, len = data.length; i < len; i++) {\n      const item = data[i];\n\n      if (i) result += separator;\n\n      result += `<a class=\"${className}-link\" href=\"${link(item)}\">`;\n      result += transform ? transform(item.name) : item.name;\n\n      if (showCount) {\n        result += `<span class=\"${className}-count\">${item.count}</span>`;\n      }\n\n      result += '</a>';\n    }\n  }\n\n  return result;\n}\n\nexport = listArchivesHelper;\n"
  },
  {
    "path": "lib/plugins/helper/list_categories.ts",
    "content": "import { url_for } from 'hexo-util';\nimport type { CategorySchema, LocalsType } from '../../types';\nimport type Query from 'warehouse/dist/query';\nimport type Document from 'warehouse/dist/document';\n\ninterface Options {\n  style?: string | false;\n  class?: string;\n  depth?: number | string;\n  orderby?: string;\n  order?: number;\n  show_count?: boolean;\n  show_current?: boolean;\n  transform?: (name: string) => string;\n  separator?: string;\n  suffix?: string;\n  children_indicator?: string | boolean;\n}\n\nfunction listCategoriesHelper(this: LocalsType, categories?: Query<CategorySchema> | Options, options?: Options) {\n  if (!options && (!categories || !Object.prototype.hasOwnProperty.call(categories, 'length'))) {\n    options = categories as Options;\n    categories = this.site.categories;\n  }\n  categories = categories as Query<CategorySchema>;\n\n  if (!categories || !categories.length) return '';\n  options = options || {};\n\n  const { style = 'list', transform, separator = ', ', suffix = '' } = options;\n  const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true;\n  const className = options.class || 'category';\n  const depth = options.depth ? parseInt(String(options.depth), 10) : 0;\n  const orderby = options.orderby || 'name';\n  const order = options.order || 1;\n  const showCurrent = options.show_current || false;\n  const childrenIndicator = Object.prototype.hasOwnProperty.call(options, 'children_indicator') ? options.children_indicator : false;\n\n  const prepareQuery = parent => {\n    const query: { parent?: any } = {};\n\n    if (parent) {\n      query.parent = parent;\n    } else {\n      query.parent = {$exists: false};\n    }\n\n    return (categories as Query<CategorySchema>).find(query).sort(orderby, order);\n  };\n\n  const hierarchicalList = (level: number, parent?: any) => {\n    let result = '';\n\n    prepareQuery(parent).forEach((cat: Document<CategorySchema> & CategorySchema) => {\n      let child;\n      if (!depth || level + 1 < depth) {\n        child = hierarchicalList(level + 1, cat._id);\n      }\n\n      let isCurrent = false;\n      if (showCurrent && this.page) {\n        for (let j = 0; j < cat.length; j++) {\n          const post = cat.posts.data[j];\n          if (post && post._id === this.page._id) {\n            isCurrent = true;\n            break;\n          }\n        }\n\n        // special case: category page\n        isCurrent = isCurrent || (this.page.base && this.page.base.startsWith(cat.path));\n      }\n\n      const additionalClassName = child && childrenIndicator ? ` ${childrenIndicator}` : '';\n\n      result += `<li class=\"${className}-list-item${additionalClassName}\">`;\n\n      result += `<a class=\"${className}-list-link${isCurrent ? ' current' : ''}\" href=\"${url_for.call(this, cat.path)}${suffix}\">`;\n      result += transform ? transform(cat.name) : cat.name;\n      result += '</a>';\n\n      if (showCount) {\n        result += `<span class=\"${className}-list-count\">${cat.length}</span>`;\n      }\n\n      if (child) {\n        result += `<ul class=\"${className}-list-child\">${child}</ul>`;\n      }\n\n      result += '</li>';\n    });\n\n    return result;\n  };\n\n  const flatList = (level: number, parent?: any) => {\n    let result = '';\n\n    prepareQuery(parent).forEach((cat, i) => {\n      if (i || level) result += separator;\n\n      result += `<a class=\"${className}-link\" href=\"${url_for.call(this, cat.path)}${suffix}\">`;\n      result += transform ? transform(cat.name) : cat.name;\n\n      if (showCount) {\n        result += `<span class=\"${className}-count\">${cat.length}</span>`;\n      }\n\n      result += '</a>';\n\n      if (!depth || level + 1 < depth) {\n        result += flatList(level + 1, cat._id);\n      }\n    });\n\n    return result;\n  };\n\n  if (style === 'list') {\n    return `<ul class=\"${className}-list\">${hierarchicalList(0)}</ul>`;\n  }\n\n  return flatList(0);\n}\n\nexport = listCategoriesHelper;\n"
  },
  {
    "path": "lib/plugins/helper/list_posts.ts",
    "content": "import { url_for } from 'hexo-util';\nimport type { LocalsType, PostSchema } from '../../types';\nimport type Query from 'warehouse/dist/query';\n\ninterface Options {\n  style?: string | false;\n  class?: string;\n  amount?: number;\n  orderby?: string;\n  order?: number;\n  transform?: (name: string) => string;\n  separator?: string;\n}\n\nfunction listPostsHelper(this: LocalsType, posts?: Query<PostSchema> | Options, options?: Options) {\n  if (!options && (!posts || !Object.prototype.hasOwnProperty.call(posts, 'length'))) {\n    options = posts as Options;\n    posts = this.site.posts;\n  }\n\n  posts = posts as Query<PostSchema>;\n\n  options = options || {};\n\n  const { style = 'list', transform, separator = ', ' } = options;\n  const orderby = options.orderby || 'date';\n  const order = options.order || -1;\n  const className = options.class || 'post';\n  const amount = options.amount || 6;\n\n  // Sort the posts\n  posts = posts.sort(orderby, order);\n\n  // Limit the number of posts\n  if (amount) posts = posts.limit(amount);\n\n  let result = '';\n\n  if (style === 'list') {\n    result += `<ul class=\"${className}-list\">`;\n\n    posts.forEach(post => {\n      const title = post.title || post.slug;\n\n      result += `<li class=\"${className}-list-item\">`;\n\n      result += `<a class=\"${className}-list-link\" href=\"${url_for.call(this, post.path)}\">`;\n      result += transform ? transform(title) : title;\n      result += '</a>';\n\n      result += '</li>';\n    });\n\n    result += '</ul>';\n  } else {\n    posts.forEach((post, i) => {\n      if (i) result += separator;\n\n      const title = post.title || post.slug;\n\n      result += `<a class=\"${className}-link\" href=\"${url_for.call(this, post.path)}\">`;\n      result += transform ? transform(title) : title;\n      result += '</a>';\n    });\n  }\n\n  return result;\n}\n\nexport = listPostsHelper;\n"
  },
  {
    "path": "lib/plugins/helper/list_tags.ts",
    "content": "import { url_for, escapeHTML } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType, TagSchema } from '../../types';\nimport type Query from 'warehouse/dist/query';\n\ninterface Options {\n  style?: string | false;\n  class?: any;\n  amount?: number;\n  orderby?: string;\n  order?: number;\n  transform?: (name: string) => string;\n  separator?: string;\n  show_count?: boolean;\n  suffix?: string;\n}\n\nfunction listTagsHelper(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) {\n  if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {\n    options = tags as Options;\n    tags = this.site.tags;\n  }\n  tags = tags as Query<TagSchema>;\n\n  if (!tags || !tags.length) return '';\n  options = options || {};\n\n  const { style = 'list', transform, separator = ', ', suffix = '' } = options;\n  const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true;\n  const classStyle = typeof style === 'string' ? `-${style}` : '';\n  let className, ulClass, liClass, aClass, labelClass, countClass, labelSpan;\n  if (typeof options.class !== 'undefined') {\n    if (typeof options.class === 'string') {\n      className = options.class;\n    } else {\n      className = 'tag';\n    }\n\n    ulClass = options.class.ul || `${className}${classStyle}`;\n    liClass = options.class.li || `${className}${classStyle}-item`;\n    aClass = options.class.a || `${className}${classStyle}-link`;\n    labelClass = options.class.label || `${className}${classStyle}-label`;\n    countClass = options.class.count || `${className}${classStyle}-count`;\n\n    labelSpan = Object.prototype.hasOwnProperty.call(options.class, 'label');\n  } else {\n    className = 'tag';\n    ulClass = `${className}${classStyle}`;\n    liClass = `${className}${classStyle}-item`;\n    aClass = `${className}${classStyle}-link`;\n    labelClass = `${className}${classStyle}-label`;\n    countClass = `${className}${classStyle}-count`;\n\n    labelSpan = false;\n  }\n  const orderby = options.orderby || 'name';\n  const order = options.order || 1;\n  let result = '';\n\n  // Sort the tags\n  tags = tags.sort(orderby, order);\n\n  // Limit the number of tags\n  if (options.amount) tags = tags.limit(options.amount);\n\n  if (style === 'list') {\n    result += `<ul class=\"${ulClass}\" itemprop=\"keywords\">`;\n\n    tags.forEach(tag => {\n      result += `<li class=\"${liClass}\">`;\n\n      result += `<a class=\"${aClass}\" href=\"${url_for.call(this, tag.path)}${suffix}\" rel=\"tag\">`;\n      result += transform ? transform(tag.name) : escapeHTML(tag.name);\n      result += '</a>';\n\n      if (showCount) {\n        result += `<span class=\"${countClass}\">${tag.length}</span>`;\n      }\n\n      result += '</li>';\n    });\n\n    result += '</ul>';\n  } else {\n    tags.forEach((tag, i) => {\n      if (i) result += separator;\n\n      result += `<a class=\"${aClass}\" href=\"${url_for.call(this, tag.path)}${suffix}\" rel=\"tag\">`;\n      if (labelSpan) {\n        result += `<span class=\"${labelClass}\">${transform ? transform(tag.name) : tag.name}</span>`;\n      } else {\n        result += transform ? transform(tag.name) : tag.name;\n      }\n\n      if (showCount) {\n        result += `<span class=\"${countClass}\">${tag.length}</span>`;\n      }\n\n      result += '</a>';\n    });\n  }\n\n  return result;\n}\n\nfunction listTagsHelperFactory(tags?: Query<TagSchema> | Options, options?: Options) {\n  const transformArgs = () => {\n    if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {\n      options = tags as Options;\n      tags = this.site.tags;\n    }\n    tags = tags as Query<TagSchema>;\n\n    return [tags.toArray(), options];\n  };\n\n  return moize(listTagsHelper.bind(this), {\n    maxSize: 5,\n    isDeepEqual: true,\n    transformArgs\n  }).call(this, tags, options);\n}\n\nexport = listTagsHelperFactory;\n"
  },
  {
    "path": "lib/plugins/helper/mail_to.ts",
    "content": "import { htmlTag } from 'hexo-util';\nimport moize from 'moize';\n\ninterface Options {\n  href?: string;\n  title?: string;\n  class?: string | string[];\n  subject?: string;\n  cc?: string | string[];\n  bcc?: string | string[];\n  id?: string;\n  body?: string;\n}\n\ninterface Attrs {\n  href: string;\n  title: string;\n  class?: string;\n  subject?: string;\n  cc?: string;\n  bcc?: string;\n  id?: string;\n  body?: string;\n  [key: string]: any;\n}\n\nfunction mailToHelper(path: string | string[], text?: string, options: Options = {}) {\n  if (Array.isArray(path)) path = path.join(',');\n  if (!text) text = path;\n\n  const attrs = Object.assign({\n    href: `mailto:${path}`,\n    title: text\n  }, options);\n\n  if (attrs.class && Array.isArray(attrs.class)) {\n    attrs.class = attrs.class.join(' ');\n  }\n\n  const data = {};\n\n  ['subject', 'cc', 'bcc', 'body'].forEach(i => {\n    const item = attrs[i];\n\n    if (item) {\n      data[i] = Array.isArray(item) ? item.join(',') : item;\n      attrs[i] = null;\n    }\n  });\n\n  const querystring = new URLSearchParams(data).toString();\n  if (querystring) attrs.href += `?${querystring}`;\n\n  return htmlTag('a', attrs as Attrs, text);\n}\n\nexport = moize(mailToHelper, {\n  maxSize: 10,\n  isDeepEqual: true\n});\n"
  },
  {
    "path": "lib/plugins/helper/markdown.ts",
    "content": "import type { LocalsType } from '../../types';\n\nfunction markdownHelper(this: LocalsType, text: string, options?: any) {\n  return this.render(text, 'markdown', options);\n}\n\nexport = markdownHelper;\n"
  },
  {
    "path": "lib/plugins/helper/meta_generator.ts",
    "content": "import type { LocalsType } from '../../types';\n\nfunction metaGeneratorHelper(this: LocalsType) {\n  return `<meta name=\"generator\" content=\"Hexo ${this.env.version}\">`;\n}\n\nexport = metaGeneratorHelper;\n"
  },
  {
    "path": "lib/plugins/helper/number_format.ts",
    "content": "interface Options {\n  delimiter?: string;\n  separator?: string;\n  precision?: number | false;\n}\n\nfunction numberFormatHelper(num: number, options: Options = {}) {\n  const split = num.toString().split('.');\n  let before = split.shift();\n  let after = split.length ? split[0] : '';\n  const delimiter = options.delimiter || ',';\n  const separator = options.separator || '.';\n  const { precision } = options;\n\n  if (delimiter) {\n    const beforeArr: string[] = [];\n    const beforeLength = before.length;\n    const beforeFirst = beforeLength % 3;\n\n    if (beforeFirst) beforeArr.push(before.slice(0, beforeFirst));\n\n    for (let i = beforeFirst; i < beforeLength; i += 3) {\n      beforeArr.push(before.slice(i, i + 3));\n    }\n\n    before = beforeArr.join(delimiter);\n  }\n\n  if (precision) {\n    const afterLength = after.length;\n    let afterResult = '';\n\n    if (afterLength > precision) {\n      const afterLast = after[precision];\n      const last = parseInt(after[precision - 1], 10);\n\n      afterResult = after.substring(0, precision - 1) + (Number(afterLast) < 5 ? last : last + 1);\n    } else {\n      afterResult = after;\n      for (let i = 0, len = precision - afterLength; i < len; i++) {\n        afterResult += '0';\n      }\n    }\n\n    after = afterResult;\n  } else if (precision === 0) {\n    after = '';\n  }\n\n  return before + (after ? separator + after : '');\n}\n\nexport = numberFormatHelper;\n"
  },
  {
    "path": "lib/plugins/helper/open_graph.ts",
    "content": "import { isMoment, isDate, Moment } from 'moment';\nimport { encodeURL, prettyUrls, stripHTML, escapeHTML } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType } from '../../types';\n\nconst localeMap = {\n  'en': 'en_US',\n  'de': 'de_DE',\n  'es': 'es_ES',\n  'fr': 'fr_FR',\n  'hu': 'hu_HU',\n  'id': 'id_ID',\n  'it': 'it_IT',\n  'ja': 'ja_JP',\n  'ko': 'ko_KR',\n  'nl': 'nl_NL',\n  'ru': 'ru_RU',\n  'th': 'th_TH',\n  'tr': 'tr_TR',\n  'vi': 'vi_VN'\n};\n\nconst localeToTerritory = moize.shallow(str => {\n  if (str.length === 2 && localeMap[str]) return localeMap[str];\n\n  if (str.length === 5) {\n    let territory = [];\n    if (str.includes('-')) {\n      territory = str.split('-');\n    } else {\n      territory = str.split('_');\n    }\n\n    if (territory.length === 2) return territory[0].toLowerCase() + '_' + territory[1].toUpperCase();\n  }\n});\n\nconst meta = (name: string, content: string | URL, escape?: boolean) => {\n  if (escape !== false && typeof content === 'string') {\n    content = escapeHTML(content);\n  }\n\n  if (content) return `<meta name=\"${name}\" content=\"${content}\">\\n`;\n  return `<meta name=\"${name}\">\\n`;\n};\n\nconst og = (name: string, content?: string, escape?: boolean) => {\n  if (escape !== false && typeof content === 'string') {\n    content = escapeHTML(content);\n  }\n\n  if (content) return `<meta property=\"${name}\" content=\"${content}\">\\n`;\n  return `<meta property=\"${name}\">\\n`;\n};\n\ninterface Options {\n  image?: string;\n  images?: string[];\n  description?: string;\n  title?: string;\n  type?: string;\n  url?: string;\n  site_name?: string;\n  twitter_card?: string;\n  date?: Moment | Date | false;\n  updated?: Moment | Date | false;\n  language?: string;\n  author?: string;\n  twitter_image?: string;\n  twitter_id?: string;\n  twitter_site?: string;\n  fb_admins?: string;\n  fb_app_id?: string;\n}\n\nfunction openGraphHelper(this: LocalsType, options: Options = {}) {\n  const { config, page } = this;\n  const { content } = page;\n  let images = options.image || options.images || page.photos || [];\n  let description = options.description || page.description || page.excerpt || content || config.description;\n  let keywords = (page.tags && page.tags.length ? page.tags : undefined) || (config as any).keywords || false;\n  const title = options.title || page.title || config.title;\n  const type = options.type || (this.is_post() ? 'article' : 'website');\n  const url = prettyUrls(options.url || this.url, config.pretty_urls);\n  const siteName = options.site_name || config.title;\n  const twitterCard = options.twitter_card || 'summary';\n  const date = options.date !== false ? options.date || page.date : false;\n  const updated = options.updated !== false ? options.updated || page.updated : false;\n  const language = options.language || page.lang || page.language || config.language;\n  const author = options.author || config.author;\n\n  if (!Array.isArray(images)) images = [images];\n\n  if (description) {\n    description = escapeHTML(stripHTML(description).substring(0, 200)\n      .trim() // Remove prefixing/trailing spaces\n    ).replace(/\\n/g, ' '); // Replace new lines by spaces\n  }\n\n  if (!images.length && content) {\n    images = images.slice();\n\n    if (content.includes('<img')) {\n      let img;\n      const imgPattern = /<img [^>]*src=['\"]([^'\"]+)([^>]*>)/gi;\n      while ((img = imgPattern.exec(content)) !== null) {\n        images.push(img[1]);\n      }\n    }\n\n  }\n\n  let result = '';\n\n  if (description) {\n    result += meta('description', description);\n  }\n\n  result += og('og:type', type);\n  result += og('og:title', title);\n\n  if (url) {\n    result += og('og:url', encodeURL(url), false);\n  } else {\n    result += og('og:url');\n  }\n\n  result += og('og:site_name', siteName);\n  if (description) {\n    result += og('og:description', description, false);\n  }\n\n  if (language) {\n    result += og('og:locale', localeToTerritory(language), false);\n  }\n\n  images = images.map(path => new URL(path, url || config.url).toString())\n    .filter(url => !url.startsWith('data:'));\n\n  images.forEach(path => {\n    result += og('og:image', path, false);\n  });\n\n  if (date) {\n    if ((isMoment(date) || isDate(date)) && !isNaN(date.valueOf())) {\n      result += og('article:published_time', date.toISOString());\n    }\n  }\n\n  if (updated) {\n    if ((isMoment(updated) || isDate(updated)) && !isNaN(updated.valueOf())) {\n      result += og('article:modified_time', updated.toISOString());\n    }\n  }\n\n  if (author) {\n    result += og('article:author', author);\n  }\n\n  if (keywords) {\n    if (typeof keywords === 'string') keywords = [keywords];\n\n    keywords.map(tag => {\n      return tag.name ? tag.name : tag;\n    }).filter(Boolean).sort().forEach(keyword => {\n      result += og('article:tag', keyword);\n    });\n  }\n\n  result += meta('twitter:card', twitterCard);\n\n  if (options.twitter_image) {\n    let twitter_image: string | URL = options.twitter_image;\n    twitter_image = new URL(twitter_image, url || config.url);\n    result += meta('twitter:image', twitter_image, false);\n  } else if (images.length) {\n    result += meta('twitter:image', images[0], false);\n  }\n\n  if (options.twitter_id) {\n    let twitterId = options.twitter_id;\n    if (!twitterId.startsWith('@')) twitterId = `@${twitterId}`;\n\n    result += meta('twitter:creator', twitterId);\n  }\n\n  if (options.twitter_site) {\n    result += meta('twitter:site', options.twitter_site, false);\n  }\n\n  if (options.fb_admins) {\n    result += og('fb:admins', options.fb_admins);\n  }\n\n  if (options.fb_app_id) {\n    result += og('fb:app_id', options.fb_app_id);\n  }\n\n  return result.trim();\n}\n\nexport = openGraphHelper;\n"
  },
  {
    "path": "lib/plugins/helper/paginator.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  base?: string;\n  current?: number;\n  format?: string;\n  total?: number;\n  end_size?: number;\n  mid_size?: number;\n  space?: string;\n  next_text?: string;\n  prev_text?: string;\n  prev_next?: boolean;\n  escape?: boolean;\n  page_class?: string;\n  current_class?: string;\n  space_class?: string;\n  prev_class?: string;\n  next_class?: string;\n  force_prev_next?: boolean;\n  show_all?: boolean;\n  transform?: (i: number) => any;\n}\n\nconst createLink = (options: Options, ctx: LocalsType) => {\n  const { base, format } = options;\n\n  return (i: number) => url_for.call(ctx, i === 1 ? base : base + format.replace('%d', String(i)));\n};\n\nconst createPageTag = (options: Options, ctx: LocalsType) => {\n  const link = createLink(options, ctx);\n  const {\n    current,\n    escape,\n    transform,\n    page_class: pageClass,\n    current_class: currentClass\n  } = options;\n\n  return (i: number) => {\n    if (i === current) {\n      return htmlTag('span', { class: pageClass + ' ' + currentClass }, transform ? transform(i) : i, escape);\n    }\n    return htmlTag('a', { class: pageClass, href: link(i) }, transform ? transform(i) : i, escape);\n  };\n};\n\nconst showAll = (tags: string[], options: Options, ctx: LocalsType) => {\n  const { total } = options;\n\n  const pageLink = createPageTag(options, ctx);\n\n  for (let i = 1; i <= total; i++) {\n    tags.push(pageLink(i));\n  }\n};\n\nconst paginationPartShow = (tags, options, ctx: LocalsType) => {\n  const {\n    current,\n    total,\n    space,\n    end_size: endSize,\n    mid_size: midSize,\n    space_class: spaceClass\n  } = options;\n\n  const leftEnd = Math.min(endSize, current - 1);\n  const rightEnd = Math.max(total - endSize + 1, current + 1);\n  const leftMid = Math.max(leftEnd + 1, current - midSize);\n  const rightMid = Math.min(rightEnd - 1, current + midSize);\n  const spaceHtml = htmlTag('span', { class: spaceClass }, space, false);\n\n  const pageTag = createPageTag(options, ctx);\n\n  // Display pages on the left edge\n  for (let i = 1; i <= leftEnd; i++) {\n    tags.push(pageTag(i));\n  }\n\n  // Display spaces between edges and middle pages\n  if (space && leftMid - leftEnd > 1) {\n    tags.push(spaceHtml);\n  }\n\n  // Display left middle pages\n  for (let i = leftMid; i < current; i++) {\n    tags.push(pageTag(i));\n  }\n\n  // Display the current page\n  tags.push(pageTag(current));\n\n  // Display right middle pages\n  for (let i = current + 1; i <= rightMid; i++) {\n    tags.push(pageTag(i));\n  }\n\n  // Display spaces between edges and middle pages\n  if (space && rightEnd - rightMid > 1) {\n    tags.push(spaceHtml);\n  }\n\n  // Display pages on the right edge\n  for (let i = rightEnd; i <= total; i++) {\n    tags.push(pageTag(i));\n  }\n};\n\nfunction paginatorHelper(this: LocalsType, options: Options = {}) {\n  options = Object.assign({\n    base: this.page.base || '',\n    current: this.page.current || 0,\n    format: `${this.config.pagination_dir}/%d/`,\n    total: this.page.total || 1,\n    end_size: 1,\n    mid_size: 2,\n    space: '&hellip;',\n    next_text: 'Next',\n    prev_text: 'Prev',\n    prev_next: true,\n    escape: true,\n    page_class: 'page-number',\n    current_class: 'current',\n    space_class: 'space',\n    prev_class: 'extend prev',\n    next_class: 'extend next',\n    force_prev_next: false\n  }, options);\n\n  const {\n    current,\n    total,\n    prev_text: prevText,\n    next_text: nextText,\n    prev_next: prevNext,\n    escape,\n    prev_class: prevClass,\n    next_class: nextClass,\n    force_prev_next: forcePrevNext\n  } = options;\n\n  if (!current) return '';\n\n  const link = createLink(options, this);\n\n  const tags = [];\n\n  // Display the link to the previous page\n  if (prevNext && current > 1) {\n    tags.push(htmlTag('a', { class: prevClass, rel: 'prev', href: link(current - 1)}, prevText, escape));\n  } else if (forcePrevNext) {\n    tags.push(htmlTag('span', { class: prevClass, rel: 'prev' }, prevText, escape));\n  }\n\n  if (options.show_all) {\n    showAll(tags, options, this);\n  } else {\n    paginationPartShow(tags, options, this);\n  }\n\n  // Display the link to the next page\n  if (prevNext && current < total) {\n    tags.push(htmlTag('a', { class: nextClass, rel: 'next', href: link(current + 1) }, nextText, escape));\n  } else if (forcePrevNext) {\n    tags.push(htmlTag('span', { class: nextClass, rel: 'next' }, nextText, escape));\n  }\n\n  return tags.join('');\n}\n\nexport = paginatorHelper;\n"
  },
  {
    "path": "lib/plugins/helper/partial.ts",
    "content": "import { dirname, join } from 'path';\nimport type Hexo from '../../hexo';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  cache?: boolean | string;\n  only?: boolean;\n}\n\nexport = (ctx: Hexo) => function partial(this: LocalsType, name: string, locals?: any, options: Options = {}) {\n  if (typeof name !== 'string') throw new TypeError('name must be a string!');\n\n  const { cache } = options;\n  const viewDir = this.view_dir;\n  const currentView = this.filename.substring(viewDir.length);\n  const path = join(dirname(currentView), name);\n  const view = ctx.theme.getView(path) || ctx.theme.getView(name);\n  const viewLocals: Record<string, any> = {};\n\n  if (!view) {\n    throw new Error(`Partial ${name} does not exist. (in ${currentView})`);\n  }\n\n  if (options.only) {\n    Object.assign(viewLocals, locals);\n  } else {\n    Object.assign(viewLocals, this, locals);\n  }\n\n  // Partial don't need layout\n  viewLocals.layout = false;\n\n  if (cache) {\n    const cacheId = typeof cache === 'string' ? cache : view.path;\n\n    return this.fragment_cache(cacheId, () => view.renderSync(viewLocals));\n  }\n\n  return view.renderSync(viewLocals);\n};\n"
  },
  {
    "path": "lib/plugins/helper/relative_url.ts",
    "content": "import { relative_url } from 'hexo-util';\n\nexport = function(from: string, to: string) {\n  return relative_url(from, to);\n}\n"
  },
  {
    "path": "lib/plugins/helper/render.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => function render(text: string, engine: string, options:object = {}) {\n  return ctx.render.renderSync({\n    text,\n    engine\n  }, options);\n};\n"
  },
  {
    "path": "lib/plugins/helper/search_form.ts",
    "content": "import moize from 'moize';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  class?: string;\n  text?: string | null;\n  button?: string | boolean;\n}\n\nfunction searchFormHelper(this: LocalsType, options: Options = {}) {\n  const { config } = this;\n  const className = options.class || 'search-form';\n  const { text = 'Search', button } = options;\n\n  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>`;\n}\n\nexport = moize.deep(searchFormHelper);\n"
  },
  {
    "path": "lib/plugins/helper/tagcloud.ts",
    "content": "import { Color, url_for } from 'hexo-util';\nimport moize from 'moize';\nimport type { LocalsType, TagSchema } from '../../types';\nimport type Query from 'warehouse/dist/query';\n\ninterface Options {\n  min_font?: number;\n  max_font?: number;\n  orderby?: string;\n  order?: number;\n  unit?: string;\n  color?: boolean;\n  class?: string;\n  show_count?: boolean;\n  count_class?: string;\n  level?: number;\n  transform?: (name: string) => string;\n  separator?: string;\n  amount?: number;\n  start_color?: string;\n  end_color?: string;\n}\n\nfunction tagcloudHelper(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) {\n  if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {\n    options = tags as Options;\n    tags = this.site.tags;\n  }\n  tags = tags as Query<TagSchema>;\n\n  if (!tags || !tags.length) return '';\n  options = options || {};\n\n  const min = options.min_font || 10;\n  const max = options.max_font || 20;\n  const orderby = options.orderby || 'name';\n  const order = options.order || 1;\n  const unit = options.unit || 'px';\n  const color = options.color;\n  const className = options.class;\n  const showCount = options.show_count;\n  const countClassName = options.count_class || 'count';\n  const level = options.level || 10;\n  const { transform } = options;\n  const separator = options.separator || ' ';\n  const result = [];\n  let startColor, endColor;\n\n  if (color) {\n    if (!options.start_color) throw new TypeError('start_color is required!');\n    if (!options.end_color) throw new TypeError('end_color is required!');\n\n    startColor = new Color(options.start_color);\n    endColor = new Color(options.end_color);\n  }\n\n  // Sort the tags\n  if (orderby === 'random' || orderby === 'rand') {\n    tags = tags.random();\n  } else {\n    tags = tags.sort(orderby, order);\n  }\n\n  // Limit the number of tags\n  if (options.amount) {\n    tags = tags.limit(options.amount);\n  }\n\n  const sizes = [];\n\n  tags.sort('length').forEach(tag => {\n    const { length } = tag;\n    if (sizes.includes(length)) return;\n\n    sizes.push(length);\n  });\n\n  const length = sizes.length - 1;\n\n  tags.forEach(tag => {\n    const ratio = length ? sizes.indexOf(tag.length) / length : 0;\n    const size = min + ((max - min) * ratio);\n    let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};`;\n    const attr = className ? ` class=\"${className}-${Math.round(ratio * level)}\"` : '';\n\n    if (color) {\n      const midColor = startColor.mix(endColor, ratio);\n      style += ` color: ${midColor.toString()}`;\n    }\n\n    result.push(\n      `<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>`\n    );\n  });\n\n  return result.join(separator);\n}\n\nfunction tagcloudHelperFactory(this: LocalsType, tags?: Query<TagSchema> | Options, options?: Options) {\n  const transformArgs = () => {\n    if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {\n      options = tags as Options;\n      tags = this.site.tags;\n    }\n    tags = tags as Query<TagSchema>;\n\n    return [tags.toArray(), options];\n  };\n\n  return moize(tagcloudHelper.bind(this), {\n    maxSize: 5,\n    isDeepEqual: true,\n    transformArgs\n  }).call(this, tags, options);\n}\n\nexport = tagcloudHelperFactory;\n"
  },
  {
    "path": "lib/plugins/helper/toc.ts",
    "content": "import { tocObj, escapeHTML } from 'hexo-util';\n\ninterface Options {\n  min_depth?: number;\n  max_depth?: number;\n  max_items?: number;\n  class?: string;\n  class_item?: string;\n  class_link?: string;\n  class_text?: string;\n  class_child?: string;\n  class_number?: string;\n  class_level?: string;\n  list_number?: boolean;\n}\n\n/**\n * Hexo TOC helper: generates a nested <ol> list from markdown headings\n * @param {string} str      Raw markdown/html string\n * @param {Options} options Configuration options\n */\nfunction tocHelper(str, options: Options = {}) {\n  // Default options\n  options = Object.assign({\n    min_depth: 1,\n    max_depth: 6,\n    max_items: Infinity,\n    class: 'toc',\n    class_item: '',\n    class_link: '',\n    class_text: '',\n    class_child: '',\n    class_number: '',\n    class_level: '',\n    list_number: true\n  }, options);\n\n  // Extract and truncate flat TOC data\n  const flat = getAndTruncateTocObj(\n    str,\n    { min_depth: options.min_depth, max_depth: options.max_depth },\n    options.max_items\n  );\n  if (!flat.length) return '';\n\n  // Prepare class names\n  const className = escapeHTML(options.class);\n  const itemClassName = escapeHTML(options.class_item || options.class + '-item');\n  const linkClassName = escapeHTML(options.class_link || options.class + '-link');\n  const textClassName = escapeHTML(options.class_text || options.class + '-text');\n  const childClassName = escapeHTML(options.class_child || options.class + '-child');\n  const numberClassName = escapeHTML(options.class_number || options.class + '-number');\n  const levelClassName = escapeHTML(options.class_level || options.class + '-level');\n  const listNumber = options.list_number;\n\n  // Build tree, assign numbers, render HTML\n  const tree = buildTree(flat);\n  if (listNumber) assignNumbers(tree);\n\n  function render(list, depth = 0) {\n    if (!list.length) return '';\n    const olCls = depth === 0 ? className : childClassName;\n    let out = `<ol class=\"${olCls}\">`;\n\n    list.forEach(node => {\n      const lvl = node.level;\n      out += `<li class=\"${itemClassName} ${levelClassName}-${lvl}\">`;\n      out += `<a class=\"${linkClassName}\"${node.id ? ` href=\"#${encodeURI(node.id)}\"` : ''}>`;\n      if (listNumber && !node.unnumbered) {\n        out += `<span class=\"${numberClassName}\">${node.number}</span> `;\n      }\n      out += `<span class=\"${textClassName}\">${node.text}</span></a>`;\n      out += render(node.children, depth + 1);\n      out += '</li>';\n    });\n\n    out += '</ol>';\n    return out;\n  }\n\n  return render(tree);\n}\n\n/**\n * Extract flat TOC data and enforce max_items\n */\nfunction getAndTruncateTocObj(str, { min_depth, max_depth }, max_items) {\n  let data = tocObj(str, { min_depth, max_depth });\n  if (max_items < Infinity && data.length > max_items) {\n    const levels = data.map(i => i.level);\n    const min = Math.min(...levels);\n    let curMax = Math.max(...levels);\n    // remove deeper headings until within limit\n    while (data.length > max_items && curMax > min) {\n      // eslint-disable-next-line no-loop-func\n      data = data.filter(i => i.level < curMax);\n      curMax--;\n    }\n    data = data.slice(0, max_items);\n  }\n  return data;\n}\n\n/**\n * Build nested tree from flat heading list\n */\nfunction buildTree(headings) {\n  const root = { level: 0, children: [] };\n  const stack = [root];\n\n  headings.forEach(h => {\n    // pop until parent.level < h.level\n    while (stack[stack.length - 1].level >= h.level) {\n      stack.pop();\n    }\n    const parent = stack[stack.length - 1];\n    const node = { ...h, children: [] };\n    parent.children.push(node);\n    stack.push(node);\n  });\n\n  return root.children;\n}\n\n/**\n * Assign hierarchical numbering to each node\n */\nfunction assignNumbers(nodes) {\n  const counters = [];\n  function dfs(list, depth) {\n    counters[depth] = 0;\n    list.forEach(node => {\n      counters[depth]++;\n      node.number = counters.slice(0, depth + 1).join('.') + '.';\n      if (node.children.length) dfs(node.children, depth + 1);\n    });\n  }\n  dfs(nodes, 0);\n}\n\nexport = tocHelper;\n"
  },
  {
    "path": "lib/plugins/helper/url_for.ts",
    "content": "import { url_for } from 'hexo-util';\nimport type { LocalsType } from '../../types';\n\ninterface Options {\n  relative?: boolean\n}\n\nexport = function(this: LocalsType, path: string, options: Options = {}) {\n  return url_for.call(this, path, options);\n}\n"
  },
  {
    "path": "lib/plugins/highlight/highlight.ts",
    "content": "import type { HighlightOptions } from '../../extend/syntax_highlight';\nimport type Hexo from '../../hexo';\n\n// Lazy require highlight.js\nlet highlight: typeof import('hexo-util').highlight;\n\nmodule.exports = function highlightFilter(this: Hexo, code: string, options: HighlightOptions) {\n  const hljsCfg = this.config.highlight || {} as any;\n  const line_threshold = options.line_threshold || hljsCfg.line_threshold || 0;\n  const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? hljsCfg.line_number : options.line_number;\n  const surpassesLineThreshold = options.lines_length > line_threshold;\n  const gutter = shouldUseLineNumbers && surpassesLineThreshold;\n  const languageAttr = typeof options.language_attr === 'undefined' ? hljsCfg.language_attr : options.language_attr;\n\n  const hljsOptions = {\n    autoDetect: hljsCfg.auto_detect,\n    caption: options.caption,\n    firstLine: options.firstLine as number,\n    gutter,\n    hljs: hljsCfg.hljs,\n    lang: options.lang,\n    languageAttr,\n    mark: options.mark as number[],\n    tab: hljsCfg.tab_replace,\n    wrap: hljsCfg.wrap,\n    stripIndent: hljsCfg.strip_indent\n  };\n  if (hljsCfg.first_line_number === 'inline') {\n    if (typeof options.firstLineNumber !== 'undefined') {\n      hljsOptions.firstLine = options.firstLineNumber as number;\n    } else {\n      hljsOptions.gutter = false;\n    }\n  }\n\n  if (Array.isArray(hljsCfg.exclude_languages) && hljsCfg.exclude_languages.includes(hljsOptions.lang)) {\n    // Only wrap with <pre><code class=\"lang\"></code></pre>\n    hljsOptions.wrap = false;\n    hljsOptions.gutter = false;\n    hljsOptions.autoDetect = false;\n  }\n\n  if (!highlight) highlight = require('hexo-util').highlight;\n\n  return highlight(code, hljsOptions);\n};\n"
  },
  {
    "path": "lib/plugins/highlight/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nmodule.exports = (ctx: Hexo) => {\n  const { highlight } = ctx.extend;\n\n  highlight.register('highlight.js', require('./highlight'));\n  highlight.register('prismjs', require('./prism'));\n};\n"
  },
  {
    "path": "lib/plugins/highlight/prism.ts",
    "content": "import type { HighlightOptions } from '../../extend/syntax_highlight';\nimport type Hexo from '../../hexo';\n\n// Lazy require prismjs\nlet prismHighlight: typeof import('hexo-util').prismHighlight;\n\nmodule.exports = function(this: Hexo, code: string, options: HighlightOptions) {\n  const prismjsCfg = this.config.prismjs || {} as any;\n  const line_threshold = options.line_threshold || prismjsCfg.line_threshold || 0;\n  const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? prismjsCfg.line_number : options.line_number;\n  const surpassesLineThreshold = options.lines_length > line_threshold;\n  const lineNumber = shouldUseLineNumbers && surpassesLineThreshold;\n\n  const prismjsOptions = {\n    caption: options.caption,\n    firstLine: options.firstLine as number,\n    isPreprocess: prismjsCfg.preprocess,\n    lang: options.lang,\n    lineNumber,\n    mark: Array.isArray(options.mark) ? String(options.mark) : options.mark,\n    tab: prismjsCfg.tab_replace,\n    stripIndent: prismjsCfg.strip_indent\n  };\n\n  if (!prismHighlight) prismHighlight = require('hexo-util').prismHighlight;\n\n  if (Array.isArray(prismjsCfg.exclude_languages) && prismjsCfg.exclude_languages.includes(prismjsOptions.lang)) {\n    // Only wrap with <pre><code class=\"lang\"></code></pre>\n    return `<pre><code class=\"${prismjsOptions.lang}\">${require('hexo-util').escapeHTML(code)}</code></pre>`;\n  }\n  return prismHighlight(code, prismjsOptions);\n};\n"
  },
  {
    "path": "lib/plugins/injector/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const { injector } = ctx.extend;\n};\n"
  },
  {
    "path": "lib/plugins/processor/asset.ts",
    "content": "import { adjustDateForTimezone, toDate, isExcludedFile, isMatch } from './common';\nimport Promise from 'bluebird';\nimport { parse as yfm } from 'hexo-front-matter';\nimport { extname, relative } from 'path';\nimport { Pattern } from 'hexo-util';\nimport { magenta } from 'picocolors';\nimport type { _File } from '../../box';\nimport type Hexo from '../../hexo';\nimport type { Stats } from 'fs';\nimport { PageSchema } from '../../types';\n\nexport = (ctx: Hexo) => {\n  return {\n    pattern: new Pattern(path => {\n      if (isExcludedFile(path, ctx.config)) return;\n\n      return {\n        renderable: ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render)\n      };\n    }),\n\n    process: function assetProcessor(file: _File) {\n      if (file.params.renderable) {\n        return processPage(ctx, file);\n      }\n\n      return processAsset(ctx, file);\n    }\n  };\n};\n\nfunction processPage(ctx: Hexo, file: _File) {\n  const Page = ctx.model('Page');\n  const { path } = file;\n  const doc = Page.findOne({source: path});\n  const { config } = ctx;\n  const { timezone } = config;\n  const updated_option = config.updated_option;\n\n  if (file.type === 'skip' && doc) {\n    return;\n  }\n\n  if (file.type === 'delete') {\n    if (doc) {\n      return doc.remove();\n    }\n\n    return;\n  }\n\n  return Promise.all([\n    file.stat(),\n    file.read()\n  ]).spread((stats: Stats, content: string) => {\n    const data: PageSchema = yfm(content);\n    const output = ctx.render.getOutput(path);\n\n    data.source = path;\n    data.raw = content;\n\n    data.date = toDate(data.date) as any;\n\n    if (data.date) {\n      if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any;\n    } else {\n      data.date = stats.ctime as any;\n    }\n\n    data.updated = toDate(data.updated) as any;\n\n    if (data.updated) {\n      if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;\n    } else if (updated_option === 'date') {\n      data.updated = data.date;\n    } else if (updated_option === 'empty') {\n      data.updated = undefined;\n    } else {\n      data.updated = stats.mtime as any;\n    }\n\n    if (data.permalink) {\n      data.path = data.permalink;\n      data.permalink = undefined;\n\n      if (data.path.endsWith('/')) {\n        data.path += 'index';\n      }\n\n      if (!extname(data.path)) {\n        data.path += `.${output}`;\n      }\n    } else {\n      data.path = `${path.substring(0, path.length - extname(path).length)}.${output}`;\n    }\n\n    if (!data.layout && output !== 'html' && output !== 'htm') {\n      data.layout = false;\n    }\n\n    if (doc) {\n      if (file.type !== 'update') {\n        ctx.log.warn(`Trying to \"create\" ${magenta(file.path)}, but the file already exists!`);\n      }\n      return doc.replace(data);\n    }\n\n    return Page.insert(data);\n  });\n}\n\nfunction processAsset(ctx: Hexo, file: _File) {\n  const id = relative(ctx.base_dir, file.source).replace(/\\\\/g, '/');\n  const Asset = ctx.model('Asset');\n  const doc = Asset.findById(id);\n\n  if (file.type === 'delete') {\n    if (doc) {\n      return doc.remove();\n    }\n\n    return;\n  }\n\n  return Asset.save({\n    _id: id,\n    path: file.path,\n    modified: file.type !== 'skip',\n    renderable: file.params.renderable\n  });\n}\n"
  },
  {
    "path": "lib/plugins/processor/common.ts",
    "content": "import moment from 'moment-timezone';\nimport micromatch from 'micromatch';\n\nconst DURATION_MINUTE = 1000 * 60;\n\nfunction isMatch(path: string, patterns?: string| string[]) {\n  if (!patterns) return false;\n\n  return micromatch.isMatch(path, patterns);\n}\n\nfunction isTmpFile(path: string) {\n  return path.endsWith('%') || path.endsWith('~');\n}\n\nfunction isHiddenFile(path: string) {\n  return /(^|\\/)[_.]/.test(path);\n}\n\nfunction isExcludedFile(path: string, config) {\n  if (isTmpFile(path)) return true;\n  if (isMatch(path, config.exclude)) return true;\n  if (isHiddenFile(path) && !isMatch(path, config.include)) return true;\n  return false;\n}\n\nexport {isTmpFile};\nexport {isHiddenFile};\nexport {isExcludedFile};\n\nexport function toDate(date?: string | number | Date | moment.Moment): Date | undefined | moment.Moment {\n  if (!date || moment.isMoment(date)) return date as any;\n\n  if (!(date instanceof Date)) {\n    date = new Date(date);\n  }\n\n  if (isNaN(date.getTime())) return;\n\n  return date;\n}\n\nexport function adjustDateForTimezone(date: Date | moment.Moment, timezone: string) {\n  if (moment.isMoment(date)) date = date.toDate();\n\n  const offset = date.getTimezoneOffset();\n  const ms = date.getTime();\n  const target = moment.tz.zone(timezone).utcOffset(ms);\n  const diff = (offset - target) * DURATION_MINUTE;\n\n  return new Date(ms - diff);\n}\n\nexport {isMatch};\n"
  },
  {
    "path": "lib/plugins/processor/data.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport { extname } from 'path';\nimport type Hexo from '../../hexo';\nimport type { _File } from '../../box';\n\nexport = (ctx: Hexo) => ({\n  pattern: new Pattern('_data/*path'),\n\n  process: function dataProcessor(file: _File) {\n    const Data = ctx.model('Data');\n    const { path } = file.params;\n    const id = path.substring(0, path.length - extname(path).length);\n    const doc = Data.findById(id);\n\n    if (file.type === 'skip' && doc) {\n      return;\n    }\n\n    if (file.type === 'delete') {\n      if (doc) {\n        return doc.remove();\n      }\n\n      return;\n    }\n\n    return file.render().then(result => {\n      if (result == null) return;\n\n      return Data.save({\n        _id: id,\n        data: result\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "lib/plugins/processor/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { processor } = ctx.extend;\n\n  function register(name: string) {\n    const obj = require(`./${name}`)(ctx);\n    processor.register(obj.pattern, obj.process);\n  }\n\n  register('asset');\n  register('data');\n  register('post');\n};\n"
  },
  {
    "path": "lib/plugins/processor/post.ts",
    "content": "import { toDate, adjustDateForTimezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } from './common';\nimport Promise from 'bluebird';\nimport { parse as yfm } from 'hexo-front-matter';\nimport { extname, join, posix, sep } from 'path';\nimport { stat, listDir } from 'hexo-fs';\nimport { slugize, Pattern, Permalink } from 'hexo-util';\nimport { magenta } from 'picocolors';\nimport type { _File } from '../../box';\nimport type Hexo from '../../hexo';\nimport type { Stats } from 'fs';\nimport { PostAssetSchema, PostSchema } from '../../types';\nimport type Document from 'warehouse/dist/document';\n\nconst postDir = '_posts/';\nconst draftDir = '_drafts/';\nlet permalink: Permalink;\n\nconst preservedKeys = {\n  title: true,\n  year: true,\n  month: true,\n  day: true,\n  i_month: true,\n  i_day: true,\n  hash: true\n};\n\nexport = (ctx: Hexo) => {\n  return {\n    pattern: new Pattern(path => {\n      if (isTmpFile(path)) return;\n\n      let result;\n\n      if (path.startsWith(postDir)) {\n        result = {\n          published: true,\n          path: path.substring(postDir.length)\n        };\n      } else if (path.startsWith(draftDir)) {\n        result = {\n          published: false,\n          path: path.substring(draftDir.length)\n        };\n      }\n\n      if (!result || isHiddenFile(result.path)) return;\n\n      // checks only if there is a renderer for the file type or if is included in skip_render\n      result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render);\n\n      // if post_asset_folder is set, restrict renderable files to default file extension\n      if (result.renderable && ctx.config.post_asset_folder) {\n        result.renderable = (extname(ctx.config.new_post_name) === extname(path));\n      }\n\n      return result;\n    }),\n\n    process: function postProcessor(file: _File) {\n      if (file.params.renderable) {\n        return processPost(ctx, file);\n      } else if (ctx.config.post_asset_folder) {\n        return processAsset(ctx, file);\n      }\n    }\n  };\n};\n\nfunction processPost(ctx: Hexo, file: _File) {\n  const Post = ctx.model('Post');\n  const { path } = file.params;\n  const doc = Post.findOne({source: file.path});\n  const { config } = ctx;\n  const { timezone, updated_option, use_slug_as_post_title } = config;\n\n  let categories, tags;\n\n  if (file.type === 'skip' && doc) {\n    return;\n  }\n\n  if (file.type === 'delete') {\n    if (doc) {\n      return doc.remove();\n    }\n\n    return;\n  }\n\n  return Promise.all([\n    file.stat(),\n    file.read()\n  ]).spread((stats: Stats, content: string) => {\n    const data: PostSchema = yfm(content);\n    const info = parseFilename(config.new_post_name, path);\n    const keys = Object.keys(info);\n\n    data.source = file.path;\n    data.raw = content;\n    data.slug = info.title;\n\n    if (file.params.published) {\n      if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true;\n    } else {\n      data.published = false;\n    }\n\n    for (let i = 0, len = keys.length; i < len; i++) {\n      const key = keys[i];\n      if (!preservedKeys[key]) data[key] = info[key];\n    }\n\n    // use `slug` as `title` of post when `title` is not specified.\n    // https://github.com/hexojs/hexo/issues/5372\n    if (use_slug_as_post_title && !('title' in data)) {\n      // @ts-expect-error - title is not in data\n      data.title = info.title;\n    }\n\n    if (data.date) {\n      data.date = toDate(data.date) as any;\n    } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) {\n      data.date = new Date(\n        info.year,\n        parseInt(info.month || info.i_month, 10) - 1,\n        parseInt(info.day || info.i_day, 10)\n      ) as any;\n    }\n\n    if (data.date) {\n      if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any;\n    } else {\n      data.date = stats.birthtime as any;\n    }\n\n    data.updated = toDate(data.updated) as any;\n\n    if (data.updated) {\n      if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;\n    } else if (updated_option === 'date') {\n      data.updated = data.date;\n    } else if (updated_option === 'empty') {\n      data.updated = undefined;\n    } else {\n      data.updated = stats.mtime as any;\n    }\n\n    if (data.category && !data.categories) {\n      data.categories = data.category;\n      data.category = undefined;\n    }\n\n    if (data.tag && !data.tags) {\n      data.tags = data.tag;\n      data.tag = undefined;\n    }\n\n    categories = data.categories || [];\n    tags = data.tags || [];\n\n    if (!Array.isArray(categories)) categories = [categories];\n    if (!Array.isArray(tags)) tags = [tags];\n\n    if (data.photo && !data.photos) {\n      data.photos = data.photo;\n      data.photo = undefined;\n    }\n\n    if (data.photos && !Array.isArray(data.photos)) {\n      data.photos = [data.photos];\n    }\n\n    if (data.permalink) {\n      data.__permalink = data.permalink;\n      data.permalink = undefined;\n    }\n\n    if (doc) {\n      if (file.type !== 'update') {\n        ctx.log.warn(`Trying to \"create\" ${magenta(file.path)}, but the file already exists!`);\n      }\n      return doc.replace(data);\n    }\n\n    return Post.insert(data);\n  }).then((doc: PostSchema) => Promise.all([\n    doc.setCategories(categories),\n    doc.setTags(tags),\n    scanAssetDir(ctx, doc)\n  ]));\n}\n\nfunction parseFilename(config: string, path: string) {\n  config = config.substring(0, config.length - extname(config).length);\n  path = path.substring(0, path.length - extname(path).length);\n\n  if (!permalink || permalink.rule !== config) {\n    permalink = new Permalink(config, {\n      segments: {\n        year: /(\\d{4})/,\n        month: /(\\d{2})/,\n        day: /(\\d{2})/,\n        i_month: /(\\d{1,2})/,\n        i_day: /(\\d{1,2})/,\n        hash: /([0-9a-f]{12})/\n      }\n    });\n  }\n\n  const data = permalink.parse(path) as Record<string, any>;\n\n  if (data) {\n    if (data.title !== undefined) {\n      return data;\n    }\n    return Object.assign(data, {\n      title: slugize(path)\n    });\n  }\n\n  return {\n    title: slugize(path)\n  };\n}\n\nfunction scanAssetDir(ctx: Hexo, post: PostSchema) {\n  if (!ctx.config.post_asset_folder) return;\n\n  const assetDir = post.asset_dir;\n  const baseDir = ctx.base_dir;\n  const sourceDir = ctx.config.source_dir;\n  const baseDirLength = baseDir.length;\n  const sourceDirLength = sourceDir.length;\n  const PostAsset = ctx.model('PostAsset');\n\n  return stat(assetDir).then(stats => {\n    if (!stats.isDirectory()) return [];\n\n    return listDir(assetDir);\n  }).catch(err => {\n    if (err && err.code === 'ENOENT') return [];\n    throw err;\n  }).filter(item => !isExcludedFile(item, ctx.config)).map(item => {\n    const id = join(assetDir, item).substring(baseDirLength).replace(/\\\\/g, '/');\n    const renderablePath = id.substring(sourceDirLength + 1);\n    const asset = PostAsset.findById(id);\n\n    if (shouldSkipAsset(ctx, post, asset)) return undefined;\n\n    return PostAsset.save({\n      _id: id,\n      post: post._id,\n      slug: item,\n      modified: true,\n      renderable: ctx.render.isRenderable(renderablePath) && !isMatch(renderablePath, ctx.config.skip_render)\n    });\n  });\n}\n\nfunction shouldSkipAsset(ctx: Hexo, post: PostSchema, asset: Document<PostAssetSchema>) {\n  if (!ctx._showDrafts()) {\n    if (post.published === false && asset) {\n      // delete existing draft assets if draft posts are hidden\n      asset.remove();\n    }\n    if (post.published === false) {\n      // skip draft assets if draft posts are hidden\n      return true;\n    }\n  }\n\n  return asset !== undefined; // skip already existing assets\n}\n\nfunction processAsset(ctx: Hexo, file: _File) {\n  const PostAsset = ctx.model('PostAsset');\n  const Post = ctx.model('Post');\n  const id = file.source.substring(ctx.base_dir.length).replace(/\\\\/g, '/');\n  const postAsset = PostAsset.findById(id);\n\n  if (file.type === 'delete' || Post.length === 0) {\n    if (postAsset) {\n      return postAsset.remove();\n    }\n    return;\n  }\n\n  const savePostAsset = (post: Document<PostSchema>) => {\n    return PostAsset.save({\n      _id: id,\n      slug: file.source.substring(post.asset_dir.length),\n      post: post._id,\n      modified: file.type !== 'skip',\n      renderable: file.params.renderable\n    });\n  };\n\n  if (postAsset) {\n    // `postAsset.post` is `Post.id`.\n    const post = Post.findById(postAsset.post);\n    if (post != null && (post.published || ctx._showDrafts())) {\n      return savePostAsset(post);\n    }\n  }\n\n  // NOTE: Must use `posix.sep` ('/') because id is normalized to use forward slashes.\n  //       Using os-specific `sep` would fail on Windows where backslashes wouldn't match.\n  const relativeAssetDirPath = id.slice(0, id.lastIndexOf(posix.sep));\n\n  // Convert relative path to OS-specific absolute path with trailing separator\n  const absoluteAssetDirPath = join(ctx.base_dir, relativeAssetDirPath) + sep;\n\n  /* NOTE:\n     Using `Post.filter()` instead to ensure we get the correct post.\n     Because `Post.findOne()` with a query function or object does not work as expected here.\n     It returns an incorrect post even when the condition doesn't match.\n\n     Examples that didn't work:\n       - `Post.findOne(p => p.asset_dir === absoluteAssetDirPath)`  // returned wrong post\n       - `Post.findOne({asset_dir: absoluteAssetDirPath})`          // returned null\n  */\n  const posts = Post.filter(p => p.asset_dir === absoluteAssetDirPath);\n  const post = posts.length === 1 ? posts.data[0] : null;\n  if (post != null && (post.published || ctx._showDrafts())) {\n    return savePostAsset(post);\n  }\n\n  // NOTE: Probably, unreachable.\n  if (postAsset) {\n    return postAsset.remove();\n  }\n}\n"
  },
  {
    "path": "lib/plugins/renderer/index.ts",
    "content": "import type Hexo from '../../hexo';\n\nexport = (ctx: Hexo) => {\n  const { renderer } = ctx.extend;\n\n  const plain = require('./plain');\n\n  renderer.register('htm', 'html', plain, true);\n  renderer.register('html', 'html', plain, true);\n  renderer.register('css', 'css', plain, true);\n  renderer.register('js', 'js', plain, true);\n\n  renderer.register('json', 'json', require('./json'), true);\n\n  const yaml = require('./yaml');\n\n  renderer.register('yml', 'json', yaml, true);\n  renderer.register('yaml', 'json', yaml, true);\n\n  const nunjucks = require('./nunjucks');\n\n  renderer.register('njk', 'html', nunjucks, true);\n  renderer.register('j2', 'html', nunjucks, true);\n};\n"
  },
  {
    "path": "lib/plugins/renderer/json.ts",
    "content": "import type { StoreFunctionData } from '../../extend/renderer';\n\nfunction jsonRenderer(data: StoreFunctionData): any {\n  return JSON.parse(data.text);\n}\n\nexport = jsonRenderer;\n"
  },
  {
    "path": "lib/plugins/renderer/nunjucks.ts",
    "content": "import nunjucks, { Environment } from 'nunjucks';\nimport { readFileSync } from 'hexo-fs';\nimport { dirname } from 'path';\nimport type { StoreFunctionData } from '../../extend/renderer';\n\nfunction toArray(value) {\n  if (Array.isArray(value)) {\n    // Return if given value is an Array\n    return value;\n  } else if (typeof value.toArray === 'function') {\n    return value.toArray();\n  } else if (value instanceof Map) {\n    const arr = [];\n    value.forEach(v => arr.push(v));\n    return arr;\n  } else if (value instanceof Set || typeof value === 'string') {\n    return [...value];\n  } else if (typeof value === 'object' && value instanceof Object && Boolean(value)) {\n    return Object.values(value);\n  }\n\n  return [];\n}\n\nfunction safeJsonStringify(json: any, spacer = undefined): string {\n  if (typeof json !== 'undefined' && json !== null) {\n    return JSON.stringify(json, null, spacer);\n  }\n\n  return '\"\"';\n}\n\nconst nunjucksCfg = {\n  autoescape: false,\n  throwOnUndefined: false,\n  trimBlocks: false,\n  lstripBlocks: false\n};\n\nconst nunjucksAddFilter = (env: Environment): void => {\n  env.addFilter('toarray', toArray);\n  env.addFilter('safedump', safeJsonStringify);\n};\n\nfunction njkCompile(data: StoreFunctionData): nunjucks.Template {\n  let env: Environment;\n  if (data.path) {\n    env = nunjucks.configure(dirname(data.path), nunjucksCfg);\n  } else {\n    env = nunjucks.configure(nunjucksCfg);\n  }\n  nunjucksAddFilter(env);\n\n  const text = 'text' in data ? data.text : readFileSync(data.path);\n\n  return nunjucks.compile(text, env, data.path);\n}\n\nfunction njkRenderer(data: StoreFunctionData, locals?: any): string {\n  return njkCompile(data).render(locals);\n}\n\nnjkRenderer.compile = (data: StoreFunctionData): (locals: any) => string => {\n  // Need a closure to keep the compiled template.\n  return locals => njkCompile(data).render(locals);\n};\n\nexport = njkRenderer;\n"
  },
  {
    "path": "lib/plugins/renderer/plain.ts",
    "content": "import type { StoreFunctionData } from '../../extend/renderer';\n\nfunction plainRenderer(data: StoreFunctionData): string {\n  return data.text;\n}\n\nexport = plainRenderer;\n"
  },
  {
    "path": "lib/plugins/renderer/yaml.ts",
    "content": "import yaml from 'js-yaml';\nimport { escape } from 'hexo-front-matter';\nimport logger from 'hexo-log';\nimport type { StoreFunctionData } from '../../extend/renderer';\n\nlet schema: yaml.Schema;\n// FIXME: workaround for https://github.com/hexojs/hexo/issues/4917\ntry {\n  schema = yaml.DEFAULT_SCHEMA.extend(require('js-yaml-js-types').all);\n} catch (e) {\n  if (e instanceof yaml.YAMLException) {\n    logger().warn('YAMLException: please see https://github.com/hexojs/hexo/issues/4917');\n  } else {\n    throw e;\n  }\n}\n\nfunction yamlHelper(data: StoreFunctionData): any {\n  return yaml.load(escape(data.text), { schema });\n}\n\nexport = yamlHelper;\n"
  },
  {
    "path": "lib/plugins/tag/asset_img.ts",
    "content": "import img from './img';\nimport { encodeURL } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\n/**\n * Asset image tag\n *\n * Syntax:\n *   {% asset_img [class names] slug [width] [height] [title text [alt text]]%}\n */\nexport = (ctx: Hexo) => {\n  const PostAsset = ctx.model('PostAsset');\n\n  return function assetImgTag(args: string[]) {\n    const len = args.length;\n\n    // Find image URL\n    for (let i = 0; i < len; i++) {\n      const asset = PostAsset.findOne({post: this._id, slug: args[i]});\n      if (asset) {\n        // img tag will call url_for so no need to call it here\n        args[i] = encodeURL(new URL(asset.path, ctx.config.url).pathname);\n        return img(ctx)(args);\n      }\n    }\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/asset_link.ts",
    "content": "import { url_for, escapeHTML } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\n/**\n * Asset link tag\n *\n * Syntax:\n *   {% asset_link slug [title] [escape] %}\n */\nexport = (ctx: Hexo) => {\n  const PostAsset = ctx.model('PostAsset');\n\n  return function assetLinkTag(args: string[]) {\n    const slug = args.shift();\n    if (!slug) return;\n\n    const asset = PostAsset.findOne({post: this._id, slug});\n    if (!asset) return;\n\n    let escape = args[args.length - 1];\n    if (escape === 'true' || escape === 'false') {\n      args.pop();\n    } else {\n      escape = 'true';\n    }\n\n    let title = args.length ? args.join(' ') : asset.slug;\n    const attrTitle = escapeHTML(title);\n    if (escape === 'true') title = attrTitle;\n\n    const link = url_for.call(ctx, asset.path);\n\n    return `<a href=\"${link}\" title=\"${attrTitle}\">${title}</a>`;\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/asset_path.ts",
    "content": "import { url_for } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\n/**\n * Asset path tag\n *\n * Syntax:\n *   {% asset_path slug %}\n */\nexport = (ctx: Hexo) => {\n  const PostAsset = ctx.model('PostAsset');\n\n  return function assetPathTag(args: string[]) {\n    const slug = args.shift();\n    if (!slug) return;\n\n    const asset = PostAsset.findOne({post: this._id, slug});\n    if (!asset) return;\n\n    const path = url_for.call(ctx, asset.path);\n\n    return path;\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/blockquote.ts",
    "content": "// Based on: https://raw.github.com/imathis/octopress/master/plugins/blockquote.rb\n\nimport titlecase from 'titlecase';\nimport type Hexo from '../../hexo';\n\nconst rFullCiteWithTitle = /(\\S.*)\\s+(https?:\\/\\/\\S+)\\s+(.+)/i;\nconst rFullCite = /(\\S.*)\\s+(https?:\\/\\/\\S+)/i;\nconst rAuthorTitle = /([^,]+),\\s*([^,]+)/;\n\n/**\n * @param {string[]} args\n * @param {Hexo} ctx\n */\nconst parseFooter = (args: string[], ctx: Hexo) => {\n  const str = args.join(' ');\n  if (!str) return '';\n\n  let author = '';\n  let source = '';\n  let title = '';\n  let match;\n\n  if ((match = rFullCiteWithTitle.exec(str))) {\n    author = match[1];\n    source = match[2];\n    title = ctx.config.titlecase ? titlecase(match[3]) : match[3];\n  } else if ((match = rFullCite.exec(str))) {\n    author = match[1];\n    source = match[2];\n  } else if ((match = rAuthorTitle.exec(str))) {\n    author = match[1];\n    title = ctx.config.titlecase ? titlecase(match[2]) : match[2];\n  } else {\n    author = str;\n  }\n\n  let footer = '';\n  if (author) footer += `<strong>${author}</strong>`;\n\n  if (source) {\n    const link = source.replace(/^https?:\\/\\/|\\/(index.html?)?$/g, '');\n    footer += `<cite><a href=\"${source}\">${title ? title : link}</a></cite>`;\n  } else if (title) {\n    footer += `<cite>${title}</cite>`;\n  }\n\n  return footer;\n};\n\n/**\n* Blockquote tag\n*\n* Syntax:\n*   {% blockquote [author[, source]] [link] [source_link_title] %}\n*   Quote string\n*   {% endblockquote %}\n*/\n\nexport = (ctx: Hexo) => function blockquoteTag(args: string[], content: string) {\n  const footer = parseFooter(args, ctx);\n\n  let result = '<blockquote>';\n  result += ctx.render.renderSync({text: content, engine: 'markdown'});\n  if (footer) result += `<footer>${footer}</footer>`;\n  result += '</blockquote>';\n\n  return result;\n};\n"
  },
  {
    "path": "lib/plugins/tag/code.ts",
    "content": "// Based on: https://raw.github.com/imathis/octopress/master/plugins/code_block.rb\n\nimport { escapeHTML, htmlTag } from 'hexo-util';\nimport type Hexo from '../../hexo';\nimport type { HighlightOptions } from '../../extend/syntax_highlight';\n\nconst rCaptionUrlTitle = /(\\S[\\S\\s]*)\\s+(https?:\\/\\/\\S+)\\s+(.+)/i;\nconst rCaptionUrl = /(\\S[\\S\\s]*)\\s+(https?:\\/\\/\\S+)/i;\nconst rCaption = /\\S[\\S\\s]*/;\n\n/**\n * Code block tag\n * Syntax:\n * {% codeblock [options] %}\n * code snippet\n * {% endcodeblock %}\n * @param {String} title Caption text\n * @param {Object} lang Specify language\n * @param {String} url Source link\n * @param {String} link_text Text of the link\n * @param {Object} line_number Show line number, value must be a boolean\n * @param {Object} highlight Enable code highlighting, value must be a boolean\n * @param {Object} first_line Specify the first line number, value must be a number\n * @param {Object} mark Line highlight specific line(s), each value separated by a comma. Specify number range using a dash\n * Example: `mark:1,4-7,10` will mark line 1, 4 to 7 and 10.\n * @param {Object} wrap Wrap the code block in <table>, value must be a boolean\n * @returns {String} Code snippet with code highlighting\n*/\n\nfunction parseArgs(args: string[]): HighlightOptions {\n  const _else = [];\n  const len = args.length;\n  let lang: string, language_attr: boolean,\n    line_number: boolean, line_threshold: number, wrap: boolean;\n  let firstLine = 1;\n  const mark = [];\n  for (let i = 0; i < len; i++) {\n    const colon = args[i].indexOf(':');\n\n    if (colon === -1) {\n      _else.push(args[i]);\n      continue;\n    }\n\n    const key = args[i].slice(0, colon);\n    const value = args[i].slice(colon + 1);\n\n    switch (key) {\n      case 'lang':\n        lang = value;\n        break;\n      case 'line_number':\n        line_number = value === 'true';\n        break;\n      case 'line_threshold':\n        if (!isNaN(Number(value))) line_threshold = +value;\n        break;\n      case 'first_line':\n        if (!isNaN(Number(value))) firstLine = +value;\n        break;\n      case 'wrap':\n        wrap = value === 'true';\n        break;\n      case 'mark': {\n        for (const cur of value.split(',')) {\n          const hyphen = cur.indexOf('-');\n          if (hyphen !== -1) {\n            let a = +cur.slice(0, hyphen);\n            let b = +cur.slice(hyphen + 1);\n            if (Number.isNaN(a) || Number.isNaN(b)) continue;\n            if (b < a) { // switch a & b\n              [a, b] = [b, a];\n            }\n\n            for (; a <= b; a++) {\n              mark.push(a);\n            }\n          }\n          if (!isNaN(Number(cur))) mark.push(+cur);\n        }\n        break;\n      }\n      case 'language_attr': {\n        language_attr = value === 'true';\n        break;\n      }\n      default: {\n        _else.push(args[i]);\n      }\n    }\n  }\n\n  const arg = _else.join(' ');\n  // eslint-disable-next-line one-var\n  let match, caption = '';\n\n  if ((match = arg.match(rCaptionUrlTitle)) != null) {\n    caption = htmlTag('span', {}, match[1]) + htmlTag('a', { href: match[2] }, match[3]);\n  } else if ((match = arg.match(rCaptionUrl)) != null) {\n    caption = htmlTag('span', {}, match[1]) + htmlTag('a', { href: match[2] }, 'link');\n  } else if ((match = arg.match(rCaption)) != null) {\n    caption = htmlTag('span', {}, match[0]);\n  }\n\n  return {\n    lang,\n    language_attr,\n    firstLine,\n    caption,\n    line_number,\n    line_threshold,\n    mark,\n    wrap\n  };\n}\n\nexport = (ctx: Hexo) => function codeTag(args: string[], content: string) {\n\n  // If neither highlight.js nor prism.js is enabled, return escaped code directly\n  if (!ctx.extend.highlight.query(ctx.config.syntax_highlighter)) {\n    return `<pre><code>${escapeHTML(content)}</code></pre>`;\n  }\n\n  let index: number;\n  let enableHighlight = true;\n\n  if ((index = args.findIndex(item => item.startsWith('highlight:'))) !== -1) {\n    const arg = args[index];\n    const highlightStr = arg.slice(10);\n    enableHighlight = highlightStr === 'true';\n    args.splice(index, 1);\n  }\n\n  // If 'highlight: false' is given, return escaped code directly\n  if (!enableHighlight) {\n    return `<pre><code>${escapeHTML(content)}</code></pre>`;\n  }\n\n  const options = parseArgs(args);\n  options.lines_length = content.split('\\n').length;\n  content = ctx.extend.highlight.exec(ctx.config.syntax_highlighter, {\n    context: ctx,\n    args: [content, options]\n  });\n\n  return content.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n};\n"
  },
  {
    "path": "lib/plugins/tag/full_url_for.ts",
    "content": "import { full_url_for, htmlTag } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\n/**\n * Full url for tag\n *\n * Syntax:\n *   {% full_url_for text path %}\n */\nexport = (ctx: Hexo) => {\n  return function fullUrlForTag([text, path]) {\n    const url = full_url_for.call(ctx, path);\n    const attrs = {\n      href: url\n    };\n    return htmlTag('a', attrs, text);\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/iframe.ts",
    "content": "import { htmlTag } from 'hexo-util';\n\n/**\n* Iframe tag\n*\n* Syntax:\n*   {% iframe url [width] [height] %}\n*/\n\nfunction iframeTag(args: string[]) {\n  const src = args[0];\n  const width = args[1] && args[1] !== 'default' ? args[1] : '100%';\n  const height = args[2] && args[2] !== 'default' ? args[2] : '300';\n\n  const attrs = {\n    src,\n    width,\n    height,\n    frameborder: '0',\n    loading: 'lazy',\n    allowfullscreen: true\n  };\n\n  return htmlTag('iframe', attrs, '');\n}\n\nexport = iframeTag;\n"
  },
  {
    "path": "lib/plugins/tag/img.ts",
    "content": "import { htmlTag, url_for } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\nconst rUrl = /((([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[-;:&=+$,\\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\\w]+@)[A-Za-z0-9.-]+)((?:\\/[+~%/.\\w-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\w]*))?)/;\nconst rMetaDoubleQuote = /\"?([^\"]+)?\"?/;\nconst rMetaSingleQuote = /'?([^']+)?'?/;\n\n/**\n* Image tag\n*\n* Syntax:\n*   {% img [class names] /path/to/image [width] [height] [title text [alt text]] %}\n*/\nexport = (ctx: Hexo) => {\n\n  return function imgTag(args: string[]) {\n    const classes = [];\n    let src, width, height, title, alt;\n\n    // Find image URL and class name\n    while (args.length > 0) {\n      const item = args.shift();\n      if (rUrl.test(item) || item.startsWith('/')) {\n        src = url_for.call(ctx, item);\n        break;\n      } else {\n        classes.push(item);\n      }\n    }\n\n    // Find image width and height\n    if (args && args.length) {\n      if (!/\\D+/.test(args[0])) {\n        width = args.shift();\n\n        if (args.length && !/\\D+/.test(args[0])) {\n          height = args.shift();\n        }\n      }\n\n      const meta = args.join(' ');\n      const rMetaTitle = meta.startsWith('\"') ? rMetaDoubleQuote : rMetaSingleQuote;\n      const rMetaAlt = meta.endsWith('\"') ? rMetaDoubleQuote : rMetaSingleQuote;\n      const match = new RegExp(`${rMetaTitle.source}\\\\s*${rMetaAlt.source}`).exec(meta);\n\n      // Find image title and alt\n      if (match != null) {\n        title = match[1];\n        alt = match[2];\n      }\n    }\n\n    const attrs = {\n      src,\n      class: classes.join(' '),\n      width,\n      height,\n      title,\n      alt\n    };\n\n    return htmlTag('img', attrs);\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/include_code.ts",
    "content": "import { basename, extname, join } from 'path';\nimport { htmlTag, url_for } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\nconst rCaptionTitleFile = /(.*)?(?:\\s+|^)(\\/*\\S+)/;\nconst rLang = /\\s*lang:(\\w+)/i;\nconst rFrom = /\\s*from:(\\d+)/i;\nconst rTo = /\\s*to:(\\d+)/i;\n\n/**\n* Include code tag\n*\n* Syntax:\n*   {% include_code [title] [lang:language] path/to/file %}\n*/\n\nexport = (ctx: Hexo) => function includeCodeTag(args: string[]) {\n  let codeDir = ctx.config.code_dir;\n  let arg = args.join(' ');\n\n  // Add trailing slash to codeDir\n  if (!codeDir.endsWith('/')) codeDir += '/';\n\n  let lang = '';\n  arg = arg.replace(rLang, (match, _lang) => {\n    lang = _lang;\n    return '';\n  });\n  let from = 0;\n  arg = arg.replace(rFrom, (match, _from) => {\n    from = _from - 1;\n    return '';\n  });\n  let to = Number.MAX_VALUE;\n  arg = arg.replace(rTo, (match, _to) => {\n    to = _to;\n    return '';\n  });\n\n  const match = arg.match(rCaptionTitleFile);\n\n  // Exit if path is not defined\n  if (!match) return;\n\n  const path = match[2];\n\n  // If the language is not defined, use file extension instead\n  lang = lang || extname(path).substring(1);\n\n  const source = join(codeDir, path).replace(/\\\\/g, '/');\n\n  // Prevent path traversal: https://github.com/hexojs/hexo/issues/5250\n  const Page = ctx.model('Page');\n  const doc = Page.findOne({ source });\n  if (!doc) return;\n\n  let code = doc.content;\n  const lines = code.split('\\n');\n  code = lines.slice(from, to).join('\\n').trim();\n\n  // If the title is not defined, use file name instead\n  const title = match[1] || basename(path);\n  const caption = htmlTag('span', {}, title) + `<a href=\"${url_for.call(ctx, doc.path)}\">view raw</a>`;\n\n  if (ctx.extend.highlight.query(ctx.config.syntax_highlighter)) {\n    const options = {\n      lang,\n      caption,\n      lines_length: lines.length\n    };\n    return ctx.extend.highlight.exec(ctx.config.syntax_highlighter, {\n      context: ctx,\n      args: [code, options]\n    });\n  }\n  return `<pre><code>${code}</code></pre>`;\n};\n"
  },
  {
    "path": "lib/plugins/tag/index.ts",
    "content": "import moize from 'moize';\nimport type Hexo from '../../hexo';\n\nexport default (ctx: Hexo) => {\n  const { tag } = ctx.extend;\n\n  const blockquote = require('./blockquote')(ctx);\n\n  tag.register('quote', blockquote, true);\n  tag.register('blockquote', blockquote, true);\n\n  const code = require('./code')(ctx);\n\n  tag.register('code', code, true);\n  tag.register('codeblock', code, true);\n\n  tag.register('iframe', require('./iframe'));\n\n  const img = require('./img')(ctx);\n\n  tag.register('img', img);\n  tag.register('image', img);\n\n  const includeCode = require('./include_code')(ctx);\n\n  tag.register('include_code', includeCode, {async: true});\n  tag.register('include-code', includeCode, {async: true});\n\n  const link = require('./link');\n\n  tag.register('a', link);\n  tag.register('link', link);\n  tag.register('anchor', link);\n\n  tag.register('post_path', require('./post_path')(ctx));\n  tag.register('post_link', require('./post_link')(ctx));\n\n  tag.register('asset_path', require('./asset_path')(ctx));\n  tag.register('asset_link', require('./asset_link')(ctx));\n\n  const assetImg = require('./asset_img')(ctx);\n\n  tag.register('asset_img', assetImg);\n  tag.register('asset_image', assetImg);\n\n  tag.register('pullquote', require('./pullquote')(ctx), true);\n\n  tag.register('url_for', require('./url_for')(ctx));\n  tag.register('full_url_for', require('./full_url_for')(ctx));\n};\n\n// Use WeakMap to track different ctx (in case there is any)\nconst moized = new WeakMap();\n\nexport function postFindOneFactory(ctx: Hexo) {\n  if (moized.has(ctx)) {\n    return moized.get(ctx);\n  }\n\n  const moizedPostFindOne = moize(createPostFindOne(ctx), {\n    isDeepEqual: true,\n    maxSize: 20\n  });\n  moized.set(ctx, moizedPostFindOne);\n\n  return moizedPostFindOne;\n}\n\nfunction createPostFindOne(ctx: Hexo) {\n  const Post = ctx.model('Post');\n  return Post.findOne.bind(Post);\n}\n"
  },
  {
    "path": "lib/plugins/tag/link.ts",
    "content": "import { htmlTag } from 'hexo-util';\n\nconst rUrl = /((([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[-;:&=+$,\\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\\w]+@)[A-Za-z0-9.-]+)((?:\\/[+~%/.\\w-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\w]*))?)/;\n\n/**\n* Link tag\n*\n* Syntax:\n*   {% link text url [external] [title] %}\n*/\n\nfunction linkTag(args: string[]) {\n  let url = '';\n  const text = [];\n  let external = false;\n  let title = '';\n  let i = 0;\n  const len = args.length;\n\n  // Find link URL and text\n  for (; i < len; i++) {\n    const item = args[i];\n\n    if (rUrl.test(item)) {\n      url = item;\n      break;\n    } else {\n      text.push(item);\n    }\n  }\n\n  // Delete link URL and text from arguments\n  args = args.slice(i + 1);\n\n  // Check if the link should be open in a new window\n  // and collect the last text as the link title\n  if (args.length) {\n    const shift = args[0];\n\n    if (shift === 'true' || shift === 'false') {\n      external = shift === 'true';\n      args.shift();\n    }\n\n    title = args.join(' ');\n  }\n\n  const attrs = {\n    href: url,\n    title,\n    target: external ? '_blank' : ''\n  };\n\n  return htmlTag('a', attrs, text.join(' '));\n}\n\nexport = linkTag;\n"
  },
  {
    "path": "lib/plugins/tag/post_link.ts",
    "content": "import { url_for, escapeHTML } from 'hexo-util';\nimport { postFindOneFactory } from './';\nimport type Hexo from '../../hexo';\n\n/**\n * Post link tag\n *\n * Syntax:\n *   {% post_link slug | title [title] [escape] %}\n */\nexport = (ctx: Hexo) => {\n  return function postLinkTag(args: string[]) {\n    let slug = args.shift();\n    if (!slug) {\n      throw new Error(`Post not found: \"${slug}\" doesn't exist for {% post_link %}`);\n    }\n\n    let hash = '';\n    const parts = slug.split('#');\n\n    if (parts.length === 2) {\n      slug = parts[0];\n      hash = parts[1];\n    }\n\n    let escape = args[args.length - 1];\n    if (escape === 'true' || escape === 'false') {\n      args.pop();\n    } else {\n      escape = 'true';\n    }\n\n    const factory = postFindOneFactory(ctx);\n    const post = factory({ slug }) || factory({ title: slug });\n    if (!post) {\n      throw new Error(`Post not found: post_link ${slug}.`);\n    }\n\n    let title = args.length ? args.join(' ') : post.title || post.slug;\n    // Let attribute be the true post title so it appears in tooltip.\n    const attrTitle = escapeHTML(post.title || post.slug);\n    if (escape === 'true') title = escapeHTML(title);\n\n    const link = url_for.call(ctx, post.path + (hash ? `#${hash}` : ''));\n\n    return `<a href=\"${link}\" title=\"${attrTitle}\">${title}</a>`;\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/post_path.ts",
    "content": "import { url_for } from 'hexo-util';\nimport { postFindOneFactory } from './';\nimport type Hexo from '../../hexo';\n\n/**\n * Post path tag\n *\n * Syntax:\n *   {% post_path slug | title %}\n */\nexport = (ctx: Hexo) => {\n  return function postPathTag(args: any[]) {\n    const slug = args.shift();\n    if (!slug) return;\n\n    const factory = postFindOneFactory(ctx);\n    const post = factory({ slug }) || factory({ title: slug });\n    if (!post) return;\n\n    const link = url_for.call(ctx, post.path);\n\n    return link;\n  };\n};\n"
  },
  {
    "path": "lib/plugins/tag/pullquote.ts",
    "content": "import type Hexo from '../../hexo';\n\n/**\n* Pullquote tag\n*\n* Syntax:\n*   {% pullquote [class] %}\n*   Quote string\n*   {% endpullquote %}\n*/\nexport = (ctx: Hexo) => function pullquoteTag(args: string[], content: string) {\n  args.unshift('pullquote');\n\n  const result = ctx.render.renderSync({text: content, engine: 'markdown'});\n\n  return `<blockquote class=\"${args.join(' ')}\">${result}</blockquote>`;\n};\n"
  },
  {
    "path": "lib/plugins/tag/url_for.ts",
    "content": "import { url_for, htmlTag } from 'hexo-util';\nimport type Hexo from '../../hexo';\n\n/**\n * Url for tag\n *\n * Syntax:\n *   {% url_for text path [relative] %}\n */\nexport = (ctx: Hexo) => {\n  return function urlForTag([text, path, relative]) {\n    const url = url_for.call(ctx, path, relative ? { relative: relative !== 'false' } : undefined);\n    const attrs = {\n      href: url\n    };\n    return htmlTag('a', attrs, text);\n  };\n};\n"
  },
  {
    "path": "lib/theme/index.ts",
    "content": "import { extname } from 'path';\nimport Box from '../box';\nimport View from './view';\nimport I18n from 'hexo-i18n';\nimport { config } from './processors/config';\nimport { i18n } from './processors/i18n';\nimport { source } from './processors/source';\nimport { view } from './processors/view';\nimport type Hexo from '../hexo';\n\nclass Theme extends Box {\n  public config: any;\n  public views: Record<string, Record<string, View>>;\n  public i18n: I18n;\n  public View: typeof View;\n\n  constructor(ctx: Hexo, options?: any) {\n    super(ctx, ctx.theme_dir, options);\n\n    this.config = {};\n\n    this.views = {};\n\n    this.processors = [\n      config,\n      i18n,\n      source,\n      view\n    ];\n\n    let languages: string | string[] = ctx.config.language;\n\n    if (!Array.isArray(languages)) languages = [languages];\n\n    languages.push('default');\n\n    this.i18n = new I18n({\n      languages: [...new Set(languages.filter(Boolean))]\n    });\n\n    class _View extends View {}\n\n    this.View = _View;\n\n    _View.prototype._theme = this;\n    _View.prototype._render = ctx.render;\n    _View.prototype._helper = ctx.extend.helper;\n  }\n\n  getView(path: string): View {\n    // Replace backslashes on Windows\n    path = path.replace(/\\\\/g, '/');\n\n    const ext = extname(path);\n    const name = path.substring(0, path.length - ext.length);\n    const views = this.views[name];\n\n    if (!views) return;\n\n    if (ext) {\n      return views[ext];\n    }\n\n    return views[Object.keys(views)[0]];\n  }\n\n  setView(path: string, data: string): void {\n    const ext = extname(path);\n    const name = path.substring(0, path.length - ext.length);\n    this.views[name] = this.views[name] || {};\n    const views = this.views[name];\n\n    views[ext] = new this.View(path, data);\n  }\n\n  removeView(path: string): void {\n    const ext = extname(path);\n    const name = path.substring(0, path.length - ext.length);\n    const views = this.views[name];\n\n    if (!views) return;\n\n    views[ext] = undefined;\n  }\n}\n\nexport = Theme;\n"
  },
  {
    "path": "lib/theme/processors/config.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport type { _File } from '../../box';\nimport Theme from '..';\n\nfunction process(file: _File) {\n  if (file.type === 'delete') {\n    (file.box as Theme).config = {};\n    return;\n  }\n\n  return file.render().then(result => {\n    (file.box as Theme).config = result;\n    this.log.debug('Theme config loaded.');\n  }).catch(err => {\n    this.log.error('Theme config load failed.');\n    throw err;\n  });\n}\n\nconst pattern = new Pattern(/^_config\\.\\w+$/);\n\nexport const config = {\n  pattern,\n  process\n};\n"
  },
  {
    "path": "lib/theme/processors/i18n.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport { extname } from 'path';\nimport type { _File } from '../../box';\nimport type Theme from '..';\n\nfunction process(file: _File) {\n  const { path } = file.params;\n  const ext = extname(path);\n  const name = path.substring(0, path.length - ext.length);\n  const { i18n } = (file.box as Theme);\n\n  if (file.type === 'delete') {\n    i18n.remove(name);\n    return;\n  }\n\n  return file.render().then(data => {\n    if (typeof data !== 'object') return;\n    i18n.set(name, data);\n  });\n}\n\nconst pattern = new Pattern('languages/*path');\n\nexport const i18n = {\n  pattern,\n  process\n};\n"
  },
  {
    "path": "lib/theme/processors/source.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport * as common from '../../plugins/processor/common';\nimport type { _File } from '../../box';\n\nfunction process(file: _File) {\n  const Asset = this.model('Asset');\n  const id = file.source.substring(this.base_dir.length).replace(/\\\\/g, '/');\n  const { path } = file.params;\n  const doc = Asset.findById(id);\n\n  if (file.type === 'delete') {\n    if (doc) {\n      return doc.remove();\n    }\n\n    return;\n  }\n\n  return Asset.save({\n    _id: id,\n    path,\n    modified: file.type !== 'skip'\n  });\n}\n\nconst pattern = new Pattern(path => {\n  if (!path.startsWith('source/')) return false;\n\n  path = path.substring(7);\n  if (common.isHiddenFile(path) || common.isTmpFile(path) || path.includes('node_modules')) return false;\n\n  return {path};\n});\n\nexport const source = {\n  pattern,\n  process\n};\n"
  },
  {
    "path": "lib/theme/processors/view.ts",
    "content": "import { Pattern } from 'hexo-util';\nimport type { _File } from '../../box';\nimport type Theme from '..';\n\nfunction process(file: _File): Promise<void> {\n  const { path } = file.params;\n\n  if (file.type === 'delete') {\n    (file.box as Theme).removeView(path);\n    return;\n  }\n\n  return file.read().then(result => {\n    (file.box as Theme).setView(path, result);\n  });\n}\n\nconst pattern = new Pattern('layout/*path');\n\nexport const view = {\n  pattern,\n  process\n};\n"
  },
  {
    "path": "lib/theme/view.ts",
    "content": "import { dirname, extname, join } from 'path';\nimport { parse as yfm } from 'hexo-front-matter';\nimport Promise from 'bluebird';\nimport type Theme from '.';\nimport type Render from '../hexo/render';\nimport type { NodeJSLikeCallback } from '../types';\nimport type { Helper } from '../extend';\n\nconst assignIn = (target: any, ...sources: any[]) => {\n  const length = sources.length;\n\n  if (length < 1 || target == null) return target;\n  for (let i = 0; i < length; i++) {\n    const source = sources[i];\n\n    for (const key in source) {\n      target[key] = source[key];\n    }\n  }\n  return target;\n};\n\nclass Options {\n  layout?: any;\n  [key: string]: any;\n}\n\nclass View {\n  public path: string;\n  public source: string;\n  public _theme: Theme;\n  public data: any;\n  public _compiled: (locals: any) => Promise<any>;\n  public _compiledSync: (locals: any) => any;\n  public _helper: Helper;\n  public _render: Render;\n\n  constructor(path: string, data: string) {\n    this.path = path;\n    this.source = join(this._theme.base, 'layout', path);\n    this.data = typeof data === 'string' ? yfm(data) : data;\n\n    this._precompile();\n  }\n\n  render(callback: NodeJSLikeCallback<any>): Promise<any>;\n  render(options: Options, callback?: NodeJSLikeCallback<any>): Promise<any>;\n  render(options: Options | NodeJSLikeCallback<any> = {}, callback?: NodeJSLikeCallback<any>): Promise<any> {\n    if (!callback && typeof options === 'function') {\n      callback = options;\n      options = {};\n    }\n    const { data } = this;\n    const { layout = (options as Options).layout } = data;\n    const locals = this._buildLocals(options as Options);\n\n    return this._compiled(this._bindHelpers(locals)).then(result => {\n      if (result == null || !layout) return result;\n\n      const layoutView = this._resolveLayout(layout);\n      if (!layoutView) return result;\n\n      const layoutLocals = {\n        ...locals,\n        body: result,\n        layout: false\n      };\n\n      return layoutView.render(layoutLocals, callback);\n    }).asCallback(callback);\n  }\n\n  renderSync(options: Options = {}) {\n    const { data } = this;\n    const { layout = options.layout } = data;\n    const locals = this._buildLocals(options);\n    const result = this._compiledSync(this._bindHelpers(locals));\n\n    if (result == null || !layout) return result;\n\n    const layoutView = this._resolveLayout(layout);\n    if (!layoutView) return result;\n\n    const layoutLocals = {\n      ...locals,\n      body: result,\n      layout: false\n    };\n\n    return layoutView.renderSync(layoutLocals);\n  }\n\n  _buildLocals(locals: Options) {\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const { layout, _content, ...data } = this.data;\n    return assignIn({}, locals, data, {\n      filename: this.source\n    });\n  }\n\n  _bindHelpers(locals) {\n    const helpers = this._helper.list();\n    const keys = Object.keys(helpers);\n\n    for (const key of keys) {\n      locals[key] = helpers[key].bind(locals);\n    }\n\n    return locals;\n  }\n\n  _resolveLayout(name: string): View {\n    // Relative path\n    const layoutPath = join(dirname(this.path), name);\n    let layoutView = this._theme.getView(layoutPath);\n\n    if (layoutView && layoutView.source !== this.source) return layoutView;\n\n    // Absolute path\n    layoutView = this._theme.getView(name);\n    if (layoutView && layoutView.source !== this.source) return layoutView;\n  }\n\n  _precompile(): void {\n    const render = this._render;\n    const ctx = render.context;\n    const ext = extname(this.path);\n    const renderer = render.getRenderer(ext);\n    const data = {\n      path: this.source,\n      text: this.data._content\n    };\n\n    function buildFilterArguments(result: any): [string, any, { context: any, args: any[] }] {\n      const output = render.getOutput(ext) || ext;\n      return [\n        `after_render:${output}`,\n        result,\n        {\n          context: ctx,\n          args: [data]\n        }\n      ];\n    }\n\n    if (renderer && typeof renderer.compile === 'function') {\n      const compiled = renderer.compile(data);\n\n      this._compiledSync = locals => {\n        const result = compiled(locals);\n        return ctx.execFilterSync(...buildFilterArguments(result));\n      };\n\n      this._compiled = locals => Promise.resolve(compiled(locals))\n        .then(result => ctx.execFilter(...buildFilterArguments(result)));\n    } else {\n      this._compiledSync = locals => render.renderSync(data, locals);\n\n      this._compiled = locals => render.render(data, locals);\n    }\n  }\n}\n\nexport = View;\n"
  },
  {
    "path": "lib/types.ts",
    "content": "import moment from 'moment';\nimport type default_config from './hexo/default_config';\nimport type i18n from 'hexo-i18n';\nimport type Query from 'warehouse/dist/query';\nimport type css from './plugins/helper/css';\nimport type { date, date_xml, time, full_date, relative_date, time_tag, moment as _moment } from './plugins/helper/date';\nimport type { inspectObject, log } from './plugins/helper/debug';\nimport type favicon_tag from './plugins/helper/favicon_tag';\nimport type feed_tag from './plugins/helper/feed_tag';\nimport type { titlecase, word_wrap, truncate, stripHTML, escapeHTML } from './plugins/helper/format';\nimport type fragment_cache from './plugins/helper/fragment_cache';\nimport type full_url_for from './plugins/helper/full_url_for';\nimport type gravatar from './plugins/helper/gravatar';\nimport type image_tag from './plugins/helper/image_tag';\nimport type { current, home, home_first_page, post, page, archive, year, month, category, tag } from './plugins/helper/is';\nimport type js from './plugins/helper/js';\nimport type link_to from './plugins/helper/link_to';\nimport type list_archives from './plugins/helper/list_archives';\nimport type list_categories from './plugins/helper/list_categories';\nimport type list_posts from './plugins/helper/list_posts';\nimport type list_tags from './plugins/helper/list_tags';\nimport type mail_to from './plugins/helper/mail_to';\nimport type markdown from './plugins/helper/markdown';\nimport type meta_generator from './plugins/helper/meta_generator';\nimport type number_format from './plugins/helper/number_format';\nimport type open_graph from './plugins/helper/open_graph';\nimport type paginator from './plugins/helper/paginator';\nimport type relative_url from './plugins/helper/relative_url';\nimport type render from './plugins/helper/render';\nimport type search_form from './plugins/helper/search_form';\nimport type tag_cloud from './plugins/helper/tagcloud';\nimport type toc from './plugins/helper/toc';\nimport type url_for from './plugins/helper/url_for';\n\nexport type NodeJSLikeCallback<R, E = any> = (err: E, result?: R) => void\n\nexport interface RenderData {\n  engine?: string;\n  content?: string;\n  disableNunjucks?: boolean;\n  markdown?: any;\n  source?: string;\n  titlecase?: boolean;\n  title?: string;\n  excerpt?: string;\n  more?: string;\n}\n\n// Schema\nexport interface TagSchema {\n  id?: string;\n  _id?: string;\n  name: string;\n  slug: string;\n  path: string;\n  permalink: string;\n  posts: any;\n  length: number;\n}\n\nexport interface DataSchema {\n  id?: string;\n  data: any;\n}\n\nexport interface CategorySchema {\n  id?: string;\n  _id?: string;\n  name: string;\n  parent?: string;\n  slug: string;\n  path: string;\n  permalink: string;\n  posts: any;\n  length: number;\n}\n\nexport interface PostCategorySchema {\n  _id?: string;\n  post_id: string;\n  category_id: string;\n}\n\nexport interface PostTagSchema {\n  _id?: string;\n  post_id: string;\n  tag_id: string;\n}\n\nexport interface PostAssetSchema {\n  _id: string;\n  slug: string;\n  modified: boolean;\n  post: string;\n  renderable: boolean;\n  path: string;\n  source: string;\n}\n\nexport interface BasePagePostSchema {\n\n  /**\n   * ID generated by warehouse\n   */\n  _id?: string;\n\n  /**\n   * Article title\n   */\n  title: string;\n\n  /**\n   * \tArticle created date\n   */\n  date: moment.Moment,\n\n  /**\n   * Article last updated date\n   */\n  updated: moment.Moment,\n\n  /**\n   * \tComment enabled or not\n   */\n  comments: boolean;\n\n  /**\n   * \tLayout name\n   */\n  layout: string | false;\n\n  /**\n   * The full processed content of the article\n   */\n  _content: string;\n\n  /**\n   * The full processed content of the article\n   */\n  content?: string;\n\n  /**\n   * The path of the source file\n   */\n  source: string;\n\n  /**\n   * The URL of the article without root URL.\n   * We usually use url_for(page.path) in theme.\n   */\n  path: string;\n\n  /**\n   * The raw data of the article\n   */\n  raw: string;\n\n  /**\n   * Article excerpt\n   */\n  excerpt?: string;\n\n  /**\n   * Contents except article excerpt\n   */\n  more?: string;\n\n  /**\n   * Full path of the source file\n   */\n  full_source: string;\n\n  /**\n   * Full (encoded) URL of the article\n   */\n  permalink: string;\n\n  /**\n   * The photos of the article (Used in gallery posts)\n   */\n  photos?: string[];\n\n  /**\n   * The external link of the article (Used in link posts)\n   */\n  link?: string;\n\n  /**\n   * The language of the article\n   */\n  lang?: string;\n\n  /**\n   * The language of the article\n   */\n  language?: string;\n\n  /**\n   * Base URL\n   */\n  base?: string;\n\n  /**\n   * Whether the page is a page\n   */\n  __page?: boolean;\n\n  /**\n   * Whether the page is a post\n   */\n  __post?: boolean;\n\n  /**\n   * Whether the page is a home page\n   */\n  __index?: boolean;\n\n  /**\n   * custom variables set in front-matter.\n   */\n  [key: string]: any;\n}\n\nexport interface PostSchema extends BasePagePostSchema {\n\n  /**\n   * Post ID\n   */\n  id?: string;\n\n  /**\n   * The slug of the post\n   */\n  slug: string;\n\n  /**\n   * True if the post is not a draft\n   */\n  published: boolean;\n\n  /**\n   * The path of the asset directory\n   */\n  asset_dir: string;\n\n  /**\n   * All categories of the post\n   */\n  categories: Query<CategorySchema>;\n\n  /**\n   * All tags of the post\n   */\n  tags: Query<TagSchema>;\n\n  /**\n   * Inner usage\n   */\n  __permalink?: string;\n\n  /**\n   * \tThe previous post, `null` if the post is the first post\n   */\n  prev?: PostSchema | null;\n\n  /**\n   * \tThe next post, `null` if the post is the last post\n   */\n  next?: PostSchema | null;\n\n  notPublished: () => boolean;\n  setTags: (tags: string[]) => any;\n  setCategories: (cats: (string | string[])[]) => any;\n}\n\nexport interface PageSchema extends BasePagePostSchema {\n\n  /**\n   * Posts displayed per page, only available on home page\n   */\n  per_page?: number;\n\n  /**\n   * Total number of pages, only available on home page\n   */\n  total?: number;\n\n  /**\n   * \tCurrent page number, only available on home page\n   */\n  current?: number;\n\n  /**\n   * The URL of current page, only available on home page\n   */\n  current_url?: string;\n\n  /**\n   * Posts in this page, only available on home page\n   */\n  posts?: any;\n\n  /**\n   * Previous page number. `0` if the current page is the first. only available on home page\n   */\n  prev?: number;\n\n  /**\n   * The URL of previous page. `''` if the current page is the first. only available on home page\n   */\n  prev_link?: string;\n\n  /**\n   * Next page number. `0` if the current page is the last. only available on home page\n   */\n  next?: number;\n\n  /**\n   * The URL of next page. `''` if the current page is the last. only available on home page\n   */\n  next_link?: string;\n\n  /**\n   * Equals true, only available on archive page\n   */\n  archive?: boolean;\n\n  /**\n   * Archive year (4-digit), only available on archive page\n   */\n  year?: number;\n\n  /**\n   * Archive month (2-digit without leading zeros), only available on archive page\n   */\n  month?: number;\n\n  /**\n   * Category name, only available on category page\n   */\n  category?: string;\n\n  /**\n   * Tag name, only available on tag page\n   */\n  tag?: string;\n}\n\nexport interface AssetSchema {\n  _id?: string;\n  path: string;\n  modified: boolean;\n  renderable: boolean;\n  source: string;\n}\n\nexport interface CacheSchema {\n  _id: string;\n  hash: string;\n  modified: number;\n}\n\n// Generator return types\nexport interface BaseGeneratorReturn {\n\n  /**\n   * Path not including the prefixing `/`.\n   */\n  path: string;\n\n  /**\n   * Data\n   */\n  data?: any;\n\n  /**\n   * Layout. Specify the layouts for rendering. The value can be a string or an array.\n   * If it’s ignored then the route will return `data` directly.\n   */\n  layout?: string | string[];\n}\n\nexport interface SiteLocals {\n\n  /**\n   * All posts\n   */\n  posts: Query<PostSchema>;\n\n  /**\n   * \tAll pages\n   */\n  pages: Query<PageSchema>;\n\n  /**\n   * All categories\n   */\n  categories: Query<CategorySchema>;\n\n  /**\n   * All tags\n   */\n  tags: Query<TagSchema>;\n  data: any;\n}\n\nexport interface LocalsType {\n  // original properties from Locals class\n  /**\n   * Page specific information and custom variables set in front-matter.\n   */\n  page: BasePagePostSchema;\n\n  /**\n   * Path of current page\n   */\n  path: string;\n\n  /**\n   * Full URL of current page\n   */\n  url: string;\n\n  /**\n   * Site configuration.\n   */\n  config: typeof default_config;\n\n  /**\n   * \tTheme configuration. Inherits from site configuration.\n   */\n  theme: any;\n  layout: string | boolean;\n\n  /**\n   * \tEnvironment variables\n   */\n  env: any;\n  view_dir: string;\n\n  /**\n   * Sitewide information.\n   */\n  site: SiteLocals;\n  cache?: boolean;\n\n  // i18n properties from i18nLocalsFilter\n  /**\n   * https://hexo.io/docs/internationalization#Templates\n   */\n  __: ReturnType<i18n['__']>;\n\n  /**\n   * https://hexo.io/docs/internationalization#Templates\n   */\n  _p: ReturnType<i18n['_p']>;\n\n  // result after renderer.compile\n  body?: string;\n  // from _buildLocals\n  filename?: string;\n\n  // helper functions from _bindHelpers\n  css: typeof css;\n  date: typeof date;\n  date_xml: typeof date_xml;\n  escape_html: typeof escapeHTML;\n  favicon_tag: typeof favicon_tag;\n  feed_tag: typeof feed_tag;\n  fragment_cache: ReturnType<typeof fragment_cache>;\n  full_date: typeof full_date;\n  full_url_for: typeof full_url_for;\n  gravatar: typeof gravatar;\n  image_tag: typeof image_tag;\n  inspect: typeof inspectObject;\n  is_archive: typeof archive;\n  is_category: typeof category;\n  is_current: typeof current;\n  is_home: typeof home;\n  is_home_first_page: typeof home_first_page;\n  is_month: typeof month;\n  is_page: typeof page;\n  is_post: typeof post;\n  is_tag: typeof tag;\n  is_year: typeof year;\n  js: typeof js;\n  link_to: typeof link_to;\n  list_archives: typeof list_archives;\n  list_categories: typeof list_categories;\n  list_posts: typeof list_posts;\n  list_tags: typeof list_tags;\n  log: typeof log;\n  mail_to: typeof mail_to;\n  markdown: typeof markdown;\n  meta_generator: typeof meta_generator;\n  moment: typeof _moment;\n  number_format: typeof number_format;\n  open_graph: typeof open_graph;\n  paginator: typeof paginator;\n  partial: ReturnType<typeof render>;\n  relative_date: typeof relative_date;\n  relative_url: typeof relative_url;\n  render: ReturnType<typeof render>;\n  search_form: typeof search_form;\n  strip_html: typeof stripHTML;\n  tag_cloud: typeof tag_cloud;\n  tagcloud: typeof tag_cloud;\n  time: typeof time;\n  time_tag: typeof time_tag;\n  titlecase: typeof titlecase;\n  toc: typeof toc;\n  trim: typeof stripHTML;\n  truncate: typeof truncate;\n  url_for: typeof url_for;\n  word_wrap: typeof word_wrap;\n}\n\nexport interface FilterOptions {\n  context?: any;\n  args?: any[];\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hexo\",\n  \"version\": \"8.1.1\",\n  \"description\": \"A fast, simple & powerful blog framework, powered by Node.js.\",\n  \"main\": \"dist/hexo\",\n  \"bin\": {\n    \"hexo\": \"./bin/hexo\"\n  },\n  \"scripts\": {\n    \"prepublishOnly\": \"npm install && npm run clean && npm run build\",\n    \"build\": \"tsc -b\",\n    \"clean\": \"tsc -b --clean\",\n    \"eslint\": \"eslint lib test\",\n    \"test\": \"mocha test/scripts/**/*.ts --require ts-node/register\",\n    \"test-cov\": \"c8 --reporter=lcovonly npm test -- --no-parallel\",\n    \"prepare\": \"husky\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"bin/\"\n  ],\n  \"types\": \"./dist/hexo/index.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/hexojs/hexo.git\"\n  },\n  \"homepage\": \"https://hexo.io/\",\n  \"funding\": {\n    \"type\": \"opencollective\",\n    \"url\": \"https://opencollective.com/hexo\"\n  },\n  \"keywords\": [\n    \"website\",\n    \"blog\",\n    \"cms\",\n    \"framework\",\n    \"hexo\"\n  ],\n  \"author\": \"Tommy Chen <tommy351@gmail.com> (https://zespia.tw)\",\n  \"maintainers\": [\n    \"Abner Chou <hi@abnerchou.me> (https://abnerchou.me)\"\n  ],\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"abbrev\": \"^3.0.0\",\n    \"bluebird\": \"^3.7.2\",\n    \"fast-archy\": \"^1.0.0\",\n    \"fast-text-table\": \"^1.0.1\",\n    \"hexo-cli\": \"^4.3.2\",\n    \"hexo-front-matter\": \"^4.2.1\",\n    \"hexo-fs\": \"^5.0.0\",\n    \"hexo-i18n\": \"^2.0.0\",\n    \"hexo-log\": \"^4.1.0\",\n    \"hexo-util\": \"^4.0.0\",\n    \"js-yaml\": \"^4.1.0\",\n    \"js-yaml-js-types\": \"^1.0.1\",\n    \"micromatch\": \"^4.0.8\",\n    \"moize\": \"^6.1.6\",\n    \"moment\": \"^2.30.1\",\n    \"moment-timezone\": \"^0.5.46\",\n    \"nunjucks\": \"^3.2.4\",\n    \"picocolors\": \"^1.1.1\",\n    \"pretty-hrtime\": \"^1.0.3\",\n    \"strip-ansi\": \"^7.1.0\",\n    \"tildify\": \"^2.0.0\",\n    \"titlecase\": \"^1.1.3\",\n    \"warehouse\": \"^6.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/abbrev\": \"^1.1.3\",\n    \"@types/bluebird\": \"^3.5.37\",\n    \"@types/chai\": \"^4.3.11\",\n    \"@types/graceful-fs\": \"^4.1.9\",\n    \"@types/js-yaml\": \"^4.0.9\",\n    \"@types/micromatch\": \"^4.0.7\",\n    \"@types/mocha\": \"^10.0.9\",\n    \"@types/node\": \"^20.17.6\",\n    \"@types/nunjucks\": \"^3.2.2\",\n    \"@types/sinon\": \"^17.0.3\",\n    \"0x\": \"^5.1.2\",\n    \"c8\": \"^9.0.0\",\n    \"chai\": \"^4.3.6\",\n    \"cheerio\": \"1.0.0\",\n    \"decache\": \"^4.6.1\",\n    \"eslint\": \"^9.17.0\",\n    \"eslint-config-hexo\": \"^6.0.0\",\n    \"hexo-renderer-marked\": \"^6.0.0\",\n    \"husky\": \"^9.1.7\",\n    \"lint-staged\": \"^15.3.0\",\n    \"mocha\": \"^10.0.0\",\n    \"sinon\": \"^17.0.1\",\n    \"ts-node\": \"^10.9.1\",\n    \"typescript\": \"^5.3.2\"\n  },\n  \"engines\": {\n    \"node\": \">=20.19.0\"\n  }\n}\n"
  },
  {
    "path": "test/benchmark.js",
    "content": "const { performance, PerformanceObserver } = require('perf_hooks');\nconst { spawn } = require('child_process');\nconst { spawn: spawnAsync } = require('hexo-util');\nconst { rmdir, exists } = require('hexo-fs');\nconst { appendFileSync: appendFile } = require('fs');\nconst { resolve } = require('path');\nconst log = require('hexo-log').default();\nconst { red } = require('picocolors');\nconst hooks = [\n  { regex: /Hexo version/, tag: 'hexo-begin' },\n  { regex: /Start processing/, tag: 'processing' },\n  { regex: /Rendering post/, tag: 'render-post' },\n  { regex: /Files loaded/, tag: 'file-loaded' },\n  { regex: /generated in/, tag: 'generated' },\n  { regex: /Database saved/, tag: 'database-saved' }\n];\n\nconst isWin32 = require('os').platform() === 'win32';\n\nconst npmScript = isWin32 ? 'npm.cmd' : 'npm';\n\nconst testDir = resolve('.tmp-hexo-theme-unit-test');\nconst zeroEksDir = resolve(testDir, '0x');\nconst hexoBin = resolve(testDir, 'node_modules/.bin/hexo');\n\nconst isGitHubActions = process.env.GITHUB_ACTIONS;\n\nconst zeroEks = require('0x');\n\nlet isProfiling = process.argv.join(' ').includes('--profiling');\nlet isBenchmark = process.argv.join(' ').includes('--benchmark');\n\nif (!isProfiling && !isBenchmark) {\n  isProfiling = true;\n  isBenchmark = true;\n}\n\n(async () => {\n  await init();\n\n  if (isBenchmark) {\n    log.info('Running benchmark');\n\n    if (isGitHubActions) {\n      log.info('Running in GitHub Actions.');\n      appendFile(process.env.GITHUB_STEP_SUMMARY, '# Benchmark Result\\n');\n    }\n\n    await cleanUp();\n    await run_benchmark('Cold processing');\n    await run_benchmark('Hot processing');\n    await cleanUp();\n    await run_benchmark('Another Cold processing');\n  }\n\n  if (isProfiling) {\n    await cleanUp();\n    await profiling();\n  }\n})();\n\nasync function run_benchmark(name) {\n  let measureFinished = false;\n\n  return new Promise(resolve => {\n    const result = {};\n    const obs = new PerformanceObserver(list => {\n      list\n        .getEntries()\n        .sort((a, b) => a.detail - b.detail)\n        .forEach(entry => {\n          const { name, duration: _duration } = entry;\n          const duration = _duration / 1000;\n          result[name] = {\n            'Cost time (s)': `${duration.toFixed(2)}s`\n          };\n          if (duration > 20) {\n            log.fatal(red('!! Performance regression detected !!'));\n          }\n        });\n\n      if (measureFinished) {\n        obs.disconnect();\n\n        if (isGitHubActions) {\n          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`);\n        }\n\n        console.log(name);\n        console.table(result);\n\n        resolve(result);\n      }\n    });\n    obs.observe({ entryTypes: ['measure'] });\n\n    const hexo = spawn('node', [hexoBin, 'g', '--debug'], { cwd: testDir });\n    hooks.forEach(({ regex, tag }) => {\n      hexo.stdout.on('data', function listener(data) {\n        const string = data.toString('utf-8');\n        if (regex.test(string)) {\n          performance.mark(tag);\n          hexo.stdout.removeListener('data', listener);\n        }\n      });\n    });\n\n    hexo.on('close', () => {\n      performance.measure('Load Plugin/Scripts/Database', 'hexo-begin', 'processing');\n\n      if (name === 'Hot processing') {\n        performance.measure('Process Source', {\n          start: 'processing',\n          end: 'file-loaded',\n          detail: 0\n        });\n      } else {\n        performance.measure('Process Source', {\n          start: 'processing',\n          end: 'render-post',\n          detail: 1\n        });\n        performance.measure('Render Posts', {\n          start: 'render-post',\n          end: 'file-loaded',\n          detail: 2\n        });\n      }\n\n      performance.measure('Render Files', {\n        start: 'file-loaded',\n        end: 'generated',\n        detail: 3\n      });\n      performance.measure('Save Database', {\n        start: 'generated',\n        end: 'database-saved',\n        detail: 4\n      });\n\n      performance.measure('Total time', {\n        start: 'hexo-begin',\n        end: 'database-saved',\n        detail: 5\n      });\n\n      measureFinished = true;\n    });\n  });\n}\n\nasync function cleanUp() {\n  return spawnAsync(hexoBin, ['clean'], { cwd: testDir });\n}\n\nasync function gitClone(repo, dir, depth = 1) {\n  return spawnAsync('git', ['clone', repo, dir, `--depth=${depth}`]);\n}\n\nasync function init() {\n  if (await exists(testDir)) {\n    log.info(`\"${testDir}\" already exists. Skipping benchmark environment setup.`);\n  } else {\n    log.info('Setting up a dummy hexo site with 500 posts');\n    await gitClone('https://github.com/hexojs/hexo-theme-unit-test.git', testDir);\n    await gitClone('https://github.com/hexojs/hexo-theme-landscape', resolve(testDir, 'themes', 'landscape'));\n    await gitClone('https://github.com/hexojs/hexo-many-posts.git', resolve(testDir, 'source', '_posts', 'hexo-many-posts'));\n  }\n\n  log.info('Installing dependencies');\n  // Always re-install dependencies\n  if (await exists(resolve(testDir, 'node_modules'))) await rmdir(resolve(testDir, 'node_modules'));\n  await spawnAsync(npmScript, ['install', '--silent'], { cwd: testDir });\n\n  log.info('Build hexo');\n  await spawnAsync(npmScript, ['run', 'build']);\n\n  log.info('Replacing hexo');\n  await rmdir(resolve(testDir, 'node_modules', 'hexo'));\n\n  if (isWin32) {\n    await spawnAsync('cmd', [\n      '/s', '/c', 'mklink', '/D',\n      resolve(testDir, 'node_modules', 'hexo'),\n      resolve(__dirname, '..')\n    ]);\n\n    await rmdir(resolve(testDir, 'node_modules', 'hexo-cli'));\n\n    await spawnAsync('cmd', [\n      '/s', '/c', 'mklink', '/D',\n      resolve(testDir, 'node_modules', 'hexo-cli'),\n      resolve(__dirname, '..', 'node_modules', 'hexo-cli')\n    ]);\n  } else {\n    await spawnAsync('ln', [\n      '-sf',\n      resolve(__dirname, '..'),\n      resolve(testDir, 'node_modules', 'hexo')\n    ]);\n  }\n}\n\nasync function profiling() {\n  // Clean up 0x dir before profiling\n  if (await exists(zeroEksDir)) await rmdir(zeroEksDir);\n\n  const zeroEksOpts = {\n    argv: [hexoBin, 'g', '--cwd', testDir],\n    workingDir: '.', // A workaround for https://github.com/davidmarkclements/0x/issues/228\n    outputDir: zeroEksDir,\n    title: 'Hexo Flamegraph'\n  };\n\n  log.info('Profiling');\n\n  const file = await zeroEks(zeroEksOpts);\n\n  // A small hack that workaround 0x's broken stdout handling\n  console.log('');\n\n  log.info(file);\n}\n"
  },
  {
    "path": "test/fixtures/_config.json",
    "content": "{\n\t\"author\": \"waldo\",\n\t\"favorites\": {\n\t\t\"food\": \"ice cream\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/hello.njk",
    "content": "Hello {{ name }}!\n"
  },
  {
    "path": "test/fixtures/post_render.ts",
    "content": "import { highlight } from 'hexo-util';\n\nconst code = [\n  'if tired && night:',\n  '  sleep()'\n].join('\\n');\n\nexport const content = [\n  '# Title',\n  '``` python',\n  code,\n  '```',\n  'some content',\n  '',\n  '## Another title',\n  '{% blockquote %}',\n  'quote content 1',\n  '{% endblockquote %}',\n  '',\n  '{% quote Hello World %}',\n  'quote content 2',\n  '{% endquote %}'\n].join('\\n');\n\nexport const expected = [\n  '<h1 id=\"Title\"><a href=\"#Title\" class=\"headerlink\" title=\"Title\"></a>Title</h1>',\n  highlight(code, {lang: 'python'}),\n  '\\n<p>some content</p>\\n',\n  '<h2 id=\"Another-title\"><a href=\"#Another-title\" class=\"headerlink\" title=\"Another title\"></a>Another title</h2>',\n  '<blockquote>',\n  '<p>quote content 1</p>\\n',\n  '</blockquote>\\n\\n',\n  '<blockquote><p>quote content 2</p>\\n',\n  '<footer><strong>Hello World</strong></footer></blockquote>'\n].join('');\n\nexport const expected_disable_nunjucks = [\n  '<h1 id=\"Title\"><a href=\"#Title\" class=\"headerlink\" title=\"Title\"></a>Title</h1>',\n  highlight(code, {lang: 'python'}),\n  '\\n<p>some content</p>\\n',\n  '<h2 id=\"Another-title\"><a href=\"#Another-title\" class=\"headerlink\" title=\"Another title\"></a>Another title</h2>',\n  '<p>{% blockquote %}<br>',\n  'quote content 1<br>',\n  '{% endblockquote %}</p>\\n',\n  '<p>{% quote Hello World %}<br>',\n  'quote content 2<br>',\n  '{% endquote %}</p>'\n].join('');\n\nexport const content_for_issue_3346 = [\n  '# Title',\n  '```',\n  '{% test1 %}',\n  '{{ test2 }}',\n  '```',\n  'some content',\n  '',\n  '## Another title',\n  '{% blockquote %}',\n  'quote content',\n  '{% endblockquote %}'\n].join('\\n');\n\nexport const expected_for_issue_3346 = [\n  '<h1 id=\"Title\"><a href=\"#Title\" class=\"headerlink\" title=\"Title\"></a>Title</h1>',\n  highlight('{% test1 %}\\n{{ test2 }}').replace(/{/g, '&#123;').replace(/}/g, '&#125;'), // Escaped by backtick_code_block\n  '\\n<p>some content</p>\\n',\n  '<h2 id=\"Another-title\"><a href=\"#Another-title\" class=\"headerlink\" title=\"Another title\"></a>Another title</h2>',\n  '<blockquote>',\n  '<p>quote content</p>\\n',\n  '</blockquote>'\n].join('');\n\nexport const content_for_issue_4460 = [\n  '```html',\n  '<body>',\n  '<!-- here goes the rest of the page -->',\n  '</body>',\n  '```'\n].join('\\n');\n"
  },
  {
    "path": "test/scripts/box/box.ts",
    "content": "import { join, sep } from 'path';\nimport { appendFile, mkdir, mkdirs, rename, rmdir, stat, unlink, writeFile } from 'hexo-fs';\nimport { hash, Pattern } from 'hexo-util';\nimport { spy, match, assert as sinonAssert } from 'sinon';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport Box from '../../../lib/box';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Box', () => {\n  const baseDir = join(__dirname, 'box_tmp');\n\n  const newBox = (path?, config?) => {\n    const hexo = new Hexo(baseDir, { silent: true });\n    hexo.config = Object.assign(hexo.config, config);\n    const base = path ? join(baseDir, path) : baseDir;\n    return new Box(hexo, base);\n  };\n\n  before(() => mkdir(baseDir));\n\n  after(() => rmdir(baseDir));\n\n  it('constructor - add trailing \"/\" to the base path', () => {\n    const box = newBox('foo');\n    box.base.should.eql(join(baseDir, 'foo') + sep);\n  });\n\n  it('addProcessor() - no pattern', () => {\n    const box = newBox();\n\n    box.addProcessor(() => 'test');\n\n    const p = box.processors[0];\n\n    p.pattern.match('').should.eql({});\n    p.process().should.eql('test');\n  });\n\n  it('addProcessor() - with regex', () => {\n    const box = newBox();\n\n    box.addProcessor(/^foo/, () => 'test');\n\n    const p = box.processors[0];\n\n    p.pattern.match('foobar').should.be.ok;\n    p.pattern.should.be.an.instanceof(Pattern);\n    p.process().should.eql('test');\n  });\n\n  it('addProcessor() - with pattern', () => {\n    const box = newBox();\n\n    box.addProcessor(new Pattern(/^foo/), () => 'test');\n\n    const p = box.processors[0];\n\n    p.pattern.match('foobar').should.be.ok;\n    p.pattern.should.be.an.instanceof(Pattern);\n    p.process().should.eql('test');\n  });\n\n  it('addProcessor() - no fn', () => {\n    const box = newBox();\n    // @ts-expect-error\n    should.throw(() => box.addProcessor('test'), 'fn must be a function');\n  });\n\n  it('process()', async () => {\n    const box = newBox('test');\n    const data: Record<string, any> = {};\n\n    box.addProcessor(file => {\n      data[file.path] = file;\n    });\n\n    await BluebirdPromise.all([\n      writeFile(join(box.base, 'a.txt'), 'a'),\n      writeFile(join(box.base, 'b', 'c.js'), 'c')\n    ]);\n\n    await box.process();\n\n    for (const [key, item] of Object.entries(data)) {\n      item.path.should.eql(key);\n      item.source.should.eql(join(box.base, key));\n      item.type.should.eql('create');\n      item.params.should.eql({});\n    }\n\n    await rmdir(box.base);\n  });\n\n  it('process() - do nothing if target does not exist', async () => {\n    const box = newBox('test');\n\n    return box.process();\n  });\n\n  it('process() - create', async () => {\n    const box = newBox('test');\n    const name = 'a.txt';\n    const path = join(box.base, name);\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await writeFile(path, 'a');\n    await box.process();\n\n    sinonAssert.calledWithMatch(processor, { type: 'create', path: name });\n\n    await rmdir(box.base);\n  });\n\n  it('process() - update (mtime changed and hash changed)', async () => {\n    const box = newBox('test');\n    const name = 'a.txt';\n    const path = join(box.base, name);\n    const cacheId = 'test/' + name;\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(path, 'a'),\n      box.Cache.insert({\n        _id: cacheId,\n        modified: 0,\n        hash: hash('b').toString('hex')\n      })\n    ]);\n    await box.process();\n\n    sinonAssert.calledWithMatch(processor, { type: 'update', path: name });\n\n    await rmdir(box.base);\n  });\n\n  it('process() - skip (mtime changed but hash matched)', async () => {\n    const box = newBox('test');\n    const name = 'a.txt';\n    const path = join(box.base, name);\n    const cacheId = 'test/' + name;\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await writeFile(path, 'a');\n    await stat(path);\n    await box.Cache.insert({\n      _id: cacheId,\n      modified: 0,\n      hash: hash('a').toString('hex')\n    });\n    await box.process();\n\n    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });\n\n    await rmdir(box.base);\n  });\n\n  it('process() - skip (hash changed but mtime matched)', async () => {\n    const box = newBox('test');\n    const name = 'a.txt';\n    const path = join(box.base, name);\n    const cacheId = 'test/' + name;\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await writeFile(path, 'a');\n    const stats = await stat(path);\n    await box.Cache.insert({\n      _id: cacheId,\n      modified: stats.mtime,\n      hash: hash('b').toString('hex')\n    });\n    await box.process();\n\n    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });\n\n    await rmdir(box.base);\n  });\n\n  it('process() - skip (mtime matched and hash matched)', async () => {\n    const box = newBox('test');\n    const name = 'a.txt';\n    const path = join(box.base, name);\n    const cacheId = 'test/' + name;\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await writeFile(path, 'a');\n    const stats = await stat(path);\n    await box.Cache.insert({\n      _id: cacheId,\n      modified: stats.mtime,\n      hash: hash('a').toString('hex')\n    });\n    await box.process();\n\n    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });\n\n    await rmdir(box.base);\n  });\n\n  it('process() - delete', async () => {\n    const box = newBox('test');\n    const cacheId = 'test/a.txt';\n\n    const processor = spy();\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      mkdirs(box.base),\n      box.Cache.insert({\n        _id: cacheId\n      })\n    ]);\n    await box.process();\n\n    sinonAssert.calledWith(processor, match.has('type', 'delete'));\n    processor.calledOnce.should.be.true;\n\n    await rmdir(box.base);\n  });\n\n  it('process() - params', async () => {\n    const box = newBox('test');\n    const path = join(box.base, 'posts', '123456');\n\n    const processor = spy();\n\n    box.addProcessor('posts/:id', processor);\n\n    await writeFile(path, 'a');\n    await box.process();\n\n    sinonAssert.calledWith(processor, match.has('params', match.has('id', '123456')));\n    processor.calledOnce.should.be.true;\n\n    await rmdir(box.base);\n  });\n\n  it('process() - handle null ignore', async () => {\n    const box = newBox('test', { ignore: null });\n    const data = {};\n\n    box.addProcessor(file => {\n      data[file.path] = file;\n    });\n\n    await writeFile(join(box.base, 'foo.txt'), 'foo');\n    await box.process();\n\n    data.should.have.all.keys(['foo.txt']);\n\n    await rmdir(box.base);\n  });\n\n  it('process() - error ignore - 1', async () => {\n    const box = newBox('test', { ignore: [null] });\n    box.options.ignored.should.eql([]);\n  });\n\n  it('process() - error ignore - 2', async () => {\n    const box = newBox('test', { ignore: [111] });\n    box.options.ignored.should.eql([]);\n  });\n\n  it('process() - skip files if they match a glob epression in ignore', async () => {\n    const box = newBox('test', { ignore: '**/ignore_me' });\n    const data: object = {};\n\n    box.addProcessor(file => {\n      data[file.path] = file;\n    });\n\n    await BluebirdPromise.all([\n      writeFile(join(box.base, 'foo.txt'), 'foo'),\n      writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me')\n    ]);\n    await box.process();\n\n    data.should.have.all.keys(['foo.txt']);\n\n    await rmdir(box.base);\n  });\n\n  it('process() - skip files if they match any of the glob expressions in ignore', async () => {\n    const box = newBox('test', { ignore: ['**/ignore_me', '**/ignore_me_too.txt'] });\n    const data = {};\n\n    box.addProcessor(file => {\n      data[file.path] = file;\n    });\n\n    await BluebirdPromise.all([\n      writeFile(join(box.base, 'foo.txt'), 'foo'),\n      writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me'),\n      writeFile(join(box.base, 'ignore_me_too.txt'), 'ignore_me_too')\n    ]);\n    await box.process();\n\n    data.should.have.all.keys(['foo.txt']);\n\n    await rmdir(box.base);\n  });\n\n  it('watch() - create', async () => {\n    const box = newBox('test');\n    const path = 'a.txt';\n    const src = join(box.base, path);\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await writeFile(src, 'a');\n    await box.watch();\n    box.isWatching().should.be.true;\n    await BluebirdPromise.delay(500);\n\n    sinonAssert.calledWithMatch(processor.firstCall, {\n      source: src,\n      path: path,\n      type: 'create',\n      params: {}\n    });\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - update', async () => {\n    const box = newBox('test');\n    const path = 'a.txt';\n    const src = join(box.base, path);\n    const cacheId = 'test/' + path;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src, 'a'),\n      Cache.insert({_id: cacheId})\n    ]);\n    await box.watch();\n    await appendFile(src, 'b');\n    await BluebirdPromise.delay(500);\n\n    sinonAssert.calledWithMatch(processor.lastCall, {\n      source: src,\n      path: path,\n      type: 'update',\n      params: {}\n    });\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - delete', async () => {\n    const box = newBox('test');\n    const path = 'a.txt';\n    const src = join(box.base, path);\n    const cacheId = 'test/' + path;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src, 'a'),\n      Cache.insert({_id: cacheId})\n    ]);\n    await box.watch();\n    await unlink(src);\n    await BluebirdPromise.delay(500);\n\n    sinonAssert.calledWithMatch(processor.lastCall, {\n      source: src,\n      path: path,\n      type: 'delete',\n      params: {}\n    });\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - rename file', async () => {\n    const box = newBox('test');\n    const path = 'a.txt';\n    const src = join(box.base, path);\n    const newPath = 'b.txt';\n    const newSrc = join(box.base, newPath);\n    const cacheId = 'test/' + path;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src, 'a'),\n      Cache.insert({_id: cacheId})\n    ]);\n    await box.watch();\n    await rename(src, newSrc);\n    await BluebirdPromise.delay(500);\n\n    for (const [file] of processor.args.slice(-2)) {\n      switch (file.type) {\n        case 'create':\n          file.source.should.eql(newSrc);\n          file.path.should.eql(newPath);\n          break;\n\n        case 'delete':\n          file.source.should.eql(src);\n          file.path.should.eql(path);\n          break;\n      }\n    }\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - rename folder', async () => {\n    const box = newBox('test');\n    const path = 'a/b.txt';\n    const src = join(box.base, path);\n    const newPath = 'b/b.txt';\n    const newSrc = join(box.base, newPath);\n    const cacheId = 'test/' + path;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src, 'a'),\n      Cache.insert({_id: cacheId})\n    ]);\n    await box.watch();\n    await rename(join(box.base, 'a'), join(box.base, 'b'));\n    await BluebirdPromise.delay(500);\n\n    for (const [file] of processor.args.slice(-2)) {\n      switch (file.type) {\n        case 'create':\n          file.source.should.eql(newSrc);\n          file.path.should.eql(newPath);\n          break;\n\n        case 'delete':\n          file.source.should.eql(src);\n          file.path.should.eql(path);\n          break;\n      }\n    }\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - update with simple \"ignore\" option', async () => {\n    const box = newBox('test', {ignore: '**/ignore_me/**'});\n    const path1 = 'a.txt';\n    const path2 = 'b.txt';\n    const src1 = join(box.base, path1);\n    const src2 = join(box.base, 'ignore_me', path2);\n    const cacheId1 = 'test/' + path1;\n    const cacheId2 = 'test/ignore_me/' + path2;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src1, 'a'),\n      Cache.insert({_id: cacheId1})\n    ]);\n    await BluebirdPromise.all([\n      writeFile(src2, 'b'),\n      Cache.insert({_id: cacheId2})\n    ]);\n    await box.watch();\n    await appendFile(src1, 'aaa');\n    await BluebirdPromise.delay(500);\n\n    const file = processor.lastCall.args[0];\n\n    file.should.deep.include({\n      source: src1,\n      path: path1,\n      type: 'update',\n      params: {}\n    });\n\n    await appendFile(src2, 'bbb');\n    await BluebirdPromise.delay(500);\n\n    const file2 = processor.lastCall.args[0];\n    file2.should.eql(file); // not changed\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - update with complex \"ignore\" option', async () => {\n    const box = newBox('test', {ignore: ['**/ignore_me/**', '**/ignore_me_too.txt']});\n    const path1 = 'a.txt';\n    const path2 = 'b.txt';\n    const path3 = 'ignore_me_too.txt';\n    const src1 = join(box.base, path1);\n    const src2 = join(box.base, 'ignore_me', path2);\n    const src3 = join(box.base, path3);\n    const cacheId1 = 'test/' + path1;\n    const cacheId2 = 'test/ignore_me/' + path2;\n    const cacheId3 = 'test/' + path3;\n    const { Cache } = box;\n    const processor = spy();\n\n    box.addProcessor(processor);\n\n    await BluebirdPromise.all([\n      writeFile(src1, 'a'),\n      Cache.insert({_id: cacheId1})\n    ]);\n    await BluebirdPromise.all([\n      writeFile(src2, 'b'),\n      Cache.insert({_id: cacheId2})\n    ]);\n    await BluebirdPromise.all([\n      writeFile(src3, 'c'),\n      Cache.insert({_id: cacheId3})\n    ]);\n    await box.watch();\n    await appendFile(src1, 'aaa');\n    await BluebirdPromise.delay(500);\n\n    const file = processor.lastCall.args[0];\n\n    file.should.deep.include({\n      source: src1,\n      path: path1,\n      type: 'update',\n      params: {}\n    });\n\n    await appendFile(src2, 'bbb');\n    await BluebirdPromise.delay(500);\n\n    processor.lastCall.args[0].should.eql(file); // not changed\n\n    await appendFile(src3, 'ccc');\n    await BluebirdPromise.delay(500);\n\n    processor.lastCall.args[0].should.eql(file); // not changed\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('watch() - watcher has started', async () => {\n    const box = newBox();\n\n    await box.watch();\n\n    await box.watch().then(() => {\n      should.fail('Return value must be rejected');\n    }, err => {\n      err.should.property('message', 'Watcher has already started.');\n    });\n\n    box.unwatch();\n  });\n\n  it('watch() - run process() before start watching', async () => {\n    const box = newBox('test');\n    const data: string[] = [];\n\n    box.addProcessor(file => {\n      data.push(file.path);\n    });\n\n    await BluebirdPromise.all([\n      writeFile(join(box.base, 'a.txt'), 'a'),\n      writeFile(join(box.base, 'b', 'c.js'), 'c')\n    ]);\n    await box.watch();\n    data.should.have.members(['a.txt', 'b/c.js']);\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('unwatch()', async () => {\n    const box = newBox('test');\n    const processor = spy();\n\n    await box.watch();\n    box.addProcessor(processor);\n    box.unwatch();\n\n    await writeFile(join(box.base, 'a.txt'), 'a');\n    processor.called.should.be.false;\n\n    box.unwatch();\n    await rmdir(box.base);\n  });\n\n  it('isWatching()', async () => {\n    const box = newBox();\n\n    box.isWatching().should.be.false;\n\n    await box.watch();\n    box.isWatching().should.be.true;\n\n    box.unwatch();\n    box.isWatching().should.be.false;\n\n    box.unwatch();\n  });\n\n  it('processBefore & processAfter events', async () => {\n    const box = newBox('test');\n\n    const beforeSpy = spy();\n    const afterSpy = spy();\n\n    box.on('processBefore', beforeSpy);\n    box.on('processAfter', afterSpy);\n\n    await writeFile(join(box.base, 'a.txt'), 'a');\n    await box.process();\n\n    sinonAssert.calledWithMatch(beforeSpy, { type: 'create', path: 'a.txt' });\n    sinonAssert.calledWithMatch(afterSpy, { type: 'create', path: 'a.txt' });\n    beforeSpy.calledOnce.should.be.true;\n    afterSpy.calledOnce.should.be.true;\n\n    await rmdir(box.base);\n  });\n});\n"
  },
  {
    "path": "test/scripts/box/file.ts",
    "content": "import { join } from 'path';\nimport { rmdir, stat, statSync, writeFile } from 'hexo-fs';\nimport { load } from 'js-yaml';\nimport Hexo from '../../../lib/hexo';\nimport Box from '../../../lib/box';\n\ndescribe('File', () => {\n  const hexo = new Hexo(__dirname);\n  const box = new Box(hexo, join(hexo.base_dir, 'file_test'));\n  const { File } = box;\n\n  const body = [\n    'name:',\n    '  first: John',\n    '  last: Doe',\n    '',\n    'age: 23',\n    '',\n    'list:',\n    '- Apple',\n    '- Banana'\n  ].join('\\n');\n\n  const obj = load(body);\n  const path = 'test.yml';\n\n  const makeFile = (path, props) => {\n    return new File(Object.assign({\n      source: join(box.base, path),\n      path\n    }, props));\n  };\n\n  const file = makeFile(path, {\n    source: join(box.base, path),\n    path,\n    type: 'create',\n    params: {foo: 'bar'}\n  });\n\n  // NOTE: Do not use `arrow function` here.\n  //       See https://mochajs.org/#arrow-functions\n  before(async function() {\n    this.timeout(20000);\n    await Promise.all([\n      writeFile(file.source, body),\n      hexo.init()\n    ]);\n    stat(file.source);\n  });\n\n  after(() => rmdir(box.base));\n\n  it('read()', async () => {\n    const result = await file.read();\n    result.should.eql(body);\n  });\n\n  it('readSync()', () => {\n    file.readSync().should.eql(body);\n  });\n\n  it('stat()', async () => {\n    const stats = await Promise.all([\n      stat(file.source),\n      file.stat()\n    ]);\n    stats[0].should.eql(stats[1]);\n  });\n\n  it('statSync()', () => {\n    file.statSync().should.eql(statSync(file.source));\n  });\n\n  it('render()', async () => {\n    const result = await file.render();\n    result.should.eql(obj);\n  });\n\n  it('renderSync()', () => {\n    file.renderSync().should.eql(obj);\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/clean.ts",
    "content": "import { exists, mkdirs, unlink, writeFile } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport cleanConsole from '../../../lib/plugins/console/clean';\ntype OriginalParams = Parameters<typeof cleanConsole>;\ntype OriginalReturn = ReturnType<typeof cleanConsole>;\n\ndescribe('clean', () => {\n  let hexo: Hexo, clean: (...args: OriginalParams) => OriginalReturn;\n\n  beforeEach(() => {\n    hexo = new Hexo(__dirname, {silent: true});\n    clean = cleanConsole.bind(hexo);\n  });\n\n\n  it('delete database', async () => {\n    const dbPath = hexo.database.options.path;\n\n    await writeFile(dbPath, '');\n    await clean();\n    const exist = await exists(dbPath);\n\n    exist.should.be.false;\n  });\n\n  it('delete public folder', async () => {\n    const publicDir = hexo.public_dir;\n\n    await mkdirs(publicDir);\n    await clean();\n    const exist = await exists(publicDir);\n\n    exist.should.be.false;\n  });\n\n  it('execute corresponding filter', async () => {\n    const extraDbPath = hexo.database.options.path + '.tmp';\n\n    hexo.extend.filter.register('after_clean', () => {\n      return unlink(extraDbPath);\n    });\n\n    await writeFile(extraDbPath, '');\n    await clean();\n    const exist = await exists(extraDbPath);\n\n    exist.should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/config.ts",
    "content": "import { mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs';\nimport { join } from 'path';\nimport { load } from 'js-yaml';\nimport { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport configConsole from '../../../lib/plugins/console/config';\ntype OriginalParams = Parameters<typeof configConsole>;\ntype OriginalReturn = ReturnType<typeof configConsole>;\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('config', () => {\n  const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true});\n  const config: (...args: OriginalParams) => OriginalReturn = configConsole.bind(hexo);\n\n  before(async () => {\n    await mkdirs(hexo.base_dir);\n    hexo.init();\n  });\n\n  beforeEach(() => writeFile(hexo.config_path, ''));\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('read all config', async () => {\n    const logStub = stub(console, 'log');\n\n    try {\n      await config({_: []});\n    } finally {\n      logStub.restore();\n    }\n\n    sinonAssert.calledWith(logStub, hexo.config);\n  });\n\n  it('read config', async () => {\n    const logStub = stub(console, 'log');\n\n    try {\n      await config({_: ['title']});\n    } finally {\n      logStub.restore();\n    }\n\n    sinonAssert.calledWith(logStub, hexo.config.title);\n  });\n\n  it('read nested config', async () => {\n    const logStub = stub(console, 'log');\n\n    try {\n      (hexo.config as any).server = {\n        port: 12345\n      };\n\n      await config({_: ['server.port']});\n      sinonAssert.calledWith(logStub, (hexo.config as any).server.port);\n    } finally {\n      delete(hexo.config as any).server;\n      logStub.restore();\n    }\n  });\n\n  async function writeConfig(...args) {\n    await config({_: args});\n    const content = await readFile(hexo.config_path);\n    return load(content) as any;\n  }\n\n  it('write config', async () => {\n    const config = await writeConfig('title', 'My Blog');\n    config.title.should.eql('My Blog');\n  });\n\n  it('write config: number', async () => {\n    const config = await writeConfig('server.port', '5000');\n    config.server.port.should.eql(5000);\n  });\n\n  it('write config: false', async () => {\n    const config = await writeConfig('post_asset_folder', 'false');\n    config.post_asset_folder.should.be.false;\n  });\n\n  it('write config: true', async () => {\n    const config = await writeConfig('post_asset_folder', 'true');\n    config.post_asset_folder.should.be.true;\n  });\n\n  it('write config: null', async () => {\n    const config = await writeConfig('language', 'null');\n    should.not.exist(config.language);\n  });\n\n  it('write config: undefined', async () => {\n    const config = await writeConfig('meta_generator', 'undefined');\n    should.not.exist(config.meta_generator);\n  });\n\n  it('write config: json', async () => {\n    const configPath = join(hexo.base_dir, '_config.json');\n    hexo.config_path = join(hexo.base_dir, '_config.json');\n\n    await writeFile(configPath, '{}');\n    await config({_: ['title', 'My Blog']});\n\n    return readFile(configPath).then(content => {\n      const json = JSON.parse(content);\n\n      json.title.should.eql('My Blog');\n\n      hexo.config_path = join(hexo.base_dir, '_config.yml');\n      return unlink(configPath);\n    });\n  });\n\n  it('create config if not exist', async () => {\n    await unlink(hexo.config_path);\n    const config = await writeConfig('subtitle', 'Hello world');\n    config.subtitle.should.eql('Hello world');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/deploy.ts",
    "content": "import { exists, mkdirs, readFile, rmdir, writeFile } from 'hexo-fs';\nimport { join } from 'path';\nimport { spy, stub, assert as sinonAssert } from 'sinon';\nimport chai from 'chai';\nconst should = chai.should();\nimport Hexo from '../../../lib/hexo';\nimport deployConsole from '../../../lib/plugins/console/deploy';\ntype OriginalParams = Parameters<typeof deployConsole>;\ntype OriginalReturn = ReturnType<typeof deployConsole>;\n\ndescribe('deploy', () => {\n  const hexo = new Hexo(join(__dirname, 'deploy_test'), { silent: true });\n  const deploy: (...args: OriginalParams) => OriginalReturn = deployConsole.bind(hexo);\n\n  before(async () => {\n    await mkdirs(hexo.public_dir);\n    hexo.init();\n  });\n\n  beforeEach(() => {\n    hexo.config.deploy = { type: 'foo' };\n    hexo.extend.deployer.register('foo', () => { });\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('no deploy config', () => {\n    delete(hexo.config as any).deploy;\n\n    const logStub = stub(console, 'log');\n\n    try {\n      should.not.exist(deploy({ test: true }));\n    } finally {\n      logStub.restore();\n    }\n\n    sinonAssert.calledWithMatch(\n      logStub,\n      'You should configure deployment settings in _config.yml first!'\n    );\n  });\n\n  it('single deploy setting', async () => {\n    hexo.config.deploy = {\n      type: 'foo',\n      foo: 'bar'\n    };\n\n    const deployer = spy();\n    const beforeListener = spy();\n    const afterListener = spy();\n\n    hexo.once('deployBefore', beforeListener);\n    hexo.once('deployAfter', afterListener);\n    hexo.extend.deployer.register('foo', deployer);\n\n    await deploy({ foo: 'foo', bar: 'bar' });\n    deployer.calledOnce.should.be.true;\n    beforeListener.calledOnce.should.be.true;\n    afterListener.calledOnce.should.be.true;\n\n    sinonAssert.calledWith(deployer, {\n      type: 'foo',\n      foo: 'foo',\n      bar: 'bar'\n    });\n  });\n\n  it('multiple deploy setting', async () => {\n    const deployer1 = spy();\n    const deployer2 = spy();\n\n    hexo.config.deploy = [\n      { type: 'foo', foo: 'foo' },\n      { type: 'bar', bar: 'bar' }\n    ];\n\n    hexo.extend.deployer.register('foo', deployer1);\n    hexo.extend.deployer.register('bar', deployer2);\n\n    await deploy({ test: true });\n    deployer1.calledOnce.should.be.true;\n    deployer2.calledOnce.should.be.true;\n\n    sinonAssert.calledWith(deployer1, {\n      type: 'foo',\n      foo: 'foo',\n      test: true\n    });\n    sinonAssert.calledWith(deployer2, {\n      type: 'bar',\n      bar: 'bar',\n      test: true\n    });\n  });\n\n  it('deployer not found', async () => {\n    const logSpy = spy();\n    const hexo = new Hexo(join(__dirname, 'deploy_test'));\n    hexo.log.error = logSpy;\n\n    const deploy: (...args: OriginalParams) => OriginalReturn = deployConsole.bind(hexo);\n\n    hexo.extend.deployer.register('baz', () => { });\n    hexo.config.deploy = {\n      type: 'foo',\n      foo: 'bar'\n    };\n\n    await deploy({});\n\n    logSpy.called.should.be.true;\n    logSpy.args[0][0].should.contains('Deployer not found: %s');\n    logSpy.args[0][1].should.contains('foo');\n  });\n\n  it('generate', async () => {\n    await writeFile(join(hexo.source_dir, 'test.txt'), 'test');\n    await deploy({ generate: true });\n    const content = await readFile(join(hexo.public_dir, 'test.txt'));\n\n    content.should.eql('test');\n\n    await rmdir(hexo.source_dir);\n  });\n\n  it('run generate if public directory not exist', async () => {\n    await rmdir(hexo.public_dir);\n    await deploy({});\n    const exist = await exists(hexo.public_dir);\n\n    exist.should.be.true;\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/generate.ts",
    "content": "import { join } from 'path';\nimport { emptyDir, exists, mkdirs, readFile, rmdir, stat, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport { spy } from 'sinon';\nimport chai from 'chai';\nconst should = chai.should();\nimport Hexo from '../../../lib/hexo';\nimport generateConsole from '../../../lib/plugins/console/generate';\ntype OriginalParams = Parameters<typeof generateConsole>;\ntype OriginalReturn = ReturnType<typeof generateConsole>;\n\ndescribe('generate', () => {\n  let hexo: Hexo, generate: (...args: OriginalParams) => OriginalReturn;\n\n  beforeEach(async function() {\n    this.timeout(5000);\n    hexo = new Hexo(join(__dirname, 'generate_test'), {silent: true});\n    generate = generateConsole.bind(hexo);\n\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n  });\n\n  afterEach(async () => {\n    const exist = await exists(hexo.base_dir);\n    if (exist) {\n      await emptyDir(hexo.base_dir);\n      await rmdir(hexo.base_dir);\n    }\n  });\n\n  const testGenerate = async (options?: any) => {\n    await BluebirdPromise.all([\n      // Add some source files\n      writeFile(join(hexo.source_dir, 'test.txt'), 'test'),\n      writeFile(join(hexo.source_dir, 'faz', 'yo.txt'), 'yoooo'),\n      // Add some files to public folder\n      writeFile(join(hexo.public_dir, 'foo.txt'), 'foo'),\n      writeFile(join(hexo.public_dir, 'bar', 'boo.txt'), 'boo'),\n      writeFile(join(hexo.public_dir, 'faz', 'yo.txt'), 'yo')\n    ]);\n    await generate(options);\n\n    const result = await BluebirdPromise.all([\n      readFile(join(hexo.public_dir, 'test.txt')),\n      readFile(join(hexo.public_dir, 'faz', 'yo.txt')),\n      exists(join(hexo.public_dir, 'foo.txt')),\n      exists(join(hexo.public_dir, 'bar', 'boo.txt'))\n    ]);\n    // Check the new file\n    result[0].should.eql('test');\n    // Check the updated file\n    result[1].should.eql('yoooo');\n    // Old files should not be deleted\n    result[2].should.be.true;\n    result[3].should.be.true;\n  };\n\n  it('default', () => testGenerate());\n\n  it('public_dir is not a directory', async () => {\n    await BluebirdPromise.all([\n      // Add some source files\n      writeFile(join(hexo.source_dir, 'test.txt'), 'test'),\n      // Add some files to public folder\n      writeFile(join(hexo.public_dir, 'foo.txt'), 'foo')\n    ]);\n    const old = hexo.public_dir;\n    hexo.public_dir = join(hexo.public_dir, 'foo.txt');\n    try {\n      await generate();\n    } catch (e) {\n      e.message.split(' ').slice(1).join(' ').should.eql('is not a directory');\n    }\n    hexo.public_dir = old;\n  });\n\n  it('write file if not exist', async () => {\n    const src = join(hexo.source_dir, 'test.txt');\n    const dest = join(hexo.public_dir, 'test.txt');\n    const content = 'test';\n\n    // Add some source files\n    await writeFile(src, content);\n\n    // First generation\n    await generate();\n\n    // Delete generated files\n    await unlink(dest);\n\n    // Second generation\n    await generate();\n\n    const result = await readFile(dest);\n\n    result.should.eql(content);\n\n    // Remove source files and generated files\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('don\\'t write if file unchanged', async () => {\n    const src = join(hexo.source_dir, 'test.txt');\n    const dest = join(hexo.public_dir, 'test.txt');\n    const content = 'test';\n    const newContent = 'newtest';\n\n    // Add some source files\n    await writeFile(src, content);\n\n    // First generation\n    await generate();\n\n    // Change the generated file\n    await writeFile(dest, newContent);\n\n    // Second generation\n    await generate();\n\n    // Read the generated file\n    const result = await readFile(dest);\n\n    // Make sure the generated file didn't change\n    result.should.eql(newContent);\n\n    // Remove source files and generated files\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('force regenerate', async () => {\n    const src = join(hexo.source_dir, 'test.txt');\n    const dest = join(hexo.public_dir, 'test.txt');\n    const content = 'test';\n\n    await writeFile(src, content);\n\n    // First generation\n    await generate();\n\n    // Read file status\n    let stats = await stat(dest);\n    const mtime = stats.mtime.getTime();\n\n    await BluebirdPromise.delay(1000);\n\n    // Force regenerate\n    await generate({ force: true });\n    stats = await stat(dest);\n\n    stats.mtime.getTime().should.above(mtime);\n\n    // Remove source files and generated files\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('watch - update', async () => {\n    const src = join(hexo.source_dir, 'test.txt');\n    const dest = join(hexo.public_dir, 'test.txt');\n    const content = 'test';\n\n    await testGenerate({ watch: true });\n\n    // Update the file\n    await writeFile(src, content);\n\n    await BluebirdPromise.delay(300);\n\n    // Check the updated file\n    const result = await readFile(dest);\n    result.should.eql(content);\n\n    // Stop watching\n    hexo.unwatch();\n    await BluebirdPromise.delay(300);\n  });\n\n  it('deploy', async () => {\n    const deployer = spy();\n\n    hexo.extend.deployer.register('test', deployer);\n\n    hexo.config.deploy = {\n      type: 'test'\n    };\n\n    await generate({ deploy: true });\n\n    deployer.calledOnce.should.be.true;\n  });\n\n  it('update theme source files', async () => {\n    // Add some source files\n    await BluebirdPromise.all([\n      // Add some source files\n      writeFile(join(hexo.theme_dir, 'source', 'a.txt'), 'a'),\n      writeFile(join(hexo.theme_dir, 'source', 'b.txt'), 'b'),\n      writeFile(join(hexo.theme_dir, 'source', 'c.njk'), 'c')\n    ]);\n    await BluebirdPromise.delay(300);\n    await generate();\n\n    // Update source file\n    await BluebirdPromise.all([\n      writeFile(join(hexo.theme_dir, 'source', 'b.txt'), 'bb'),\n      writeFile(join(hexo.theme_dir, 'source', 'c.njk'), 'cc')\n    ]);\n    await BluebirdPromise.delay(300);\n    // Generate again\n    await generate();\n\n    await BluebirdPromise.delay(300);\n\n    // Read the updated source file\n    const result = await BluebirdPromise.all([\n      readFile(join(hexo.public_dir, 'b.txt')),\n      readFile(join(hexo.public_dir, 'c.html'))\n    ]);\n\n    result[0].should.eql('bb');\n    result[1].should.eql('cc');\n  });\n\n  it('proceeds after error when bail option is not set', async () => {\n    hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception')));\n    hexo.extend.generator.register('test_page', () =>\n      [\n        {\n          path: 'testing-path',\n          layout: 'post',\n          data: {}\n        }\n      ]\n    );\n\n    await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post');\n    return generate();\n  });\n\n  it('proceeds after error when bail option is set to false', async () => {\n    hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception')));\n    hexo.extend.generator.register('test_page', () =>\n      [\n        {\n          path: 'testing-path',\n          layout: 'post',\n          data: {}\n        }\n      ]\n    );\n\n    await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post');\n    return generate({ bail: false });\n  });\n\n  it('breaks after error when bail option is set to true', async () => {\n    hexo.extend.renderer.register('err', 'html', () => BluebirdPromise.reject(new Error('Testing unhandled exception')));\n    hexo.extend.generator.register('test_page', () =>\n      [\n        {\n          path: 'testing-path',\n          layout: 'post',\n          data: {}\n        }\n      ]\n    );\n\n    await writeFile(join(hexo.theme_dir, 'layout', 'post.err'), 'post');\n\n    return generate({ bail: true }).then(() => {\n      should.fail('Return value must be rejected');\n    }, err => {\n      err.should.property('message', 'Testing unhandled exception');\n    });\n  });\n\n  it('should generate all files when bail option is set to true and no errors', async () => {\n    // Test cases for hexojs/hexo#4499\n    hexo.extend.generator.register('resource', () =>\n      [\n        {\n          path: 'resource-1',\n          data: 'string'\n        },\n        {\n          path: 'resource-2',\n          data: {}\n        },\n        {\n          path: 'resource-3',\n          data: () => BluebirdPromise.resolve(Buffer.from('string'))\n        }\n      ]\n    );\n    return generate({ bail: true });\n  });\n\n  it('should generate all files even when concurrency is set', async () => {\n    await generate({ concurrency: '1' });\n    return generate({ concurrency: '2' });\n  });\n});\n\n// #3975 workaround for Windows\ndescribe('generate - watch (delete)', () => {\n  const hexo = new Hexo(join(__dirname, 'generate_test'), {silent: true});\n  const generate: (...args: OriginalParams) => OriginalReturn = generateConsole.bind(hexo);\n\n  beforeEach(async () => {\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n  });\n\n  afterEach(async () => {\n    const exist = await exists(hexo.base_dir);\n    if (exist) {\n      await emptyDir(hexo.base_dir);\n      await BluebirdPromise.delay(500);\n      await rmdir(hexo.base_dir);\n    }\n  });\n\n  const testGenerate = async options => {\n    await BluebirdPromise.all([\n      // Add some source files\n      writeFile(join(hexo.source_dir, 'test.txt'), 'test'),\n      writeFile(join(hexo.source_dir, 'faz', 'yo.txt'), 'yoooo'),\n      // Add some files to public folder\n      writeFile(join(hexo.public_dir, 'foo.txt'), 'foo'),\n      writeFile(join(hexo.public_dir, 'bar', 'boo.txt'), 'boo'),\n      writeFile(join(hexo.public_dir, 'faz', 'yo.txt'), 'yo')\n    ]);\n    await generate(options);\n\n    const result = await BluebirdPromise.all([\n      readFile(join(hexo.public_dir, 'test.txt')),\n      readFile(join(hexo.public_dir, 'faz', 'yo.txt')),\n      exists(join(hexo.public_dir, 'foo.txt')),\n      exists(join(hexo.public_dir, 'bar', 'boo.txt'))\n    ]);\n    // Check the new file\n    result[0].should.eql('test');\n    // Check the updated file\n    result[1].should.eql('yoooo');\n    // Old files should not be deleted\n    result[2].should.be.true;\n    result[3].should.be.true;\n  };\n\n  it('watch - delete', async () => {\n    await testGenerate({ watch: true });\n\n    await unlink(join(hexo.source_dir, 'test.txt'));\n    await BluebirdPromise.delay(500);\n\n    const exist = await exists(join(hexo.public_dir, 'test.txt'));\n    exist.should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list.ts",
    "content": "import { spy, stub, assert as sinonAssert, SinonSpy } from 'sinon';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport listConsole from '../../../lib/plugins/console/list';\ntype OriginalParams = Parameters<typeof listConsole>;\ntype OriginalReturn = ReturnType<typeof listConsole>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo(__dirname);\n\n  it('no args', () => {\n    hexo.call = spy();\n\n    const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo);\n\n    list({ _: [''] });\n\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._[0].should.eql('list');\n  });\n\n  it('has args', async () => {\n    const logStub = stub(console, 'log');\n\n    hexo.load = () => BluebirdPromise.resolve();\n\n    const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo);\n\n    await list({ _: ['page'] });\n\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'No pages.');\n    logStub.restore();\n  });\n\n  it('list type not found', () => {\n    hexo.call = spy();\n\n    const list: (...args: OriginalParams) => OriginalReturn = listConsole.bind(hexo);\n\n    list({ _: ['test'] });\n\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._[0].should.eql('list');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list_categories.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport listCategory from '../../../lib/plugins/console/list/category';\ntype OriginalParams = Parameters<typeof listCategory>;\ntype OriginalReturn = ReturnType<typeof listCategory>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const listCategories: (...args: OriginalParams) => OriginalReturn = listCategory.bind(hexo);\n\n  let logStub;\n\n  before(() => { logStub = stub(console, 'log'); });\n\n  afterEach(() => { logStub.reset(); });\n\n  after(() => { logStub.restore(); });\n\n  it('no categories', () => {\n    listCategories();\n    sinonAssert.calledWithMatch(logStub, 'Name');\n    sinonAssert.calledWithMatch(logStub, 'Posts');\n    sinonAssert.calledWithMatch(logStub, 'No categories.');\n  });\n\n  it('categories', async () => {\n    const posts = [\n      {source: 'foo', slug: 'foo', title: 'Its', date: 1e8},\n      {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1},\n      {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1}\n    ];\n\n    await hexo.init();\n    const output = await Post.insert(posts);\n    await BluebirdPromise.each([\n      ['foo'],\n      ['baz'],\n      ['baz']\n    ], (tags, i) => output[i].setCategories(tags));\n    await hexo.locals.invalidate();\n    listCategories();\n    sinonAssert.calledWithMatch(logStub, 'Name');\n    sinonAssert.calledWithMatch(logStub, 'Posts');\n    sinonAssert.calledWithMatch(logStub, 'baz');\n    sinonAssert.calledWithMatch(logStub, 'foo');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list_page.ts",
    "content": "import { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport listPage from '../../../lib/plugins/console/list/page';\ntype OriginalParams = Parameters<typeof listPage>;\ntype OriginalReturn = ReturnType<typeof listPage>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo();\n  const Page = hexo.model('Page');\n  const listPages: (...args: OriginalParams) => OriginalReturn = listPage.bind(hexo);\n\n  hexo.config.permalink = ':title/';\n\n  let logStub;\n\n  before(() => { logStub = stub(console, 'log'); });\n\n  afterEach(() => { logStub.reset(); });\n\n  after(() => { logStub.restore(); });\n\n  it('no page', () => {\n    listPages();\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'No pages.');\n  });\n\n  it('page', async () => {\n    await Page.insert({\n      source: 'foo',\n      title: 'Hello World',\n      path: 'bar'\n    });\n    listPages();\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'Hello World');\n    sinonAssert.calledWithMatch(logStub, 'foo');\n  });\n\n  it('page with unicode', async () => {\n    await Page.insert({\n      source: 'foo',\n      title: '\\u0100',\n      path: 'bar'\n    });\n    listPages();\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, '\\u0100');\n    sinonAssert.calledWithMatch(logStub, 'foo');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list_post.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport listPost from '../../../lib/plugins/console/list/post';\ntype OriginalParams = Parameters<typeof listPost>;\ntype OriginalReturn = ReturnType<typeof listPost>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const listPosts: (...args: OriginalParams) => OriginalReturn = listPost.bind(hexo);\n\n  let logStub;\n\n  before(() => { logStub = stub(console, 'log'); });\n\n  afterEach(() => { logStub.reset(); });\n\n  after(() => { logStub.restore(); });\n\n  it('no post', () => {\n    listPosts();\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'Category');\n    sinonAssert.calledWithMatch(logStub, 'Tags');\n    sinonAssert.calledWithMatch(logStub, 'No posts.');\n  });\n\n  it('post', async () => {\n    const posts = [\n      {source: 'foo', slug: 'foo', title: 'Its', date: 1e8},\n      {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1},\n      {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1}\n    ];\n\n    const tags = [\n      ['foo'],\n      ['baz'],\n      ['baz']\n    ];\n\n    await hexo.init();\n    const output = await Post.insert(posts);\n    await BluebirdPromise.each(tags, (tags, i) => output[i].setTags(tags));\n    await hexo.locals.invalidate();\n\n    listPosts();\n    sinonAssert.calledWithMatch(logStub, 'Date');\n    sinonAssert.calledWithMatch(logStub, 'Title');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'Category');\n    sinonAssert.calledWithMatch(logStub, 'Tags');\n    for (let i = 0; i < posts.length; i++) {\n      sinonAssert.calledWithMatch(logStub, posts[i].source);\n      sinonAssert.calledWithMatch(logStub, posts[i].slug);\n      sinonAssert.calledWithMatch(logStub, posts[i].title);\n      sinonAssert.calledWithMatch(logStub, tags[i][0]);\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list_route.ts",
    "content": "import { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport listRoute from '../../../lib/plugins/console/list/route';\ntype OriginalParams = Parameters<typeof listRoute>;\ntype OriginalReturn = ReturnType<typeof listRoute>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo(__dirname);\n\n  const listRoutes: (...args: OriginalParams) => OriginalReturn = listRoute.bind(hexo);\n  const { route } = hexo;\n\n  let logStub;\n\n  before(() => { logStub = stub(console, 'log'); });\n\n  afterEach(() => { logStub.reset(); });\n\n  after(() => { logStub.restore(); });\n\n  it('no route', () => {\n    listRoutes();\n    sinonAssert.calledWithMatch(logStub, 'Total: 0');\n  });\n\n  it('route', async () => {\n    route.set('test', 'foo');\n\n    listRoutes();\n    sinonAssert.calledWithMatch(logStub, 'Total: 1');\n    route.remove('test');\n  });\n\n  it('route with nodes', async () => {\n    route.set('test0/test1', 'foo');\n\n    listRoutes();\n    sinonAssert.calledWithMatch(logStub, 'Total: 1');\n    sinonAssert.calledWithMatch(logStub, '└─┬ test0');\n    sinonAssert.calledWithMatch(logStub, '  └── test1');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/list_tags.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport { stub, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport listTag from '../../../lib/plugins/console/list/tag';\ntype OriginalParams = Parameters<typeof listTag>;\ntype OriginalReturn = ReturnType<typeof listTag>;\n\ndescribe('Console list', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const listTags: (...args: OriginalParams) => OriginalReturn = listTag.bind(hexo);\n\n  hexo.config.permalink = ':title/';\n\n  let logStub;\n\n  before(() => { logStub = stub(console, 'log'); });\n\n  afterEach(() => { logStub.reset(); });\n\n  after(() => { logStub.restore(); });\n\n  it('no tags', () => {\n    listTags();\n    sinonAssert.calledWithMatch(logStub, 'Name');\n    sinonAssert.calledWithMatch(logStub, 'Posts');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'No tags.');\n  });\n\n  it('tags', async () => {\n    const posts = [\n      {source: 'foo', slug: 'foo', title: 'Its', date: 1e8},\n      {source: 'bar', slug: 'bar', title: 'Math', date: 1e8 + 1},\n      {source: 'baz', slug: 'baz', title: 'Dude', date: 1e8 - 1}\n    ];\n\n    await hexo.init();\n    const output = await Post.insert(posts);\n    await BluebirdPromise.each([\n      ['foo'],\n      ['baz'],\n      ['baz']\n    ], (tags, i) => output[i].setTags(tags));\n    await hexo.locals.invalidate();\n\n    listTags();\n    sinonAssert.calledWithMatch(logStub, 'Name');\n    sinonAssert.calledWithMatch(logStub, 'Posts');\n    sinonAssert.calledWithMatch(logStub, 'Path');\n    sinonAssert.calledWithMatch(logStub, 'baz');\n    sinonAssert.calledWithMatch(logStub, 'foo');\n    sinonAssert.calledWithMatch(logStub, 'tags/baz');\n    sinonAssert.calledWithMatch(logStub, 'tags/foo');\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/migrate.ts",
    "content": "import { spy, assert as sinonAssert, stub, SinonSpy } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport migrateConsole from '../../../lib/plugins/console/migrate';\ntype OriginalParams = Parameters<typeof migrateConsole>;\ntype OriginalReturn = ReturnType<typeof migrateConsole>;\n\ndescribe('migrate', () => {\n  const hexo = new Hexo(__dirname, { silent: true });\n  const migrate: (...args: OriginalParams) => OriginalReturn = migrateConsole.bind(hexo);\n\n  it('default', async () => {\n    const migrator = spy();\n\n    hexo.extend.migrator.register('test', migrator);\n\n    await migrate({ _: ['test'], foo: 1, bar: 2 });\n\n    sinonAssert.calledWithMatch(migrator, { foo: 1, bar: 2 });\n    migrator.calledOnce.should.be.true;\n  });\n\n  it('no args', async () => {\n    const hexo = new Hexo(__dirname, { silent: true });\n    hexo.call = spy();\n    const migrate: (...args: OriginalParams) => OriginalReturn = migrateConsole.bind(hexo);\n\n    await migrate({ _: [] });\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._[0].should.eql('migrate');\n  });\n\n  it('migrator not found', async () => {\n    const logStub = stub(console, 'log');\n\n    await migrate({ _: ['foo'] });\n\n    logStub.calledOnce.should.be.true;\n    logStub.args[0][0].should.contains('migrator plugin is not installed.');\n    logStub.args[0][0].should.contains('Installed migrator plugins:');\n\n    logStub.restore();\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/new.ts",
    "content": "import { exists, mkdirs, readFile, rmdir, unlink } from 'hexo-fs';\nimport moment from 'moment';\nimport { join } from 'path';\nimport BluebirdPromise from 'bluebird';\nimport { useFakeTimers, spy, SinonSpy } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport newConsole from '../../../lib/plugins/console/new';\ntype OriginalParams = Parameters<typeof newConsole>;\ntype OriginalReturn = ReturnType<typeof newConsole>;\n\ndescribe('new', () => {\n  const hexo = new Hexo(join(__dirname, 'new_test'), {silent: true});\n  const n: (...args: OriginalParams) => OriginalReturn = newConsole.bind(hexo);\n  const post = hexo.post;\n  const now = Date.now();\n  let clock;\n\n  before(async () => {\n    clock = useFakeTimers(now);\n\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n    await BluebirdPromise.all([\n      hexo.scaffold.set('post', [\n        'title: {{ title }}',\n        'date: {{ date }}',\n        'tags:',\n        '---'\n      ].join('\\n')),\n      hexo.scaffold.set('draft', [\n        'title: {{ title }}',\n        'tags:',\n        '---'\n      ].join('\\n'))\n    ]);\n  });\n\n  after(() => {\n    clock.restore();\n    return rmdir(hexo.base_dir);\n  });\n\n  it('no args', async () => {\n    hexo.call = spy();\n    await n({\n      _: []\n    });\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._[0].should.eql('new');\n  });\n\n  it('title', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World']\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('layout', async () => {\n    const path = join(hexo.source_dir, '_drafts', 'Hello-World.md');\n    const body = [\n      'title: Hello World',\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['draft', 'Hello World']\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('slug', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'foo.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World'],\n      slug: 'foo'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('slug - s', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'foo.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World'],\n      s: 'foo'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('path', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'bar.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World'],\n      slug: 'foo',\n      path: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('path - p', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'bar.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World'],\n      slug: 'foo',\n      p: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('without _', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'bar.md');\n    const body = [\n      'title: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: [],\n      path: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('rename if target existed', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md');\n\n    await post.create({\n      title: 'Hello World'\n    });\n    await n({\n      _: ['Hello World']\n    });\n    const exist = await exists(path);\n    exist.should.be.true;\n\n    await BluebirdPromise.all([\n      unlink(path),\n      unlink(join(hexo.source_dir, '_posts', 'Hello-World.md'))\n    ]);\n  });\n\n  it('replace existing files', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await post.create({\n      title: 'Hello World'\n    });\n    await n({\n      _: ['Hello World'],\n      replace: true\n    });\n    const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md'));\n    exist.should.be.false;\n\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('replace existing files - r', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await post.create({\n      title: 'Hello World'\n    });\n    await n({\n      _: ['Hello World'],\n      r: true\n    });\n    const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md'));\n    exist.should.be.false;\n\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('extra data', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: Hello World',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['Hello World'],\n      foo: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('special character - 1', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: \\'[Hello] World\\'',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['[Hello] World'],\n      foo: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('special character - 2', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: \\'{Hello} World\\'',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['{Hello} World'],\n      foo: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('special character - 3', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: \\'\\'\\'Hello\\'\\' World\\'',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['\\'Hello\\' World'],\n      foo: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n\n  it('special character - 4', async () => {\n    const date = moment(now);\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const body = [\n      'title: \\'\"Hello\" World\\'',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await n({\n      _: ['\"Hello\" World'],\n      foo: 'bar'\n    });\n    const content = await readFile(path);\n    content.should.eql(body);\n\n    await unlink(path);\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/publish.ts",
    "content": "import { exists, mkdirs, readFile, rmdir, unlink } from 'hexo-fs';\nimport moment from 'moment';\nimport { join } from 'path';\nimport BluebirdPromise from 'bluebird';\nimport { useFakeTimers, spy, SinonSpy, SinonFakeTimers } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport publishConsole from '../../../lib/plugins/console/publish';\ntype OriginalParams = Parameters<typeof publishConsole>;\ntype OriginalReturn = ReturnType<typeof publishConsole>;\n\ndescribe('publish', () => {\n  const hexo = new Hexo(join(__dirname, 'publish_test'), {silent: true});\n  const publish: (...args: OriginalParams) => OriginalReturn = publishConsole.bind(hexo);\n  const post = hexo.post;\n  const now = Date.now();\n  let clock: SinonFakeTimers;\n\n  before(async () => {\n    clock = useFakeTimers(now);\n\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n    await hexo.scaffold.set('post', [\n      '---',\n      'title: {{ title }}',\n      'date: {{ date }}',\n      'tags:',\n      '---'\n    ].join('\\n'));\n    await hexo.scaffold.set('draft', [\n      '---',\n      'title: {{ title }}',\n      'tags:',\n      '---'\n    ].join('\\n'));\n  });\n\n  after(() => {\n    clock.restore();\n    return rmdir(hexo.base_dir);\n  });\n\n  beforeEach(() => post.create({\n    title: 'Hello World',\n    layout: 'draft'\n  }));\n\n  it('slug', async () => {\n    const draftPath = join(hexo.source_dir, '_drafts', 'Hello-World.md');\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await publish({\n      _: ['Hello-World']\n    });\n\n    const exist = await exists(draftPath);\n    const data = await readFile(path);\n\n    exist.should.be.false;\n    data.should.eql(content);\n\n    await unlink(path);\n  });\n\n  it('no args', async () => {\n    const hexo = new Hexo(join(__dirname, 'publish_test'), {silent: true});\n    hexo.call = spy();\n    const publish: (...args: OriginalParams) => OriginalReturn = publishConsole.bind(hexo);\n\n    await publish({_: []});\n\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._[0].should.eql('publish');\n  });\n\n  it('layout', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'layout: photo',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await publish({\n      _: ['photo', 'Hello-World']\n    });\n    const data = await readFile(path);\n    data.should.eql(content);\n\n    await unlink(path);\n  });\n\n  it('rename if target existed', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md');\n\n    await post.create({\n      title: 'Hello World'\n    });\n    await publish({\n      _: ['Hello-World']\n    });\n\n    const exist = await exists(path);\n    exist.should.be.true;\n\n    await BluebirdPromise.all([\n      unlink(path),\n      unlink(join(hexo.source_dir, '_posts', 'Hello-World.md'))\n    ]);\n  });\n\n  it('replace existing target', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n\n    await post.create({\n      title: 'Hello World'\n    });\n    await publish({\n      _: ['Hello-World'],\n      replace: true\n    });\n    const exist = await exists(join(hexo.source_dir, '_posts', 'Hello-World-1.md'));\n    exist.should.be.false;\n\n    await unlink(path);\n  });\n});\n"
  },
  {
    "path": "test/scripts/console/render.ts",
    "content": "import { mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs';\nimport { join } from 'path';\nimport BluebirdPromise from 'bluebird';\nimport { spy, SinonSpy } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport renderConsole from '../../../lib/plugins/console/render';\ntype OriginalParams = Parameters<typeof renderConsole>;\ntype OriginalReturn = ReturnType<typeof renderConsole>;\n\ndescribe('render', () => {\n  const hexo = new Hexo(join(__dirname, 'render_test'), {silent: true});\n  const render: (...args: OriginalParams) => OriginalReturn = renderConsole.bind(hexo);\n\n  before(async () => {\n    await mkdirs(hexo.base_dir);\n    hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  const body = [\n    'foo: 1',\n    'bar:',\n    '  boo: 2'\n  ].join('\\n');\n\n  it('no args', async () => {\n    const hexo = new Hexo(join(__dirname, 'render_test'), {silent: true});\n    hexo.call = spy();\n    const render: (...args: OriginalParams) => OriginalReturn = renderConsole.bind(hexo);\n\n    await render({_: []});\n\n    (hexo.call as SinonSpy).calledOnce.should.be.true;\n    (hexo.call as SinonSpy).args[0][0].should.eql('help');\n    (hexo.call as SinonSpy).args[0][1]._.should.eql('render');\n  });\n\n  it('relative path', async () => {\n    const src = join(hexo.base_dir, 'test.yml');\n    const dest = join(hexo.base_dir, 'result.json');\n\n    await writeFile(src, body);\n    await render({_: ['test.yml'], output: 'result.json'});\n    const result = await readFile(dest);\n    JSON.parse(result).should.eql({\n      foo: 1,\n      bar: {\n        boo: 2\n      }\n    });\n\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('absolute path', async () => {\n    const src = join(hexo.base_dir, 'test.yml');\n    const dest = join(hexo.base_dir, 'result.json');\n\n    await writeFile(src, body);\n    await render({_: [src], output: 'result.json'});\n\n    const result = await readFile(dest);\n    JSON.parse(result).should.eql({\n      foo: 1,\n      bar: {\n        boo: 2\n      }\n    });\n\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('absolute output', async () => {\n    const src = join(hexo.base_dir, 'test.yml');\n    const dest = join(hexo.base_dir, 'result.json');\n\n    await writeFile(src, body);\n    await render({_: ['test.yml'], output: dest});\n\n    const result = await readFile(dest);\n    JSON.parse(result).should.eql({\n      foo: 1,\n      bar: {\n        boo: 2\n      }\n    });\n\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  // it('output'); missing-unit-test\n\n  it('engine', async () => {\n    const src = join(hexo.base_dir, 'test');\n    const dest = join(hexo.base_dir, 'result.json');\n\n    await writeFile(src, body);\n    await render({_: ['test'], output: 'result.json', engine: 'yaml'});\n\n    const result = await readFile(dest);\n    JSON.parse(result).should.eql({\n      foo: 1,\n      bar: {\n        boo: 2\n      }\n    });\n\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n\n  it('pretty', async () => {\n    const src = join(hexo.base_dir, 'test.yml');\n    const dest = join(hexo.base_dir, 'result.json');\n\n    await writeFile(src, body);\n    await render({_: ['test.yml'], output: 'result.json', pretty: true});\n\n    const result = await readFile(dest);\n    result.should.eql(JSON.stringify({\n      foo: 1,\n      bar: {\n        boo: 2\n      }\n    }, null, '  '));\n\n    await BluebirdPromise.all([\n      unlink(src),\n      unlink(dest)\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/console.ts",
    "content": "import Console from '../../../lib/extend/console';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Console', () => {\n  const ctx = {};\n  it('register()', () => {\n    const c = new Console();\n\n    // no name\n    // @ts-expect-error\n    should.throw(() => c.register(), TypeError, 'name is required');\n\n    // name, fn\n    c.register('test', () => {});\n\n    c.get('test').should.exist;\n\n    // name, not fn\n    // @ts-expect-error\n    should.throw(() => c.register('test'), TypeError, 'fn must be a function');\n\n    // name, desc, fn\n    c.register('test', 'this is a test', () => {});\n\n    c.get('test').should.exist;\n\n    c.get('test').desc!.should.eql('this is a test');\n\n    // name, desc, not fn\n    // @ts-expect-error\n    should.throw(() => c.register('test', 'this is a test'), TypeError, 'fn must be a function');\n\n    // name, options, fn\n    c.register('test', {init: true}, () => {});\n\n    c.get('test').should.exist;\n    c.get('test').options!.init!.should.be.true;\n\n    // name, desc, options, fn\n    c.register('test', 'this is a test', {init: true}, () => {});\n\n    c.get('test').should.exist;\n    c.get('test').desc!.should.eql('this is a test');\n    c.get('test').options!.init!.should.be.true;\n\n    // name, desc, options, not fn\n    // @ts-expect-error\n    should.throw(() => c.register('test', 'this is a test', {init: true}), TypeError, 'fn must be a function');\n  });\n\n  it('register() - alias', () => {\n    const c = new Console();\n\n    c.register('test', () => {});\n\n    c.alias.should.eql({\n      t: 'test',\n      te: 'test',\n      tes: 'test',\n      test: 'test'\n    });\n  });\n\n  it('register() - promisify', () => {\n    const c = new Console();\n\n    c.register('test', (args, callback) => {\n      args.should.eql({foo: 'bar'});\n      callback && callback(null, 'foo');\n    });\n\n    c.get('test').call(ctx, {\n      _: [],\n      foo: 'bar'\n    }).then(result => {\n      result.should.eql('foo');\n    });\n  });\n\n  it('list()', () => {\n    const c = new Console();\n\n    c.register('test', () => {});\n\n    c.list().should.have.all.keys(['test']);\n  });\n\n  it('get()', () => {\n    const c = new Console();\n\n    c.register('test', () => {});\n\n    c.get('test').should.exist;\n    c.get('t').should.exist;\n    c.get('te').should.exist;\n    c.get('tes').should.exist;\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/deployer.ts",
    "content": "import Deployer from '../../../lib/extend/deployer';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Deployer', () => {\n  const ctx = {};\n  it('register()', () => {\n    const d = new Deployer();\n\n    // name, fn\n    d.register('test', () => {});\n\n    d.get('test').should.exist;\n\n    // no name\n    // @ts-expect-error\n    should.throw(() => d.register(), TypeError, 'name is required');\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => d.register('test'), TypeError, 'fn must be a function');\n  });\n\n  it('register() - promisify', () => {\n    const d = new Deployer();\n\n    d.register('test', (args, callback) => {\n      args.should.eql({foo: 'bar'});\n      callback && callback(null, 'foo');\n    });\n\n    d.get('test').call(ctx, {\n      type: '',\n      foo: 'bar'\n    }).then(result => {\n      result.should.eql('foo');\n    });\n  });\n\n  it('register() - Promise.method', () => {\n    const d = new Deployer();\n\n    d.register('test', args => {\n      args.should.eql({foo: 'bar'});\n      return 'foo';\n    });\n\n    d.get('test').call(ctx, {\n      type: '',\n      foo: 'bar'\n    }).then(result => {\n      result.should.eql('foo');\n    });\n  });\n\n  it('list()', () => {\n    const d = new Deployer();\n\n    d.register('test', () => {});\n\n    d.list().should.have.all.keys(['test']);\n  });\n\n  it('get()', () => {\n    const d = new Deployer();\n\n    d.register('test', () => {});\n\n    d.get('test').should.exist;\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/filter.ts",
    "content": "import Filter from '../../../lib/extend/filter';\nimport { spy } from 'sinon';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Filter', () => {\n  it('register()', () => {\n    const f = new Filter();\n\n    // type, fn\n    f.register('test', () => {});\n\n    f.list('test')[0].should.exist;\n    f.list('test')[0].priority!.should.eql(10);\n\n    // type, fn, priority\n    f.register('test2', () => {}, 50);\n\n    f.list('test2')[0].priority!.should.eql(50);\n\n    // fn\n    f.register(() => {});\n\n    f.list('after_post_render')[0].should.exist;\n    f.list('after_post_render')[0].priority!.should.eql(10);\n\n    // fn, priority\n    f.register(() => {}, 50);\n\n    f.list('after_post_render')[1].priority!.should.eql(50);\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => f.register(), TypeError, 'fn must be a function');\n  });\n\n  it('register() - type alias', () => {\n    const f = new Filter();\n\n    // pre\n    f.register('pre', () => {});\n\n    f.list('before_post_render')[0].should.exist;\n\n    // post\n    f.register('post', () => {});\n\n    f.list('after_post_render')[0].should.exist;\n  });\n\n  it('register() - priority', () => {\n    const f = new Filter();\n\n    f.register('test', () => {});\n\n    f.register('test', () => {}, 5);\n\n    f.register('test', () => {}, 15);\n\n    f.list('test').map(item => item.priority).should.eql([5, 10, 15]);\n  });\n\n  it('unregister()', async () => {\n    const f = new Filter();\n    const filter = spy();\n\n    f.register('test', filter);\n    f.unregister('test', filter);\n\n    await f.exec('test', '');\n    filter.called.should.be.false;\n  });\n\n  it('unregister() - type is required', () => {\n    const f = new Filter();\n    // @ts-expect-error\n    should.throw(() => f.unregister(), 'type is required');\n  });\n\n  it('unregister() - fn must be a function', () => {\n    const f = new Filter();\n    // @ts-expect-error\n    should.throw(() => f.unregister('test'), 'fn must be a function');\n  });\n\n  it('list()', () => {\n    const f = new Filter();\n\n    f.register('test', () => {});\n\n    f.list().test.should.exist;\n    f.list('test')[0].should.exist;\n    f.list('foo').should.have.lengthOf(0);\n  });\n\n  it('exec()', async () => {\n    const f = new Filter();\n\n    const filter1 = spy(data => {\n      data.should.eql('');\n      return data + 'foo';\n    });\n\n    const filter2 = spy(data => {\n      data.should.eql('foo');\n      return data + 'bar';\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    const data = await f.exec('test', '');\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n    filter2.calledAfter(filter1).should.be.true;\n    data.should.eql('foobar');\n  });\n\n  it('exec() - pointer', async () => {\n    const f = new Filter();\n\n    const filter1 = spy(data => {\n      data.should.eql({});\n      data.foo = 1;\n    });\n\n    const filter2 = spy(data => {\n      data.should.eql({foo: 1});\n      data.bar = 2;\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    const data = await f.exec('test', {});\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n    filter2.calledAfter(filter1).should.be.true;\n    data.should.eql({ foo: 1, bar: 2 });\n  });\n\n  it('exec() - args', async () => {\n    const f = new Filter();\n\n    const filter1 = spy((data, arg1, arg2) => {\n      arg1.should.eql(1);\n      arg2.should.eql(2);\n    });\n\n    const filter2 = spy((data, arg1, arg2) => {\n      arg1.should.eql(1);\n      arg2.should.eql(2);\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    await f.exec('test', {}, {\n      args: [1, 2]\n    });\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n  });\n\n  it('exec() - context', async () => {\n    const f = new Filter();\n    const ctx = {foo: 1, bar: 2};\n\n    const filter1 = spy();\n    const filter2 = spy();\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    await f.exec('test', {}, { context: ctx });\n    filter1.alwaysCalledOn(ctx).should.be.true;\n    filter2.alwaysCalledOn(ctx).should.be.true;\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n  });\n\n  it('execSync()', () => {\n    const f = new Filter();\n\n    const filter1 = spy(data => {\n      data.should.eql('');\n      return data + 'foo';\n    });\n\n    const filter2 = spy(data => {\n      data.should.eql('foo');\n      return data + 'bar';\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    f.execSync('test', '').should.eql('foobar');\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n    filter2.calledAfter(filter1).should.be.true;\n  });\n\n  it('execSync() - pointer', () => {\n    const f = new Filter();\n\n    const filter1 = spy(data => {\n      data.should.eql({});\n      data.foo = 1;\n    });\n\n    const filter2 = spy(data => {\n      data.should.eql({foo: 1});\n      data.bar = 2;\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    f.execSync('test', {}).should.eql({foo: 1, bar: 2});\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n    filter2.calledAfter(filter1).should.be.true;\n  });\n\n  it('execSync() - args', () => {\n    const f = new Filter();\n\n    const filter1 = spy((data, arg1, arg2) => {\n      arg1.should.eql(1);\n      arg2.should.eql(2);\n    });\n\n    const filter2 = spy((data, arg1, arg2) => {\n      arg1.should.eql(1);\n      arg2.should.eql(2);\n    });\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    f.execSync('test', {}, {\n      args: [1, 2]\n    });\n\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n  });\n\n  it('execSync() - context', () => {\n    const f = new Filter();\n    const ctx = {foo: 1, bar: 2};\n\n    const filter1 = spy();\n    const filter2 = spy();\n\n    f.register('test', filter1);\n    f.register('test', filter2);\n\n    f.execSync('test', {}, {context: ctx});\n    filter1.alwaysCalledOn(ctx).should.be.true;\n    filter2.alwaysCalledOn(ctx).should.be.true;\n    filter1.calledOnce.should.be.true;\n    filter2.calledOnce.should.be.true;\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/generator.ts",
    "content": "import Generator from '../../../lib/extend/generator';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Generator', () => {\n  it('register()', () => {\n    const g = new Generator();\n\n    // name, fn\n    g.register('test', () => []);\n\n    g.get('test').should.exist;\n\n    // fn\n    g.register(() => []);\n\n    g.get('generator-0').should.exist;\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => g.register('test'), TypeError, 'fn must be a function');\n  });\n\n  it('register() - promisify', async () => {\n    const g = new Generator();\n\n    g.register('test', (_locals, callback) => {\n      callback && callback(null, 'foo');\n      return [];\n    });\n\n    const result = await g.get('test')({} as any);\n    result.should.eql('foo');\n  });\n\n  it('get()', () => {\n    const g = new Generator();\n\n    g.register('test', () => []);\n\n    g.get('test').should.exist;\n  });\n\n  it('list()', () => {\n    const g = new Generator();\n\n    g.register('test', () => []);\n\n    g.list().should.have.all.keys(['test']);\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/helper.ts",
    "content": "import Helper from '../../../lib/extend/helper';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Helper', () => {\n  it('register()', () => {\n    const h = new Helper();\n\n    // name, fn\n    h.register('test', () => '');\n\n    h.get('test').should.exist;\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => h.register('test'), TypeError, 'fn must be a function');\n\n    // no name\n    // @ts-expect-error\n    should.throw(() => h.register(), TypeError, 'name is required');\n  });\n\n  it('list()', () => {\n    const h = new Helper();\n\n    h.register('test', () => '');\n\n    h.list().should.have.all.keys(['test']);\n  });\n\n  it('get()', () => {\n    const h = new Helper();\n\n    h.register('test', () => '');\n\n    h.get('test').should.exist;\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/injector.ts",
    "content": "import Injector from '../../../lib/extend/injector';\n\ndescribe('Injector', () => {\n  const content = [\n    '<!DOCTYPE html>',\n    '<html lang=\"en\">',\n    '<head id=\"head\"><title>Test</title>',\n    '</head>',\n    '<body id=\"body\">',\n    '<div></div>',\n    '<p></p>',\n    '</body>',\n    '</html>'\n  ].join('');\n\n  it('register() - entry is required', () => {\n    const i = new Injector();\n\n    // no name\n    try {\n      // @ts-expect-error\n      i.register();\n    } catch (err) {\n      err.should.be\n        .instanceOf(TypeError)\n        .property('message', 'entry is required');\n    }\n  });\n\n  it('register() - string', () => {\n    const i = new Injector();\n\n    const str = '<link rel=\"stylesheet\" href=\"DPlayer.min.css\" />';\n    i.register('head_begin', str);\n    i.register('head_end', str, 'home');\n\n    i.get('head_begin').should.contains(str);\n    i.get('head_begin', 'default').should.contains(str);\n    i.get('head_end', 'home').should.contains(str);\n  });\n\n  it('register() - function', () => {\n    const i = new Injector();\n\n    const fn = () => '<link rel=\"stylesheet\" href=\"DPlayer.min.css\" />';\n    i.register('head_begin', fn);\n\n    i.get('head_begin').should.contains(fn());\n  });\n\n  it('register() - fallback when entry not exists', () => {\n    const i = new Injector();\n\n    const str = '<link rel=\"stylesheet\" href=\"DPlayer.min.css\" />';\n    // @ts-expect-error\n    i.register('foo', str);\n\n    i.get('head_end').should.contains(str);\n  });\n\n  it('list()', () => {\n    const i = new Injector();\n\n    i.register('body_begin', '<script src=\"DPlayer.min.js\"></script>');\n\n    i.list().body_begin.default.should.be.instanceOf(Set);\n    [...i.list().body_begin.default].should.not.be.empty;\n  });\n\n  it('get()', () => {\n    const i = new Injector();\n    const str = '<script src=\"jquery.min.js\"></script>';\n\n    i.register('body_begin', str);\n    i.register('body_end', str, 'home');\n\n    i.get('body_begin').should.be.instanceOf(Array);\n    i.get('body_begin').should.contains(str);\n    i.get('body_end', 'home').should.be.instanceOf(Array);\n    i.get('body_end', 'home').should.contains(str);\n\n    i.get('head_end').should.be.instanceOf(Array);\n    i.get('head_end').should.eql([]);\n  });\n\n  it('getText()', () => {\n    const i = new Injector();\n    const str = '<script src=\"jquery.min.js\"></script>';\n\n    i.register('head_end', str);\n    i.register('body_end', str, 'home');\n\n    i.getText('body_end', 'home').should.eql(str);\n    i.getText('body_end').should.eql('');\n  });\n\n  it('getSize()', () => {\n    const i = new Injector();\n    const str = '<script src=\"jquery.min.js\"></script>';\n\n    i.register('head_end', str);\n    i.register('body_end', str);\n    i.register('body_end', str, 'home');\n\n    i.getSize('head_begin').should.eql(0);\n    i.getSize('head_end').should.eql(1);\n    i.getSize('body_end').should.eql(2);\n  });\n\n  it('exec() - default', () => {\n    const i = new Injector();\n    const result = i.exec(content);\n    result.should.contain('<head id=\"head\"><title>Test</title></head>');\n    result.should.contain('<body id=\"body\"><div></div><p></p></body>');\n  });\n\n  it('exec() - default', () => {\n    const i = new Injector();\n    const result = i.exec(content);\n    result.should.contain('<head id=\"head\"><title>Test</title></head>');\n    result.should.contain('<body id=\"body\"><div></div><p></p></body>');\n  });\n\n  it('exec() - insert code', () => {\n    const i = new Injector();\n\n    i.register('head_begin', '<!-- Powered by Hexo -->');\n    i.register('head_end', '<link href=\"prism.css\" rel=\"stylesheet\">');\n    i.register('head_end', '<link href=\"prism-linenumber.css\" rel=\"stylesheet\">');\n    i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');\n    i.register('body_end', '<script src=\"prism.js\"></script>');\n\n    const result = i.exec(content);\n\n    result.should.contain('<head id=\"head\"><!-- hexo injector head_begin start --><!-- Powered by Hexo --><!-- hexo injector head_begin end -->');\n    result.should.contain('<!-- hexo injector head_end start --><link href=\"prism.css\" rel=\"stylesheet\"><link href=\"prism-linenumber.css\" rel=\"stylesheet\"><!-- hexo injector head_end end --></head>');\n    result.should.contain('<body id=\"body\"><!-- hexo injector body_begin start --><script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script><!-- hexo injector body_begin end -->');\n    result.should.contain('<!-- hexo injector body_end start --><script src=\"prism.js\"></script><!-- hexo injector body_end end --></body>');\n  });\n\n  it('exec() - no duplicate insert', () => {\n    const content = [\n      '<!DOCTYPE html>',\n      '<html lang=\"en\">',\n      '<head id=\"head\"><!-- hexo injector head_begin start --><!-- hexo injector head_begin end -->',\n      '<title>Test</title>',\n      '<!-- hexo injector head_end start --><link href=\"prism.css\" rel=\"stylesheet\"></head>',\n      '<body id=\"body\"><!-- hexo injector body_begin start --><!-- hexo injector body_begin end -->',\n      '<div></div>',\n      '<p></p>',\n      '<!-- hexo injector body_end start --><script src=\"prism.js\"></script><!-- hexo injector body_end end --></body>',\n      '</html>'\n    ].join('');\n\n    const i = new Injector();\n\n    i.register('head_begin', '<!-- Powered by Hexo -->');\n    i.register('head_end', '<link href=\"prism.css\" rel=\"stylesheet\">');\n    i.register('head_end', '<link href=\"prism-linenumber.css\" rel=\"stylesheet\">');\n    i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');\n    i.register('body_end', '<script src=\"prism.js\"></script>');\n\n    const result = i.exec(content);\n\n    result.should.contain('<head id=\"head\"><!-- hexo injector head_begin start --><!-- hexo injector head_begin end -->');\n    result.should.contain('<!-- hexo injector head_end start --><link href=\"prism.css\" rel=\"stylesheet\"></head>');\n    result.should.contain('<body id=\"body\"><!-- hexo injector body_begin start --><!-- hexo injector body_begin end -->');\n    result.should.contain('<!-- hexo injector body_end start --><script src=\"prism.js\"></script><!-- hexo injector body_end end --></body>');\n  });\n\n  it('exec() - multi-line head & body', () => {\n    const content = [\n      '<!DOCTYPE html>',\n      '<html lang=\"en\">',\n      '<head id=\"head\"><title>Test</title>',\n      '</head>',\n      '<body id=\"body\">',\n      '<div></div>',\n      '<p></p>',\n      '</body>',\n      '</html>'\n    ].join('\\n');\n\n    const i = new Injector();\n\n    i.register('head_begin', '<!-- Powered by Hexo -->');\n    i.register('head_end', '<link href=\"prism.css\" rel=\"stylesheet\">');\n    i.register('head_end', '<link href=\"prism-linenumber.css\" rel=\"stylesheet\">');\n    i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');\n    i.register('body_end', '<script src=\"prism.js\"></script>');\n\n    const result = i.exec(content);\n\n    result.should.contain('<head id=\"head\"><!-- hexo injector head_begin start --><!-- Powered by Hexo --><!-- hexo injector head_begin end -->');\n    result.should.contain('<!-- hexo injector head_end start --><link href=\"prism.css\" rel=\"stylesheet\"><link href=\"prism-linenumber.css\" rel=\"stylesheet\"><!-- hexo injector head_end end --></head>');\n    result.should.contain('<body id=\"body\"><!-- hexo injector body_begin start --><script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script><!-- hexo injector body_begin end -->');\n    result.should.contain('<!-- hexo injector body_end start --><script src=\"prism.js\"></script><!-- hexo injector body_end end --></body>');\n  });\n\n  it('exec() - inject on specific page', () => {\n    const content = [\n      '<!DOCTYPE html>',\n      '<html lang=\"en\">',\n      '<head id=\"head\"><title>Test</title>',\n      '</head>',\n      '<body id=\"body\">',\n      '<div></div>',\n      '<p></p>',\n      '</body>',\n      '</html>'\n    ].join('\\n');\n\n    const i = new Injector();\n\n    i.register('head_begin', '<!-- head_begin_default -->');\n    i.register('head_begin', '<!-- head_begin_home -->', 'home');\n    i.register('head_begin', '<!-- head_begin_post -->', 'post');\n    i.register('head_begin', '<!-- head_begin_page -->', 'page');\n    i.register('head_begin', '<!-- head_begin_archive -->', 'archive');\n    i.register('head_begin', '<!-- head_begin_category -->', 'category');\n    i.register('head_begin', '<!-- head_begin_tag -->', 'tag');\n\n    const result1 = i.exec(content, { page: {} });\n    const result2 = i.exec(content, { page: { __index: true } });\n    const result3 = i.exec(content, { page: { __post: true } });\n    const result4 = i.exec(content, { page: { __page: true } });\n    const result5 = i.exec(content, { page: { archive: true } });\n    const result6 = i.exec(content, { page: { category: true } });\n    const result7 = i.exec(content, { page: { tag: true } });\n\n    // home\n    result1.should.not.contain('<!-- head_begin_home -->');\n    result2.should.contain('<!-- head_begin_home --><!-- head_begin_default -->');\n    // post\n    result1.should.not.contain('<!-- head_begin_post -->');\n    result3.should.contain('<!-- head_begin_post --><!-- head_begin_default -->');\n    // page\n    result1.should.not.contain('<!-- head_begin_page -->');\n    result4.should.contain('<!-- head_begin_page --><!-- head_begin_default -->');\n    // archive\n    result1.should.not.contain('<!-- head_begin_archive -->');\n    result5.should.contain('<!-- head_begin_archive --><!-- head_begin_default -->');\n    // category\n    result1.should.not.contain('<!-- head_begin_category -->');\n    result6.should.contain('<!-- head_begin_category --><!-- head_begin_default -->');\n    // tag\n    result1.should.not.contain('<!-- head_begin_tag -->');\n    result7.should.contain('<!-- head_begin_tag --><!-- head_begin_default -->');\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/migrator.ts",
    "content": "import Migrator from '../../../lib/extend/migrator';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Migrator', () => {\n  const ctx = {};\n  it('register()', () => {\n    const d = new Migrator();\n\n    // name, fn\n    d.register('test', () => {});\n\n    d.get('test').should.exist;\n\n    // no name\n    // @ts-expect-error\n    should.throw(() => d.register(), TypeError, 'name is required');\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => d.register('test'), TypeError, 'fn must be a function');\n  });\n\n  it('register() - promisify', () => {\n    const d = new Migrator();\n\n    d.register('test', (args, callback) => {\n      args.should.eql({foo: 'bar'});\n      callback && callback(null, 'foo');\n    });\n\n    d.get('test').call(ctx, {\n      foo: 'bar'\n    }).then(result => {\n      result.should.eql('foo');\n    });\n  });\n\n  it('register() - Promise.method', async () => {\n    const d = new Migrator();\n\n    d.register('test', args => {\n      args.should.eql({foo: 'bar'});\n      return 'foo';\n    });\n\n    const result = await d.get('test').call(ctx, {\n      foo: 'bar'\n    });\n\n    result.should.eql('foo');\n  });\n\n  it('list()', () => {\n    const d = new Migrator();\n\n    d.register('test', () => {});\n\n    d.list().should.have.all.keys(['test']);\n  });\n\n  it('get()', () => {\n    const d = new Migrator();\n\n    d.register('test', () => {});\n\n    d.get('test').should.exist;\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/processor.ts",
    "content": "import Processor from '../../../lib/extend/processor';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Processor', () => {\n  it('register()', () => {\n    const p = new Processor();\n\n    // pattern, fn\n    p.register('test', () => {});\n\n    p.list()[0].should.exist;\n\n    // fn\n    p.register(() => {});\n\n    p.list()[1].should.exist;\n\n    // more than one arg\n    // @ts-expect-error\n    p.register((_a, _b) => {});\n\n    p.list()[1].should.exist;\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => p.register(), TypeError, 'fn must be a function');\n  });\n\n  it('list()', () => {\n    const p = new Processor();\n\n    p.register('test', () => {});\n\n    p.list().should.have.lengthOf(1);\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/renderer.ts",
    "content": "import Renderer from '../../../lib/extend/renderer';\nimport BluebirdPromise from 'bluebird';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Renderer', () => {\n\n  it('register()', () => {\n    const r = new Renderer();\n\n    // name, output, fn\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.get('yaml').should.exist;\n    r.get('yaml').output!.should.eql('json');\n\n    // name, output, fn, sync\n    r.register('yaml', 'json', () => {}, true);\n\n    r.get('yaml').should.exist;\n    r.get('yaml').output!.should.eql('json');\n    r.get('yaml', true).should.exist;\n    r.get('yaml', true).output!.should.eql('json');\n\n    // no fn\n    // @ts-expect-error\n    should.throw(() => r.register('yaml', 'json'), TypeError, 'fn must be a function');\n\n    // no output\n    // @ts-expect-error\n    should.throw(() => r.register('yaml'), TypeError, 'output is required');\n\n    // no name\n    // @ts-expect-error\n    should.throw(() => r.register(), TypeError, 'name is required');\n  });\n\n  it('register() - promisify', async () => {\n    const r = new Renderer();\n\n    // async\n    r.register('yaml', 'json', (_data, _options, callback) => {\n      callback && callback(null, 'foo');\n      return BluebirdPromise.resolve();\n    });\n\n    const yaml = await r.get('yaml')({}, {});\n    yaml.should.eql('foo');\n\n    // sync\n    r.register('swig', 'html', (_data, _options) => 'foo', true);\n\n    const swig = await r.get('swig')({}, {});\n    swig.should.eql('foo');\n  });\n\n  it('register() - compile', () => {\n    const r = new Renderer();\n\n    function renderer(_data, _locals) {\n      return BluebirdPromise.resolve();\n    }\n\n    renderer.compile = _ => {\n      return () => {};\n    };\n\n    r.register('swig', 'html', renderer);\n    r.get('swig').compile!.should.eql(renderer.compile);\n  });\n\n  it('getOutput()', () => {\n    const r = new Renderer();\n\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.getOutput('yaml').should.eql('json');\n    r.getOutput('.yaml').should.eql('json');\n    r.getOutput('config.yaml').should.eql('json');\n    r.getOutput('foo.xml').should.not.ok;\n  });\n\n  it('isRenderable()', () => {\n    const r = new Renderer();\n\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.isRenderable('yaml').should.be.true;\n    r.isRenderable('.yaml').should.be.true;\n    r.isRenderable('config.yaml').should.be.true;\n    r.isRenderable('foo.xml').should.be.false;\n  });\n\n  it('isRenderableSync()', () => {\n    const r = new Renderer();\n\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.isRenderableSync('yaml').should.be.false;\n\n    r.register('njk', 'html', () => {}, true);\n\n    r.isRenderableSync('njk').should.be.true;\n    r.isRenderableSync('.njk').should.be.true;\n    r.isRenderableSync('layout.njk').should.be.true;\n    r.isRenderableSync('foo.html').should.be.false;\n  });\n\n  it('get()', () => {\n    const r = new Renderer();\n\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.get('yaml').should.exist;\n    r.get('.yaml').should.exist;\n    r.get('config.yaml').should.exist;\n    should.not.exist(r.get('foo.xml'));\n    should.not.exist(r.get('yaml', true));\n\n    r.register('swig', 'html', () => {}, true);\n\n    r.get('swig').should.exist;\n    r.get('swig', true).should.exist;\n  });\n\n  it('list()', () => {\n    const r = new Renderer();\n\n    r.register('yaml', 'json', () => BluebirdPromise.resolve());\n\n    r.register('swig', 'html', () => {}, true);\n\n    r.list().should.have.all.keys(['yaml', 'swig']);\n    r.list(true).should.have.all.keys(['swig']);\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/tag.ts",
    "content": "import { join } from 'path';\nimport Tag from '../../../lib/extend/tag';\nimport chai from 'chai';\nimport Hexo from '../../../lib/hexo';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport posts from '../../../lib/plugins/processor/post';\nimport Filter from '../../../lib/extend/filter';\nimport renderPostFilter from '../../../lib/plugins/filter/before_generate/render_post';\nimport { mkdirs, rmdir, writeFile } from 'hexo-fs';\n// @ts-ignore\nimport Promise from 'bluebird';\nconst should = chai.should();\n\ntype PostParams = Parameters<ReturnType<typeof posts>['process']>\ntype PostReturn = ReturnType<ReturnType<typeof posts>['process']>\n\ndescribe('Tag', () => {\n  const tag = new Tag();\n\n  const baseDir = join(__dirname, 'post_test');\n  const hexo = new Hexo(baseDir);\n  const post = posts(hexo);\n  const process: (...args: PostParams) => Promise<PostReturn> = Promise.method(post.process.bind(hexo));\n  const { source } = hexo;\n  const { File } = source;\n\n  function newFile(options) {\n    const { path } = options;\n\n    options.path = (options.published ? '_posts' : '_drafts') + '/' + path;\n    options.source = join(source.base, options.path);\n\n    options.params = {\n      published: options.published,\n      path,\n      renderable: options.renderable\n    };\n\n    return new File(options);\n  }\n\n  before(async () => {\n    await mkdirs(baseDir);\n    hexo.init();\n  });\n\n  beforeEach(() => { hexo.config = Object.assign({}, defaultConfig); });\n\n  after(() => rmdir(baseDir));\n\n  it('register()', async () => {\n    const tag = new Tag();\n\n    tag.register('test', (args, _content) => args.join(' '));\n\n    const result = await tag.render('{% test foo.bar | abcdef > fn(a, b, c) < fn() %}');\n    result.should.eql('foo.bar | abcdef > fn(a, b, c) < fn()');\n  });\n\n  it('register() - async', async () => {\n    const tag = new Tag();\n\n    tag.register('test', async (args, _content) => args.join(' '), { async: true });\n\n    const result = await tag.render('{% test foo bar %}');\n    result.should.eql('foo bar');\n  });\n\n  it('register() - block', async () => {\n    const tag = new Tag();\n\n    tag.register('test', (args, content) => args.join(' ') + ' ' + content, true);\n\n    const str = [\n      '{% test foo bar %}',\n      'test content',\n      '{% endtest %}'\n    ].join('\\n');\n\n    const result = await tag.render(str);\n    result.should.eql('foo bar test content');\n  });\n\n  it('register() - async block', async () => {\n    const tag = new Tag();\n\n    tag.register('test', async (args, content) => args.join(' ') + ' ' + content, { ends: true, async: true });\n\n    const str = [\n      '{% test foo bar %}',\n      'test content',\n      '{% endtest %}'\n    ].join('\\n');\n\n    const result = await tag.render(str);\n    result.should.eql('foo bar test content');\n  });\n\n  it('register() - nested test', async () => {\n    const tag = new Tag();\n\n    tag.register('test', (_args, content) => content, true);\n\n    const str = [\n      '{% test %}',\n      '123456',\n      '  {% raw %}',\n      '  raw',\n      '  {% endraw %}',\n      '  {% test %}',\n      '  test',\n      '  {% endtest %}',\n      '789012',\n      '{% endtest %}'\n    ].join('\\n');\n\n    const result = await tag.render(str);\n    result.replace(/\\s/g, '').should.eql('123456rawtest789012');\n  });\n\n  it('register() - nested async / async test', async () => {\n    const tag = new Tag();\n\n    tag.register('test', (args, content) => content, {ends: true, async: true});\n    tag.register('async', async (args, content) => args.join(' ') + ' ' + content, { ends: true, async: true });\n\n    const str = [\n      '{% test %}',\n      '123456',\n      '  {% async %}',\n      '  async',\n      '  {% endasync %}',\n      '789012',\n      '{% endtest %}'\n    ].join('\\n');\n\n    const result = await tag.render(str);\n    result.replace(/\\s/g, '').should.eql('123456async789012');\n  });\n\n  it('register() - strip indention', async () => {\n    const tag = new Tag();\n\n    tag.register('test', (args, content) => content, true);\n\n    const str = [\n      '{% test %}',\n      '  test content',\n      '{% endtest %}'\n    ].join('\\n');\n\n    const result = await tag.render(str);\n    result.should.eql('test content');\n  });\n\n  it('register() - async callback', async () => {\n    const tag = new Tag();\n\n    tag.register('test', async (args, _content, callback) => {\n      callback && callback(null, args.join(' '));\n      return '';\n    }, { async: true });\n\n    const result = await tag.render('{% test foo bar %}');\n    result.should.eql('foo bar');\n  });\n\n  it('register() - name is required', () => {\n    // @ts-expect-error\n    should.throw(() => tag.register(), 'name is required');\n  });\n\n  it('register() - fn must be a function', () => {\n    // @ts-expect-error\n    should.throw(() => tag.register('test'), 'fn must be a function');\n  });\n\n  it('unregister()', () => {\n    const tag = new Tag();\n\n    tag.register('test', async (args, _content) => args.join(' '), {async: true});\n    tag.unregister('test');\n\n    return tag.render('{% test foo bar %}')\n      .then(result => {\n        console.log(result);\n        throw new Error('should return error');\n      })\n      .catch(err => {\n        err.should.have.property('type', 'unknown block tag: test');\n      });\n  });\n\n  it('unregister() - name is required', () => {\n    // @ts-expect-error\n    should.throw(() => tag.unregister(), 'name is required');\n  });\n\n  it('render() - context', async () => {\n    const tag = new Tag();\n\n    tag.register('test', function() {\n      return this.foo;\n    });\n\n    const result = await tag.render('{% test %}', { foo: 'bar' });\n    result.should.eql('bar');\n  });\n\n  it('render() - callback', () => {\n    const tag = new Tag();\n\n    // spy() is not a function\n    let spy = false;\n    const callback = () => {\n      spy = true;\n    };\n\n    tag.register('test', () => 'foo');\n\n    return tag.render('{% test %}', callback).then(result => {\n      result.should.eql('foo');\n      spy.should.eql(true);\n    });\n  });\n\n  it('tag should get right locals', async () => {\n    let count = 0;\n    hexo.extend.filter = new Filter();\n    hexo.extend.tag = new Tag();\n    hexo.extend.tag.register('series', () => {\n      count = hexo.locals.get('posts').length;\n      return '';\n    }, {ends: false});\n    hexo.extend.filter.register('before_generate', renderPostFilter.bind(hexo));\n\n    const body1 = [\n      'title: \"test1\"',\n      'date: 2023-09-03 16:59:42',\n      'tags: foo',\n      '---',\n      '{% series %}'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'test1.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const body2 = [\n      '---',\n      'title: test2',\n      'date: 2023-09-03 16:59:46',\n      'tags: foo',\n      '---'\n    ].join('\\n');\n\n    const file2 = newFile({\n      path: 'test2.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const body3 = [\n      'title: test3',\n      'date: 2023-09-03 16:59:49',\n      'tags: foo',\n      '---'\n    ].join('\\n');\n\n    const file3 = newFile({\n      path: 'test3.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await Promise.all([\n      writeFile(file.source, body1),\n      writeFile(file2.source, body2),\n      writeFile(file3.source, body3)\n    ]);\n\n    await Promise.all([\n      process(file),\n      process(file2),\n      process(file3)\n    ]);\n\n    await hexo._generate({ cache: false });\n\n    count.should.eql(3);\n  });\n});\n"
  },
  {
    "path": "test/scripts/extend/tag_errors.ts",
    "content": "import Tag from '../../../lib/extend/tag';\n\ndescribe('Tag Errors', () => {\n  const assertNunjucksError = (err, line, type) => {\n    err.should.have.property('name', 'Nunjucks Error');\n    err.should.have.property('message');\n    err.should.have.property('line', line);\n    err.should.have.property('type', type);\n  };\n\n  it('unknown tag', async () => {\n    const tag = new Tag();\n\n    const body = [\n      '{% abc %}',\n      '  content',\n      '{% endabc %}'\n    ].join('\\n');\n\n    try {\n      await tag.render(body);\n    } catch (err) {\n      assertNunjucksError(err, 1, 'unknown block tag: abc');\n    }\n  });\n\n  it('no closing tag 1', async () => {\n    const tag = new Tag();\n\n    tag.register('test',\n      (_args, _content) => { return ''; },\n      { ends: true });\n\n    const body = [\n      '{% test %}',\n      '  content'\n    ].join('\\n');\n\n    try {\n      await tag.render(body);\n    } catch (err) {\n      err.should.have.property('name', 'Template render error');\n      err.should.have.property('message');\n      err.message.should.have.string('unexpected end of file');\n    }\n  });\n\n  it('no closing tag 2', async () => {\n    const tag = new Tag();\n\n    tag.register('test',\n      (_args, _content) => { return ''; },\n      { ends: true });\n\n    const body = [\n      '{% test %}',\n      '  content',\n      '{% test %}'\n    ].join('\\n');\n\n    try {\n      await tag.render(body);\n    } catch (err) {\n      err.should.have.property('name', 'Template render error');\n      err.should.have.property('message');\n      err.message.should.have.string('unexpected end of file');\n    }\n  });\n\n  it('curly braces', async () => {\n    const tag = new Tag();\n\n    const body = [\n      '<code>{{docker ps -aq | map docker inspect -f \"{{.Name}} {{.Mounts}}\"}}</code>'\n    ].join('\\n');\n\n    try {\n      await tag.render(body);\n    } catch (err) {\n      assertNunjucksError(err, 1, 'expected variable end');\n    }\n  });\n\n  it('nested curly braces', async () => {\n    const tag = new Tag();\n\n    tag.register('test',\n      (_args, _content) => { return ''; },\n      { ends: true });\n\n    const body = [\n      '{% test %}',\n      '  {{docker ps -aq | map docker inspect -f \"{{.Name}} {{.Mounts}}\"}}',\n      '{% endtest %}'\n    ].join('\\n');\n\n    try {\n      await tag.render(body);\n    } catch (err) {\n      assertNunjucksError(err, 2, 'expected variable end');\n    }\n  });\n\n  it('source file path', async () => {\n    const source = '_posts/hello-world.md';\n    const tag = new Tag();\n\n    tag.register('test',\n      (_args, _content) => { return ''; },\n      { ends: true });\n\n    const body = [\n      '{% test %}',\n      '  {{docker ps -aq | map docker inspect -f \"{{.Name}} {{.Mounts}}\"}}',\n      '{% endtest %}'\n    ].join('\\n');\n\n    try {\n      // Add { source } as option\n      await tag.render(body, { source });\n    } catch (err) {\n      err.message.should.contains(source);\n    }\n  });\n\n  it('source file path 2', async () => {\n    const source = '_posts/hello-world.md';\n    const tag = new Tag();\n\n    tag.register('test',\n      (_args, _content) => { return ''; },\n      { ends: true });\n\n    const body = [\n      '{% test %}',\n      '${#var}',\n      '{% endtest %}'\n    ].join('\\n');\n\n    try {\n      await tag.render(body, { source });\n    } catch (err) {\n      err.should.have.property('message');\n      err.message.should.contains(source);\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/backtick_code_block.ts",
    "content": "import { highlight as highlightJs, prismHighlight, escapeHTML } from 'hexo-util';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport Hexo from '../../../lib/hexo';\nimport defaultCodeBlock from '../../../lib/plugins/filter/before_post_render/backtick_code_block';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Backtick code block', () => {\n  const hexo = new Hexo();\n  require('../../../lib/plugins/highlight/')(hexo);\n  const codeBlock = defaultCodeBlock(hexo);\n\n  const code = [\n    'if (tired && night) {',\n    '  sleep();',\n    '}'\n  ].join('\\n');\n\n  const escapeSwigTag = (str: string) => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n\n  function highlight(code: string, options?) {\n    return highlightJs(code, options || {})\n      .replace(/{/g, '&#123;')\n      .replace(/}/g, '&#125;');\n  }\n\n  function prism(code: string, options?) {\n    return prismHighlight(code, options || {})\n      .replace(/{/g, '&#123;')\n      .replace(/}/g, '&#125;');\n  }\n\n  function createCodeWithOptions(options: string, source = code) {\n    return [\n      '```' + options,\n      source,\n      '```'\n    ].join('\\n');\n  }\n\n  beforeEach(() => {\n    // Reset config\n    hexo.config.highlight = Object.assign({}, defaultConfig.highlight);\n    hexo.config.prismjs = Object.assign({}, defaultConfig.prismjs);\n  });\n\n  after(() => {\n    // Reset config for further test\n    hexo.config.highlight = defaultConfig.highlight;\n    hexo.config.prismjs = defaultConfig.prismjs;\n  });\n\n  it('disabled', () => {\n    const content = [\n      '``` js',\n      code,\n      '```'\n    ].join('\\n');\n\n    const data = {content};\n\n    hexo.config.syntax_highlighter = '';\n    codeBlock(data);\n    data.content.should.eql(content);\n  });\n\n  it('with no config (disabled)', () => {\n    const content = [\n      '``` js',\n      code,\n      '```'\n    ].join('\\n');\n\n    const data = {content};\n\n    const oldHljsCfg = hexo.config.highlight;\n    const oldPrismCfg = hexo.config.prismjs;\n    delete(hexo.config as any).highlight;\n    delete(hexo.config as any).prismjs;\n\n    codeBlock(data);\n    data.content.should.eql(content);\n\n    hexo.config.highlight = oldHljsCfg;\n    hexo.config.prismjs = oldPrismCfg;\n  });\n\n  describe('highlightjs', () => {\n    beforeEach(() => {\n      hexo.config.syntax_highlighter = 'highlight.js';\n    });\n\n    it('shorthand', () => {\n      const data = {\n        content: 'Hello, world!'\n      };\n\n      should.not.exist(codeBlock(data));\n    });\n\n    it('default', () => {\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + highlight(code, {lang: 'js'}) + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('without language name', () => {\n      const data = {\n        content: [\n          '```',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code);\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('without language name - ignore tab character', () => {\n      const data = {\n        content: [\n          '``` \\t',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code);\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('title', () => {\n      const data = {\n        content: [\n          '``` js Hello world',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span>'\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('url', () => {\n      const data = {\n        content: [\n          '``` js Hello world https://hexo.io/',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">link</a>'\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('link text', () => {\n      const data = {\n        content: [\n          '``` js Hello world https://hexo.io/ Hexo',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">Hexo</a>'\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('indent', () => {\n      const indentCode = code.split('\\n').map(line => '  ' + line).join('\\n');\n\n      const data = {\n        content: [\n          '``` js Hello world https://hexo.io/',\n          indentCode,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">link</a>'\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number false', () => {\n      hexo.config.highlight.line_number = false;\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number false, don`t first_line_number always1', () => {\n      hexo.config.highlight.line_number = false;\n      hexo.config.highlight.first_line_number = 'always1';\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('only wrap with pre and code', () => {\n      hexo.config.highlight.exclude_languages = ['js'];\n      hexo.config.highlight.hljs = true;\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: false,\n        hljs: true,\n        wrap: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number false, don`t care first_line_number inline', () => {\n      hexo.config.highlight.line_number = false;\n      hexo.config.highlight.first_line_number = 'inline';\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number true', () => {\n      hexo.config.highlight.line_number = true;\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: true\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number, first_line_number always1, js=', () => {\n      hexo.config.highlight.line_number = true;\n      hexo.config.highlight.first_line_number = 'always1';\n\n      const data = {\n        content: [\n          '``` js=',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: true,\n        firstLine: 1\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number, first_line_number inline, js', () => {\n      hexo.config.highlight.line_number = true;\n      hexo.config.highlight.first_line_number = 'inline';\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: false,\n        firstLine: 0\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number, first_line_number inline, js=1', () => {\n      hexo.config.highlight.line_number = true;\n      hexo.config.highlight.first_line_number = 'inline';\n\n      const data = {\n        content: [\n          '``` js=1',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: true,\n        firstLine: 1\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number, first_line_number inline, js=2', () => {\n      hexo.config.highlight.line_number = true;\n      hexo.config.highlight.first_line_number = 'inline';\n\n      const data = {\n        content: [\n          '``` js=2',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        gutter: true,\n        firstLine: 2\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('tab replace', () => {\n      hexo.config.highlight.tab_replace = '  ';\n\n      const code = [\n        'if (tired && night){',\n        '\\tsleep();',\n        '}'\n      ].join('\\n');\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = highlight(code, {\n        lang: 'js',\n        tab: '  '\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('wrap', () => {\n      hexo.config.highlight.wrap = false;\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js', wrap: false }) + '</hexoPostRenderCodeBlock>');\n\n      hexo.config.highlight.wrap = true;\n    });\n\n    // test for Issue #4220\n    it('skip a Swig template', () => {\n      const data = {\n        content: [\n          '```foo```',\n          '',\n          '```',\n          code,\n          '```'\n        ].join('\\n')\n      };\n      codeBlock(data);\n\n      data.content.should.eql('```foo```\\n\\n<hexoPostRenderCodeBlock>' + highlight(code, {}) + '</hexoPostRenderCodeBlock>');\n    });\n\n    // test for Issue #4190\n    it('ignore triple backticks at the line which is started by extra characters', () => {\n      const data = {\n        content: [\n          '```',\n          code,\n          'foo```',\n          '',\n          'bar```',\n          'baz',\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + highlight(code + '\\nfoo```\\n\\nbar```\\nbaz', {}) + '</hexoPostRenderCodeBlock>');\n    });\n\n    // test for Issue #4573\n    it('ignore trailing spaces', () => {\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '``` ',\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.not.contain('`');\n    });\n\n    // test for Issue #4573\n    it('ignore trailing spaces but not newlines', () => {\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```',\n          '',\n          '# New line'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.contain('\\n\\n# New line');\n    });\n\n    it('highlight disable', () => {\n      const data = {\n        content: createCodeWithOptions('js highlight:false')\n      };\n      const expected = escapeSwigTag(data.content);\n      codeBlock(data);\n      data.content.should.eql(expected);\n    });\n\n    it('line_number', () => {\n      let data = {\n        content: createCodeWithOptions('js line_number:false')\n      };\n      let expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true')\n      };\n      expected = highlight(code, {\n        lang: 'js',\n        gutter: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line_threshold', () => {\n      let data = {\n        content: createCodeWithOptions('js line_number:false line_threshold:1')\n      };\n      let expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:1')\n      };\n      expected = highlight(code, {\n        lang: 'js',\n        gutter: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:3')\n      };\n      expected = highlight(code, {\n        lang: 'js',\n        gutter: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('first_line', () => {\n      let data = {\n        content: createCodeWithOptions('js first_line:1234')\n      };\n      let expected = highlight(code, {\n        lang: 'js',\n        firstLine: 1234\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js')\n      };\n      expected = highlight(code, {\n        lang: 'js',\n        firstLine: 1\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('mark', () => {\n      const source = [\n        'const http = require(\\'http\\');',\n        '',\n        'const hostname = \\'127.0.0.1\\';',\n        'const port = 1337;',\n        '',\n        'http.createServer((req, res) => {',\n        '  res.writeHead(200, { \\'Content-Type\\': \\'text/plain\\' });',\n        '  res.end(\\'Hello World\\n\\');',\n        '}).listen(port, hostname, () => {',\n        '  console.log(`Server running at http://${hostname}:${port}/`);',\n        '});'\n      ].join('\\n');\n\n      let data = {\n        content: createCodeWithOptions('js mark:1,7-9,11', source)\n      };\n      let expected = highlight(source, {\n        lang: 'js',\n        mark: [1, 7, 8, 9, 11]\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js mark:11,9-7,1', source)\n      };\n      expected = highlight(source, {\n        lang: 'js',\n        mark: [1, 7, 8, 9, 11]\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('wrap', () => {\n      let data = {\n        content: createCodeWithOptions('js wrap:false')\n      };\n      let expected = highlight(code, {\n        lang: 'js',\n        wrap: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js wrap:true')\n      };\n      expected = highlight(code, {\n        lang: 'js',\n        wrap: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('language_attr', () => {\n      const data = {\n        content: createCodeWithOptions('js language_attr:true')\n      };\n      const expected = highlight(code, {\n        lang: 'js',\n        languageAttr: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('hybrid', () => {\n      let data = {\n        content: createCodeWithOptions('js Hello world https://hexo.io/ Hexo line_number:true line_threshold:1')\n      };\n      const expected = highlight(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">Hexo</a>',\n        gutter: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:1 Hello world https://hexo.io/ Hexo')\n      };\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n      data = {\n        content: createCodeWithOptions('js Hello world line_number:true line_threshold:1 https://hexo.io/ Hexo')\n      };\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    // https://github.com/hexojs/hexo/issues/5423\n    it('with ordered list', () => {\n      const data = {\n        content: [\n          '1. ``` js',\n          code,\n          '```',\n          '2. ``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql([\n        '1. <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>',\n        '2. <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>'\n      ].join('\\n'));\n    });\n\n    // https://github.com/hexojs/hexo/issues/5423\n    it('with unordered list', () => {\n      let data = {\n        content: [\n          '- ``` js',\n          code,\n          '```',\n          '- ``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql([\n        '- <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>',\n        '- <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>'\n      ].join('\\n'));\n\n      data = {\n        content: [\n          '* ``` js',\n          code,\n          '```',\n          '* ``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql([\n        '* <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>',\n        '* <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>'\n      ].join('\\n'));\n\n      data = {\n        content: [\n          '+ ``` js',\n          code,\n          '```',\n          '+ ``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n      data.content.should.eql([\n        '+ <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>',\n        '+ <hexoPostRenderCodeBlock>' + highlight(code, { lang: 'js' }) + '</hexoPostRenderCodeBlock>'\n      ].join('\\n'));\n    });\n  });\n\n  describe('prismjs', () => {\n    beforeEach(() => {\n      hexo.config.syntax_highlighter = 'prismjs';\n    });\n\n    it('default', () => {\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      codeBlock(data);\n\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + prism(code, {lang: 'js'}) + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('without language name', () => {\n      const data = {\n        content: [\n          '```',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code);\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n\n    it('without language name - ignore tab character', () => {\n      const data = {\n        content: [\n          '``` \\t',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code);\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('indent', () => {\n      const indentCode = code.split('\\n').map(line => '  ' + line).join('\\n');\n\n      const data = {\n        content: [\n          '``` js',\n          indentCode,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code, { lang: 'js' });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line number false', () => {\n      hexo.config.prismjs.line_number = false;\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code, {\n        lang: 'js',\n        lineNumber: false\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('tab replace', () => {\n      hexo.config.prismjs.tab_replace = '  ';\n\n      const code = [\n        'if (tired && night){',\n        '\\tsleep();',\n        '}'\n      ].join('\\n');\n\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code, {\n        lang: 'js',\n        tab: '  '\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('title', () => {\n      const data = {\n        content: [\n          '``` js Hello world',\n          code,\n          '```'\n        ].join('\\n')\n      };\n\n      const expected = prism(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span>'\n      });\n\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('prism only wrap with pre and code', () => {\n      hexo.config.prismjs.exclude_languages = ['js'];\n      const data = {\n        content: [\n          '``` js',\n          code,\n          '```'\n        ].join('\\n')\n      };\n      const escapeSwigTag = str => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n      const expected = `<pre><code class=\"js\">${escapeSwigTag(escapeHTML(code))}</code></pre>`;\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n      hexo.config.prismjs.exclude_languages = [];\n    });\n\n    it('highlight disable', () => {\n      const data = {\n        content: createCodeWithOptions('js highlight:false')\n      };\n      const expected = escapeSwigTag(data.content);\n      codeBlock(data);\n      data.content.should.eql(expected);\n    });\n\n    it('line_number', () => {\n      let data = {\n        content: createCodeWithOptions('js line_number:false')\n      };\n      let expected = prism(code, {\n        lang: 'js',\n        lineNumber: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true')\n      };\n      expected = prism(code, {\n        lang: 'js',\n        lineNumber: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('line_threshold', () => {\n      let data = {\n        content: createCodeWithOptions('js line_number:false line_threshold:1')\n      };\n      let expected = prism(code, {\n        lang: 'js',\n        lineNumber: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:1')\n      };\n      expected = prism(code, {\n        lang: 'js',\n        lineNumber: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:3')\n      };\n      expected = prism(code, {\n        lang: 'js',\n        lineNumber: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('first_line', () => {\n      let data = {\n        content: createCodeWithOptions('js first_line:1234')\n      };\n      let expected = prism(code, {\n        lang: 'js',\n        firstLine: 1234\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js')\n      };\n      expected = prism(code, {\n        lang: 'js',\n        firstLine: 1\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('mark', () => {\n      const source = [\n        'const http = require(\\'http\\');',\n        '',\n        'const hostname = \\'127.0.0.1\\';',\n        'const port = 1337;',\n        '',\n        'http.createServer((req, res) => {',\n        '  res.writeHead(200, { \\'Content-Type\\': \\'text/plain\\' });',\n        '  res.end(\\'Hello World\\n\\');',\n        '}).listen(port, hostname, () => {',\n        '  console.log(`Server running at http://${hostname}:${port}/`);',\n        '});'\n      ].join('\\n');\n\n      let data = {\n        content: createCodeWithOptions('js mark:1,7-9,11', source)\n      };\n      let expected = prism(source, {\n        lang: 'js',\n        mark: [1, 7, 8, 9, 11]\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js mark:11,9-7,1', source)\n      };\n      expected = prism(source, {\n        lang: 'js',\n        mark: [1, 7, 8, 9, 11]\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('wrap', () => {\n      let data = {\n        content: createCodeWithOptions('js wrap:false')\n      };\n      let expected = prism(code, {\n        lang: 'js',\n        wrap: false\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n\n      data = {\n        content: createCodeWithOptions('js wrap:true')\n      };\n      expected = prism(code, {\n        lang: 'js',\n        wrap: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('language_attr', () => {\n      const data = {\n        content: createCodeWithOptions('js language_attr:true')\n      };\n      const expected = prism(code, {\n        lang: 'js',\n        languageAttr: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n\n    it('hybrid', () => {\n      let data = {\n        content: createCodeWithOptions('js Hello world https://hexo.io/ Hexo line_number:true line_threshold:1')\n      };\n      const expected = prism(code, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">Hexo</a>',\n        lineNumber: true\n      });\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n      data = {\n        content: createCodeWithOptions('js line_number:true line_threshold:1 Hello world https://hexo.io/ Hexo')\n      };\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n      data = {\n        content: createCodeWithOptions('js Hello world line_number:true line_threshold:1 https://hexo.io/ Hexo')\n      };\n      codeBlock(data);\n      data.content.should.eql('<hexoPostRenderCodeBlock>' + expected + '</hexoPostRenderCodeBlock>');\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/excerpt.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport excerptFilter from '../../../lib/plugins/filter/after_post_render/excerpt';\ntype ExcerptFilterParams = Parameters<typeof excerptFilter>;\ntype ExcerptFilterReturn = ReturnType<typeof excerptFilter>;\n\ndescribe('Excerpt', () => {\n  const hexo = new Hexo();\n  const excerpt: (...args: ExcerptFilterParams) => ExcerptFilterReturn = excerptFilter.bind(hexo);\n\n  it('without <!-- more -->', () => {\n    const content = [\n      'foo',\n      'bar',\n      'baz'\n    ].join('\\n');\n\n    const data: {\n      content: string;\n      excerpt?: string;\n      more?: string;\n    } = {\n      content\n    };\n\n    excerpt(data);\n    data.content.should.eql(content);\n    data.excerpt!.should.eql('');\n    data.more!.should.eql(content);\n  });\n\n  it('with <!-- more -->', () => {\n    const _moreCases = [\n      '<!-- more -->',\n      '<!-- more-->',\n      '<!--more -->',\n      '<!--more-->'\n    ];\n\n    _moreCases.forEach(moreCase => _test(moreCase));\n\n    function _test(more) {\n      const content = [\n        'foo',\n        'bar',\n        more,\n        'baz'\n      ].join('\\n');\n\n      const data: {\n        content: string;\n        excerpt?: string;\n        more?: string;\n      } = {\n        content\n      };\n\n      excerpt(data);\n\n      data.content.should.eql([\n        'foo',\n        'bar',\n        '<span id=\"more\"></span>',\n        'baz'\n      ].join('\\n'));\n\n      data.excerpt!.should.eql([\n        'foo',\n        'bar'\n      ].join('\\n'));\n\n      data.more!.should.eql([\n        'baz'\n      ].join('\\n'));\n    }\n  });\n\n  it('multiple <!-- more -->', () => {\n    const content = [\n      'foo',\n      '<!-- more -->',\n      'bar',\n      '<!-- more -->',\n      'baz'\n    ].join('\\n');\n\n    const data: {\n      content: string;\n      excerpt?: string;\n      more?: string;\n    } = {\n      content\n    };\n\n    excerpt(data);\n\n    data.content.should.eql([\n      'foo',\n      '<span id=\"more\"></span>',\n      'bar',\n      '<!-- more -->',\n      'baz'\n    ].join('\\n'));\n\n    data.excerpt!.should.eql([\n      'foo'\n    ].join('\\n'));\n\n    data.more!.should.eql([\n      'bar',\n      '<!-- more -->',\n      'baz'\n    ].join('\\n'));\n  });\n\n  it('skip processing if post/page.excerpt is present in the front-matter', () => {\n    const content = [\n      'foo',\n      '<!-- more -->',\n      'bar'\n    ].join('\\n');\n\n    const data: {\n      content: string;\n      excerpt: string;\n      more?: string;\n    } = {\n      content,\n      excerpt: 'baz'\n    };\n\n    excerpt(data);\n\n    data.content.should.eql([\n      'foo',\n      '<!-- more -->',\n      'bar'\n    ].join('\\n'));\n\n    data.excerpt.should.eql([\n      'baz'\n    ].join('\\n'));\n\n    data.more!.should.eql([\n      'foo',\n      '<!-- more -->',\n      'bar'\n    ].join('\\n'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/external_link.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport decache from 'decache';\nimport externalLinkFilter from '../../../lib/plugins/filter/after_render/external_link';\nimport externalLinkPostFilter from '../../../lib/plugins/filter/after_post_render/external_link';\nimport chai from 'chai';\nconst should = chai.should();\ntype ExternalLinkParams = Parameters<typeof externalLinkFilter>;\ntype ExternalLinkReturn = ReturnType<typeof externalLinkFilter>;\ntype ExternalLinkPostParams = Parameters<typeof externalLinkPostFilter>;\ntype ExternalLinkPostReturn = ReturnType<typeof externalLinkPostFilter>;\n\ndescribe('External link', () => {\n  const hexo = new Hexo();\n  let externalLink: (...args: ExternalLinkParams) => ExternalLinkReturn;\n\n  beforeEach(() => {\n    decache('../../../lib/plugins/filter/after_render/external_link');\n    externalLink = require('../../../lib/plugins/filter/after_render/external_link').bind(hexo);\n  });\n  hexo.config = {\n    url: 'https://example.com',\n    external_link: {\n      enable: true,\n      field: 'site',\n      exclude: ''\n    }\n  } as any;\n\n  it('disabled', () => {\n    const content = 'foo'\n      + '<a href=\"https://hexo.io/\">Hexo</a>'\n      + 'bar';\n\n    hexo.config.external_link.enable = false;\n\n    should.not.exist(externalLink(content));\n    hexo.config.external_link.enable = true;\n  });\n\n  it('field is post', () => {\n    const content = 'foo'\n      + '<a href=\"https://hexo.io/\">Hexo</a>'\n      + 'bar';\n\n    hexo.config.external_link.field = 'post';\n\n    should.not.exist(externalLink(content));\n    hexo.config.external_link.field = 'site';\n  });\n\n  it('enabled', () => {\n    const content = [\n      '# External link test',\n      '1. External link',\n      '<a href=\"https://hexo.io/\">Hexo</a>',\n      '2. External link with \"rel\" Attribute',\n      '<a rel=\"external\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"external\">Hexo</a>',\n      '<a rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"noopener\">Hexo</a>',\n      '<a rel=\"external noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '3. External link with Other Attributes',\n      '<a class=\"img\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" class=\"img\">Hexo</a>',\n      '4. Internal link',\n      '<a href=\"/archives/foo.html\">Link</a>',\n      '5. Ignore links have \"target\" attribute',\n      '<a href=\"https://hexo.io/\" target=\"_blank\">Hexo</a>',\n      '6. Ignore links don\\'t have \"href\" attribute',\n      '<a>Anchor</a>',\n      '7. Ignore links whose hostname is same as config',\n      '<a href=\"https://example.com\">Example Domain</a>'\n    ].join('\\n');\n\n    const result = externalLink(content);\n\n    result.should.eql([\n      '# External link test',\n      '1. External link',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '2. External link with \"rel\" Attribute',\n      '<a rel=\"external noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '<a rel=\"noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"noopener\">Hexo</a>',\n      '<a rel=\"external noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '3. External link with Other Attributes',\n      '<a class=\"img\" target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\" class=\"img\">Hexo</a>',\n      '4. Internal link',\n      '<a href=\"/archives/foo.html\">Link</a>',\n      '5. Ignore links have \"target\" attribute',\n      '<a href=\"https://hexo.io/\" target=\"_blank\">Hexo</a>',\n      '6. Ignore links don\\'t have \"href\" attribute',\n      '<a>Anchor</a>',\n      '7. Ignore links whose hostname is same as config',\n      '<a href=\"https://example.com\">Example Domain</a>'\n    ].join('\\n'));\n  });\n\n  it('exclude - string', () => {\n    const content = [\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n');\n\n    hexo.config.external_link.exclude = 'foo.com';\n\n    const result = externalLink(content);\n\n    result.should.eql([\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://bar.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n'));\n\n    hexo.config.external_link.exclude = '';\n  });\n\n  it('exclude - array', () => {\n    const content = [\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n');\n\n    // @ts-expect-error\n    hexo.config.external_link.exclude = ['foo.com', 'bar.com'];\n\n    const result = externalLink(content);\n\n    result.should.eql([\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n'));\n\n    hexo.config.external_link.exclude = '';\n  });\n});\n\ndescribe('External link - post', () => {\n  const Hexo = require('../../../lib/hexo');\n  const hexo = new Hexo();\n\n  let externalLink: (...args: ExternalLinkPostParams) => ExternalLinkPostReturn;\n\n  beforeEach(() => {\n    decache('../../../lib/plugins/filter/after_post_render/external_link');\n    externalLink = require('../../../lib/plugins/filter/after_post_render/external_link').bind(hexo);\n  });\n\n  hexo.config = {\n    url: 'https://example.com',\n    external_link: {\n      enable: true,\n      field: 'post',\n      exclude: ''\n    }\n  };\n\n  it('disabled', () => {\n    const content = 'foo<a href=\"https://hexo.io/\">Hexo</a>bar';\n\n    const data = {content};\n    hexo.config.external_link.enable = false;\n\n    externalLink(data);\n    data.content.should.eql(content);\n    hexo.config.external_link.enable = true;\n  });\n\n  it('field is site', () => {\n    const content = 'foo'\n      + '<a href=\"https://hexo.io/\">Hexo</a>'\n      + 'bar';\n\n    const data = {content};\n    hexo.config.external_link.field = 'site';\n\n    externalLink(data);\n    data.content.should.eql(content);\n    hexo.config.external_link.field = 'post';\n  });\n\n  it('enabled', () => {\n    const content = [\n      '# External link test',\n      '1. External link',\n      '<a href=\"https://hexo.io/\">Hexo</a>',\n      '2. Link with hash (#), mailto: , javascript: shouldn\\'t be processed',\n      '<a href=\"#top\">Hexo</a>',\n      '<a href=\"mailto:hi@hexo.io\">Hexo</a>',\n      '<a href=\"javascript:alert(\\'Hexo is awesome!\\');\">Hexo</a>',\n      '3. External link with \"rel\" Attribute',\n      '<a rel=\"external\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"external\">Hexo</a>',\n      '<a rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"noopener\">Hexo</a>',\n      '<a rel=\"external noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '4. External link with Other Attributes',\n      '<a class=\"img\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a href=\"https://hexo.io/\" class=\"img\">Hexo</a>',\n      '5. Internal link',\n      '<a href=\"/archives/foo.html\">Link</a>',\n      '6. Ignore links have \"target\" attribute',\n      '<a href=\"https://hexo.io/\" target=\"_blank\">Hexo</a>',\n      '7. Ignore links don\\'t have \"href\" attribute',\n      '<a>Anchor</a>',\n      '8. Ignore links whose hostname is same as config',\n      '<a href=\"https://example.com\">Example Domain</a>'\n    ].join('\\n');\n\n    const data = {content};\n    externalLink(data);\n\n    data.content.should.eql([\n      '# External link test',\n      '1. External link',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '2. Link with hash (#), mailto: , javascript: shouldn\\'t be processed',\n      '<a href=\"#top\">Hexo</a>',\n      '<a href=\"mailto:hi@hexo.io\">Hexo</a>',\n      '<a href=\"javascript:alert(\\'Hexo is awesome!\\');\">Hexo</a>',\n      '3. External link with \"rel\" Attribute',\n      '<a rel=\"external noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '<a rel=\"noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"noopener\">Hexo</a>',\n      '<a rel=\"external noopener\" target=\"_blank\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" href=\"https://hexo.io/\" rel=\"external noopener\">Hexo</a>',\n      '4. External link with Other Attributes',\n      '<a class=\"img\" target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://hexo.io/\" class=\"img\">Hexo</a>',\n      '5. Internal link',\n      '<a href=\"/archives/foo.html\">Link</a>',\n      '6. Ignore links have \"target\" attribute',\n      '<a href=\"https://hexo.io/\" target=\"_blank\">Hexo</a>',\n      '7. Ignore links don\\'t have \"href\" attribute',\n      '<a>Anchor</a>',\n      '8. Ignore links whose hostname is same as config',\n      '<a href=\"https://example.com\">Example Domain</a>'\n    ].join('\\n'));\n  });\n\n\n  it('backward compatibility', () => {\n    const content = 'foo'\n      + '<a href=\"https://hexo.io/\">Hexo</a>'\n      + 'bar';\n\n    const data = {content};\n    hexo.config.external_link = false;\n\n    externalLink(data);\n    data.content.should.eql(content);\n\n    hexo.config.external_link = {\n      enable: true,\n      field: 'post',\n      exclude: ''\n    };\n  });\n\n  it('exclude - string', () => {\n    const content = [\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n');\n\n    hexo.config.external_link.exclude = 'foo.com';\n\n    const data = {content};\n    externalLink(data);\n\n    data.content.should.eql([\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://bar.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n'));\n\n    hexo.config.external_link.exclude = '';\n  });\n\n  it('exclude - array', () => {\n    const content = [\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n');\n\n    hexo.config.external_link.exclude = ['foo.com', 'bar.com'];\n\n    const data = {content};\n    externalLink(data);\n\n    data.content.should.eql([\n      '<a href=\"https://foo.com/\">Hexo</a>',\n      '<a href=\"https://bar.com/\">Hexo</a>',\n      '<a target=\"_blank\" rel=\"noopener\" href=\"https://baz.com/\">Hexo</a>'\n    ].join('\\n'));\n\n    hexo.config.external_link.exclude = '';\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/i18n_locals.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport i18nLocalsFilter from '../../../lib/plugins/filter/template_locals/i18n';\ntype I18nLocalsFilterParams = Parameters<typeof i18nLocalsFilter>;\ntype I18nLocalsFilterReturn = ReturnType<typeof i18nLocalsFilter>;\n\ndescribe('i18n locals', () => {\n  const hexo = new Hexo();\n  const i18nFilter: (...args: I18nLocalsFilterParams) => I18nLocalsFilterReturn = i18nLocalsFilter.bind(hexo);\n  const theme = hexo.theme;\n  const i18n = theme.i18n;\n\n  // Default language\n  i18n.languages = ['en', 'default'];\n\n  // Fixtures\n  i18n.set('de', {\n    Home: 'Zuhause'\n  });\n\n  i18n.set('default', {\n    Home: 'Default Home'\n  });\n\n  i18n.set('en', {\n    Home: 'Home'\n  });\n\n  i18n.set('zh-tw', {\n    Home: '首頁'\n  });\n\n  it('page.lang set', () => {\n    const locals = {\n      config: hexo.config,\n      page: {\n        lang: 'zh-tw'\n      }\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.__('Home').should.eql('首頁');\n  });\n\n  it('page.language set', () => {\n    const locals = {\n      config: hexo.config,\n      page: {\n        language: 'zh-tw'\n      }\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.__('Home').should.eql('首頁');\n  });\n\n  it('detect by path (lang found)', () => {\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'zh-tw/index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('zh-tw');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('首頁');\n  });\n\n  it('detect by path (lang not found)', () => {\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'news/index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('en');\n    locals.page.canonical_path.should.eql('news/index.html');\n    locals.__('Home').should.eql('Home');\n  });\n\n  it('use config by default', () => {\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('en');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('Home');\n  });\n\n  it('use config by default - with multiple languages, first language should be used', () => {\n    const oldConfig = i18n.languages;\n    i18n.languages = ['zh-tw', 'en', 'default'];\n\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('zh-tw');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('首頁');\n\n    i18n.languages = oldConfig;\n  });\n\n  it('use config by default - with no languages, default language should be used', () => {\n    const oldConfig = i18n.languages;\n    i18n.languages = ['default'];\n\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('default');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('Default Home');\n\n    i18n.languages = oldConfig;\n  });\n\n  it('use config by default - with unknown language, default language should be used', () => {\n    const oldConfig = i18n.languages;\n    i18n.languages = ['fr', 'default'];\n\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('fr');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('Default Home');\n\n    i18n.languages = oldConfig;\n  });\n\n  it('use config by default - with no set language and no default file take first available', () => {\n    const oldConfig = i18n.languages;\n    const oldSet = i18n.get('default');\n    i18n.remove('default');\n    i18n.languages = ['default'];\n\n    const locals = {\n      config: hexo.config,\n      page: {} as any,\n      path: 'index.html'\n    } as any;\n\n    i18nFilter(locals);\n\n    locals.page.lang.should.eql('default');\n    locals.page.canonical_path.should.eql('index.html');\n    locals.__('Home').should.eql('Zuhause');\n\n    i18n.set('default', oldSet);\n    i18n.languages = oldConfig;\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/meta_generator.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport decache from 'decache';\nimport * as cheerio from 'cheerio';\nimport type hexoMetaGeneratorInject from '../../../lib/plugins/filter/after_render/meta_generator';\nimport chai from 'chai';\nconst should = chai.should();\ntype hexoMetaGeneratorInjectParams = Parameters<typeof hexoMetaGeneratorInject>;\ntype hexoMetaGeneratorInjectReturn = ReturnType<typeof hexoMetaGeneratorInject>;\n\ndescribe('Meta Generator', () => {\n  const hexo = new Hexo();\n  let metaGenerator: (...args: hexoMetaGeneratorInjectParams) => hexoMetaGeneratorInjectReturn;\n\n  beforeEach(() => {\n    decache('../../../lib/plugins/filter/after_render/meta_generator');\n    metaGenerator = require('../../../lib/plugins/filter/after_render/meta_generator').bind(hexo);\n  });\n\n  it('default', () => {\n    const content = '<head><link></head>';\n    const result = metaGenerator(content);\n\n    const $ = cheerio.load(result);\n    $('meta[name=\"generator\"]').should.have.lengthOf(1);\n    $('meta[name=\"generator\"]').attr('content')!.should.eql(`Hexo ${hexo.version}`);\n  });\n\n  it('disable meta_generator', () => {\n    const content = '<head><link></head>';\n    hexo.config.meta_generator = false;\n    const result = metaGenerator(content);\n\n    should.not.exist(result);\n  });\n\n  it('no duplicate generator tag', () => {\n    hexo.config.meta_generator = true;\n\n    should.not.exist(metaGenerator('<head><link><meta name=\"generator\" content=\"foo\"></head>'));\n    should.not.exist(metaGenerator('<head><link><meta content=\"foo\" name=\"generator\"></head>'));\n  });\n\n  // Test for Issue #3777\n  it('multi-line head', () => {\n    const content = '<head>\\n<link>\\n</head>';\n    hexo.config.meta_generator = true;\n    const result = metaGenerator(content);\n\n    const $ = cheerio.load(result);\n    $('meta[name=\"generator\"]').should.have.lengthOf(1);\n\n    const expected = '<head>\\n<link>\\n<meta name=\"generator\" content=\"Hexo ' + hexo.version + '\"></head>';\n\n    result.should.eql(expected);\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/new_post_path.ts",
    "content": "import { join } from 'path';\nimport moment from 'moment';\nimport { createSha1Hash } from 'hexo-util';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport newPostPathFilter from '../../../lib/plugins/filter/new_post_path';\ntype NewPostPathFilterParams = Parameters<typeof newPostPathFilter>;\ntype NewPostPathFilterReturn = ReturnType<typeof newPostPathFilter>;\n\ndescribe('new_post_path', () => {\n  const hexo = new Hexo(join(__dirname, 'new_post_path_test'));\n  const newPostPath: (...args: NewPostPathFilterParams) => NewPostPathFilterReturn = newPostPathFilter.bind(hexo);\n  const sourceDir = hexo.source_dir;\n  const draftDir = join(sourceDir, '_drafts');\n  const postDir = join(sourceDir, '_posts');\n\n  before(async () => {\n    hexo.config.new_post_name = ':title.md';\n\n    await mkdirs(hexo.base_dir);\n    hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('page layout + path', async () => {\n    const target = await newPostPath({\n      path: 'foo',\n      layout: 'page'\n    });\n    target.should.eql(join(sourceDir, 'foo.md'));\n  });\n\n  it('draft layout + path', async () => {\n    const target = await newPostPath({\n      path: 'foo',\n      layout: 'draft'\n    });\n    target.should.eql(join(draftDir, 'foo.md'));\n  });\n\n  it('default layout + path', async () => {\n    const target = await newPostPath({\n      path: 'foo'\n    });\n    target.should.eql(join(postDir, 'foo.md'));\n  });\n\n  it('page layout + slug', async () => {\n    const target = await newPostPath({\n      slug: 'foo',\n      layout: 'page'\n    });\n    target.should.eql(join(sourceDir, 'foo', 'index.md'));\n  });\n\n  it('draft layout + slug', async () => {\n    const target = await newPostPath({\n      slug: 'foo',\n      layout: 'draft'\n    });\n    target.should.eql(join(draftDir, 'foo.md'));\n  });\n\n  it('default layout + slug', async () => {\n    const now = moment();\n    hexo.config.new_post_name = ':year-:month-:day-:title.md';\n\n    const target = await newPostPath({\n      slug: 'foo'\n    });\n    target.should.eql(join(postDir, now.format('YYYY-MM-DD') + '-foo.md'));\n  });\n\n  it('date', async () => {\n    const date = moment([2014, 0, 1]);\n    hexo.config.new_post_name = ':year-:i_month-:i_day-:title.md';\n\n    const target = await newPostPath({\n      slug: 'foo',\n      date: date.toDate() as any\n    });\n    target.should.eql(join(postDir, date.format('YYYY-M-D') + '-foo.md'));\n  });\n\n  it('extra data', async () => {\n    hexo.config.new_post_name = ':foo-:bar-:title.md';\n\n    const target = await newPostPath({\n      slug: 'foo',\n      foo: 'oh',\n      bar: 'ya'\n    });\n    target.should.eql(join(postDir, 'oh-ya-foo.md'));\n  });\n\n  it('append extension name if not existed', async () => {\n    hexo.config.new_post_name = ':title';\n\n    const target = await newPostPath({\n      slug: 'foo'\n    });\n    target.should.eql(join(postDir, 'foo.md'));\n  });\n\n  it('hash', async () => {\n    const now = moment();\n    const slug = 'foo';\n    const sha1 = createSha1Hash();\n    const hash = sha1.update(slug + now.unix().toString())\n      .digest('hex').slice(0, 12);\n    hexo.config.new_post_name = ':title-:hash';\n\n    const target = await newPostPath({\n      slug,\n      title: 'tree',\n      date: now.format('YYYY-MM-DD HH:mm:ss') as any\n    });\n\n    target.should.eql(join(postDir, `${slug}-${hash}.md`));\n  });\n\n  it('don\\'t append extension name if existed', async () => {\n    const target = await newPostPath({\n      path: 'foo.markdown'\n    });\n    target.should.eql(join(postDir, 'foo.markdown'));\n  });\n\n  it('replace existing files', async () => {\n    const filename = 'test.md';\n    const path = join(postDir, filename);\n\n    await writeFile(path, '');\n    const target = await newPostPath({\n      path: filename\n    }, true);\n\n    target.should.eql(path);\n    await unlink(path);\n  });\n\n  it('rename if target existed', async () => {\n    const filename = [\n      'test.md',\n      'test-1.md',\n      'test-2.md',\n      'test-foo.md'\n    ];\n\n    const path = filename.map(item => join(postDir, item));\n\n    await Promise.all(path.map(item => writeFile(item, '')));\n    const target = await newPostPath({\n      path: filename[0]\n    });\n    target.should.eql(join(postDir, 'test-3.md'));\n\n    await Promise.all(path.map(item => unlink(item)));\n  });\n\n  it('data is required', async () => {\n    try {\n      await newPostPath();\n    } catch (err) {\n      err.message.should.have.string('Either data.path or data.slug is required!');\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/post_permalink.ts",
    "content": "import moment from 'moment';\nimport Hexo from '../../../lib/hexo';\nimport postPermalinkFilter from '../../../lib/plugins/filter/post_permalink';\ntype PostPermalinkFilterParams = Parameters<typeof postPermalinkFilter>;\ntype PostPermalinkFilterReturn = ReturnType<typeof postPermalinkFilter>;\n\ndescribe('post_permalink', () => {\n  const hexo = new Hexo();\n  const postPermalink: (...args: PostPermalinkFilterParams) => PostPermalinkFilterReturn = postPermalinkFilter.bind(hexo);\n  const Post = hexo.model('Post');\n  let post;\n\n  before(async () => {\n    hexo.config.permalink = ':year/:month/:day/:title/';\n    hexo.config.permalink_defaults = {};\n\n    await hexo.init();\n    const apost = await Post.insert({\n      source: 'foo.md',\n      slug: 'foo',\n      date: moment('2014-01-02')\n    });\n    const id = apost._id;\n    await apost.setCategories(['foo', 'bar']);\n    post = Post.findById(id);\n  });\n\n  it('default', () => {\n    postPermalink(post).should.eql('2014/01/02/foo/');\n  });\n\n  it('categories', () => {\n    hexo.config.permalink = ':category/:title/';\n    postPermalink(post).should.eql('foo/bar/foo/');\n  });\n\n  it('uncategorized', async () => {\n    hexo.config.permalink = ':category/:title/';\n\n    const post = await Post.insert({\n      source: 'bar.md',\n      slug: 'bar'\n    });\n    postPermalink(post).should.eql(hexo.config.default_category + '/bar/');\n    Post.removeById(post._id);\n  });\n\n  it('extra data', () => {\n    hexo.config.permalink = ':layout/:title/';\n    postPermalink(post).should.eql(post.layout + '/foo/');\n  });\n\n  it('id', () => {\n    hexo.config.permalink = ':id';\n\n    postPermalink(post).should.eql(post._id);\n\n    post.id = 1;\n    postPermalink(post).should.eql('1');\n  });\n\n  it('name', async () => {\n    hexo.config.permalink = ':title/:name';\n\n    const post = await Post.insert({\n      source: 'sub/bar.md',\n      slug: 'sub/bar'\n    });\n    postPermalink(post).should.eql('sub/bar/bar');\n    Post.removeById(post._id);\n  });\n\n  it('post_title', async () => {\n    hexo.config.permalink = ':year/:month/:day/:post_title/';\n\n    const post = await Post.insert({\n      source: 'sub/2015-05-06-my-new-post.md',\n      slug: '2015-05-06-my-new-post',\n      title: 'My New Post',\n      date: moment('2015-05-06')\n    });\n    postPermalink(post).should.eql('2015/05/06/my-new-post/');\n    Post.removeById(post._id);\n  });\n\n  it('hour minute and second', async () => {\n    hexo.config.permalink = ':year/:month/:day/:hour/:minute/:second/:post_title/';\n\n    const post = await Post.insert({\n      source: 'sub/2015-05-06-my-new-post.md',\n      slug: '2015-05-06-my-new-post',\n      title: 'My New Post',\n      date: moment('2015-05-06 12:13:14')\n    });\n    postPermalink(post).should.eql('2015/05/06/12/13/14/my-new-post/');\n    Post.removeById(post._id);\n  });\n\n  it('timestamp', async () => {\n    hexo.config.permalink = ':timestamp/:slug';\n    const timestamp = '1736401514';\n    const dates = [\n      moment('2025-01-09 05:45:14Z'),\n      moment('2025-01-08 22:45:14-07')\n    ];\n    const posts = await Post.insert(\n      dates.map((date, idx) => {\n        return { source: `test${idx}.md`, slug: `test${idx}`, date: date };\n      })\n    );\n\n    postPermalink(posts[0]).should.eql(`${timestamp}/test0`);\n    postPermalink(posts[1]).should.eql(`${timestamp}/test1`);\n\n    return Promise.all(\n      posts.map(post => {\n        return Post.removeById(post._id);\n      })\n    );\n  });\n\n  it('time is omitted in front-matter', async () => {\n    hexo.config.permalink = ':year/:month/:day/:hour/:minute/:second/:post_title/';\n\n    const post = await Post.insert({\n      source: 'sub/2015-05-06-my-new-post.md',\n      slug: '2015-05-06-my-new-post',\n      title: 'My New Post',\n      date: moment('2015-05-06')\n    });\n    postPermalink(post).should.eql('2015/05/06/00/00/00/my-new-post/');\n    Post.removeById(post._id);\n  });\n\n  it('permalink_defaults', async () => {\n    hexo.config.permalink = 'posts/:lang/:title/';\n    hexo.config.permalink_defaults = {lang: 'en'};\n\n    const posts = await Post.insert([{\n      source: 'my-new-post.md',\n      slug: 'my-new-post',\n      title: 'My New Post1'\n    }, {\n      source: 'my-new-fr-post.md',\n      slug: 'my-new-fr-post',\n      title: 'My New Post2',\n      lang: 'fr'\n    }]);\n    postPermalink(posts[0]).should.eql('posts/en/my-new-post/');\n    postPermalink(posts[1]).should.eql('posts/fr/my-new-fr-post/');\n\n    await Promise.all(posts.map(post => Post.removeById(post._id)));\n  });\n\n  it('permalink_defaults - null', async () => {\n    hexo.config.permalink = 'posts/:lang/:title/';\n    hexo.config.permalink_defaults = null as any;\n\n    const posts = await Post.insert([{\n      source: 'my-new-post.md',\n      slug: 'my-new-post',\n      title: 'My New Post1',\n      lang: 'en'\n    }, {\n      source: 'my-new-post-2.md',\n      slug: 'my-new-post-2',\n      title: 'My New Post2',\n      lang: 'fr'\n    }]);\n    postPermalink(posts[0]).should.eql('posts/en/my-new-post/');\n    postPermalink(posts[1]).should.eql('posts/fr/my-new-post-2/');\n\n    await Promise.all(posts.map(post => Post.removeById(post._id)));\n  });\n\n  it('permalink - should override everything', async () => {\n    hexo.config.permalink = ':year/:month/:day/:title/';\n\n    const posts = await Post.insert([{\n      source: 'my-new-post.md',\n      slug: 'hexo/permalink-test',\n      __permalink: 'hexo/permalink-test',\n      title: 'Permalink Test',\n      date: moment('2014-01-02')\n    }, {\n      source: 'another-new-post.md',\n      slug: '/hexo-hexo/permalink-test-2',\n      __permalink: '/hexo-hexo/permalink-test-2',\n      title: 'Permalink Test',\n      date: moment('2014-01-02')\n    }]);\n\n    postPermalink(posts[0]).should.eql('/hexo/permalink-test');\n    postPermalink(posts[1]).should.eql('/hexo-hexo/permalink-test-2');\n\n    await Promise.all(posts.map(post => Post.removeById(post._id)));\n  });\n\n  it('permalink - should end with / or .html - 1', async () => {\n    hexo.config.post_asset_folder = true;\n    hexo.config.permalink = ':year/:month/:day/:title';\n\n    const post = await Post.insert({\n      source: 'foo.md',\n      slug: 'foo',\n      date: moment('2014-01-02')\n    });\n\n    postPermalink(post).should.eql('2014/01/02/foo/');\n\n    Post.removeById(post._id);\n    hexo.config.post_asset_folder = false;\n  });\n\n\n  it('permalink - should end with / or .html - 2', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const posts = await Post.insert([{\n      source: 'my-new-post.md',\n      slug: 'hexo/permalink-test',\n      __permalink: 'hexo/permalink-test',\n      title: 'Permalink Test',\n      date: moment('2014-01-02')\n    }, {\n      source: 'another-new-post.md',\n      slug: '/hexo-hexo/permalink-test-2',\n      __permalink: '/hexo-hexo/permalink-test-2/',\n      title: 'Permalink Test',\n      date: moment('2014-01-02')\n    }, {\n      source: 'another-another-new-post.md',\n      slug: '/hexo-hexo/permalink-test-3',\n      __permalink: '/hexo-hexo/permalink-test-3.html',\n      title: 'Permalink Test',\n      date: moment('2014-01-02')\n    }]);\n\n    postPermalink(posts[0]).should.eql('/hexo/permalink-test/');\n    postPermalink(posts[1]).should.eql('/hexo-hexo/permalink-test-2/');\n    postPermalink(posts[2]).should.eql('/hexo-hexo/permalink-test-3.html');\n\n    await Promise.all(posts.map(post => Post.removeById(post._id)));\n    hexo.config.post_asset_folder = false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/render_post.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport renderPostFilter from '../../../lib/plugins/filter/before_generate/render_post';\nimport { content, expected } from '../../fixtures/post_render';\ntype RenderPostFilterParams = Parameters<typeof renderPostFilter>;\ntype RenderPostFilterReturn = ReturnType<typeof renderPostFilter>;\n\ndescribe('Render post', () => {\n  const hexo = new Hexo();\n  const Post = hexo.model('Post');\n  const Page = hexo.model('Page');\n  const renderPost: (...args: RenderPostFilterParams) => RenderPostFilterReturn = renderPostFilter.bind(hexo);\n\n  before(async () => {\n    await hexo.init();\n    await hexo.loadPlugin(require.resolve('hexo-renderer-marked'));\n  });\n\n  it('post', async () => {\n    let post = await Post.insert({\n      source: 'foo.md',\n      slug: 'foo',\n      _content: content\n    });\n\n    const id = post._id;\n    await renderPost();\n\n    post = Post.findById(id);\n    post.content.trim().should.eql(expected);\n\n    post.remove();\n  });\n\n  it('page', async () => {\n    let page = await Page.insert({\n      source: 'foo.md',\n      path: 'foo.html',\n      _content: content\n    });\n\n    const id = page._id;\n    await renderPost();\n\n    page = Page.findById(id);\n    page.content.trim().should.eql(expected);\n\n    page.remove();\n  });\n\n});\n"
  },
  {
    "path": "test/scripts/filters/save_database.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport { exists, unlink } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport saveDatabaseFilter from '../../../lib/plugins/filter/before_exit/save_database';\ntype SaveDatabaseFilterParams = Parameters<typeof saveDatabaseFilter>\ntype SaveDatabaseFilterReturn = ReturnType<typeof saveDatabaseFilter>\n\ndescribe('Save database', () => {\n  const hexo = new Hexo();\n  const saveDatabase: (...args: SaveDatabaseFilterParams) => BluebirdPromise<SaveDatabaseFilterReturn> = BluebirdPromise.method(saveDatabaseFilter).bind(hexo);\n  const dbPath = hexo.database.options.path;\n\n  it('default', async () => {\n    hexo.env.init = true;\n    hexo._dbLoaded = true;\n\n    await saveDatabase();\n    const exist = await exists(dbPath);\n    exist.should.be.true;\n\n    unlink(dbPath);\n  });\n\n  it('do nothing if hexo is not initialized', async () => {\n    hexo.env.init = false;\n    hexo._dbLoaded = true;\n\n    await saveDatabase();\n    const exist = await exists(dbPath);\n    exist.should.be.false;\n  });\n\n  it('do nothing if database is not loaded', async () => {\n    hexo.env.init = true;\n    hexo._dbLoaded = false;\n\n    await saveDatabase();\n    const exist = await exists(dbPath);\n    exist.should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/filters/titlecase.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport titlecaseFilter from '../../../lib/plugins/filter/before_post_render/titlecase';\ntype titlecaseFilterParams = Parameters<typeof titlecaseFilter>;\ntype titlecaseFilterReturn = ReturnType<typeof titlecaseFilter>;\n\ndescribe('Titlecase', () => {\n  const hexo = new Hexo();\n  const titlecase: (...args: titlecaseFilterParams) => titlecaseFilterReturn = titlecaseFilter.bind(hexo);\n\n  it('disabled', () => {\n    const title = 'Today is a good day';\n    const data = {title};\n    hexo.config.titlecase = false;\n\n    titlecase(data);\n    data.title.should.eql(title);\n  });\n\n  it('enabled', () => {\n    const title = 'Today is a good day';\n    const data = {title};\n    hexo.config.titlecase = true;\n\n    titlecase(data);\n    data.title.should.eql('Today Is a Good Day');\n  });\n\n  it('enabled globally but disabled in a specify post', () => {\n    const title = 'Today is a good day';\n    const data = {title, titlecase: false};\n    hexo.config.titlecase = true;\n\n    titlecase(data);\n    data.title.should.eql('Today is a good day');\n  });\n\n  it('disabled globally but enabled in a specify post', () => {\n    const title = 'Today is a good day';\n    const data = {title, titlecase: true};\n    hexo.config.titlecase = false;\n\n    titlecase(data);\n    data.title.should.eql('Today Is a Good Day');\n  });\n\n});\n"
  },
  {
    "path": "test/scripts/generators/asset.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport { readStream } from '../../util';\nimport Hexo from '../../../lib/hexo';\nimport assetGenerator from '../../../lib/plugins/generator/asset';\nimport { spy } from 'sinon';\nimport chai from 'chai';\nconst should = chai.should();\ntype AssetParams = Parameters<typeof assetGenerator>\ntype AssetReturn = ReturnType<typeof assetGenerator>\n\ndescribe('asset', () => {\n  const hexo = new Hexo(join(__dirname, 'asset_test'), {silent: true});\n  const generator: (...args: AssetParams) => AssetReturn = assetGenerator.bind(hexo);\n  const Asset = hexo.model('Asset');\n\n  const checkStream = async (stream, expected) => {\n    const data = await readStream(stream);\n    data.should.eql(expected);\n  };\n\n  before(async () => {\n    await mkdirs(hexo.base_dir);\n    hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('renderable', async () => {\n    const path = 'test.yml';\n    const source = join(hexo.base_dir, path);\n    const content = 'foo: bar';\n\n    await Promise.all([\n      Asset.insert({_id: path, path}),\n      writeFile(source, content)\n    ]);\n    const data = await generator();\n    data[0].path.should.eql('test.json');\n    data[0].data.modified.should.be.true;\n\n    const result = await data[0].data.data!();\n    result.should.eql('{\"foo\":\"bar\"}');\n\n    await Promise.all([\n      Asset.removeById(path),\n      unlink(source)\n    ]);\n  });\n\n  it('renderable - error', async () => {\n    const logSpy = spy();\n    hexo.log.error = logSpy;\n    const path = 'test.yml';\n    const source = join(hexo.base_dir, path);\n    const content = 'foo: :';\n\n    await Promise.all([\n      Asset.insert({_id: path, path}),\n      writeFile(source, content)\n    ]);\n    const data = await generator();\n    data[0].path.should.eql('test.json');\n    data[0].data.modified.should.be.true;\n    await data[0].data.data!();\n    logSpy.called.should.be.true;\n\n    logSpy.args[0][1].should.contains('Asset render failed: %s');\n    logSpy.args[0][2].should.contains('test.json');\n    await Promise.all([\n      Asset.removeById(path),\n      unlink(source)\n    ]);\n  });\n\n  it('not renderable', async () => {\n    const path = 'test.txt';\n    const source = join(hexo.base_dir, path);\n    const content = 'test content';\n\n    await Promise.all([\n      Asset.insert({_id: path, path}),\n      writeFile(source, content)\n    ]);\n    const data = await generator();\n    data[0].path.should.eql(path);\n    data[0].data.modified.should.be.true;\n\n    await checkStream(data[0].data.data!(), content);\n\n    await Promise.all([\n      Asset.removeById(path),\n      unlink(source)\n    ]);\n  });\n\n  it('skip render', async () => {\n    const path = 'test.yml';\n    const source = join(hexo.base_dir, path);\n    const content = 'foo: bar';\n\n    await Promise.all([\n      Asset.insert({_id: path, path, renderable: false}),\n      writeFile(source, content)\n    ]);\n    const data = await generator();\n    data[0].path.should.eql('test.yml');\n    data[0].data.modified.should.be.true;\n\n    await checkStream(data[0].data.data!(), content);\n    await Promise.all([\n      Asset.removeById(path),\n      unlink(source)\n    ]);\n  });\n\n  it('remove assets which does not exist', async () => {\n    const path = 'test.txt';\n\n    await Asset.insert({\n      _id: path,\n      path\n    });\n    await generator();\n    should.not.exist(Asset.findById(path));\n  });\n\n  it('don\\'t remove extension name', async () => {\n    const path = 'test.min.js';\n    const source = join(hexo.base_dir, path);\n\n    await Promise.all([\n      Asset.insert({_id: path, path}),\n      writeFile(source, '')\n    ]);\n    const data = await generator();\n    data[0].path.should.eql('test.min.js');\n\n    await Promise.all([\n      Asset.removeById(path),\n      unlink(source)\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/scripts/generators/page.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport pageGenerator from '../../../lib/plugins/generator/page';\nimport chai from 'chai';\nimport { BaseGeneratorReturn } from '../../../lib/types';\nconst should = chai.should();\ntype PageGeneratorParams = Parameters<typeof pageGenerator>;\ntype PageGeneratorReturn = ReturnType<typeof pageGenerator>;\n\ndescribe('page', () => {\n  const hexo = new Hexo(__dirname, {silent: true});\n  const Page = hexo.model('Page');\n  const generator: (...args: PageGeneratorParams) => BluebirdPromise<PageGeneratorReturn> = BluebirdPromise.method(pageGenerator.bind(hexo));\n\n  const locals = (): any => {\n    hexo.locals.invalidate();\n    return hexo.locals.toObject();\n  };\n\n  it('default layout', async () => {\n    const page = await Page.insert({\n      source: 'foo',\n      path: 'bar'\n    });\n    const data = await generator(locals());\n    page.__page = true;\n\n    data.should.eql([\n      {\n        path: page.path,\n        layout: ['page', 'post', 'index'],\n        data: page\n      }\n    ]);\n\n    page.remove();\n  });\n\n  it('custom layout', async () => {\n    const page = await Page.insert({\n      source: 'foo',\n      path: 'bar',\n      layout: 'photo'\n    });\n    const data = await generator(locals()) as BaseGeneratorReturn[];\n    data[0].layout!.should.eql(['photo', 'page', 'post', 'index']);\n\n    page.remove();\n  });\n\n  [false, 'false', 'off'].forEach(layout => {\n    it('layout = ' + JSON.stringify(layout), async () => {\n      const page = await Page.insert({\n        source: 'foo',\n        path: 'bar',\n        layout\n      });\n      const data = await generator(locals()) as BaseGeneratorReturn[];\n      should.not.exist(data[0].layout);\n\n      page.remove();\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/generators/post.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport postGenerator from '../../../lib/plugins/generator/post';\nimport { BaseGeneratorReturn } from '../../../lib/types';\nimport chai from 'chai';\nconst should = chai.should();\ntype PostGeneratorParams = Parameters<typeof postGenerator>;\ntype PostGeneratorReturn = ReturnType<typeof postGenerator>;\n\ndescribe('post', () => {\n  const hexo = new Hexo(__dirname, {silent: true});\n  const Post = hexo.model('Post');\n  const generator: (...args: PostGeneratorParams) => BluebirdPromise<PostGeneratorReturn> = BluebirdPromise.method(postGenerator.bind(hexo));\n\n  hexo.config.permalink = ':title/';\n\n  const locals = (): any => {\n    hexo.locals.invalidate();\n    return hexo.locals.toObject();\n  };\n\n  before(() => hexo.init());\n\n  it('default layout', async () => {\n    const post = await Post.insert({\n      source: 'foo',\n      slug: 'bar'\n    });\n    const data = await generator(locals());\n    post.__post = true;\n\n    data.should.eql([\n      {\n        path: 'bar/',\n        layout: ['post', 'page', 'index'],\n        data: post\n      }\n    ]);\n\n    post.remove();\n  });\n\n  it('custom layout', async () => {\n    const post = await Post.insert({\n      source: 'foo',\n      slug: 'bar',\n      layout: 'photo'\n    });\n    const data = await generator(locals()) as BaseGeneratorReturn[];\n    data[0].layout!.should.eql(['photo', 'post', 'page', 'index']);\n\n    post.remove();\n  });\n\n  it('layout disabled', async () => {\n    const post = await Post.insert({\n      source: 'foo',\n      slug: 'bar',\n      layout: false\n    });\n    const data = await generator(locals()) as BaseGeneratorReturn[];\n    should.not.exist(data[0].layout);\n\n    post.remove();\n  });\n\n  it('prev/next post', async () => {\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo', date: 1e8},\n      {source: 'bar', slug: 'bar', date: 1e8 + 1},\n      {source: 'baz', slug: 'baz', date: 1e8 - 1}\n    ]);\n    const data = await generator(locals()) as BaseGeneratorReturn[];\n    should.not.exist(data[0].data.prev);\n    data[0].data.next!._id!.should.eq(posts[0]._id);\n    data[1].data.prev!._id!.should.eq(posts[1]._id);\n    data[1].data.next!._id!.should.eq(posts[2]._id);\n    data[2].data.prev!._id!.should.eq(posts[0]._id);\n    should.not.exist(data[2].data.next);\n\n    await BluebirdPromise.all(posts.map(post => post.remove()));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/css.ts",
    "content": "import * as cheerio from 'cheerio';\nimport Hexo from '../../../lib/hexo';\nimport cssHelper from '../../../lib/plugins/helper/css';\ntype CssHelperParams = Parameters<typeof cssHelper>;\ntype CssHelperReturn = ReturnType<typeof cssHelper>;\n\ndescribe('css', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const css: (...args: CssHelperParams) => CssHelperReturn = cssHelper.bind(ctx);\n\n  function assertResult(result, expected) {\n    const $ = cheerio.load(result);\n\n    if (!Array.isArray(expected)) {\n      expected = [expected];\n    }\n\n    expected.forEach((item, index) => {\n      if (typeof item === 'string' || item instanceof String) {\n        $('link').eq(index).attr('href')!.should.eql(item);\n      } else {\n        for (const attribute in item) {\n          $('link').eq(index).attr(attribute)!.should.eql(item[attribute]);\n        }\n      }\n    });\n  }\n\n  it('a string', () => {\n    assertResult(css('style'), '/style.css');\n    assertResult(css('style.css'), '/style.css');\n    assertResult(css('https://hexo.io/style.css'), 'https://hexo.io/style.css');\n    assertResult(css('//hexo.io/style.css'), '//hexo.io/style.css');\n  });\n\n  it('an array', () => {\n    assertResult(css(['//hexo.io/style.css']), '//hexo.io/style.css');\n\n    assertResult(css(['foo', 'bar', 'baz']), ['/foo.css', '/bar.css', '/baz.css']);\n  });\n\n  it('multiple strings', () => {\n    assertResult(css('foo', 'bar', 'baz'), ['/foo.css', '/bar.css', '/baz.css']);\n  });\n\n  it('multiple arrays', () => {\n    assertResult(css(['foo', 'bar'], ['baz']), ['/foo.css', '/bar.css', '/baz.css']);\n  });\n\n  it('mixed', () => {\n    assertResult(css(['foo', 'bar'], 'baz'), ['/foo.css', '/bar.css', '/baz.css']);\n  });\n\n  it('an object', () => {\n    assertResult(css({href: 'script.css'}), {href: '/script.css'});\n    assertResult(css({href: '/script.css'}), {href: '/script.css'});\n    assertResult(css({href: 'script'}), {href: '/script.css'});\n    assertResult(css({href: '/script.css', foo: 'bar'}), {href: '/script.css', foo: 'bar'});\n  });\n\n  it('multiple objects', () => {\n    assertResult(css({href: '/foo.css'}, {href: '/bar.css'}), [{href: '/foo.css'}, {href: '/bar.css'}]);\n    assertResult(css({href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}),\n      [{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]);\n  });\n\n  it('an array of objects', () => {\n    assertResult(css([{href: '/foo.css'}, {href: '/bar.css'}]), [{href: '/foo.css'}, {href: '/bar.css'}]);\n    assertResult(css([{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]),\n      [{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]);\n  });\n\n  it('relative link', () => {\n    ctx.config.relative_link = true;\n    ctx.config.root = '/';\n\n    ctx.path = '';\n    assertResult(css('style'), 'style.css');\n\n    ctx.path = 'foo/bar/';\n    assertResult(css('style'), '../../style.css');\n\n    ctx.config.relative_link = false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/date.ts",
    "content": "import moment from 'moment-timezone';\nimport { useFakeTimers } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport { date as dateHelper, date_xml, relative_date, time as timeHelper, full_date, time_tag, toMomentLocale } from '../../../lib/plugins/helper/date';\ntype DateHelperParams = Parameters<typeof dateHelper>;\ntype DateHelperReturn = ReturnType<typeof dateHelper>;\ntype TimeHelperParams = Parameters<typeof timeHelper>;\ntype TimeHelperReturn = ReturnType<typeof timeHelper>;\ntype FullDateHelperParams = Parameters<typeof full_date>;\ntype FullDateHelperReturn = ReturnType<typeof full_date>;\ntype TimeTagHelperParams = Parameters<typeof time_tag>;\ntype TimeTagHelperReturn = ReturnType<typeof time_tag>;\ntype RelativeDateHelperParams = Parameters<typeof relative_date>;\ntype RelativeDateHelperReturn = ReturnType<typeof relative_date>;\n\ndescribe('date', () => {\n  const hexo = new Hexo();\n  let clock;\n\n  before(() => {\n    clock = useFakeTimers(Date.now());\n  });\n\n  after(() => {\n    clock.restore();\n  });\n\n  it('date', () => {\n    const ctx: any = {\n      config: hexo.config,\n      page: {}\n    };\n\n    const date: (...args: DateHelperParams) => DateHelperReturn = dateHelper.bind(ctx);\n\n    // now\n    date().should.eql(moment().format(hexo.config.date_format));\n\n    // moment\n    date(moment()).should.eql(moment().format(hexo.config.date_format));\n    date(moment(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // date\n    date(new Date()).should.eql(moment().format(hexo.config.date_format));\n    date(new Date(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // number\n    date(Date.now()).should.eql(moment().format(hexo.config.date_format));\n    date(Date.now(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // page.lang\n    ctx.page.lang = 'zh-tw';\n    date(Date.now(), 'MMM D YYYY').should.eql(moment().locale('zh-tw').format('MMM D YYYY'));\n    ctx.page.lang = '';\n\n    // config.language\n    ctx.config.language = 'ja';\n    date(Date.now(), 'MMM D YYYY').should.eql(moment().locale('ja').format('MMM D YYYY'));\n    ctx.config.language = '';\n\n    // timezone\n    ctx.config.timezone = 'UTC';\n    date(Date.now(), 'LLL').should.eql(moment().tz('UTC').format('LLL'));\n    ctx.config.timezone = '';\n  });\n\n  it('date_xml', () => {\n    const dateXML = date_xml;\n\n    // now\n    dateXML().should.eql(moment().toISOString());\n\n    // moment\n    dateXML(moment()).should.eql(moment().toISOString());\n\n    // date\n    dateXML(new Date()).should.eql(moment().toISOString());\n\n    // number\n    dateXML(Date.now()).should.eql(moment().toISOString());\n  });\n\n  it('relative_date', () => {\n    const ctx = {\n      config: hexo.config,\n      page: {}\n    };\n\n    const relativeDate: (...args: RelativeDateHelperParams) => RelativeDateHelperReturn = relative_date.bind(ctx);\n\n    // now\n    relativeDate().should.eql(moment().fromNow());\n\n    // moment\n    relativeDate(moment()).should.eql(moment().fromNow());\n\n    // date\n    relativeDate(new Date()).should.eql(moment().fromNow());\n\n    // number\n    relativeDate(Date.now()).should.eql(moment().fromNow());\n  });\n\n  it('time', () => {\n    const ctx: any = {\n      config: hexo.config,\n      page: {}\n    };\n\n    const time: (...args: TimeHelperParams) => TimeHelperReturn = timeHelper.bind(ctx);\n\n    // now\n    time().should.eql(moment().format(hexo.config.time_format));\n\n    // moment\n    time(moment()).should.eql(moment().format(hexo.config.time_format));\n    time(moment(), 'H:mm').should.eql(moment().format('H:mm'));\n\n    // date\n    time(new Date()).should.eql(moment().format(hexo.config.time_format));\n    time(new Date(), 'H:mm').should.eql(moment().format('H:mm'));\n\n    // number\n    time(Date.now()).should.eql(moment().format(hexo.config.time_format));\n    time(Date.now(), 'H:mm').should.eql(moment().format('H:mm'));\n\n    // page.lang\n    ctx.page.lang = 'zh-tw';\n    time(Date.now(), 'A H:mm').should.eql(moment().locale('zh-tw').format('A H:mm'));\n    ctx.page.lang = '';\n\n    // config.language\n    ctx.config.language = 'ja';\n    time(Date.now(), 'A H:mm').should.eql(moment().locale('ja').format('A H:mm'));\n    ctx.config.language = '';\n\n    // timezone\n    ctx.config.timezone = 'UTC';\n    time().should.eql(moment().tz('UTC').format(hexo.config.time_format));\n    ctx.config.timezone = '';\n  });\n\n  it('full_date', () => {\n    const ctx: any = {\n      config: hexo.config,\n      date: dateHelper,\n      time: timeHelper,\n      page: {}\n    };\n\n    const fullDate: (...args: FullDateHelperParams) => FullDateHelperReturn = full_date.bind(ctx);\n    const fullDateFormat = hexo.config.date_format + ' ' + hexo.config.time_format;\n\n    // now\n    fullDate().should.eql(moment().format(fullDateFormat));\n\n    // moment\n    fullDate(moment()).should.eql(moment().format(fullDateFormat));\n    fullDate(moment(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // date\n    fullDate(new Date()).should.eql(moment().format(fullDateFormat));\n    fullDate(new Date(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // number\n    fullDate(Date.now()).should.eql(moment().format(fullDateFormat));\n    fullDate(Date.now(), 'MMM-D-YYYY').should.eql(moment().format('MMM-D-YYYY'));\n\n    // page.lang\n    ctx.page.lang = 'zh-tw';\n    fullDate(Date.now(), 'LLL').should.eql(moment().locale('zh-tw').format('LLL'));\n    ctx.page.lang = '';\n\n    // config.language\n    ctx.config.language = 'ja';\n    fullDate(Date.now(), 'LLL').should.eql(moment().locale('ja').format('LLL'));\n    ctx.config.language = '';\n\n    // timezone\n    ctx.config.timezone = 'UTC';\n    fullDate().should.eql(moment().tz('UTC').format(fullDateFormat));\n    ctx.config.timezone = '';\n  });\n\n  it('time_tag', () => {\n    const ctx: any = {\n      config: hexo.config,\n      date: dateHelper,\n      page: {}\n    };\n\n    const timeTag: (...args: TimeTagHelperParams) => TimeTagHelperReturn = time_tag.bind(ctx);\n\n    function result(date?, format?) {\n      date = date || new Date();\n      format = format || hexo.config.date_format;\n      return '<time datetime=\"' + moment(date).toISOString() + '\">' + moment(date).format(format) + '</time>';\n    }\n\n    function check(date, format?) {\n      format = format || hexo.config.date_format;\n      timeTag(date, format).should.eql(result(date, format));\n    }\n\n    // now\n    timeTag().should.eql(result());\n\n    // moment\n    check(moment());\n    check(moment(), 'MMM-D-YYYY');\n\n    // date\n    check(new Date());\n    check(new Date(), 'MMM-D-YYYY');\n\n    // number\n    check(Date.now());\n    check(Date.now(), 'MMM-D-YYYY');\n\n    // page.lang\n    ctx.page.lang = 'zh-tw';\n    timeTag(Date.now(), 'LLL').should.eql('<time datetime=\"' + moment().toISOString() + '\">' + moment().locale('zh-tw').format('LLL') + '</time>');\n    ctx.page.lang = '';\n\n    // config.language\n    ctx.config.language = 'ja';\n    timeTag(Date.now(), 'LLL').should.eql('<time datetime=\"' + moment().toISOString() + '\">' + moment().locale('ja').format('LLL') + '</time>');\n    ctx.config.language = '';\n\n    // timezone\n    ctx.config.timezone = 'UTC';\n    timeTag(Date.now(), 'LLL').should.eql('<time datetime=\"' + moment().toISOString() + '\">' + moment().tz('UTC').format('LLL') + '</time>');\n    ctx.config.timezone = '';\n  });\n\n  it('toMomentLocale', () => {\n    (toMomentLocale(undefined) === undefined).should.be.true;\n    // @ts-ignore\n    toMomentLocale(null)!.should.eql('en');\n    toMomentLocale('')!.should.eql('en');\n    toMomentLocale('en')!.should.eql('en');\n    toMomentLocale('default')!.should.eql('en');\n    toMomentLocale('zh-CN')!.should.eql('zh-cn');\n    toMomentLocale('zh_CN')!.should.eql('zh-cn');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/debug.ts",
    "content": "import { stub } from 'sinon';\nimport { inspectObject, log } from '../../../lib/plugins/helper/debug';\nimport { inspect } from 'util';\n\ndescribe('debug', () => {\n  it('inspect simple object', () => {\n    const obj = { foo: 'bar' };\n    inspectObject(obj).should.eql(inspect(obj));\n  });\n\n  it('inspect circular object', () => {\n    const obj: any = { foo: 'bar' };\n    obj.circular = obj;\n    inspectObject(obj).should.eql(inspect(obj));\n  });\n\n  it('inspect deep object', () => {\n    const obj = { baz: { thud: 'narf', dur: { foo: 'bar', baz: { bang: 'zoom' } } } };\n    inspectObject(obj, {depth: 2}).should.not.eql(inspect(obj, {depth: 5}));\n    inspectObject(obj, {depth: 5}).should.eql(inspect(obj, {depth: 5}));\n  });\n\n  it('log should print to console', () => {\n    const logStub = stub(console, 'log');\n\n    try {\n      log('Hello %s from debug.log()!', 'World');\n    } finally {\n      logStub.restore();\n    }\n\n    logStub.calledWithExactly('Hello %s from debug.log()!', 'World').should.be.true;\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/escape_html.ts",
    "content": "import { escapeHTML } from '../../../lib/plugins/helper/format';\n\ndescribe('escape_html', () => {\n  it('default', () => {\n    escapeHTML('<p class=\"foo\">Hello \"world\".</p>').should.eql('&lt;p class&#x3D;&quot;foo&quot;&gt;Hello &quot;world&quot;.&lt;&#x2F;p&gt;');\n  });\n\n  it('str must be a string', () => {\n    escapeHTML.should.throw('str must be a string!');\n  });\n\n  it('avoid double escape', () => {\n    escapeHTML('&lt;foo>bar</foo&gt;').should.eql('&lt;foo&gt;bar&lt;&#x2F;foo&gt;');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/favicon_tag.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport faviconTag from '../../../lib/plugins/helper/favicon_tag';\ntype faviconTagParams = Parameters<typeof faviconTag>;\ntype faviconTagReturn = ReturnType<typeof faviconTag>;\n\ndescribe('favicon_tag', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx = {\n    config: hexo.config\n  };\n\n  const favicon: (...args: faviconTagParams) => faviconTagReturn = faviconTag.bind(ctx);\n\n  it('path', () => {\n    favicon('favicon.ico').should.eql('<link rel=\"shortcut icon\" href=\"/favicon.ico\">');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/feed_tag.ts",
    "content": "import feedTag from '../../../lib/plugins/helper/feed_tag';\nimport chai from 'chai';\nconst should = chai.should();\ntype FeedTagParams = Parameters<typeof feedTag>;\ntype FeedTagReturn = ReturnType<typeof feedTag>;\n\ndescribe('feed_tag', () => {\n  const ctx: any = {\n    config: {\n      title: 'Hexo',\n      url: 'http://example.com',\n      root: '/',\n      feed: {}\n    }\n  };\n\n  beforeEach(() => { ctx.config.feed = {}; });\n\n  const feed: (...args: FeedTagParams) => FeedTagReturn = feedTag.bind(ctx);\n\n  it('path - atom', () => {\n    feed('atom.xml').should.eql('<link rel=\"alternate\" href=\"/atom.xml\" title=\"Hexo\" type=\"application/atom+xml\">');\n  });\n\n  it('path - rss', () => {\n    feed('rss2.xml').should.eql('<link rel=\"alternate\" href=\"/rss2.xml\" title=\"Hexo\" type=\"application/rss+xml\">');\n  });\n\n  it('title', () => {\n    feed('atom.xml', {title: 'RSS Feed'}).should.eql('<link rel=\"alternate\" href=\"/atom.xml\" title=\"RSS Feed\" type=\"application/atom+xml\">');\n  });\n\n  it('type', () => {\n    feed('rss.xml', {type: 'rss'}).should.eql('<link rel=\"alternate\" href=\"/rss.xml\" title=\"Hexo\" type=\"application/rss+xml\">');\n  });\n\n  it('type - null', () => {\n    feed('foo.xml', {type: null}).should.eql('<link rel=\"alternate\" href=\"/foo.xml\" title=\"Hexo\" >');\n  });\n\n  it('invalid input - number', () => {\n    // @ts-expect-error\n    should.throw(() => feed(123), 'path must be a string!');\n  });\n\n  it('invalid input - undefined', () => {\n    delete ctx.config.feed;\n    feed().should.eql('');\n  });\n\n  it('invalid input - empty', () => {\n    ctx.config.feed = {};\n    feed().should.eql('');\n  });\n\n  it('feed - parse argument if available', () => {\n    ctx.config.feed = {\n      type: 'atom',\n      path: 'atom.xml'\n    };\n\n    feed('atomic.xml').should.eql('<link rel=\"alternate\" href=\"/atomic.xml\" title=\"Hexo\" type=\"application/atom+xml\">');\n  });\n\n  it('feed - atom', () => {\n    ctx.config.feed = {\n      type: 'atom',\n      path: 'atom.xml'\n    };\n\n    feed().should.eql('<link rel=\"alternate\" href=\"/atom.xml\" title=\"Hexo\" type=\"application/atom+xml\">');\n  });\n\n  it('feed - rss2', () => {\n    ctx.config.feed = {\n      type: 'rss2',\n      path: 'rss.xml'\n    };\n\n    feed().should.eql('<link rel=\"alternate\" href=\"/rss.xml\" title=\"Hexo\" type=\"application/rss+xml\">');\n  });\n\n  it('feed - rss2', () => {\n    ctx.config.feed = {\n      type: ['atom', 'rss2'],\n      path: ['atom.xml', 'rss.xml']\n    };\n\n    feed().should.eql([\n      '<link rel=\"alternate\" href=\"/atom.xml\" title=\"Hexo\" type=\"application/atom+xml\">',\n      '<link rel=\"alternate\" href=\"/rss.xml\" title=\"Hexo\" type=\"application/rss+xml\">'\n    ].join(''));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/fragment_cache.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport fragmentCache from '../../../lib/plugins/helper/fragment_cache';\n\ndescribe('fragment_cache', () => {\n  const hexo = new Hexo(__dirname);\n  const fragment_cache = fragmentCache(hexo);\n\n  fragment_cache.call({cache: true}, 'foo', () => 123);\n\n  it('cache enabled', () => {\n    fragment_cache.call({cache: true}, 'foo').should.eql(123);\n  });\n\n  it('cache disabled', () => {\n    fragment_cache.call({cache: false}, 'foo', () => 456).should.eql(456);\n  });\n\n  it('should reset cache on generateBefore', () => {\n    fragment_cache.call({cache: true}, 'foo', () => 789).should.eql(456);\n    // reset cache\n    hexo.emit('generateBefore');\n    fragment_cache.call({cache: true}, 'foo', () => 789).should.eql(789);\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/full_url_for.ts",
    "content": "import fullUrlForHelper from '../../../lib/plugins/helper/full_url_for';\ntype FullUrlForHelperParams = Parameters<typeof fullUrlForHelper>;\ntype FullUrlForHelperReturn = ReturnType<typeof fullUrlForHelper>;\n\ndescribe('full_url_for', () => {\n  const ctx: any = {\n    config: { url: 'https://example.com' }\n  };\n\n  const fullUrlFor: (...args: FullUrlForHelperParams) => FullUrlForHelperReturn = fullUrlForHelper.bind(ctx);\n\n  it('no path input', () => {\n    fullUrlFor().should.eql(ctx.config.url + '/');\n  });\n\n  it('internal url', () => {\n    fullUrlFor('index.html').should.eql(ctx.config.url + '/index.html');\n    fullUrlFor('/').should.eql(ctx.config.url + '/');\n    fullUrlFor('/index.html').should.eql(ctx.config.url + '/index.html');\n  });\n\n  it('internal url (pretty_urls.trailing_index disabled)', () => {\n    ctx.config.pretty_urls = { trailing_index: false };\n    fullUrlFor('index.html').should.eql(ctx.config.url + '/');\n    fullUrlFor('/index.html').should.eql(ctx.config.url + '/');\n  });\n\n\n  it('external url', () => {\n    [\n      'https://hexo.io/',\n      '//google.com/',\n      // 'index.html' in external link should not be removed\n      '//google.com/index.html'\n    ].forEach(url => {\n      fullUrlFor(url).should.eql(url);\n    });\n  });\n\n  it('only hash', () => {\n    fullUrlFor('#test').should.eql(ctx.config.url + '/#test');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/gravatar.ts",
    "content": "import crypto from 'crypto';\nimport gravatarHelper from '../../../lib/plugins/helper/gravatar';\n\ndescribe('gravatar', () => {\n  function md5(str) {\n    return crypto.createHash('md5').update(str).digest('hex');\n  }\n\n  const gravatar = gravatarHelper;\n\n  const email = 'abc@abc.com';\n  const hash = md5(email);\n\n  it('default', () => {\n    gravatar(email).should.eql('https://www.gravatar.com/avatar/' + hash);\n  });\n\n  it('size', () => {\n    gravatar(email, 100).should.eql('https://www.gravatar.com/avatar/' + hash + '?s=100');\n  });\n\n  it('options', () => {\n    gravatar(email, {\n      s: 200,\n      r: 'pg',\n      d: 'mm'\n    }).should.eql('https://www.gravatar.com/avatar/' + hash + '?s=200&r=pg&d=mm');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/image_tag.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport imageTag from '../../../lib/plugins/helper/image_tag';\ntype imageTagParams = Parameters<typeof imageTag>;\ntype imageTagReturn = ReturnType<typeof imageTag>;\n\ndescribe('image_tag', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const img: (...args: imageTagParams) => imageTagReturn = imageTag.bind(ctx);\n\n  it('path', () => {\n    img('https://hexo.io/image.jpg').should.eql('<img src=\"https://hexo.io/image.jpg\">');\n  });\n\n  it('class (string)', () => {\n    img('https://hexo.io/image.jpg', {class: 'foo'})\n      .should.eql('<img src=\"https://hexo.io/image.jpg\" class=\"foo\">');\n  });\n\n  it('class (array)', () => {\n    img('https://hexo.io/image.jpg', {class: ['foo', 'bar']})\n      .should.eql('<img src=\"https://hexo.io/image.jpg\" class=\"foo bar\">');\n  });\n\n  it('alt', () => {\n    img('https://hexo.io/image.jpg', {alt: 'Image caption'})\n      .should.eql('<img src=\"https://hexo.io/image.jpg\" alt=\"Image caption\">');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/is.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport { current, home, home_first_page, post, page, archive, year, month, category, tag } from '../../../lib/plugins/helper/is';\n\ndescribe('is', () => {\n  const hexo = new Hexo(__dirname);\n\n  it('is_current', async () => {\n    await current.call({path: 'index.html', config: hexo.config}).should.be.true;\n    await current.call({path: 'tags/index.html', config: hexo.config}).should.be.false;\n    await current.call({path: 'index.html', config: hexo.config}, '/').should.be.true;\n    await current.call({path: 'index.html', config: hexo.config}, 'index.html').should.be.true;\n    await current.call({path: 'tags/index.html', config: hexo.config}, '/').should.be.false;\n    await current.call({path: 'tags/index.html', config: hexo.config}, '/index.html').should.be.false;\n    await current.call({path: 'index.html', config: hexo.config}, '/', true).should.be.true;\n    await current.call({path: 'index.html', config: hexo.config}, '/index.html', true).should.be.true;\n    await current.call({path: 'foo/bar', config: hexo.config}, 'foo', true).should.be.false;\n    await current.call({path: 'foo/bar', config: hexo.config}, 'foo').should.be.true;\n    await current.call({path: 'foo/bar', config: hexo.config}, 'foo/bar').should.be.true;\n    await current.call({path: 'foo/bar', config: hexo.config}, 'foo/baz').should.be.false;\n  });\n\n  it('is_home', async () => {\n    await home.call({page: {__index: true}}).should.be.true;\n    await home.call({page: {}}).should.be.false;\n  });\n\n  it('is_home_first_page', async () => {\n    await home_first_page.call({page: {__index: true, current: 1}}).should.be.true;\n    await home_first_page.call({page: {__index: true, current: 2}}).should.be.false;\n    await home_first_page.call({page: {__index: true}}).should.be.false;\n    await home_first_page.call({page: {}}).should.be.false;\n  });\n\n  it('is_post', async () => {\n    await post.call({page: {__post: true}}).should.be.true;\n    await post.call({page: {}}).should.be.false;\n  });\n\n  it('is_page', async () => {\n    await page.call({page: {__page: true}}).should.be.true;\n    await page.call({page: {}}).should.be.false;\n  });\n\n  it('is_archive', async () => {\n    await archive.call({page: {}}).should.be.false;\n    await archive.call({page: {archive: true}}).should.be.true;\n    await archive.call({page: {archive: false}}).should.be.false;\n  });\n\n  it('is_year', async () => {\n    await year.call({page: {}}).should.be.false;\n    await year.call({page: {archive: true}}).should.be.false;\n    await year.call({page: {archive: true, year: 2014}}).should.be.true;\n    await year.call({page: {archive: true, year: 2014}}, 2014).should.be.true;\n    await year.call({page: {archive: true, year: 2014}}, 2015).should.be.false;\n    await year.call({page: {archive: true, year: 2014, month: 10}}).should.be.true;\n  });\n\n  it('is_month', async () => {\n    await month.call({page: {}}).should.be.false;\n    await month.call({page: {archive: true}}).should.be.false;\n    await month.call({page: {archive: true, year: 2014}}).should.be.false;\n    await month.call({page: {archive: true, year: 2014, month: 10}}).should.be.true;\n    await month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 10).should.be.true;\n    await month.call({page: {archive: true, year: 2014, month: 10}}, 2015, 10).should.be.false;\n    await month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 12).should.be.false;\n    await month.call({page: {archive: true, year: 2014, month: 10}}, 10).should.be.true;\n    await month.call({page: {archive: true, year: 2014, month: 10}}, 12).should.be.false;\n  });\n\n  it('is_category', async () => {\n    await category.call({page: {category: 'foo'}}).should.be.true;\n    await category.call({page: {category: 'foo'}}, 'foo').should.be.true;\n    await category.call({page: {category: 'foo'}}, 'bar').should.be.false;\n    await category.call({page: {}}).should.be.false;\n  });\n\n  it('is_tag', async () => {\n    await tag.call({page: {tag: 'foo'}}).should.be.true;\n    await tag.call({page: {tag: 'foo'}}, 'foo').should.be.true;\n    await tag.call({page: {tag: 'foo'}}, 'bar').should.be.false;\n    await tag.call({page: {}}).should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/js.ts",
    "content": "import * as cheerio from 'cheerio';\nimport Hexo from '../../../lib/hexo';\nimport jsHelper from '../../../lib/plugins/helper/js';\ntype JsHelperParams = Parameters<typeof jsHelper>;\ntype JsHelperReturn = ReturnType<typeof jsHelper>;\n\ndescribe('js', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const js: (...args: JsHelperParams) => JsHelperReturn = jsHelper.bind(ctx);\n\n  function assertResult(result, expected) {\n    const $ = cheerio.load(result);\n\n    if (!Array.isArray(expected)) {\n      expected = [expected];\n    }\n\n    expected.forEach((item, index) => {\n      if (typeof item === 'string' || item instanceof String) {\n        $('script').eq(index).attr('src')!.should.eql(item);\n      } else {\n        for (const attribute in item) {\n          if (item[attribute] === true) {\n            $('script').eq(index).attr(attribute)!.should.eql(attribute);\n          } else {\n            $('script').eq(index).attr(attribute)!.should.eql(item[attribute]);\n          }\n        }\n      }\n    });\n  }\n\n  it('a string', () => {\n    assertResult(js('script'), '/script.js');\n    assertResult(js('script.js'), '/script.js');\n    assertResult(js('https://hexo.io/script.js'), 'https://hexo.io/script.js');\n    assertResult(js('//hexo.io/script.js'), '//hexo.io/script.js');\n  });\n\n  it('an array', () => {\n    assertResult(js(['//hexo.io/script.js']), '//hexo.io/script.js');\n\n    assertResult(js(['foo', 'bar', 'baz']), ['/foo.js', '/bar.js', '/baz.js']);\n  });\n\n  it('multiple strings', () => {\n    assertResult(js('foo', 'bar', 'baz'), ['/foo.js', '/bar.js', '/baz.js']);\n  });\n\n  it('multiple arrays', () => {\n    assertResult(js(['foo', 'bar'], ['baz']), ['/foo.js', '/bar.js', '/baz.js']);\n  });\n\n  it('mixed', () => {\n    assertResult(js(['foo', 'bar'], 'baz'), ['/foo.js', '/bar.js', '/baz.js']);\n  });\n\n  it('an object', () => {\n    assertResult(js({src: 'script.js'}), {src: '/script.js'});\n    assertResult(js({src: '/script.js'}), {src: '/script.js'});\n    assertResult(js({src: 'script'}), {src: '/script.js'});\n    assertResult(js({src: '/script.js', foo: 'bar'}), {src: '/script.js', foo: 'bar'});\n  });\n\n  it('multiple objects', () => {\n    assertResult(js({src: '/foo.js'}, {src: '/bar.js'}), [{src: '/foo.js'}, {src: '/bar.js'}]);\n    assertResult(js({src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}),\n      [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]);\n  });\n\n  it('an array of objects', () => {\n    assertResult(js([{src: '/foo.js'}, {src: '/bar.js'}]), [{src: '/foo.js'}, {src: '/bar.js'}]);\n    assertResult(js([{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]),\n      [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]);\n  });\n\n  it('async and defer attributes', () => {\n    assertResult(js({src: '/foo.js', 'async': true}), {src: '/foo.js', 'async': true});\n    assertResult(js({src: '/bar.js', 'defer': true}), {src: '/bar.js', 'defer': true});\n  });\n\n  it('relative link', () => {\n    ctx.config.relative_link = true;\n    ctx.config.root = '/';\n\n    ctx.path = '';\n    assertResult(js('script'), 'script.js');\n\n    ctx.path = 'foo/bar/';\n    assertResult(js('script'), '../../script.js');\n\n    ctx.config.relative_link = false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/link_to.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport linkToHelper from '../../../lib/plugins/helper/link_to';\ntype LinkToHelperParams = Parameters<typeof linkToHelper>;\ntype LinkToHelperReturn = ReturnType<typeof linkToHelper>;\n\ndescribe('link_to', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const linkTo: (...args: LinkToHelperParams) => LinkToHelperReturn = linkToHelper.bind(ctx);\n\n  it('path', () => {\n    linkTo('https://hexo.io/').should.eql('<a href=\"https://hexo.io/\" title=\"hexo.io\">hexo.io</a>');\n  });\n\n  it('title', () => {\n    linkTo('https://hexo.io/', 'Hexo').should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\">Hexo</a>');\n  });\n\n  it('external (boolean)', () => {\n    linkTo('https://hexo.io/', 'Hexo', true)\n      .should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\" target=\"_blank\" rel=\"noopener\">Hexo</a>');\n  });\n\n  it('external (object)', () => {\n    linkTo('https://hexo.io/', 'Hexo', {external: true})\n      .should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\" target=\"_blank\" rel=\"noopener\">Hexo</a>');\n  });\n\n  it('class (string)', () => {\n    linkTo('https://hexo.io/', 'Hexo', {class: 'foo'})\n      .should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\" class=\"foo\">Hexo</a>');\n  });\n\n  it('class (array)', () => {\n    linkTo('https://hexo.io/', 'Hexo', {class: ['foo', 'bar']})\n      .should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\" class=\"foo bar\">Hexo</a>');\n  });\n\n  it('id', () => {\n    linkTo('https://hexo.io/', 'Hexo', {id: 'foo'})\n      .should.eql('<a href=\"https://hexo.io/\" title=\"Hexo\" id=\"foo\">Hexo</a>');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/list_archives.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport listArchivesHelper from '../../../lib/plugins/helper/list_archives';\ntype ListArchivesHelperParams = Parameters<typeof listArchivesHelper>;\ntype ListArchivesHelperReturn = ReturnType<typeof listArchivesHelper>;\n\ndescribe('list_archives', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const ctx: any = {\n    config: hexo.config,\n    page: {}\n  };\n\n  const listArchives: (...args: ListArchivesHelperParams) => ListArchivesHelperReturn = listArchivesHelper.bind(ctx);\n\n  function resetLocals() {\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n  }\n\n  before(async () => {\n    await hexo.init();\n    await Post.insert([\n      {source: 'foo', slug: 'foo', date: new Date(2014, 1, 2)},\n      {source: 'bar', slug: 'bar', date: new Date(2013, 5, 6)},\n      {source: 'baz', slug: 'baz', date: new Date(2013, 9, 10)},\n      {source: 'boo', slug: 'boo', date: new Date(2013, 5, 8)}\n    ]);\n    resetLocals();\n  });\n\n  it('default', () => {\n    const result = listArchives();\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">February 2014</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">October 2013</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">June 2013</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('type: yearly', () => {\n    const result = listArchives({\n      type: 'yearly'\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/\">2014</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/\">2013</a><span class=\"archive-list-count\">3</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('format', () => {\n    const result = listArchives({\n      format: 'YYYY/M'\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">2014/2</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">2013/10</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">2013/6</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('style: false', () => {\n    const result = listArchives({\n      style: false\n    });\n\n    result.should.eql([\n      '<a class=\"archive-link\" href=\"/archives/2014/02/\">February 2014<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/10/\">October 2013<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/06/\">June 2013<span class=\"archive-count\">2</span></a>'\n    ].join(', '));\n  });\n\n  it('show_count', () => {\n    const result = listArchives({\n      show_count: false\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">February 2014</a></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">October 2013</a></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">June 2013</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('show_count + style: false', () => {\n    const result = listArchives({\n      style: false,\n      show_count: false\n    });\n\n    result.should.eql([\n      '<a class=\"archive-link\" href=\"/archives/2014/02/\">February 2014</a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/10/\">October 2013</a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/06/\">June 2013</a>'\n    ].join(', '));\n  });\n\n  it('order', () => {\n    const result = listArchives({\n      order: 1\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">June 2013</a><span class=\"archive-list-count\">2</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">October 2013</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">February 2014</a><span class=\"archive-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('transform', () => {\n    const result = listArchives({\n      transform(str) {\n        return str.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">FEBRUARY 2014</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">OCTOBER 2013</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">JUNE 2013</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('transform + style: false', () => {\n    const result = listArchives({\n      style: false,\n      transform(str) {\n        return str.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<a class=\"archive-link\" href=\"/archives/2014/02/\">FEBRUARY 2014<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/10/\">OCTOBER 2013<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/06/\">JUNE 2013<span class=\"archive-count\">2</span></a>'\n    ].join(', '));\n  });\n\n  it('separator', () => {\n    const result = listArchives({\n      style: false,\n      separator: ''\n    });\n\n    result.should.eql([\n      '<a class=\"archive-link\" href=\"/archives/2014/02/\">February 2014<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/10/\">October 2013<span class=\"archive-count\">1</span></a>',\n      '<a class=\"archive-link\" href=\"/archives/2013/06/\">June 2013<span class=\"archive-count\">2</span></a>'\n    ].join(''));\n  });\n\n  it('class', () => {\n    const result = listArchives({\n      class: 'test'\n    });\n\n    result.should.eql([\n      '<ul class=\"test-list\">',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/archives/2014/02/\">February 2014</a><span class=\"test-list-count\">1</span></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/archives/2013/10/\">October 2013</a><span class=\"test-list-count\">1</span></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/archives/2013/06/\">June 2013</a><span class=\"test-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('page.lang', () => {\n    ctx.page.lang = 'zh-tw';\n    const result = listArchives();\n    ctx.page.lang = '';\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">二月 2014</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">十月 2013</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">六月 2013</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('config.language', () => {\n    ctx.config.language = 'de';\n    const result = listArchives();\n    ctx.config.language = '';\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">Februar 2014</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">Oktober 2013</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">Juni 2013</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('timezone', () => {\n    ctx.config.timezone = 'Asia/Tokyo';\n    const result = listArchives({\n      format: 'YYYY MM ZZ'\n    });\n\n    result.should.eql([\n      '<ul class=\"archive-list\">',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2014/02/\">2014 02 +0900</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/10/\">2013 10 +0900</a><span class=\"archive-list-count\">1</span></li>',\n      '<li class=\"archive-list-item\"><a class=\"archive-list-link\" href=\"/archives/2013/06/\">2013 06 +0900</a><span class=\"archive-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n\n    ctx.config.timezone = '';\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/list_categories.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport listCategoriesHelper from '../../../lib/plugins/helper/list_categories';\ntype ListCategoriesHelperParams = Parameters<typeof listCategoriesHelper>;\ntype ListCategoriesHelperReturn = ReturnType<typeof listCategoriesHelper>;\n\ndescribe('list_categories', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n  const Category = hexo.model('Category');\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const listCategories: (...args: ListCategoriesHelperParams) => ListCategoriesHelperReturn = listCategoriesHelper.bind(ctx);\n\n  before(async () => {\n    await hexo.init();\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo'},\n      {source: 'bar', slug: 'bar'},\n      {source: 'baz', slug: 'baz'},\n      {source: 'boo', slug: 'boo'},\n      {source: 'bat', slug: 'bat'}\n    ]);\n    await Promise.all([\n      ['baz'],\n      ['baz', 'bar'],\n      ['foo'],\n      ['baz'],\n      ['bat', ['baz', 'bar']]\n    ].map((cats, i) => posts[i].setCategories(cats)));\n\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n    ctx.page = ctx.site.posts.data[1];\n  });\n\n  it('default', () => {\n    const result = listCategories();\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">bar</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('specified collection', () => {\n    const result = listCategories(Category.find({\n      parent: {$exists: false}\n    }));\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('style: false', () => {\n    const result = listCategories({\n      style: false\n    });\n\n    result.should.eql([\n      '<a class=\"category-link\" href=\"/categories/bat/\">bat<span class=\"category-count\">1</span></a>',\n      '<a class=\"category-link\" href=\"/categories/baz/\">baz<span class=\"category-count\">4</span></a>',\n      '<a class=\"category-link\" href=\"/categories/baz/bar/\">bar<span class=\"category-count\">2</span></a>',\n      '<a class=\"category-link\" href=\"/categories/foo/\">foo<span class=\"category-count\">1</span></a>'\n    ].join(', '));\n  });\n\n  it('show_count: false', () => {\n    const result = listCategories({\n      show_count: false\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">bar</a>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('class', () => {\n    const result = listCategories({\n      class: 'test'\n    });\n\n    result.should.eql([\n      '<ul class=\"test-list\">',\n      '<li class=\"test-list-item\">',\n      '<a class=\"test-list-link\" href=\"/categories/bat/\">bat</a><span class=\"test-list-count\">1</span>',\n      '</li>',\n      '<li class=\"test-list-item\">',\n      '<a class=\"test-list-link\" href=\"/categories/baz/\">baz</a><span class=\"test-list-count\">4</span>',\n      '<ul class=\"test-list-child\">',\n      '<li class=\"test-list-item\">',\n      '<a class=\"test-list-link\" href=\"/categories/baz/bar/\">bar</a><span class=\"test-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"test-list-item\">',\n      '<a class=\"test-list-link\" href=\"/categories/foo/\">foo</a><span class=\"test-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('depth', () => {\n    const result = listCategories({\n      depth: 1\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('orderby', () => {\n    const result = listCategories({\n      orderby: 'length'\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">bar</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('order', () => {\n    const result = listCategories({\n      order: -1\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">bar</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('transform', () => {\n    const result = listCategories({\n      transform(name) {\n        return name.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">BAT</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">BAZ</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">BAR</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">FOO</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('separator (blank)', () => {\n    const result = listCategories({\n      style: false,\n      separator: ''\n    });\n\n    result.should.eql([\n      '<a class=\"category-link\" href=\"/categories/bat/\">bat<span class=\"category-count\">1</span></a>',\n      '<a class=\"category-link\" href=\"/categories/baz/\">baz<span class=\"category-count\">4</span></a>',\n      '<a class=\"category-link\" href=\"/categories/baz/bar/\">bar<span class=\"category-count\">2</span></a>',\n      '<a class=\"category-link\" href=\"/categories/foo/\">foo<span class=\"category-count\">1</span></a>'\n    ].join(''));\n  });\n\n  it('separator (non-blank)', () => {\n    const result = listCategories({\n      style: false,\n      separator: '|'\n    });\n\n    result.should.eql([\n      '<a class=\"category-link\" href=\"/categories/bat/\">bat<span class=\"category-count\">1</span></a>|',\n      '<a class=\"category-link\" href=\"/categories/baz/\">baz<span class=\"category-count\">4</span></a>|',\n      '<a class=\"category-link\" href=\"/categories/baz/bar/\">bar<span class=\"category-count\">2</span></a>|',\n      '<a class=\"category-link\" href=\"/categories/foo/\">foo<span class=\"category-count\">1</span></a>'\n    ].join(''));\n  });\n\n  it('children-indicator', () => {\n    const result = listCategories({\n      children_indicator: 'has-children'\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item has-children\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/baz/bar/\">bar</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('show-current', () => {\n    const result = listCategories({\n      show_current: true\n    });\n\n    result.should.eql([\n      '<ul class=\"category-list\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/bat/\">bat</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link current\" href=\"/categories/baz/\">baz</a><span class=\"category-list-count\">4</span>',\n      '<ul class=\"category-list-child\">',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link current\" href=\"/categories/baz/bar/\">bar</a><span class=\"category-list-count\">2</span>',\n      '</li>',\n      '</ul>',\n      '</li>',\n      '<li class=\"category-list-item\">',\n      '<a class=\"category-list-link\" href=\"/categories/foo/\">foo</a><span class=\"category-list-count\">1</span>',\n      '</li>',\n      '</ul>'\n    ].join(''));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/list_posts.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport listPostsHelper from '../../../lib/plugins/helper/list_posts';\ntype ListPostsHelperParams = Parameters<typeof listPostsHelper>;\ntype ListPostsHelperReturn = ReturnType<typeof listPostsHelper>;\n\ndescribe('list_posts', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const listPosts: (...args: ListPostsHelperParams) => ListPostsHelperReturn = listPostsHelper.bind(ctx);\n\n  hexo.config.permalink = ':title/';\n\n  before(async () => {\n    await hexo.init();\n    await Post.insert([\n      {source: 'foo', slug: 'foo', title: 'Its', date: 1e8},\n      {source: 'bar', slug: 'bar', title: 'Chemistry', date: 1e8 + 1},\n      {source: 'baz', slug: 'baz', title: 'Bitch', date: 1e8 - 1}\n    ]);\n\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n  });\n\n  it('default', () => {\n    const result = listPosts();\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/bar/\">Chemistry</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">Its</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/baz/\">Bitch</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('specified collection', () => {\n    const result = listPosts(Post.find({\n      title: 'Its'\n    }));\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">Its</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('style: false', () => {\n    const result = listPosts({\n      style: false\n    });\n\n    result.should.eql([\n      '<a class=\"post-link\" href=\"/bar/\">Chemistry</a>',\n      '<a class=\"post-link\" href=\"/foo/\">Its</a>',\n      '<a class=\"post-link\" href=\"/baz/\">Bitch</a>'\n    ].join(', '));\n  });\n\n  it('orderby', () => {\n    const result = listPosts({\n      orderby: 'title'\n    });\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">Its</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/bar/\">Chemistry</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/baz/\">Bitch</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('order', () => {\n    const result = listPosts({\n      order: 1\n    });\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/baz/\">Bitch</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">Its</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/bar/\">Chemistry</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('class', () => {\n    const result = listPosts({\n      class: 'test'\n    });\n\n    result.should.eql([\n      '<ul class=\"test-list\">',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/bar/\">Chemistry</a></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/foo/\">Its</a></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/baz/\">Bitch</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('transform', () => {\n    const result = listPosts({\n      transform(str) {\n        return str.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/bar/\">CHEMISTRY</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">ITS</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/baz/\">BITCH</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('separator', () => {\n    const result = listPosts({\n      style: false,\n      separator: ''\n    });\n\n    result.should.eql([\n      '<a class=\"post-link\" href=\"/bar/\">Chemistry</a>',\n      '<a class=\"post-link\" href=\"/foo/\">Its</a>',\n      '<a class=\"post-link\" href=\"/baz/\">Bitch</a>'\n    ].join(''));\n  });\n\n  it('amount', () => {\n    const result = listPosts({\n      amount: 2\n    });\n\n    result.should.eql([\n      '<ul class=\"post-list\">',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/bar/\">Chemistry</a></li>',\n      '<li class=\"post-list-item\"><a class=\"post-list-link\" href=\"/foo/\">Its</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/list_tags.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport listTagsHelper from '../../../lib/plugins/helper/list_tags';\ntype ListTagsHelperParams = Parameters<typeof listTagsHelper>;\ntype ListTagsHelperReturn = ReturnType<typeof listTagsHelper>;\n\ndescribe('list_tags', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n  const Tag = hexo.model('Tag');\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const listTags: (...args: ListTagsHelperParams) => ListTagsHelperReturn = listTagsHelper.bind(ctx);\n\n  before(async () => {\n    await hexo.init();\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo'},\n      {source: 'bar', slug: 'bar'},\n      {source: 'baz', slug: 'baz'},\n      {source: 'boo', slug: 'boo'}\n    ]);\n    // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem\n    await Promise.all([\n      ['foo'],\n      ['baz'],\n      ['baz'],\n      ['bar']\n    ].map((tags, i) => posts[i].setTags(tags)));\n\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n  });\n\n  it('default', () => {\n    const result = listTags();\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"tag-list-count\">2</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/foo/\" rel=\"tag\">foo</a><span class=\"tag-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('specified collection', () => {\n    const result = listTags(Tag.find({\n      name: /^b/\n    }));\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"tag-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('style: false', () => {\n    const result = listTags({\n      style: false\n    });\n\n    result.should.eql([\n      '<a class=\"tag-link\" href=\"/tags/bar/\" rel=\"tag\">bar<span class=\"tag-count\">1</span></a>',\n      '<a class=\"tag-link\" href=\"/tags/baz/\" rel=\"tag\">baz<span class=\"tag-count\">2</span></a>',\n      '<a class=\"tag-link\" href=\"/tags/foo/\" rel=\"tag\">foo<span class=\"tag-count\">1</span></a>'\n    ].join(', '));\n  });\n\n  it('show_count: false', () => {\n    const result = listTags({\n      show_count: false\n    });\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/foo/\" rel=\"tag\">foo</a></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('class', () => {\n    const result = listTags({\n      class: 'test'\n    });\n\n    result.should.eql([\n      '<ul class=\"test-list\" itemprop=\"keywords\">',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"test-list-count\">1</span></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"test-list-count\">2</span></li>',\n      '<li class=\"test-list-item\"><a class=\"test-list-link\" href=\"/tags/foo/\" rel=\"tag\">foo</a><span class=\"test-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('custom class', () => {\n    const result = listTags({\n      class: {\n        ul: 'lorem',\n        li: 'ipsum',\n        a: 'tempor',\n        count: 'dolor'\n      }\n    });\n\n    result.should.eql([\n      '<ul class=\"lorem\" itemprop=\"keywords\">',\n      '<li class=\"ipsum\"><a class=\"tempor\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"dolor\">1</span></li>',\n      '<li class=\"ipsum\"><a class=\"tempor\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"dolor\">2</span></li>',\n      '<li class=\"ipsum\"><a class=\"tempor\" href=\"/tags/foo/\" rel=\"tag\">foo</a><span class=\"dolor\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('custom class not list', () => {\n    const result = listTags({\n      style: false,\n      show_count: true,\n      separator: '',\n      class: {\n        a: 'tempor',\n        label: 'lorem',\n        count: 'dolor'\n      }\n    });\n\n    result.should.eql([\n      '<a class=\"tempor\" href=\"/tags/bar/\" rel=\"tag\"><span class=\"lorem\">bar</span><span class=\"dolor\">1</span></a>',\n      '<a class=\"tempor\" href=\"/tags/baz/\" rel=\"tag\"><span class=\"lorem\">baz</span><span class=\"dolor\">2</span></a>',\n      '<a class=\"tempor\" href=\"/tags/foo/\" rel=\"tag\"><span class=\"lorem\">foo</span><span class=\"dolor\">1</span></a>'\n    ].join(''));\n  });\n\n  it('orderby', () => {\n    const result = listTags({\n      orderby: 'length'\n    });\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/foo/\" rel=\"tag\">foo</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"tag-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('order', () => {\n    const result = listTags({\n      order: -1\n    });\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/foo/\" rel=\"tag\">foo</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"tag-list-count\">2</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"tag-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('transform', () => {\n    const result = listTags({\n      transform(name) {\n        return name.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">BAR</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">BAZ</a><span class=\"tag-list-count\">2</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/foo/\" rel=\"tag\">FOO</a><span class=\"tag-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n\n  it('separator', () => {\n    const result = listTags({\n      style: false,\n      separator: ''\n    });\n\n    result.should.eql([\n      '<a class=\"tag-link\" href=\"/tags/bar/\" rel=\"tag\">bar<span class=\"tag-count\">1</span></a>',\n      '<a class=\"tag-link\" href=\"/tags/baz/\" rel=\"tag\">baz<span class=\"tag-count\">2</span></a>',\n      '<a class=\"tag-link\" href=\"/tags/foo/\" rel=\"tag\">foo<span class=\"tag-count\">1</span></a>'\n    ].join(''));\n  });\n\n  it('amount', () => {\n    const result = listTags({\n      amount: 2\n    });\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bar/\" rel=\"tag\">bar</a><span class=\"tag-list-count\">1</span></li>',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/baz/\" rel=\"tag\">baz</a><span class=\"tag-list-count\">2</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n});\n\ndescribe('list_tags transform', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const listTags: (...args: ListTagsHelperParams) => ListTagsHelperReturn = listTagsHelper.bind(ctx);\n\n  before(async () => {\n    await hexo.init();\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo'}\n    ]);\n\n    // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem\n    await Promise.all([\n      ['bad<b>HTML</b>']\n    ].map((tags, i) => posts[i].setTags(tags)));\n\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n  });\n\n  // no transform should escape HTML\n  it('no transform', () => {\n    const result = listTags();\n\n    result.should.eql([\n      '<ul class=\"tag-list\" itemprop=\"keywords\">',\n      '<li class=\"tag-list-item\"><a class=\"tag-list-link\" href=\"/tags/bad-b-HTML-b/\" rel=\"tag\">bad&lt;b&gt;HTML&lt;&#x2F;b&gt;</a><span class=\"tag-list-count\">1</span></li>',\n      '</ul>'\n    ].join(''));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/mail_to.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport mailToHelper from '../../../lib/plugins/helper/mail_to';\ntype MailToHelperParams = Parameters<typeof mailToHelper>;\ntype MailToHelperReturn = ReturnType<typeof mailToHelper>;\n\ndescribe('mail_to', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const mailto: (...args: MailToHelperParams) => MailToHelperReturn = mailToHelper.bind(ctx);\n\n  it('path', () => {\n    mailto('abc@example.com').should.eql('<a href=\"mailto:abc@example.com\" title=\"abc@example.com\">abc@example.com</a>');\n  });\n\n  it('path - array', () => {\n    const emails = ['abc@example.com', 'foo@example.com'];\n    const emailsStr = 'abc@example.com,foo@example.com';\n    mailto(emails).should.eql(`<a href=\"mailto:${emailsStr}\" title=\"${emailsStr}\">${emailsStr}</a>`);\n  });\n\n  it('text', () => {\n    mailto('abc@example.com', 'Email').should.eql('<a href=\"mailto:abc@example.com\" title=\"Email\">Email</a>');\n  });\n\n  it('subject', () => {\n    mailto('abc@example.com', 'Email', {subject: 'Hello'})\n      .should.eql('<a href=\"mailto:abc@example.com?subject=Hello\" title=\"Email\">Email</a>');\n  });\n\n  it('cc (string)', () => {\n    const data = {cc: 'abc@abc.com'};\n    const querystring = new URLSearchParams(data).toString();\n\n    mailto('abc@example.com', 'Email', {cc: 'abc@abc.com'})\n      .should.eql('<a href=\"mailto:abc@example.com?' + querystring + '\" title=\"Email\">Email</a>');\n  });\n\n  it('cc (array)', () => {\n    const data = {cc: 'abc@abc.com,bcd@bcd.com'};\n    const querystring = new URLSearchParams(data).toString();\n\n    mailto('abc@example.com', 'Email', {cc: ['abc@abc.com', 'bcd@bcd.com']})\n      .should.eql('<a href=\"mailto:abc@example.com?' + querystring + '\" title=\"Email\">Email</a>');\n  });\n\n  it('bcc (string)', () => {\n    const data = {bcc: 'abc@abc.com'};\n    const querystring = new URLSearchParams(data).toString();\n\n    mailto('abc@example.com', 'Email', {bcc: 'abc@abc.com'})\n      .should.eql('<a href=\"mailto:abc@example.com?' + querystring + '\" title=\"Email\">Email</a>');\n  });\n\n  it('bcc (array)', () => {\n    const data = {bcc: 'abc@abc.com,bcd@bcd.com'};\n    const querystring = new URLSearchParams(data).toString();\n\n    mailto('abc@example.com', 'Email', {bcc: ['abc@abc.com', 'bcd@bcd.com']})\n      .should.eql('<a href=\"mailto:abc@example.com?' + querystring + '\" title=\"Email\">Email</a>');\n  });\n\n  it('body', () => {\n    mailto('abc@example.com', 'Email', {body: 'Hello'})\n      .should.eql('<a href=\"mailto:abc@example.com?body=Hello\" title=\"Email\">Email</a>');\n  });\n\n  it('class (string)', () => {\n    mailto('abc@example.com', 'Email', {class: 'foo'})\n      .should.eql('<a href=\"mailto:abc@example.com\" title=\"Email\" class=\"foo\">Email</a>');\n  });\n\n  it('class (array)', () => {\n    mailto('abc@example.com', 'Email', {class: ['foo', 'bar']})\n      .should.eql('<a href=\"mailto:abc@example.com\" title=\"Email\" class=\"foo bar\">Email</a>');\n  });\n\n  it('id', () => {\n    mailto('abc@example.com', 'Email', {id: 'foo'})\n      .should.eql('<a href=\"mailto:abc@example.com\" title=\"Email\" id=\"foo\">Email</a>');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/markdown.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport renderHelper from '../../../lib/plugins/helper/render';\nimport markdownHelper from '../../../lib/plugins/helper/markdown';\ntype MarkdownHelperParams = Parameters<typeof markdownHelper>;\ntype MarkdownHelperReturn = ReturnType<typeof markdownHelper>;\n\ndescribe('markdown', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx = {\n    render: renderHelper(hexo)\n  };\n\n  const markdown: (...args: MarkdownHelperParams) => MarkdownHelperReturn = markdownHelper.bind(ctx);\n\n  before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked'))));\n\n  it('default', () => {\n    markdown('123456 **bold** and *italic*').should.eql('<p>123456 <strong>bold</strong> and <em>italic</em></p>\\n');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/meta_generator.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport metaGeneratorHelper from '../../../lib/plugins/helper/meta_generator';\nimport chai from 'chai';\nconst should = chai.should();\ntype MetaGeneratorHelperParams = Parameters<typeof metaGeneratorHelper>;\ntype MetaGeneratorHelperReturn = ReturnType<typeof metaGeneratorHelper>;\n\ndescribe('meta_generator', () => {\n  const hexo = new Hexo();\n\n  const metaGenerator: (...args: MetaGeneratorHelperParams) => MetaGeneratorHelperReturn = metaGeneratorHelper.bind(hexo);\n\n  it('default', () => {\n    const { version } = hexo;\n\n    should.exist(version);\n    metaGenerator().should.eql(`<meta name=\"generator\" content=\"Hexo ${version}\">`);\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/number_format.ts",
    "content": "import numberFormat from '../../../lib/plugins/helper/number_format';\n\ndescribe('number_format', () => {\n  it('default', () => {\n    numberFormat(1234.567).should.eql('1,234.567');\n  });\n\n  it('int', () => {\n    numberFormat(1234567).should.eql('1,234,567');\n  });\n\n  it('precision', () => {\n    numberFormat(1234.567, {precision: false}).should.eql('1,234.567');\n    numberFormat(1234.567, {precision: 0}).should.eql('1,234');\n    numberFormat(1234.567, {precision: 1}).should.eql('1,234.6');\n    numberFormat(1234.567, {precision: 2}).should.eql('1,234.57');\n    numberFormat(1234.567, {precision: 3}).should.eql('1,234.567');\n    numberFormat(1234.567, {precision: 4}).should.eql('1,234.5670');\n  });\n\n  it('delimiter', () => {\n    numberFormat(1234.567, {delimiter: ' '}).should.eql('1 234.567');\n  });\n\n  it('separator', () => {\n    numberFormat(1234.567, {separator: '*'}).should.eql('1,234*567');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/open_graph.ts",
    "content": "import moment from 'moment';\nimport * as cheerio from 'cheerio';\nimport { encodeURL, htmlTag as tag } from 'hexo-util';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport Hexo from '../../../lib/hexo';\nimport openGraph from '../../../lib/plugins/helper/open_graph';\nimport { post as isPost } from '../../../lib/plugins/helper/is';\n\ndescribe('open_graph', () => {\n  const hexo = new Hexo();\n  const Post = hexo.model('Post');\n\n  function meta(options) {\n    return tag('meta', options);\n  }\n\n  before(() => {\n    return hexo.init();\n  });\n\n  beforeEach(() => {\n    // Reset config\n    hexo.config = { ...defaultConfig };\n    hexo.config.permalink = ':title';\n  });\n\n  it('default', async () => {\n    let post = await Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    });\n    await post.setTags(['optimize', 'web']);\n\n    post = await Post.findById(post._id);\n\n    const result = openGraph.call({\n      page: post,\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.eql([\n      meta({property: 'og:type', content: 'website'}),\n      meta({property: 'og:title', content: hexo.config.title}),\n      meta({property: 'og:url'}),\n      meta({property: 'og:site_name', content: hexo.config.title}),\n      meta({property: 'og:locale', content: 'en_US'}),\n      meta({property: 'article:published_time', content: post.date.toISOString()}),\n      // page.updated will no longer exist by default\n      // See https://github.com/hexojs/hexo/pull/4278\n      // meta({property: 'article:modified_time', content: post.updated.toISOString()}),\n      meta({property: 'article:author', content: hexo.config.author}),\n      meta({property: 'article:tag', content: 'optimize'}),\n      meta({property: 'article:tag', content: 'web'}),\n      meta({name: 'twitter:card', content: 'summary'})\n    ].join('\\n'));\n\n    await Post.removeById(post._id);\n  });\n\n  it('title - page', () => {\n    const ctx = {\n      page: {title: 'Hello world'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({property: 'og:title', content: ctx.page.title}));\n  });\n\n  it('title - options', () => {\n    const result = openGraph.call({\n      page: {title: 'Hello world'},\n      config: hexo.config,\n      is_post: isPost\n    }, {title: 'test'});\n\n    result.should.have.string(meta({property: 'og:title', content: 'test'}));\n  });\n\n  it('type - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {type: 'photo'});\n\n    result.should.have.string(meta({property: 'og:type', content: 'photo'}));\n  });\n\n  it('type - is_post', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post() {\n        return true;\n      }\n    });\n\n    result.should.have.string(meta({property: 'og:type', content: 'article'}));\n  });\n\n  it('url - context', () => {\n    const ctx = {\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url: 'https://hexo.io/foo'\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({property: 'og:url', content: ctx.url}));\n  });\n\n  it('url - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url: 'https://hexo.io/foo'\n    }, {url: 'https://hexo.io/bar'});\n\n    result.should.have.string(meta({property: 'og:url', content: 'https://hexo.io/bar'}));\n  });\n\n  it('url - pretty_urls.trailing_index', () => {\n    hexo.config.pretty_urls.trailing_index = false;\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url: 'http://example.com/page/index.html'\n    });\n\n    const $ = cheerio.load(result);\n\n    $('meta[property=\"og:url\"]').attr('content')!.endsWith('index.html').should.be.false;\n\n    hexo.config.pretty_urls.trailing_index = true;\n  });\n\n  it('url - pretty_urls.trailing_html', () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url: 'http://example.com/page/about.html'\n    });\n\n    const $ = cheerio.load(result);\n\n    $('meta[property=\"og:url\"]').attr('content')!.endsWith('.html').should.be.false;\n\n    hexo.config.pretty_urls.trailing_html = true;\n  });\n\n  it('url - null pretty_urls', () => {\n    hexo.config.pretty_urls = null as any;\n    const url = 'http://example.com/page/about.html';\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url\n    });\n\n    const $ = cheerio.load(result);\n\n    $('meta[property=\"og:url\"]').attr('content')!.should.eql(url);\n\n    hexo.config.pretty_urls = {\n      trailing_index: true,\n      trailing_html: true\n    };\n  });\n\n  it('url - IDN', () => {\n    const ctx = {\n      page: {},\n      config: hexo.config,\n      is_post: isPost,\n      url: 'https://foô.com/bár'\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({property: 'og:url', content: encodeURL(ctx.url)}));\n  });\n\n  it('images - content', () => {\n    const result = openGraph.call({\n      page: {\n        content: [\n          '<p>123456789</p>',\n          '<img src=\"https://hexo.io/test.jpg\">',\n          '<img src=\"\">',\n          '<img class=\"img\">'\n        ].join('')\n      },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'}));\n  });\n\n  it('images - content with data-uri', () => {\n    const result = openGraph.call({\n      page: {\n        content: '<img src=\"data:image/svg+xml;utf8,<svg>...</svg>\">'\n      },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.not.have.string(meta({property: 'og:image', content: 'data:image/svg+xml;utf8,<svg>...</svg>'}));\n  });\n\n  it('images - string', () => {\n    const result = openGraph.call({\n      page: {\n        photos: 'https://hexo.io/test.jpg'\n      },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'}));\n  });\n\n  it('images - array', () => {\n    const result = openGraph.call({\n      page: {\n        photos: [\n          'https://hexo.io/foo.jpg',\n          'https://hexo.io/bar.jpg'\n        ]\n      },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string([\n      meta({property: 'og:image', content: 'https://hexo.io/foo.jpg'}),\n      meta({property: 'og:image', content: 'https://hexo.io/bar.jpg'})\n    ].join('\\n'));\n  });\n\n  it('images - don\\'t pollute context', () => {\n    const ctx = {\n      page: {\n        content: [\n          '<p>123456789</p>',\n          '<img src=\"https://hexo.io/test.jpg\">',\n          '<img src=\"\">',\n          '<img class=\"img\">'\n        ].join(''),\n        photos: []\n      },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    openGraph.call(ctx);\n    ctx.page.photos.should.eql([]);\n  });\n\n  it('images - options.image', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {image: 'https://hexo.io/test.jpg'});\n\n    result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'}));\n  });\n\n  it('images - options.images', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {images: 'https://hexo.io/test.jpg'});\n\n    result.should.have.string(meta({property: 'og:image', content: 'https://hexo.io/test.jpg'}));\n  });\n\n  it('images - prepend config.url to the path (without prefixing /)', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {images: 'test.jpg'});\n\n    result.should.have.string(meta({property: 'og:image', content: hexo.config.url + '/test.jpg'}));\n  });\n\n  it('images - prepend config.url to the path (with prefixing /)', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {images: '/test.jpg'});\n\n    result.should.have.string(meta({property: 'og:image', content: hexo.config.url + '/test.jpg'}));\n  });\n\n  it('images - resolve relative path when site is hosted in subdirectory', () => {\n    const config = hexo.config;\n    config.url = new URL('blog', config.url).toString();\n    config.root = 'blog';\n    const postUrl = new URL('/foo/bar/index.html', config.url).toString();\n\n    const result = openGraph.call({\n      page: {},\n      config,\n      is_post: isPost,\n      url: postUrl\n    }, {images: 'test.jpg'});\n\n    result.should.have.string(meta({property: 'og:image', content: new URL('/foo/bar/test.jpg', config.url).toString()}));\n  });\n\n  it('twitter_image - default same as og:image', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {images: 'image.jpg'});\n\n    result.should.have.string(meta({name: 'twitter:image', content: hexo.config.url + '/image.jpg'}));\n  });\n\n  it('twitter_image - different URLs for og:image and twitter:image', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_image: 'twitter.jpg', images: 'image.jpg'});\n\n    result.should.have.string(meta({name: 'twitter:image', content: hexo.config.url + '/twitter.jpg'}));\n  });\n\n  it('images - twitter_image absolute url', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_image: 'https://hexo.io/twitter.jpg', images: 'image.jpg'});\n\n    result.should.have.string(meta({name: 'twitter:image', content: 'https://hexo.io/twitter.jpg'}));\n  });\n\n  it('site_name - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {site_name: 'foo'});\n\n    result.should.have.string(meta({property: 'og:site_name', content: 'foo'}));\n  });\n\n  it('description - page', () => {\n    const ctx = {\n      page: {description: 'test'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({name: 'description', content: ctx.page.description}));\n    result.should.have.string(meta({property: 'og:description', content: ctx.page.description}));\n  });\n\n  it('description - options', () => {\n    const ctx = {\n      page: {description: 'test'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx, {description: 'foo'});\n\n    result.should.have.string(meta({name: 'description', content: 'foo'}));\n    result.should.have.string(meta({property: 'og:description', content: 'foo'}));\n  });\n\n  it('description - excerpt', () => {\n    const ctx = {\n      page: {excerpt: 'test'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({name: 'description', content: ctx.page.excerpt}));\n    result.should.have.string(meta({property: 'og:description', content: ctx.page.excerpt}));\n  });\n\n  it('description - content', () => {\n    const ctx = {\n      page: {content: 'test'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({name: 'description', content: ctx.page.content}));\n    result.should.have.string(meta({property: 'og:description', content: ctx.page.content}));\n  });\n\n  it('description - config', () => {\n    const ctx = {\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    hexo.config.description = 'test';\n\n    const result = openGraph.call(ctx);\n\n    result.should.have.string(meta({name: 'description', content: hexo.config.description}));\n    result.should.have.string(meta({property: 'og:description', content: hexo.config.description}));\n\n    hexo.config.description = '';\n  });\n\n  it('description - escape', () => {\n    const ctx = {\n      page: {description: '<b>Important!</b> Today is \"not\" \\'Xmas\\'!'},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const escaped = 'Important! Today is &quot;not&quot; &#39;Xmas&#39;!';\n\n    result.should.have.string(meta({name: 'description', content: escaped}));\n    result.should.have.string(meta({property: 'og:description', content: escaped}));\n  });\n\n  it('twitter_card - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_card: 'photo'});\n\n    result.should.have.string(meta({name: 'twitter:card', content: 'photo'}));\n  });\n\n  it('twitter_id - options (without prefixing @)', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_id: 'hexojs'});\n\n    result.should.have.string(meta({name: 'twitter:creator', content: '@hexojs'}));\n  });\n\n  it('twitter_id - options (with prefixing @)', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_id: '@hexojs'});\n\n    result.should.have.string(meta({name: 'twitter:creator', content: '@hexojs'}));\n  });\n\n  it('twitter_site - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {twitter_site: 'Hello'});\n\n    result.should.have.string(meta({name: 'twitter:site', content: 'Hello'}));\n  });\n\n  it('fb_admins - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {fb_admins: '123456789'});\n\n    result.should.have.string(meta({property: 'fb:admins', content: '123456789'}));\n  });\n\n  it('fb_app_id - options', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {fb_app_id: '123456789'});\n\n    result.should.have.string(meta({property: 'fb:app_id', content: '123456789'}));\n  });\n\n  it('updated - options', () => {\n    const result = openGraph.call({\n      page: { updated: moment('2016-05-23T21:20:21.372Z') },\n      config: hexo.config,\n      is_post: isPost\n    }, { });\n\n    result.should.have.string(meta({property: 'article:modified_time', content: '2016-05-23T21:20:21.372Z'}));\n  });\n\n  it('updated - options - allow overriding article:modified_time', () => {\n    const result = openGraph.call({\n      page: { updated: moment('2016-05-23T21:20:21.372Z') },\n      config: hexo.config,\n      is_post: isPost\n    }, { updated: moment('2015-04-22T20:19:20.371Z') });\n\n    result.should.have.string(meta({property: 'article:modified_time', content: '2015-04-22T20:19:20.371Z'}));\n  });\n\n  it('updated - options - allow disabling article:modified_time', () => {\n    const result = openGraph.call({\n      page: { updated: moment('2016-05-23T21:20:21.372Z') },\n      config: hexo.config,\n      is_post: isPost\n    }, { updated: false });\n\n    result.should.not.have.string(meta({property: 'article:modified_time', content: '2016-05-23T21:20:21.372Z'}));\n  });\n\n  it('description - do not add /(?:og:)?description/ meta tags if there is no description', () => {\n    const result = openGraph.call({\n      page: { },\n      config: hexo.config,\n      is_post: isPost\n    }, { });\n\n    result.should.not.have.string(meta({property: 'og:description'}));\n    result.should.not.have.string(meta({property: 'description'}));\n  });\n\n  it('keywords - page keywords array', () => {\n    const ctx = {\n      page: { tags: ['optimize', 'web'] },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['optimize', 'web'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n  });\n\n  it('keywords - page keywords string', () => {\n    const ctx = {\n      page: { tags: 'optimize' },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['optimize'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n  });\n\n  it('keywords - page tags', () => {\n    const ctx = {\n      page: { tags: ['optimize', 'web'] },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['optimize', 'web'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n  });\n\n  // https://github.com/hexojs/hexo/issues/5458\n  it('keywords - page tags sorted', () => {\n    const ctx = {\n      page: { tags: ['web', 'optimize'] },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['web', 'optimize'].sort();\n\n    result.should.have.string(meta({ property: 'article:tag', content: keywords[0] }) + '\\n' + meta({ property: 'article:tag', content: keywords[1] }));\n  });\n\n  it('keywords - config keywords array', () => {\n    hexo.config.keywords = ['optimize', 'web'];\n    const ctx = {\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['optimize', 'web'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n  });\n\n  it('keywords - page tags first', () => {\n    hexo.config.keywords = ['web3', 'web4'];\n    const ctx = {\n      page: {\n        tags: ['web1', 'web2']\n      },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['web1', 'web2'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n  });\n\n  it('keywords - use config.keywords if no tags', () => {\n    hexo.config.keywords = ['web5', 'web6'];\n    const ctx = {\n      page: { tags: [] },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['web5', 'web6'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n  });\n\n  it('keywords - null', () => {\n    const ctx = {\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n\n    result.should.not.have.string('<meta property=\"article:tag\"');\n  });\n\n  it('keywords - escape', () => {\n    const ctx = {\n      page: { tags: ['optimize', 'web&<>\"\\'/', 'site'] },\n      config: hexo.config,\n      is_post: isPost\n    };\n\n    const result = openGraph.call(ctx);\n    const keywords = ['optimize', 'web&<>\"\\'/', 'site'];\n\n    result.should.have.string(meta({property: 'article:tag', content: keywords[0]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[1]}));\n    result.should.have.string(meta({property: 'article:tag', content: keywords[2]}));\n  });\n\n  it('og:locale - options.language', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {language: 'es-cr'});\n\n    result.should.have.string(meta({property: 'og:locale', content: 'es_CR'}));\n  });\n\n  it('og:locale - options.language (incorrect format)', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {language: 'foo-bar'});\n\n    result.should.have.string(meta({property: 'og:locale', content: undefined}));\n  });\n\n  it('og:locale - page.lang', () => {\n    const result = openGraph.call({\n      page: { lang: 'es-mx' },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:locale', content: 'es_MX'}));\n  });\n\n  it('og:locale - page.language', () => {\n    const result = openGraph.call({\n      page: { language: 'es-gt' },\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:locale', content: 'es_GT'}));\n  });\n\n  it('og:locale - config.language', () => {\n    hexo.config.language = 'es-pa';\n\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:locale', content: 'es_PA'}));\n  });\n\n  it('og:locale - convert territory to uppercase', () => {\n    hexo.config.language = 'fr-fr';\n\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'og:locale', content: 'fr_FR'}));\n  });\n\n  it('og:locale - no language set', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.not.have.string(meta({property: 'og:locale'}));\n  });\n\n  it('og:locale - language is not in lang-TERRITORY format', () => {\n    hexo.config.language = 'en';\n    openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }).should.have.string(meta({property: 'og:locale', content: 'en_US'}));\n\n    hexo.config.language = 'Fr_fr';\n    openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }).should.have.string(meta({property: 'og:locale', content: 'fr_FR'}));\n\n    hexo.config.language = 'zh-CN';\n    openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }).should.have.string(meta({property: 'og:locale', content: 'zh_CN'}));\n  });\n\n  it('article:author - options.author', () => {\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    }, {author: 'Jane Doe'});\n\n    result.should.have.string(meta({property: 'article:author', content: 'Jane Doe'}));\n  });\n\n  it('article:author - config.language', () => {\n    hexo.config.language = 'es-pa';\n\n    const result = openGraph.call({\n      page: {},\n      config: hexo.config,\n      is_post: isPost\n    });\n\n    result.should.have.string(meta({property: 'article:author', content: 'John Doe'}));\n  });\n\n  it('article:author - no author set', () => {\n    const result = openGraph.call({\n      page: {},\n      config: { author: undefined },\n      is_post: isPost\n    });\n\n    result.should.not.have.string(meta({property: 'article:author'}));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/paginator.ts",
    "content": "import { url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport paginatorHelper from '../../../lib/plugins/helper/paginator';\ntype PaginatorHelperParams = Parameters<typeof paginatorHelper>;\ntype PaginatorHelperReturn = ReturnType<typeof paginatorHelper>;\n\ndescribe('paginator', () => {\n  const hexo = new Hexo(__dirname);\n\n  const ctx: any = {\n    page: {\n      base: '',\n      total: 10\n    },\n    site: hexo.locals,\n    config: hexo.config\n  };\n\n  const paginator: (...args: PaginatorHelperParams) => PaginatorHelperReturn = paginatorHelper.bind(ctx);\n\n  function link(i) {\n    return url_for.call(ctx, i === 1 ? '' : 'page/' + i + '/');\n  }\n\n  function checkResult(result, data) {\n    let expected = '';\n    const current = data.current;\n    const total = data.total;\n    const pages = data.pages;\n    const space = data.space || '&hellip;';\n    const prevNext = Object.prototype.hasOwnProperty.call(data, 'prev_next') ? data.prev_next : true;\n    let num;\n\n    if (prevNext && current > 1) {\n      expected += '<a class=\"extend prev\" rel=\"prev\" href=\"' + link(current - 1) + '\">Prev</a>';\n    }\n\n    for (let i = 0, len = pages.length; i < len; i++) {\n      num = pages[i];\n\n      if (!num) {\n        expected += '<span class=\"space\">' + space + '</span>';\n      } else if (num === current) {\n        expected += '<span class=\"page-number current\">' + current + '</span>';\n      } else {\n        expected += '<a class=\"page-number\" href=\"' + link(num) + '\">' + num + '</a>';\n      }\n    }\n\n    if (prevNext && current < total) {\n      expected += '<a class=\"extend next\" rel=\"next\" href=\"' + link(current + 1) + '\">Next</a>';\n    }\n\n    result.should.eql(expected);\n  }\n\n  [\n    [1, 2, 3, 0, 10],\n    [1, 2, 3, 4, 0, 10],\n    [1, 2, 3, 4, 5, 0, 10],\n    [1, 2, 3, 4, 5, 6, 0, 10],\n    [1, 0, 3, 4, 5, 6, 7, 0, 10],\n    [1, 0, 4, 5, 6, 7, 8, 0, 10],\n    [1, 0, 5, 6, 7, 8, 9, 10],\n    [1, 0, 6, 7, 8, 9, 10],\n    [1, 0, 7, 8, 9, 10],\n    [1, 0, 8, 9, 10]\n  ].forEach((pages, i, arr) => {\n    const current = i + 1;\n    const total = arr.length;\n\n    it('current = ' + current, () => {\n      const result = paginator({\n        current,\n        total\n      });\n\n      checkResult(result, {\n        current,\n        total,\n        pages\n      });\n    });\n  });\n\n  it('show_all', () => {\n    const result = paginator({\n      current: 5,\n      show_all: true\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n    });\n  });\n\n  it('end_size', () => {\n    const result = paginator({\n      current: 5,\n      end_size: 2\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 2, 3, 4, 5, 6, 7, 0, 9, 10]\n    });\n  });\n\n  it('end_size = 0', () => {\n    const result = paginator({\n      current: 5,\n      end_size: 0\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [0, 3, 4, 5, 6, 7, 0]\n    });\n  });\n\n  it('mid_size', () => {\n    const result = paginator({\n      current: 5,\n      mid_size: 1\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 0, 4, 5, 6, 0, 10]\n    });\n  });\n\n  it('mid_size = 0', () => {\n    const result = paginator({\n      current: 5,\n      mid_size: 0\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 0, 5, 0, 10]\n    });\n  });\n\n  it('space', () => {\n    const result = paginator({\n      current: 5,\n      space: '~'\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 0, 3, 4, 5, 6, 7, 0, 10],\n      space: '~'\n    });\n  });\n\n  it('no space', () => {\n    const result = paginator({\n      current: 5,\n      space: ''\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 3, 4, 5, 6, 7, 10]\n    });\n  });\n\n  it('base', () => {\n    const result = paginator({\n      current: 1,\n      base: 'archives/'\n    });\n\n    result.should.eql([\n      '<span class=\"page-number current\">1</span>',\n      '<a class=\"page-number\" href=\"/archives/page/2/\">2</a>',\n      '<a class=\"page-number\" href=\"/archives/page/3/\">3</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/archives/page/10/\">10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/archives/page/2/\">Next</a>'\n    ].join(''));\n  });\n\n  it('format', () => {\n    const result = paginator({\n      current: 1,\n      format: 'index-%d.html'\n    });\n\n    result.should.eql([\n      '<span class=\"page-number current\">1</span>',\n      '<a class=\"page-number\" href=\"/index-2.html\">2</a>',\n      '<a class=\"page-number\" href=\"/index-3.html\">3</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/index-10.html\">10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/index-2.html\">Next</a>'\n    ].join(''));\n  });\n\n  it('prev_text / next_text', () => {\n    const result = paginator({\n      current: 2,\n      prev_text: 'Newer',\n      next_text: 'Older'\n    });\n\n    result.should.eql([\n      '<a class=\"extend prev\" rel=\"prev\" href=\"/\">Newer</a>',\n      '<a class=\"page-number\" href=\"/\">1</a>',\n      '<span class=\"page-number current\">2</span>',\n      '<a class=\"page-number\" href=\"/page/3/\">3</a>',\n      '<a class=\"page-number\" href=\"/page/4/\">4</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/page/3/\">Older</a>'\n    ].join(''));\n  });\n\n  it('prev_next', () => {\n    const result = paginator({\n      current: 2,\n      prev_next: false\n    });\n\n    result.should.eql([\n      '<a class=\"page-number\" href=\"/\">1</a>',\n      '<span class=\"page-number current\">2</span>',\n      '<a class=\"page-number\" href=\"/page/3/\">3</a>',\n      '<a class=\"page-number\" href=\"/page/4/\">4</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">10</a>'\n    ].join(''));\n  });\n\n  it('transform', () => {\n    const result = paginator({\n      current: 2,\n      transform(page) {\n        return 'Page ' + page;\n      }\n    });\n\n    result.should.eql([\n      '<a class=\"extend prev\" rel=\"prev\" href=\"/\">Prev</a>',\n      '<a class=\"page-number\" href=\"/\">Page 1</a>',\n      '<span class=\"page-number current\">Page 2</span>',\n      '<a class=\"page-number\" href=\"/page/3/\">Page 3</a>',\n      '<a class=\"page-number\" href=\"/page/4/\">Page 4</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">Page 10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/page/3/\">Next</a>'\n    ].join(''));\n  });\n\n  it('context', () => {\n    ctx.page.current = 5;\n    const result = paginator({\n      space: ''\n    });\n\n    checkResult(result, {\n      current: 5,\n      total: 10,\n      pages: [1, 3, 4, 5, 6, 7, 10]\n    });\n  });\n\n  it('current = 0', () => {\n    ctx.page.current = 0;\n    const result = paginator({});\n\n    result.should.eql('');\n  });\n\n  it('escape', () => {\n    const result = paginator({\n      current: 2,\n      prev_text: '<foo>',\n      next_text: '<bar>',\n      escape: false\n    });\n\n    result.should.eql([\n      '<a class=\"extend prev\" rel=\"prev\" href=\"/\">',\n      '<foo></a>',\n      '<a class=\"page-number\" href=\"/\">1</a>',\n      '<span class=\"page-number current\">2</span>',\n      '<a class=\"page-number\" href=\"/page/3/\">3</a>',\n      '<a class=\"page-number\" href=\"/page/4/\">4</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/page/3/\">',\n      '<bar></a>'\n    ].join(''));\n  });\n\n  it('custom_class', () => {\n    const result = paginator({\n      current: 2,\n      current_class: 'current-class',\n      space_class: 'space-class',\n      page_class: 'page-class',\n      prev_class: 'prev-class',\n      next_class: 'next-class'\n    });\n\n    result.should.eql([\n      '<a class=\"prev-class\" rel=\"prev\" href=\"/\">Prev</a>',\n      '<a class=\"page-class\" href=\"/\">1</a>',\n      '<span class=\"page-class current-class\">2</span>',\n      '<a class=\"page-class\" href=\"/page/3/\">3</a>',\n      '<a class=\"page-class\" href=\"/page/4/\">4</a>',\n      '<span class=\"space-class\">&hellip;</span>',\n      '<a class=\"page-class\" href=\"/page/10/\">10</a>',\n      '<a class=\"next-class\" rel=\"next\" href=\"/page/3/\">Next</a>'\n    ].join(''));\n  });\n\n  it('force_prev_next', () => {\n    const result = paginator({\n      current: 1,\n      force_prev_next: true\n    });\n\n    result.should.eql([\n      '<span class=\"extend prev\" rel=\"prev\">Prev</span>',\n      '<span class=\"page-number current\">1</span>',\n      '<a class=\"page-number\" href=\"/page/2/\">2</a>',\n      '<a class=\"page-number\" href=\"/page/3/\">3</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">10</a>',\n      '<a class=\"extend next\" rel=\"next\" href=\"/page/2/\">Next</a>'\n    ].join(''));\n  });\n\n  it('force_prev_next - 2', () => {\n    const result = paginator({\n      current: 1,\n      prev_next: false,\n      force_prev_next: true\n    });\n\n    result.should.eql([\n      '<span class=\"extend prev\" rel=\"prev\">Prev</span>',\n      '<span class=\"page-number current\">1</span>',\n      '<a class=\"page-number\" href=\"/page/2/\">2</a>',\n      '<a class=\"page-number\" href=\"/page/3/\">3</a>',\n      '<span class=\"space\">&hellip;</span>',\n      '<a class=\"page-number\" href=\"/page/10/\">10</a>',\n      '<span class=\"extend next\" rel=\"next\">Next</span>'\n    ].join(''));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/partial.ts",
    "content": "import pathFn from 'path';\nimport { mkdirs, writeFile, rmdir } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport fragmentCache from '../../../lib/plugins/helper/fragment_cache';\nimport partialHelper from '../../../lib/plugins/helper/partial';\nimport chai from 'chai';\nconst should = chai.should();\ntype PartialHelperParams = Parameters<ReturnType<typeof partialHelper>>;\ntype PartialHelperReturn = ReturnType<ReturnType<typeof partialHelper>>;\n\ndescribe('partial', () => {\n  const hexo = new Hexo(pathFn.join(__dirname, 'partial_test'), {silent: true});\n  const themeDir = pathFn.join(hexo.base_dir, 'themes', 'test');\n  const viewDir = pathFn.join(themeDir, 'layout') + pathFn.sep;\n  const viewName = 'article.njk';\n\n  const ctx: any = {\n    site: hexo.locals,\n    config: hexo.config,\n    view_dir: viewDir,\n    filename: pathFn.join(viewDir, 'post', viewName),\n    foo: 'foo',\n    cache: true\n  };\n\n  ctx.fragment_cache = fragmentCache(hexo);\n\n  hexo.env.init = true;\n\n  const partial: (...args: PartialHelperParams) => PartialHelperReturn = partialHelper(hexo).bind(ctx);\n\n  before(async () => {\n    await BluebirdPromise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    await hexo.init();\n    hexo.theme.setView('widget/tag.njk', 'tag widget');\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('default', () => {\n    // relative path\n    partial('../widget/tag').should.eql('tag widget');\n\n    // absolute path\n    partial('widget/tag').should.eql('tag widget');\n\n    // not found\n    should.throw(\n      () => partial('foo'),\n      `Partial foo does not exist. (in ${pathFn.join('post', viewName)})`\n    );\n  });\n\n  it('locals', () => {\n    hexo.theme.setView('test.njk', '{{ foo }}');\n\n    partial('test', {foo: 'bar'}).should.eql('bar');\n  });\n\n  it('cache', () => {\n    hexo.theme.setView('test.njk', '{{ foo }}');\n\n    partial('test', {foo: 'bar'}, {cache: true}).should.eql('bar');\n    partial('test', {}, {cache: true}).should.eql('bar');\n\n    partial('test', {foo: 'baz'}, {cache: 'ash'}).should.eql('baz');\n    partial('test', {}, {cache: 'ash'}).should.eql('baz');\n  });\n\n  it('only', () => {\n    hexo.theme.setView('test.njk', '{{ foo }}{{ bar }}');\n\n    partial('test', {bar: 'bar'}, {only: true}).should.eql('bar');\n  });\n\n  it('a partial in another partial', () => {\n    hexo.theme.setView('partial/a.njk', '{{ partial(\"b\") }}');\n    hexo.theme.setView('partial/b.njk', '{{ partial(\"c\") }}');\n    hexo.theme.setView('partial/c.njk', 'c');\n\n    partial('partial/a').should.eql('c');\n  });\n\n  it('name must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => partial(), 'name must be a string!');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/relative_url.ts",
    "content": "import relativeURL from '../../../lib/plugins/helper/relative_url';\n\ndescribe('relative_url', () => {\n  it('from root', () => {\n    relativeURL('', 'css/style.css').should.eql('css/style.css');\n    relativeURL('index.html', 'css/style.css').should.eql('css/style.css');\n  });\n\n  it('from same root', () => {\n    relativeURL('foo/', 'foo/style.css').should.eql('style.css');\n    relativeURL('foo/index.html', 'foo/style.css').should.eql('style.css');\n    relativeURL('foo/bar/', 'foo/bar/style.css').should.eql('style.css');\n    relativeURL('foo/bar/index.html', 'foo/bar/style.css').should.eql('style.css');\n  });\n\n  it('from different root', () => {\n    relativeURL('foo/', 'css/style.css').should.eql('../css/style.css');\n    relativeURL('foo/index.html', 'css/style.css').should.eql('../css/style.css');\n    relativeURL('foo/bar/', 'css/style.css').should.eql('../../css/style.css');\n    relativeURL('foo/bar/index.html', 'css/style.css').should.eql('../../css/style.css');\n  });\n\n  it('to root', () => {\n    relativeURL('index.html', '/').should.eql('index.html');\n    relativeURL('foo/', '/').should.eql('../index.html');\n    relativeURL('foo/index.html', '/').should.eql('../index.html');\n  });\n\n  it('should encode path', () => {\n    relativeURL('foo/', 'css/fôo.css').should.eql('../css/f%C3%B4o.css');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/render.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport renderHelper from '../../../lib/plugins/helper/render';\n\ndescribe('render', () => {\n  const hexo = new Hexo(__dirname);\n  const render = renderHelper(hexo);\n\n  before(() => hexo.init());\n\n  it('default', () => {\n    const body = [\n      'foo: 1',\n      'bar:',\n      '\\tbaz: 3'\n    ].join('\\n');\n\n    const result = render(body, 'yaml');\n\n    result.should.eql({\n      foo: 1,\n      bar: {\n        baz: 3\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/search_form.ts",
    "content": "import searchFormHelper from '../../../lib/plugins/helper/search_form';\ntype SearchFormHelperParams = Parameters<typeof searchFormHelper>;\ntype SearchFormHelperReturn = ReturnType<typeof searchFormHelper>;\n\ndescribe('search_form', () => {\n  const searchForm: (...args: SearchFormHelperParams) => SearchFormHelperReturn = searchFormHelper.bind({\n    config: {url: 'https://hexo.io'}\n  });\n\n  it('default', () => {\n    searchForm().should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\" placeholder=\"Search\">'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('class', () => {\n    searchForm({class: 'foo'}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"foo\">'\n      + '<input type=\"search\" name=\"q\" class=\"foo-input\" placeholder=\"Search\">'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('text', () => {\n    searchForm({text: 'Find'}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\" placeholder=\"Find\">'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('text - null', () => {\n    searchForm({text: null}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\">'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('button - true', () => {\n    searchForm({button: true, text: 'Find'}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\" placeholder=\"Find\">'\n      + '<button type=\"submit\" class=\"search-form-submit\">Find</button>'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('button - string', () => {\n    searchForm({button: 'Go', text: 'Find'}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\" placeholder=\"Find\">'\n      + '<button type=\"submit\" class=\"search-form-submit\">Go</button>'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n\n  it('button - ignore incorrect type', () => {\n    // @ts-expect-error\n    searchForm({button: {}, text: 'Find'}).should.eql('<form action=\"//google.com/search\" method=\"get\" accept-charset=\"UTF-8\" class=\"search-form\">'\n      + '<input type=\"search\" name=\"q\" class=\"search-form-input\" placeholder=\"Find\">'\n      + '<button type=\"submit\" class=\"search-form-submit\">Find</button>'\n      + '<input type=\"hidden\" name=\"sitesearch\" value=\"https://hexo.io\">'\n      + '</form>');\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/tagcloud.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport tagcloudHelper from '../../../lib/plugins/helper/tagcloud';\nimport chai from 'chai';\nconst should = chai.should();\ntype TagcloudHelperParams = Parameters<typeof tagcloudHelper>;\ntype TagcloudHelperReturn = ReturnType<typeof tagcloudHelper>;\n\ndescribe('tagcloud', () => {\n  const hexo = new Hexo(__dirname);\n  const Post = hexo.model('Post');\n  const Tag = hexo.model('Tag');\n\n  const ctx: any = {\n    config: hexo.config\n  };\n\n  const tagcloud: (...args: TagcloudHelperParams) => TagcloudHelperReturn = tagcloudHelper.bind(ctx);\n\n  before(async () => {\n    await hexo.init();\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo'},\n      {source: 'bar', slug: 'bar'},\n      {source: 'baz', slug: 'baz'},\n      {source: 'boo', slug: 'boo'}\n    ]);\n    // TODO: Warehouse needs to add a mutex lock when writing data to avoid data sync problem\n    await BluebirdPromise.all([\n      ['bcd'],\n      ['bcd', 'cde'],\n      ['bcd', 'cde', 'abc'],\n      ['bcd', 'cde', 'abc', 'def']\n    ].map((tags, i) => posts[i].setTags(tags)));\n\n    hexo.locals.invalidate();\n    ctx.site = hexo.locals.toObject();\n  });\n\n  it('default', () => {\n    const result = tagcloud();\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>'\n    ].join(' '));\n  });\n\n  it('no tags', async () => {\n    const hexo = new Hexo(__dirname);\n    await hexo.init();\n    hexo.locals.invalidate();\n    // @ts-expect-error\n    hexo.site = hexo.locals.toObject();\n    const tagcloud: (...args: TagcloudHelperParams) => TagcloudHelperReturn = tagcloudHelper.bind(hexo);\n\n    const result = tagcloud();\n\n    result.should.eql('');\n  });\n\n  it('specified collection', () => {\n    const result = tagcloud(Tag.find({\n      name: /bc/\n    }));\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 10px;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>'\n    ].join(' '));\n  });\n\n  it('font size', () => {\n    const result = tagcloud({\n      min_font: 15,\n      max_font: 30\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 20px;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 30px;\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 25px;\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 15px;\">def</a>'\n    ].join(' '));\n  });\n\n  it('font size - when every tag has the same number of posts, font-size should be minimum.', () => {\n    const result = tagcloud(Tag.find({\n      name: /abc/\n    }), {\n      min_font: 15,\n      max_font: 30\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 15px;\">abc</a>'\n    ].join(' '));\n  });\n\n  it('font unit', () => {\n    const result = tagcloud({\n      unit: 'em'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33em;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20em;\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67em;\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10em;\">def</a>'\n    ].join(' '));\n  });\n\n  it('orderby - length', () => {\n    const result = tagcloud({\n      orderby: 'length'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>',\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>'\n    ].join(' '));\n  });\n\n  it('orderby - random', () => {\n    const result1 = tagcloud({\n      orderby: 'random'\n    });\n\n    const result2 = tagcloud({\n      orderby: 'rand'\n    });\n\n    result1.should.have.string('<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>');\n    result1.should.have.string('<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>');\n    result1.should.have.string('<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>');\n    result1.should.have.string('<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>');\n    result2.should.have.string('<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>');\n    result2.should.have.string('<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>');\n    result2.should.have.string('<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>');\n    result2.should.have.string('<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>');\n  });\n\n  it('order', () => {\n    const result = tagcloud({\n      order: -1\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>',\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>'\n    ].join(' '));\n  });\n\n  it('amount', () => {\n    const result = tagcloud({\n      amount: 2\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 10px;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>'\n    ].join(' '));\n  });\n\n  it('transform', () => {\n    const result = tagcloud({\n      transform(name) {\n        return name.toUpperCase();\n      }\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">ABC</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">BCD</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">CDE</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">DEF</a>'\n    ].join(' '));\n  });\n\n  it('color: name', () => {\n    const result = tagcloud({\n      color: true,\n      start_color: 'red',\n      end_color: 'pink'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px; color: #ff4044\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px; color: #ffc0cb\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px; color: #ff8087\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px; color: #f00\">def</a>'\n    ].join(' '));\n  });\n\n  it('color: hex', () => {\n    const result = tagcloud({\n      color: true,\n      start_color: '#f00', // red\n      end_color: '#ffc0cb' // pink\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px; color: #ff4044\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px; color: #ffc0cb\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px; color: #ff8087\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px; color: #f00\">def</a>'\n    ].join(' '));\n  });\n\n  it('color: RGBA', () => {\n    const result = tagcloud({\n      color: true,\n      start_color: 'rgba(70, 130, 180, 0.3)', // steelblue\n      end_color: 'rgb(70, 130, 180)'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px; color: rgba(70, 130, 180, 0.53)\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px; color: #4682b4\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px; color: rgba(70, 130, 180, 0.77)\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px; color: rgba(70, 130, 180, 0.3)\">def</a>'\n    ].join(' '));\n  });\n\n  it('color: HSLA', () => {\n    const result = tagcloud({\n      color: true,\n      start_color: 'hsla(207, 44%, 49%, 0.3)', // rgba(70, 130, 180, 0.3)\n      end_color: 'hsl(207, 44%, 49%)' // rgb(70, 130, 180)\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px; color: rgba(70, 130, 180, 0.53)\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px; color: #4682b4\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px; color: rgba(70, 130, 180, 0.77)\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px; color: rgba(70, 130, 180, 0.3)\">def</a>'\n    ].join(' '));\n  });\n\n  it('color - when every tag has the same number of posts, start_color should be used.', () => {\n    const result = tagcloud(Tag.find({\n      name: /abc/\n    }), {\n      color: true,\n      start_color: 'red',\n      end_color: 'pink'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 10px; color: #f00\">abc</a>'\n    ].join(' '));\n  });\n\n  it('color - missing start_color', () => {\n    try {\n      tagcloud({\n        color: true,\n        end_color: 'pink'\n      });\n      should.fail();\n    } catch (err) {\n      err.message.should.eql('start_color is required!');\n    }\n  });\n\n  it('separator', () => {\n    const result = tagcloud({\n      separator: ', '\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def</a>'\n    ].join(', '));\n  });\n\n  it('class name', () => {\n    const result = tagcloud({\n      class: 'tag-cloud'\n    });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\" class=\"tag-cloud-3\">abc</a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\" class=\"tag-cloud-10\">bcd</a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\" class=\"tag-cloud-7\">cde</a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\" class=\"tag-cloud-0\">def</a>'\n    ].join(' '));\n  });\n\n  it('show_count', () => {\n    const result = tagcloud({ show_count: true });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc<span class=\"count\">2</span></a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd<span class=\"count\">4</span></a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde<span class=\"count\">3</span></a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def<span class=\"count\">1</span></a>'\n    ].join(' '));\n  });\n\n  it('show_count with custom class', () => {\n    const result = tagcloud({ show_count: true, count_class: 'tag-count' });\n\n    result.should.eql([\n      '<a href=\"/tags/abc/\" style=\"font-size: 13.33px;\">abc<span class=\"tag-count\">2</span></a>',\n      '<a href=\"/tags/bcd/\" style=\"font-size: 20px;\">bcd<span class=\"tag-count\">4</span></a>',\n      '<a href=\"/tags/cde/\" style=\"font-size: 16.67px;\">cde<span class=\"tag-count\">3</span></a>',\n      '<a href=\"/tags/def/\" style=\"font-size: 10px;\">def<span class=\"tag-count\">1</span></a>'\n    ].join(' '));\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/toc.ts",
    "content": "import { escapeHTML } from 'hexo-util';\nimport toc from '../../../lib/plugins/helper/toc';\n\ndescribe('toc', () => {\n  const html = [\n    '<h1 id=\"title_1\">Title 1</h1>',\n    '<h2 id=\"title_1_1\">Title 1.1</h2>',\n    '<h3 id=\"title_1_1_1\">Title 1.1.1</h3>',\n    '<h2 id=\"title_1_2\">Title 1.2</h2>',\n    '<h2 id=\"title_1_3\">Title 1.3</h2>',\n    '<h3 id=\"title_1_3_1\">Title 1.3.1</h3>',\n    '<h1 id=\"title_2\">Title 2</h1>',\n    '<h2 id=\"title_2_1\">Title 2.1</h2>',\n    '<h1 id=\"title_3\">Title should escape &amp;, &lt;, &#39;, and &quot;</h1>',\n    '<h1 id=\"title_4\"><a name=\"chapter1\">Chapter 1 should be printed to toc</a></h1>'\n  ].join('');\n\n  it('default', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">1.2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">1.3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3_1\">',\n      '<span class=\"' + className + '-number\">1.3.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">2.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html).should.eql(expected);\n  });\n\n  it('class', () => {\n    const className = 'foo';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">1.2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">1.3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3_1\">',\n      '<span class=\"' + className + '-number\">1.3.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">2.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { class: 'foo' }).should.eql(expected);\n  });\n\n  it('list_number', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      // '<span class=\"' + className + '-number\">1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      // '<span class=\"' + className + '-number\">1.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1_1\">',\n      // '<span class=\"' + className + '-number\">1.1.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.1.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      // '<span class=\"' + className + '-number\">1.2.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      // '<span class=\"' + className + '-number\">1.3.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3_1\">',\n      // '<span class=\"' + className + '-number\">1.3.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.3.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      // '<span class=\"' + className + '-number\">2.</span> ',\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      // '<span class=\"' + className + '-number\">2.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      // '<span class=\"' + className + '-number\">3.</span> ',\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      // '<span class=\"' + className + '-number\">4.</span> ',\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { list_number: false }).should.eql(expected);\n  });\n\n  it('max_depth', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">1.2.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">1.3.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ',\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">2.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ',\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ',\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { max_depth: 2 }).should.eql(expected);\n  });\n\n  it('min_depth', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item toc-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item toc-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.1.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item toc-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item toc-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item toc-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3_1\">',\n      '<span class=\"' + className + '-number\">3.1.</span> ',\n      '<span class=\"' + className + '-text\">Title 1.3.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item toc-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">4.</span> ',\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { min_depth: 2 }).should.eql(expected);\n  });\n\n  it('No id attribute', () => {\n    const className = 'f';\n    const input = [\n      '<h1>foo</h1>',\n      '<h1 id=\"\">bar</h1>'\n    ].join('');\n\n    const expected = [\n      `<ol class=\"${className}\">`,\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\"><span class=\"${className}-text\">foo</span></a>`,\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\"><span class=\"${className}-text\">bar</span></a>`,\n      '</li></ol>'\n    ].join('');\n\n    toc(input, { list_number: false, class: className }).should.eql(expected);\n  });\n\n  it('non-ASCII id', () => {\n    const className = 'f';\n    const zh = '这是-H1-标题';\n    const zhs = zh.replace(/-/g, ' ');\n    const de = 'Ich-♥-Deutsch';\n    const des = de.replace(/-/g, ' ');\n    const ru = 'Я-люблю-русский';\n    const rus = ru.replace(/-/g, ' ');\n    const special = '%20';\n    const input = [\n      `<h1 id=\"${zh}\">${zhs}</h1>`,\n      `<h1 id=\"${de}\">${des}</h1>`,\n      `<h1 id=\"${ru}\">${rus}</h1>`,\n      `<h1 id=\"${special}\">${special}</h1>`\n    ].join('');\n\n    const expected = [\n      `<ol class=\"${className}\">`,\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\" href=\"#${encodeURI(zh)}\"><span class=\"${className}-text\">${zhs}</span></a>`,\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\" href=\"#${encodeURI(de)}\"><span class=\"${className}-text\">${des}</span></a>`,\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\" href=\"#${encodeURI(ru)}\"><span class=\"${className}-text\">${rus}</span></a>`,\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-1\">`,\n      `<a class=\"${className}-link\" href=\"#${encodeURI(special)}\"><span class=\"${className}-text\">${special}</span></a>`,\n      '</li></ol>'\n    ].join('');\n\n    toc(input, { list_number: false, class: className }).should.eql(expected);\n  });\n\n  it('escape unsafe class name', () => {\n    const className = 'f\"b';\n    const esClass = escapeHTML(className);\n    const input = '<h1>bar</h1>';\n\n    const expected = [\n      `<ol class=\"${esClass}\">`,\n      `<li class=\"${esClass}-item ${esClass}-level-1\">`,\n      `<a class=\"${esClass}-link\"><span class=\"${esClass}-text\">bar</span></a>`,\n      '</li></ol>'\n    ].join('');\n\n    toc(input, { list_number: false, class: className }).should.eql(expected);\n  });\n\n  it('invalid input', () => {\n    const input = '<h9000>bar</h9000>';\n\n    toc(input).should.eql('');\n  });\n\n  it('skipping heading level', () => {\n    const input = [\n      '<h1>Title 1</h1>',\n      '<h3>Title 3</h3>',\n      '<h4>Title 4</h4>',\n      '<h2>Title 2</h2>',\n      '<h5>Title 5</h5>',\n      '<h1>Title 1</h1>'\n    ].join('');\n\n    toc(input).should.eql('<ol class=\"toc\"><li class=\"toc-item toc-level-1\"><a class=\"toc-link\"><span class=\"toc-number\">1.</span> <span class=\"toc-text\">Title 1</span></a><ol class=\"toc-child\"><li class=\"toc-item toc-level-3\"><a class=\"toc-link\"><span class=\"toc-number\">1.1.</span> <span class=\"toc-text\">Title 3</span></a><ol class=\"toc-child\"><li class=\"toc-item toc-level-4\"><a class=\"toc-link\"><span class=\"toc-number\">1.1.1.</span> <span class=\"toc-text\">Title 4</span></a></li></ol></li><li class=\"toc-item toc-level-2\"><a class=\"toc-link\"><span class=\"toc-number\">1.2.</span> <span class=\"toc-text\">Title 2</span></a><ol class=\"toc-child\"><li class=\"toc-item toc-level-5\"><a class=\"toc-link\"><span class=\"toc-number\">1.2.1.</span> <span class=\"toc-text\">Title 5</span></a></li></ol></li></ol></li><li class=\"toc-item toc-level-1\"><a class=\"toc-link\"><span class=\"toc-number\">2.</span> <span class=\"toc-text\">Title 1</span></a></li></ol>');\n  });\n\n  it('unnumbered headings', () => {\n    const className = 'toc';\n\n    const input = [\n      '<h3>Title 1</h3>',\n      '<h3>Title 2</h3>',\n      '<h4>Title 2.1</h4>',\n      '<h3 data-toc-unnumbered=\"true\">Reference</h3>'\n    ].join('');\n\n    const expected = [\n      `<ol class=\"${className}\">`,\n      `<li class=\"${className}-item ${className}-level-3\">`,\n      `<a class=\"${className}-link\"><span class=\"${className}-number\">1.</span> `,\n      `<span class=\"${className}-text\">Title 1</span>`,\n      '</a>',\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-3\">`,\n      `<a class=\"${className}-link\">`,\n      `<span class=\"${className}-number\">2.</span> `,\n      `<span class=\"${className}-text\">Title 2</span>`,\n      '</a>',\n      `<ol class=\"${className}-child\">`,\n      `<li class=\"${className}-item ${className}-level-4\">`,\n      `<a class=\"${className}-link\">`,\n      `<span class=\"${className}-number\">2.1.</span> `,\n      `<span class=\"${className}-text\">Title 2.1</span>`,\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      `<li class=\"${className}-item ${className}-level-3\">`,\n      `<a class=\"${className}-link\">`,\n      `<span class=\"${className}-text\">Reference</span>`,\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(input, { list_number: true, class: className }).should.eql(expected);\n  });\n\n  it('custom class', () => {\n    const className = 'foo';\n    const childClassName = 'bar';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + childClassName + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      '<ol class=\"' + childClassName + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">1.2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">1.3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      '<ol class=\"' + childClassName + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-3\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3_1\">',\n      '<span class=\"' + className + '-number\">1.3.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + childClassName + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">2.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { class: 'foo', class_child: 'bar' }).should.eql(expected);\n  });\n\n  it('max_items - result contains only h1 items', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      // '<ol class=\"' + className + '-child\">',\n      // <!-- h2 is truncated -->\n      // '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      // '<ol class=\"' + className + '-child\">',\n      // <!-- h2 is truncated -->\n      // '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { max_items: 4}).should.eql(expected); // The number of `h1` is 4\n    toc(html, { max_items: 7}).should.eql(expected); // Maximum number 7 cannot display up to `h2`\n  });\n\n  it('max_items - result contains h1 and h2 items', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_1\">',\n      '<span class=\"' + className + '-number\">1.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.1</span>',\n      '</a>',\n      // '<ol class=\"' + className + '-child\">',\n      // <!-- h3 is truncated -->\n      // '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_2\">',\n      '<span class=\"' + className + '-number\">1.2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.2</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1_3\">',\n      '<span class=\"' + className + '-number\">1.3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1.3</span>',\n      '</a>',\n      // '<ol class=\"' + className + '-child\">',\n      // <!-- h3 is truncated -->\n      // '</ol>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '<ol class=\"' + className + '-child\">',\n      '<li class=\"' + className + '-item ' + className + '-level-2\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2_1\">',\n      '<span class=\"' + className + '-number\">2.1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2.1</span>',\n      '</a>',\n      '</li>',\n      '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_3\">',\n      '<span class=\"' + className + '-number\">3.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title should escape &amp;, &lt;, &#39;, and &quot;</span>',\n      '</a>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_4\">',\n      '<span class=\"' + className + '-number\">4.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Chapter 1 should be printed to toc</span>',\n      '</a>',\n      '</li>',\n      '</ol>'\n    ].join('');\n\n    toc(html, { max_items: 8}).should.eql(expected); // Maximum number 8 can display up to `h2`\n    toc(html, { max_items: 9}).should.eql(expected); // Maximum number 10 is required to display up to `h3`\n  });\n\n  it('max_items - result of h1 was truncated', () => {\n    const className = 'toc';\n    const expected = [\n      '<ol class=\"' + className + '\">',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_1\">',\n      '<span class=\"' + className + '-number\">1.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 1</span>',\n      '</a>',\n      // '<ol class=\"' + className + '-child\">',\n      // <!-- h2 is truncated -->\n      // '</ol>',\n      '</li>',\n      '<li class=\"' + className + '-item ' + className + '-level-1\">',\n      '<a class=\"' + className + '-link\" href=\"#title_2\">',\n      '<span class=\"' + className + '-number\">2.</span> ', // list_number enabled\n      '<span class=\"' + className + '-text\">Title 2</span>',\n      '</a>',\n      '</li>',\n      // <!-- `h1` is truncated from the end -->\n      '</ol>'\n    ].join('');\n\n    toc(html, { max_items: 2}).should.eql(expected); // `h1` is truncated from the end\n  });\n});\n"
  },
  {
    "path": "test/scripts/helpers/url_for.ts",
    "content": "import urlForHelper from '../../../lib/plugins/helper/url_for';\nimport relativeUrlHelper from '../../../lib/plugins/helper/relative_url';\ntype UrlForHelperParams = Parameters<typeof urlForHelper>;\ntype UrlForHelperReturn = ReturnType<typeof urlForHelper>;\n\ndescribe('url_for', () => {\n  const ctx: any = {\n    config: { url: 'https://example.com' },\n    relative_url: relativeUrlHelper\n  };\n\n  const urlFor: (...args: UrlForHelperParams) => UrlForHelperReturn = urlForHelper.bind(ctx);\n\n  it('should encode path', () => {\n    ctx.config.root = '/';\n    urlFor('fôo.html').should.eql('/f%C3%B4o.html');\n\n    ctx.config.root = '/fôo/';\n    urlFor('bár.html').should.eql('/f%C3%B4o/b%C3%A1r.html');\n  });\n\n  it('internal url (relative off)', () => {\n    ctx.config.root = '/';\n    urlFor('index.html').should.eql('/index.html');\n    urlFor('/').should.eql('/');\n    urlFor('/index.html').should.eql('/index.html');\n\n    ctx.config.root = '/blog/';\n    urlFor('index.html').should.eql('/blog/index.html');\n    urlFor('/').should.eql('/blog/');\n    urlFor('/index.html').should.eql('/blog/index.html');\n  });\n\n  it('internal url (relative on)', () => {\n    ctx.config.relative_link = true;\n    ctx.config.root = '/';\n\n    ctx.path = '';\n    urlFor('index.html').should.eql('index.html');\n\n    ctx.path = 'foo/bar/';\n    urlFor('index.html').should.eql('../../index.html');\n\n    ctx.config.relative_link = false;\n  });\n\n  it('internal url (options.relative)', () => {\n    ctx.path = '';\n    urlFor('index.html', {relative: true}).should.eql('index.html');\n\n    ctx.config.relative_link = true;\n    urlFor('index.html', {relative: false}).should.eql('/index.html');\n    ctx.config.relative_link = false;\n  });\n\n  it('internal url (pretty_urls.trailing_index disabled)', () => {\n    ctx.config.pretty_urls = { trailing_index: false };\n    ctx.path = '';\n    ctx.config.root = '/';\n    urlFor('index.html').should.eql('/');\n    urlFor('/index.html').should.eql('/');\n\n    ctx.config.root = '/blog/';\n    urlFor('index.html').should.eql('/blog/');\n    urlFor('/index.html').should.eql('/blog/');\n  });\n\n  it('external url', () => {\n    [\n      'https://hexo.io/',\n      '//google.com/',\n      // 'index.html' in external link should not be removed\n      '//google.com/index.html'\n    ].forEach(url => {\n      urlFor(url).should.eql(url);\n    });\n  });\n\n  it('only hash', () => {\n    urlFor('#test').should.eql('#test');\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/hexo.ts",
    "content": "import { sep, join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport { spy } from 'sinon';\nimport { readStream } from '../../util';\nimport { full_url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Hexo', () => {\n  const base_dir = join(__dirname, 'hexo_test');\n  const hexo = new Hexo(base_dir, { silent: true });\n  const coreDir = join(__dirname, '../../..');\n  const { version } = require('../../../package.json');\n  const Post = hexo.model('Post');\n  const Page = hexo.model('Page');\n  const Data = hexo.model('Data');\n  const { route } = hexo;\n\n  async function checkStream(stream, expected) {\n    const data = await readStream(stream);\n    data.should.eql(expected);\n  }\n\n  function loadAssetGenerator() {\n    hexo.extend.generator.register('asset', require('../../../lib/plugins/generator/asset'));\n  }\n\n  before(async () => {\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n  });\n\n  beforeEach(() => {\n    // Unregister all generators\n    hexo.extend.generator.store = {};\n    // Remove all routes\n    route.routes = {};\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  hexo.extend.console.register('test', args => args);\n\n  it('constructor', () => {\n    const hexo = new Hexo(__dirname);\n\n    /* eslint-disable no-path-concat */\n    hexo.core_dir.should.eql(coreDir + sep);\n    hexo.lib_dir.should.eql(join(coreDir, 'lib') + sep);\n    hexo.version.should.eql(version);\n    hexo.base_dir.should.eql(__dirname + sep);\n    hexo.public_dir.should.eql(join(__dirname, 'public') + sep);\n    hexo.source_dir.should.eql(join(__dirname, 'source') + sep);\n    hexo.plugin_dir.should.eql(join(__dirname, 'node_modules') + sep);\n    hexo.script_dir.should.eql(join(__dirname, 'scripts') + sep);\n    hexo.scaffold_dir.should.eql(join(__dirname, 'scaffolds') + sep);\n    /* eslint-enable no-path-concat */\n    hexo.env.should.eql({\n      args: {},\n      debug: false,\n      safe: false,\n      silent: false,\n      env: process.env.NODE_ENV || 'development',\n      version,\n      cmd: '',\n      init: false\n    });\n    hexo.config_path.should.eql(join(__dirname, '_config.yml'));\n  });\n\n  it('constructs multi-config', () => {\n    const configs = ['../../../fixtures/_config.json', '../../../fixtures/_config.json'];\n    const args = { _: [], config: configs.join(',') };\n    const hexo = new Hexo(base_dir, args);\n    hexo.config_path.should.eql(join(base_dir, '_multiconfig.yml'));\n  });\n\n  it('call()', async () => {\n    const data = await hexo.call('test', {foo: 'bar'});\n    data.should.eql({foo: 'bar'});\n  });\n\n  it('call() - callback', callback => {\n    hexo.call('test', { foo: 'bar' }, (err, data) => {\n      should.not.exist(err);\n      data.should.eql({ foo: 'bar' });\n\n      callback();\n    });\n  });\n\n  it('call() - callback without args', callback => {\n    hexo.call('test', (err, data) => {\n      should.not.exist(err);\n      data.should.eql({});\n\n      callback();\n    });\n  });\n\n  it('call() - console not registered', async () => {\n    try {\n      await hexo.call('nothing');\n      should.fail('Return value must be rejected');\n    } catch (err) {\n      err.should.property('message', 'Console `nothing` has not been registered yet!');\n    }\n  });\n\n  it('init()', async () => {\n    const hexo = new Hexo(join(__dirname, 'hexo_test'), {silent: true});\n    const hook = spy();\n\n    hexo.extend.filter.register('after_init', hook);\n\n    await hexo.init();\n    hook.calledOnce.should.be.true;\n  });\n\n  // it('model()'); missing-unit-test\n\n  it('_showDrafts()', () => {\n    hexo._showDrafts().should.be.false;\n\n    hexo.env.args.draft = true;\n    hexo._showDrafts().should.be.true;\n    hexo.env.args.draft = false;\n\n    hexo.env.args.drafts = true;\n    hexo._showDrafts().should.be.true;\n    hexo.env.args.drafts = false;\n\n    hexo.config.render_drafts = true;\n    hexo._showDrafts().should.be.true;\n    hexo.config.render_drafts = false;\n  });\n\n  async function testLoad(path) {\n    const target = join(path, 'test.txt');\n    const body = 'test';\n\n    loadAssetGenerator();\n\n    await writeFile(target, body);\n    await hexo.load();\n    await checkStream(route.get('test.txt'), body);\n    await unlink(target);\n  }\n\n  it('load() - source', async () => await testLoad(hexo.source_dir));\n\n  it('load() - theme', async () => await testLoad(join(hexo.theme_dir, 'source')));\n\n\n  it('load() - load database', async () => {\n    hexo._dbLoaded = false;\n    const dbPath = hexo.database.options.path;\n\n    const fixture = {\n      meta: {\n        version: 1,\n        warehouse: require('warehouse').version\n      },\n      models: {\n        PostTag: [\n          { _id: 'cuid111111111111111111113', post_id: 'cuid111111111111111111111', tag_id: 'cuid111111111111111111112' }\n        ],\n        Tag: [\n          { _id: 'cuid111111111111111111112', name: 'foo' }\n        ],\n        Post: [\n          { _id: 'cuid111111111111111111111', source: 'test', slug: 'test' }\n        ]\n      }\n    };\n    await writeFile(dbPath, JSON.stringify(fixture));\n    await hexo.load();\n    // check Model\n    hexo.model('PostTag').toArray({lean: true}).length.should.eql(fixture.models.PostTag.length);\n    hexo.model('Tag').toArray({lean: true}).length.should.eql(fixture.models.Tag.length);\n    hexo.model('Post').toArray({lean: true}).length.should.eql(fixture.models.Post.length);\n    hexo._binaryRelationIndex.post_tag.keyIndex.size.should.eql(1);\n    hexo._binaryRelationIndex.post_tag.valueIndex.size.should.eql(1);\n    await unlink(dbPath);\n    // clean up\n    await hexo.model('PostTag').removeById('cuid111111111111111111113');\n    await hexo.model('Tag').removeById('cuid111111111111111111112');\n    await hexo.model('Post').removeById('cuid111111111111111111111');\n    hexo._binaryRelationIndex.post_tag.keyIndex.clear();\n    hexo._binaryRelationIndex.post_tag.valueIndex.clear();\n  });\n\n  // Issue #3964\n  it('load() - merge theme config - deep clone', async () => {\n    const hexo = new Hexo(__dirname, { silent: true });\n    hexo.theme.config = { a: { b: 1, c: 2 } };\n    hexo.config.theme_config = { a: { b: 3 } };\n\n    await hexo.load();\n\n    const { config: themeConfig } = hexo.theme;\n\n    themeConfig.a.should.have.own.property('c');\n    themeConfig.a.b.should.eql(3);\n\n    const Locals = hexo._generateLocals();\n    const { theme: themeLocals } = new Locals('', {path: '', layout: [], data: {}});\n\n    themeLocals.a.should.have.own.property('c');\n    themeLocals.a.b.should.eql(3);\n  });\n\n  it('load() - merge theme config - null theme.config', async () => {\n    const hexo = new Hexo(__dirname, { silent: true });\n    hexo.theme.config = null;\n    hexo.config.theme_config = { c: 3 };\n\n    await hexo.load();\n\n    const { config: themeConfig } = hexo.theme;\n\n    themeConfig.should.have.own.property('c');\n    themeConfig.c.should.eql(3);\n\n    const Locals = hexo._generateLocals();\n    const { theme: themeLocals } = new Locals('', {path: '', layout: [], data: {}});\n\n    themeLocals.should.have.own.property('c');\n    themeLocals.c.should.eql(3);\n  });\n\n  // Filters should be able to read the theme_config:\n  //  - before_post_render\n  //  - after_post_render\n  //  - before_generate\n  it('load() - merge theme config - filter', async () => {\n    const hexo = new Hexo(__dirname, { silent: true });\n\n    const validateThemeConfig = function() {\n      this.theme.config.a.b.should.eql(3);\n    };\n\n    hexo.theme.config = { a: { b: 1, c: 2 } };\n    hexo.config.theme_config = { a: { b: 3 } };\n\n    hexo.extend.filter.register('before_post_render', validateThemeConfig);\n    hexo.extend.filter.register('after_post_render', validateThemeConfig);\n    hexo.extend.filter.register('before_generate', validateThemeConfig);\n\n    await hexo.load();\n\n    hexo.extend.filter.unregister('before_post_render', validateThemeConfig);\n    hexo.extend.filter.unregister('after_post_render', validateThemeConfig);\n    hexo.extend.filter.unregister('before_generate', validateThemeConfig);\n  });\n\n  async function testWatch(path) {\n    const target = join(path, 'test.txt');\n    const body = 'test';\n    const newBody = body + body;\n\n    loadAssetGenerator();\n\n    await writeFile(target, body);\n    await hexo.watch();\n    await checkStream(route.get('test.txt'), body); // Test for first generation\n    await writeFile(target, newBody); // Update the file\n    await BluebirdPromise.delay(300);\n    await checkStream(route.get('test.txt'), newBody); // Check the new route\n    hexo.unwatch(); // Stop watching\n    await unlink(target); // Delete the file\n  }\n\n  it('watch() - source', async () => await testWatch(hexo.source_dir));\n\n  it('watch() - theme', async () => await testWatch(join(hexo.theme_dir, 'source')));\n\n  it('watch() - merge theme config', () => {\n    const theme_config_1 = [\n      'a:',\n      '  b: 1',\n      '  c: 2'\n    ].join('\\n');\n    const theme_config_2 = [\n      'a:',\n      '  b: 1',\n      '  c: 3'\n    ].join('\\n');\n\n    const hexo = new Hexo(__dirname, { silent: true });\n    hexo.config.theme_config = { a: { b: 3, d: 4 } };\n    const theme_config_path = join(hexo.theme_dir, '_config.yml');\n\n    return writeFile(theme_config_path, theme_config_1)\n      .then(() => hexo.init())\n      .then(() => hexo.watch())\n      .then(() => {\n        hexo.theme.config.a.should.have.own.property('d');\n        hexo.theme.config.a.d.should.eql(4);\n      })\n      .then(() => writeFile(theme_config_path, theme_config_2))\n      .delay(300)\n      .then(() => {\n        hexo.theme.config.a.should.have.own.property('d');\n        hexo.theme.config.a.d.should.eql(4);\n      })\n      .then(() => hexo.unwatch())\n      .delay(300)\n      .then(() => unlink(theme_config_path))\n      .delay(300);\n  });\n\n  // it('unwatch()'); missing-unit-test\n\n  it('exit()', async () => {\n    const hook = spy();\n    const listener = spy();\n\n    hexo.extend.filter.register('before_exit', hook);\n    hexo.once('exit', listener);\n\n    await hexo.exit();\n    hook.calledOnce.should.be.true;\n    listener.calledOnce.should.be.true;\n  });\n\n  it('exit() - error handling - callback', callback => {\n    hexo.once('exit', err => {\n      err.should.eql({ foo: 'bar' });\n      callback();\n    });\n\n    hexo.exit({ foo: 'bar' });\n  });\n\n  it('exit() - error handling - promise', () => {\n    return BluebirdPromise.all([\n      hexo.exit({ foo: 'bar' }),\n      new BluebirdPromise((resolve, reject) => {\n        hexo.once('exit', err => {\n          try {\n            err.should.eql({ foo: 'bar' });\n            resolve();\n          } catch (e) {\n            reject(e);\n          }\n        });\n      })\n    ]);\n  });\n\n  it('draft visibility', async () => {\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo', published: true},\n      {source: 'bar', slug: 'bar', published: false}\n    ]);\n    hexo.locals.invalidate();\n    hexo.locals.get('posts').toArray().should.eql(posts.slice(0, 1));\n\n    // draft visible\n    hexo.config.render_drafts = true;\n    hexo.locals.invalidate();\n    hexo.locals.get('posts').toArray().should.eql(posts);\n    hexo.config.render_drafts = false;\n\n    posts.map(post => Post.removeById(post._id));\n  });\n\n  it('future posts', async () => {\n    const posts = await Post.insert([\n      {source: 'foo', slug: 'foo', date: Date.now() - 3600},\n      {source: 'bar', slug: 'bar', date: Date.now() + 3600}\n    ]);\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    // future on\n    hexo.config.future = true;\n    hexo.locals.invalidate();\n    hexo.locals.get('posts').map(mapper).should.eql(posts.map(mapper));\n\n    // future off\n    hexo.config.future = false;\n    hexo.locals.invalidate();\n    hexo.locals.get('posts').map(mapper).should.eql([posts[0]._id]);\n\n    posts.map(post => Post.removeById(post._id));\n  });\n\n  it('future pages', async () => {\n    const pages = await Page.insert([\n      {source: 'foo', path: 'foo', date: Date.now() - 3600},\n      {source: 'bar', path: 'bar', date: Date.now() + 3600}\n    ]);\n    function mapper(page) {\n      return page._id;\n    }\n\n    // future on\n    hexo.config.future = true;\n    hexo.locals.invalidate();\n    hexo.locals.get('pages').map(mapper).should.eql(pages.map(mapper));\n\n    // future off\n    hexo.config.future = false;\n    hexo.locals.invalidate();\n    hexo.locals.get('pages').map(mapper).should.eql([pages[0]._id]);\n\n    pages.map(page => Page.removeById(page._id));\n  });\n\n  it('locals.data', async () => {\n    const data = await Data.insert([\n      {_id: 'users', data: {foo: 1}},\n      {_id: 'comments', data: {bar: 2}}\n    ]);\n    hexo.locals.invalidate();\n    hexo.locals.get('data').should.eql({\n      users: { foo: 1 },\n      comments: { bar: 2 }\n    });\n\n    data.map(data => data.remove());\n  });\n\n  it('_generate()', async () => {\n    // object\n    hexo.extend.generator.register('test_obj', (locals: any) => {\n      locals.test.should.eql('foo');\n\n      return {\n        path: 'foo',\n        data: 'foo'\n      };\n    });\n\n    // array\n    hexo.extend.generator.register('test_arr', (locals: any) => {\n      locals.test.should.eql('foo');\n\n      return [\n        { path: 'bar', data: 'bar' },\n        { path: 'baz', data: 'baz' }\n      ];\n    });\n\n    const beforeListener = spy();\n    const afterListener = spy();\n    const afterHook = spy();\n\n    const beforeHook = spy(() => {\n      hexo.locals.set('test', 'foo');\n    });\n\n    hexo.once('generateBefore', beforeListener);\n    hexo.once('generateAfter', afterListener);\n    hexo.extend.filter.register('before_generate', beforeHook);\n    hexo.extend.filter.register('after_generate', afterHook);\n\n    await hexo._generate();\n\n    route.list().should.eql(['foo', 'bar', 'baz']);\n    beforeListener.calledOnce.should.be.true;\n    afterListener.calledOnce.should.be.true;\n    beforeHook.calledOnce.should.be.true;\n    afterHook.calledOnce.should.be.true;\n\n    await BluebirdPromise.all([\n      checkStream(route.get('foo'), 'foo'),\n      checkStream(route.get('bar'), 'bar'),\n      checkStream(route.get('baz'), 'baz')\n    ]);\n  });\n\n  it('_generate() - layout', async () => {\n    hexo.theme.setView('test.njk', [\n      '{{ config.title }}',\n      '{{ page.foo }}',\n      '{{ layout }}',\n      '{{ view_dir }}'\n    ].join('\\n'));\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test',\n\n      data: {\n        foo: 'bar'\n      }\n    }));\n\n    const expected = [\n      hexo.config.title,\n      'bar',\n      'layout',\n      join(hexo.theme_dir, 'layout') + sep\n    ].join('\\n');\n\n    await hexo._generate();\n    await checkStream(route.get('test'), expected);\n  });\n\n  it('_generate() - layout array', async () => {\n    hexo.theme.setView('baz.njk', 'baz');\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: ['foo', 'bar', 'baz']\n    }));\n\n    await hexo._generate();\n    await checkStream(route.get('test'), 'baz');\n  });\n\n  it('_generate() - layout not exist', async () => {\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'nothing'\n    }));\n\n    await hexo._generate();\n    await checkStream(route.get('test'), '');\n  });\n\n  it('_generate() - remove old routes', async () => {\n    hexo.extend.generator.register('test', () => ({\n      path: 'bar',\n      data: 'newbar'\n    }));\n\n    route.set('foo', 'foo');\n    route.set('bar', 'bar');\n    route.set('baz', 'baz');\n\n    await hexo._generate();\n    should.not.exist(route.get('foo'));\n    should.not.exist(route.get('baz'));\n    await checkStream(route.get('bar'), 'newbar');\n  });\n\n  it('_generate() - _after_html_render filter', async () => {\n    const hook = spy(result => result.replace('foo', 'bar'));\n    hexo.extend.filter.register('after_render:html', hook);\n    hexo.theme.setView('test.njk', 'foo');\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test'\n    }));\n    await hexo._generate();\n    await checkStream(route.get('test'), 'bar');\n    hook.called.should.eql(true);\n  });\n\n  it('_generate() - after_render:html is alias of _after_html_render', async () => {\n    const hook = spy(result => result.replace('foo', 'bar'));\n    hexo.extend.filter.register('after_render:html', hook);\n    hexo.theme.setView('test.njk', 'foo');\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test'\n    }));\n    await hexo._generate();\n    await checkStream(route.get('test'), 'bar');\n    hook.called.should.eql(true);\n  });\n\n  it('_generate() - return nothing in generator', async () => {\n    // @ts-expect-error\n    hexo.extend.generator.register('test_nothing', () => {\n      //\n    });\n\n    hexo.extend.generator.register('test_normal', () => ({\n      path: 'bar',\n      data: 'bar'\n    }));\n\n    await hexo._generate();\n    await checkStream(route.get('bar'), 'bar');\n  });\n\n  it('_generate() - validate locals', async () => {\n    hexo.theme.setView('test.njk', [\n      '{{ path }}',\n      '{{ url }}',\n      '{{ view_dir }}'\n    ].join('\\n'));\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test'\n    }));\n\n    await hexo._generate();\n    await checkStream(route.get('test'), [\n      'test',\n      hexo.config.url + '/test',\n      join(hexo.theme_dir, 'layout') + sep\n    ].join('\\n'));\n  });\n\n  it('_generate() - should encode url', async () => {\n    const path = 'bár';\n    hexo.config.url = 'http://fôo.com';\n\n    hexo.theme.setView('test.njk', '{{ url }}');\n\n    hexo.extend.generator.register('test', () => ({\n      path,\n      layout: 'test'\n    }));\n\n    await hexo._generate();\n    await checkStream(route.get(path), full_url_for.call(hexo, path));\n  });\n\n  it('_generate() - do nothing if it\\'s generating', () => {\n    const hook = spy();\n    hexo.extend.generator.register('test', hook);\n\n    hexo._isGenerating = true;\n    hexo._generate();\n    hook.called.should.be.false;\n    hexo._isGenerating = false;\n  });\n\n  it('_generate() - reset cache for new route', async () => {\n    let count = 0;\n\n    hexo.theme.setView('test.njk', '{{ page.count() }}');\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test',\n      data: { count: () => count++ }\n    }));\n\n\n    await hexo._generate({cache: true}); // First generate\n    await checkStream(route.get('test'), '0');\n    await checkStream(route.get('test'), '0'); // should return cached result\n\n    await hexo._generate({cache: true}); // Second generate\n    await checkStream(route.get('test'), '1');\n    await checkStream(route.get('test'), '1'); // should return cached result\n  });\n\n  it('_generate() - cache disabled and use new route', async () => {\n    let count = 0;\n\n    hexo.theme.setView('test.njk', '{{ page.count() }}');\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test',\n      data: { count: () => count++ }\n    }));\n\n\n    await hexo._generate({ cache: false }); // First generate\n    await checkStream(route.get('test'), '0');\n    await checkStream(route.get('test'), '1');\n\n    await hexo._generate({ cache: false }); // Second generate\n    await checkStream(route.get('test'), '2');\n    await checkStream(route.get('test'), '3');\n  });\n\n  it('_generate() - cache disabled & update template', async () => {\n    hexo.theme.setView('test.njk', '0');\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test'\n    }));\n\n    await hexo._generate({ cache: false });\n    await checkStream(route.get('test'), '0');\n    hexo.theme.setView('test.njk', '1');\n    await checkStream(route.get('test'), '1');\n  });\n\n  it('_generate() - cache enabled & update template', async () => {\n    hexo.theme.setView('test.njk', '0');\n\n    hexo.extend.generator.register('test', () => ({\n      path: 'test',\n      layout: 'test'\n    }));\n\n    await hexo._generate({ cache: true });\n    await checkStream(route.get('test'), '0');\n    hexo.theme.setView('test.njk', '1');\n    await checkStream(route.get('test'), '0'); // should return cached result\n  });\n\n  it('execFilter()', async () => {\n    const fn = str => {\n      return str + 'foo';\n    };\n    hexo.extend.filter.register('exec_test', fn);\n\n    const result = await hexo.execFilter('exec_test', '');\n    result.should.eql('foo');\n    hexo.extend.filter.unregister('exec_test', fn);\n  });\n\n  it('execFilter() - promise', async () => {\n    const fn = str => {\n      return new BluebirdPromise((resolve, _reject) => {\n        resolve(str + 'bar');\n      });\n    };\n    hexo.extend.filter.register('exec_test', fn);\n\n    const result = await hexo.execFilter('exec_test', 'foo');\n    result.should.eql('foobar');\n    hexo.extend.filter.unregister('exec_test', fn);\n  });\n\n  it('execFilterSync()', () => {\n    hexo.extend.filter.register('exec_sync_test', data => {\n      data.should.eql('');\n      return data + 'foo';\n    });\n\n    hexo.execFilterSync('exec_sync_test', '').should.eql('foo');\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/load_config.ts",
    "content": "import { join, sep, resolve } from 'path';\nimport { writeFile, unlink, mkdirs, rmdir } from 'hexo-fs';\nimport { makeRe } from 'micromatch';\nimport loadConfig from '../../../lib/hexo/load_config';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport Hexo from '../../../lib/hexo';\n\ndescribe('Load config', () => {\n  const hexo = new Hexo(join(__dirname, 'config_test'), { silent: true });\n  hexo.env.init = true;\n\n  before(() => mkdirs(hexo.base_dir).then(() => hexo.init()));\n\n  after(() => rmdir(hexo.base_dir));\n\n  beforeEach(() => {\n    hexo.config_path = join(hexo.base_dir, '_config.yml');\n    hexo.config = JSON.parse(JSON.stringify(defaultConfig));\n  });\n\n  it('config file does not exist', async () => {\n    await loadConfig(hexo);\n    hexo.config.should.eql(defaultConfig);\n  });\n\n  it('_config.yml exists', async () => {\n    const configPath = join(hexo.base_dir, '_config.yml');\n\n    try {\n      await writeFile(configPath, 'foo: 1');\n      await loadConfig(hexo);\n      hexo.config.foo.should.eql(1);\n    } finally {\n      await unlink(configPath);\n    }\n  });\n\n  it('_config.json exists', async () => {\n    const configPath = join(hexo.base_dir, '_config.json');\n\n    try {\n      await writeFile(configPath, '{\"baz\": 3}');\n      await loadConfig(hexo);\n      hexo.config.baz.should.eql(3);\n      hexo.config_path.should.eql(configPath);\n    } finally {\n      await unlink(configPath);\n    }\n  });\n\n  it('_config.txt exists', async () => {\n    const configPath = join(hexo.base_dir, '_config.txt');\n\n    try {\n      await writeFile(configPath, 'foo: 1');\n      await loadConfig(hexo);\n      hexo.config.should.eql(defaultConfig);\n      hexo.config_path.should.not.eql(configPath);\n    } finally {\n      await unlink(configPath);\n    }\n  });\n\n  it('custom config path', async () => {\n    const configPath = join(__dirname, 'werwerwer.yml');\n    hexo.config_path = join(__dirname, 'werwerwer.yml');\n\n    try {\n      await writeFile(configPath, 'foo: 1');\n      await loadConfig(hexo);\n      hexo.config.foo.should.eql(1);\n    } finally {\n      hexo.config_path = join(hexo.base_dir, '_config.yml');\n      await unlink(configPath);\n    }\n  });\n\n  it('custom config path with different extension name', async () => {\n    const realPath = join(__dirname, 'werwerwer.json');\n    hexo.config_path = join(__dirname, 'werwerwer.yml');\n\n    try {\n      await writeFile(realPath, '{\"foo\": 2}');\n      await loadConfig(hexo);\n      hexo.config.foo.should.eql(2);\n      hexo.config_path.should.eql(realPath);\n    } finally {\n      hexo.config_path = join(hexo.base_dir, '_config.yml');\n      await unlink(realPath);\n    }\n  });\n\n  it('handle trailing \"/\" of url', async () => {\n    const content = [\n      'root: foo',\n      'url: https://hexo.io/'\n    ].join('\\n');\n\n    try {\n      await writeFile(hexo.config_path, content);\n      await loadConfig(hexo);\n      hexo.config.root.should.eql('foo/');\n      hexo.config.url.should.eql('https://hexo.io');\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('handle root is not exist', async () => {\n    try {\n      const content = 'url: https://hexo.io/';\n      await writeFile(hexo.config_path, content);\n      await loadConfig(hexo);\n      hexo.config.url.should.eql('https://hexo.io');\n      hexo.config.root.should.eql('/');\n    } finally {\n      await unlink(hexo.config_path);\n    }\n    try {\n      const content = 'url: https://hexo.io/foo/';\n      await writeFile(hexo.config_path, content);\n      await loadConfig(hexo);\n      hexo.config.url.should.eql('https://hexo.io/foo');\n      hexo.config.root.should.eql('/foo/');\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('custom public_dir', async () => {\n    try {\n      await writeFile(hexo.config_path, 'public_dir: foo');\n      await loadConfig(hexo);\n      hexo.public_dir.should.eql(resolve(hexo.base_dir, 'foo') + sep);\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('custom source_dir', async () => {\n    try {\n      await writeFile(hexo.config_path, 'source_dir: bar');\n      await loadConfig(hexo);\n      hexo.source_dir.should.eql(resolve(hexo.base_dir, 'bar') + sep);\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('custom theme - default theme_dir', async () => {\n    try {\n      await writeFile(hexo.config_path, 'theme: test');\n      await loadConfig(hexo);\n      hexo.config.theme.should.eql('test');\n      hexo.theme_dir.should.eql(join(hexo.base_dir, 'themes', 'landscape') + sep);\n      hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep);\n      hexo.theme.base.should.eql(hexo.theme_dir);\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('custom theme - base_dir/themes/[theme]', async () => {\n    try {\n      await writeFile(hexo.config_path, 'theme: test');\n      await mkdirs(join(hexo.base_dir, 'themes', 'test'));\n      await loadConfig(hexo);\n      hexo.config.theme.should.eql('test');\n      hexo.theme_dir.should.eql(join(hexo.base_dir, 'themes', 'test') + sep);\n      hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep);\n      hexo.theme.base.should.eql(hexo.theme_dir);\n      const ignore = ['**/themes/*/node_modules/**', '**/themes/*/.git/**'];\n      hexo.theme.ignore.should.eql(ignore);\n      hexo.theme.options.ignored.should.eql(ignore.map(item => makeRe(item)));\n    } finally {\n      await rmdir(join(hexo.base_dir, 'themes', 'test'));\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('custom theme - base_dir/node_modules/hexo-theme-[theme]', async () => {\n    try {\n      await writeFile(hexo.config_path, 'theme: test');\n      await mkdirs(join(hexo.plugin_dir, 'hexo-theme-test'));\n      await loadConfig(hexo);\n      hexo.config.theme.should.eql('test');\n      hexo.theme_dir.should.eql(join(hexo.plugin_dir, 'hexo-theme-test') + sep);\n      hexo.theme_script_dir.should.eql(join(hexo.theme_dir, 'scripts') + sep);\n      hexo.theme.base.should.eql(hexo.theme_dir);\n      const ignore = ['**/node_modules/hexo-theme-*/node_modules/**', '**/node_modules/hexo-theme-*/.git/**'];\n      hexo.theme.ignore.should.eql(ignore);\n      hexo.theme.options.ignored.should.eql(ignore.map(item => makeRe(item)));\n    } finally {\n      await rmdir(join(hexo.plugin_dir, 'hexo-theme-test'));\n      await unlink(hexo.config_path);\n    }\n  });\n\n  it('merge config', async () => {\n    const content = [\n      'highlight:',\n      '  tab_replace: yoooo'\n    ].join('\\n');\n\n    try {\n      await writeFile(hexo.config_path, content);\n      await loadConfig(hexo);\n      hexo.config.highlight.line_number.should.be.true;\n      hexo.config.highlight.tab_replace.should.eql('yoooo');\n    } finally {\n      await unlink(hexo.config_path);\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/load_database.ts",
    "content": "import { join } from 'path';\nimport Hexo from '../../../lib/hexo';\nimport { exists, mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport loadDatabase from '../../../lib/hexo/load_database';\n\ndescribe('Load database', () => {\n  const hexo = new Hexo(join(__dirname, 'db_test'), {silent: true});\n  const dbPath = hexo.database.options.path;\n\n  const fixture = {\n    meta: {\n      version: 1,\n      warehouse: require('warehouse').version\n    },\n    models: {\n      Test: [\n        {_id: 'A'},\n        {_id: 'B'},\n        {_id: 'C'}\n      ]\n    }\n  };\n\n  before(() => mkdirs(hexo.base_dir));\n\n  beforeEach(() => {\n    hexo._dbLoaded = false;\n  });\n\n  after(async () => {\n    const exist = await exists(dbPath);\n    if (exist) await unlink(dbPath);\n    rmdir(hexo.base_dir);\n  });\n\n  it('database does not exist', () => loadDatabase(hexo));\n\n  it('database load success', async () => {\n    await writeFile(dbPath, JSON.stringify(fixture));\n    await loadDatabase(hexo);\n    hexo._dbLoaded.should.be.true;\n    hexo.model('Test').toArray({lean: true}).should.eql(fixture.models.Test);\n    hexo.model('Test').destroy();\n\n    await unlink(dbPath);\n  });\n\n  it('don\\'t load database if loaded', async () => {\n    hexo._dbLoaded = true;\n\n    await writeFile(dbPath, JSON.stringify(fixture));\n    await loadDatabase(hexo);\n\n    hexo.model('Test').should.have.lengthOf(0);\n\n    await unlink(dbPath);\n  });\n});\n\n// #3975 workaround for Windows\n// Clean-up is not necessary (unlike the above tests),\n// because the db file is already removed if invalid\ndescribe('Load database - load failed', () => {\n  const hexo = new Hexo(join(__dirname), {silent: true});\n  const dbPath = hexo.database.options.path;\n\n  it('database load failed', async () => {\n    hexo._dbLoaded = false;\n\n    await writeFile(dbPath, '{1423432: 324');\n    await loadDatabase(hexo);\n    hexo._dbLoaded.should.be.false;\n    const exist = await exists(dbPath);\n    exist.should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/load_plugins.ts",
    "content": "import { join, dirname } from 'path';\nimport { writeFile, mkdir, rmdir, unlink } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport loadPlugins from '../../../lib/hexo/load_plugins';\nimport BluebirdPromise from 'bluebird';\nimport chai from 'chai';\nimport { spy } from 'sinon';\nconst should = chai.should();\n\ndescribe('Load plugins', () => {\n  const hexo = new Hexo(join(__dirname, 'plugin_test'), { silent: true }) as any;\n\n  const script = [\n    'hexo._script_test = {',\n    '  filename: __filename,',\n    '  dirname: __dirname,',\n    '  module: module,',\n    '  require: require',\n    '}'\n  ].join('\\n');\n\n  const asyncScript = [\n    'async function afunc() {',\n    '  return new Promise(resolve => resolve());',\n    '}',\n    'await afunc()',\n    'hexo._script_test = {',\n    '  filename: __filename,',\n    '  dirname: __dirname,',\n    '  module: module,',\n    '  require: require',\n    '}'\n  ].join('\\n');\n  function validate(path) {\n    const result = hexo._script_test;\n\n    result.filename.should.eql(path);\n    result.dirname.should.eql(dirname(path));\n    result.module.id.should.eql(path);\n    result.module.filename.should.eql(path);\n\n    delete hexo._script_test;\n  }\n\n  function createPackageFile(name, path?) {\n    const pkg = {\n      name: 'hexo-site',\n      version: '0.0.0',\n      private: true,\n      dependencies: {\n        [name]: '*'\n      }\n    };\n\n    path = path || join(hexo.base_dir, 'package.json');\n    return writeFile(path, JSON.stringify(pkg, null, '  '));\n  }\n\n  function createPackageFileWithDevDeps(name) {\n    const pkg = {\n      name: 'hexo-site',\n      version: '0.0.0',\n      private: true,\n      dependencies: {},\n      devDependencies: {\n        [name]: '*'\n      }\n    };\n\n    return writeFile(join(hexo.base_dir, 'package.json'), JSON.stringify(pkg, null, '  '));\n  }\n\n  hexo.env.init = true;\n  hexo.theme_script_dir = join(hexo.base_dir, 'themes', 'test', 'scripts');\n\n  before(() => mkdir(hexo.base_dir));\n\n  after(() => rmdir(hexo.base_dir));\n\n  afterEach(async () => {\n    await createPackageFile('hexo-another-plugin');\n  });\n\n  it('load plugins', () => {\n    const name = 'hexo-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    return BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('fail to load plugins', () => {\n    const logSpy = spy();\n    hexo.log.error = logSpy;\n    const name = 'hexo-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n    return BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, 'throw new Error(\"test\")')\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      logSpy.args[0][1].should.contains('Plugin load failed: %s');\n      logSpy.args[0][2].should.contains('hexo-plugin-test');\n    });\n  });\n\n  it('load async plugins', () => {\n    const name = 'hexo-async-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    return BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, asyncScript)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('load scoped plugins', () => {\n    const name = '@some-scope/hexo-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    return BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('load devDep plugins', () => {\n    const name = 'hexo-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    return BluebirdPromise.all([\n      createPackageFileWithDevDeps(name),\n      writeFile(path, script)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('load plugins in the theme\\'s package.json', async () => {\n    const name = 'hexo-plugin-test';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n    return BluebirdPromise.all([\n      createPackageFile(name, join(hexo.theme_dir, 'package.json')),\n      writeFile(path, script)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('ignore plugin whose name is started with \"hexo-theme-\"', async () => {\n    const script = 'hexo._script_test = true';\n    const name = 'hexo-theme-test_theme';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    await BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]);\n    await loadPlugins(hexo);\n\n    should.not.exist(hexo._script_test);\n    delete hexo.config.theme;\n    return unlink(path);\n  });\n\n  it('ignore scoped plugin whose name is started with \"hexo-theme-\"', async () => {\n    const script = 'hexo._script_test = true';\n    const name = '@hexojs/hexo-theme-test_theme';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    await BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]);\n    await loadPlugins(hexo);\n\n    should.not.exist(hexo._script_test);\n    delete hexo.config.theme;\n    return unlink(path);\n  });\n\n  it('ignore plugins whose name is not started with \"hexo-\"', async () => {\n    const script = 'hexo._script_test = true';\n    const name = 'another-plugin';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    await BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]);\n    await loadPlugins(hexo);\n\n    should.not.exist(hexo._script_test);\n    return unlink(path);\n  });\n\n  it('ignore plugins which is typescript definition', () => {\n    const script = 'hexo._script_test = true';\n    const name = '@types/hexo-test-plugin';\n    const path = join(hexo.plugin_dir, name, 'index.js');\n\n    return BluebirdPromise.all([\n      createPackageFile(name),\n      writeFile(path, script)\n    ]).then(() => loadPlugins(hexo)).then(() => {\n      should.not.exist(hexo._script_test);\n      return unlink(path);\n    });\n  });\n\n  it('ignore plugins which are in package.json but not exist actually', () => createPackageFile('hexo-plugin-test').then(() => loadPlugins(hexo)));\n\n  it('load scripts', async () => {\n    const path = join(hexo.script_dir, 'test.js');\n\n    writeFile(path, script);\n    await loadPlugins(hexo);\n\n    validate(path);\n    return unlink(path);\n  });\n\n  it('fail to load scripts', async () => {\n    const logSpy = spy();\n    hexo.log.error = logSpy;\n    const path = join(hexo.script_dir, 'test.js');\n\n    writeFile(path, 'throw new Error(\"test\")');\n    await loadPlugins(hexo);\n\n    logSpy.args[0][1].should.contains('Script load failed: %s');\n    logSpy.args[0][2].should.contains('test.js');\n    return unlink(path);\n  });\n\n\n  it('load theme scripts', () => {\n    const path = join(hexo.theme_script_dir, 'test.js');\n\n    return writeFile(path, script).then(() => loadPlugins(hexo)).then(() => {\n      validate(path);\n      return unlink(path);\n    });\n  });\n\n  it('don\\'t load plugins in safe mode', () => {\n    const script = 'hexo._script_test = true';\n    const path = join(hexo.script_dir, 'test.js');\n\n    return writeFile(path, script).then(() => {\n      hexo.env.safe = true;\n      return loadPlugins(hexo);\n    }).then(() => {\n      hexo.env.safe = false;\n      should.not.exist(hexo._script_test);\n      return unlink(path);\n    });\n  });\n\n  // Issue #4251\n  it('load scripts with sourcemap EOF', async () => {\n    const path = join(hexo.script_dir, 'test.js');\n\n    const script = [\n      '(() => {',\n      '  hexo._script_test = true;',\n      '})();',\n      '//# sourceMappingURL=data:application/json;<redacted>'\n    ].join('\\n');\n\n    writeFile(path, script);\n    await loadPlugins(hexo);\n\n    hexo._script_test.should.eql(true);\n    return unlink(path);\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/load_theme_config.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, unlink, writeFile, rmdir } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Load alternate theme config', () => {\n  const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true});\n  const loadThemeConfig = require('../../../lib/hexo/load_theme_config');\n\n  hexo.env.init = true;\n\n  before(() => mkdirs(hexo.base_dir).then(() => hexo.init()));\n\n  after(() => rmdir(hexo.base_dir));\n\n  beforeEach(() => {\n    hexo.config.theme_config = { foo: { bar: 'ahhhhhh' } };\n    hexo.config.theme = 'test_theme';\n  });\n\n  it('hexo.config.theme does not exist', async () => {\n    // @ts-ignore\n    hexo.config.theme = undefined;\n    await loadThemeConfig(hexo);\n    hexo.config.theme_config.foo.bar.should.eql('ahhhhhh');\n    hexo.config.theme_config = {};\n  });\n\n  it('_config.[theme].yml does not exist', () => loadThemeConfig(hexo).then(() => {\n    hexo.config.theme_config = {};\n  }));\n\n  it('_config.[theme].yml exists', () => {\n    const configPath = join(hexo.base_dir, '_config.test_theme.yml');\n\n    return writeFile(configPath, 'bar: 1').then(() => loadThemeConfig(hexo)).then(() => {\n      hexo.config.theme_config.bar.should.eql(1);\n    }).finally(() => unlink(configPath));\n  });\n\n  it('_config.[theme].json exists', () => {\n    const configPath = join(hexo.base_dir, '_config.test_theme.json');\n\n    return writeFile(configPath, '{\"baz\": 3}').then(() => loadThemeConfig(hexo)).then(() => {\n      hexo.config.theme_config.baz.should.eql(3);\n    }).finally(() => unlink(configPath));\n  });\n\n  it('_config.[theme].txt exists', () => {\n    const configPath = join(hexo.base_dir, '_config.test_theme.txt');\n\n    return writeFile(configPath, 'qux: 1').then(() => loadThemeConfig(hexo)).then(() => {\n      should.not.exist(hexo.config.theme_config.qux);\n    }).finally(() => unlink(configPath));\n  });\n\n  it('merge config', () => {\n    const configPath = join(hexo.base_dir, '_config.test_theme.yml');\n\n    const content = [\n      'foo:',\n      '  bar: yoooo',\n      '  baz: true'\n    ].join('\\n');\n\n    return writeFile(configPath, content).then(() => loadThemeConfig(hexo)).then(() => {\n      hexo.config.theme_config.foo.baz.should.eql(true);\n      hexo.config.theme_config.foo.bar.should.eql('ahhhhhh');\n      hexo.config.theme_config.foo.bar.should.not.eql('yoooo');\n    }).finally(() => unlink(configPath));\n  });\n\n  it('hexo.config.theme_config does not exist', async () => {\n    const configPath = join(hexo.base_dir, '_config.test_theme.yml');\n\n    hexo.config.theme_config = undefined;\n\n    const content = [\n      'foo:',\n      '  bar: yoooo',\n      '  baz: true'\n    ].join('\\n');\n\n    await writeFile(configPath, content);\n    await loadThemeConfig(hexo);\n\n    hexo.config.theme_config.foo.baz.should.eql(true);\n    hexo.config.theme_config.foo.bar.should.eql('yoooo');\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/locals.ts",
    "content": "import Locals from '../../../lib/hexo/locals';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Locals', () => {\n  const locals = new Locals();\n\n  it('get() - name must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => locals.get(), 'name must be a string!');\n  });\n\n  it('set() - function', () => {\n    locals.set('foo', () => 'foo');\n\n    // cache should be clear after new data is set\n    locals.cache.has('foo').should.be.false;\n    locals.get('foo').should.eql('foo');\n    // cache should be saved once it's get\n    locals.cache.get('foo').should.eql('foo');\n  });\n\n  it('set() - not function', () => {\n    locals.set('foo', 'foo');\n    locals.get('foo').should.eql('foo');\n  });\n\n  it('set() - name must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => locals.set(), 'name must be a string!');\n  });\n\n  it('set() - value is required', () => {\n    // @ts-expect-error\n    should.throw(() => locals.set('test'), 'value is required!');\n  });\n\n  it('remove()', () => {\n    locals.set('foo', 'foo');\n    locals.get('foo');\n    locals.remove('foo');\n\n    should.not.exist(locals.getters.foo);\n    locals.cache.has('foo').should.be.false;\n  });\n\n  it('remove() - name must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => locals.remove(), 'name must be a string!');\n  });\n\n  it('toObject()', () => {\n    const locals = new Locals();\n\n    locals.set('foo', 'foo');\n    locals.set('bar', 'bar');\n    locals.remove('bar');\n    locals.toObject().should.eql({foo: 'foo'});\n  });\n\n  it('invalidate()', () => {\n    locals.set('foo', 'foo');\n    locals.get('foo');\n    locals.invalidate();\n\n    locals.cache.has('foo').should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/multi_config_path.ts",
    "content": "import pathFn from 'path';\nimport osFn from 'os';\nimport { writeFileSync, rmdirSync, unlinkSync, readFileSync } from 'hexo-fs';\nimport yml from 'js-yaml';\nimport Hexo from '../../../lib/hexo';\nimport multiConfigPath from '../../../lib/hexo/multi_config_path';\n\ndescribe('config flag handling', () => {\n  const hexo = new Hexo(pathFn.join(__dirname, 'test_dir')) as any;\n\n  const mcp = multiConfigPath(hexo);\n  const base = hexo.base_dir;\n\n  function ConsoleReader() {\n    this.reader = [];\n    this.d = function(...args) {\n      const type = 'debug';\n      let message = '';\n      for (let i = 0; i < args.length;) {\n        message += args[i];\n        if (++i < args.length) {\n          message += ' ';\n        }\n      }\n\n      this.reader.push({\n        type,\n        msg: message\n      });\n    }.bind(this);\n\n    this.i = function(...args) {\n      const type = 'info';\n      let message = '';\n      for (let i = 0; i < args.length;) {\n        message += args[i];\n        if (++i < args.length) {\n          message += ' ';\n        }\n      }\n\n      this.reader.push({\n        type,\n        msg: message\n      });\n    }.bind(this);\n\n    this.w = function(...args) {\n      const type = 'warning';\n      let message = '';\n      for (let i = 0; i < args.length;) {\n        message += args[i];\n        if (++i < args.length) {\n          message += ' ';\n        }\n      }\n\n      this.reader.push({\n        type,\n        msg: message\n      });\n    }.bind(this);\n\n    this.e = function(...args) {\n      const type = 'error';\n      let message = '';\n      for (let i = 0; i < args.length;) {\n        message += args[i];\n        if (++i < args.length) {\n          message += ' ';\n        }\n      }\n\n      this.reader.push({\n        type,\n        msg: message\n      });\n    }.bind(this);\n  }\n\n  hexo.log = new ConsoleReader();\n\n  const testYaml1 = [\n    'author: foo',\n    'type: dinosaur',\n    'favorites:',\n    '  food: sushi',\n    '  color: purple'\n  ].join('\\n');\n\n  const testYaml2 = [\n    'author: bar',\n    'favorites:',\n    '  food: candy',\n    '  ice_cream: chocolate'\n  ].join('\\n');\n\n  const testJson1 = [\n    '{',\n    '\"author\": \"dinosaur\",',\n    '\"type\": \"elephant\",',\n    '\"favorites\": {\"food\": \"burgers\"}',\n    '}'\n  ].join('\\n');\n\n  const testJson2 = [\n    '{',\n    '\"author\": \"waldo\",',\n    '\"favorites\": {',\n    '  \"food\": \"ice cream\",',\n    '  \"ice_cream\": \"strawberry\"',\n    '  }',\n    '}'\n  ].join('\\n');\n\n  const testJson3 = [\n    '{',\n    '\"author\": \"james bond\",',\n    '\"favorites\": {',\n    '  \"food\": \"martini\",',\n    '  \"ice_cream\": \"vanilla\"',\n    '  }',\n    '}'\n  ].join('\\n');\n\n  before(() => {\n    writeFileSync(base + 'test1.yml', testYaml1);\n    writeFileSync(base + 'test2.yml', testYaml2);\n    writeFileSync(base + 'test1.json', testJson1);\n    writeFileSync(base + 'test2.json', testJson2);\n    // not supported type\n    writeFileSync(base + 'test1.xml', '');\n    writeFileSync('/tmp/test3.json', testJson3);\n  });\n\n  afterEach(() => {\n    hexo.log.reader = [];\n  });\n\n  after(() => {\n    rmdirSync(hexo.base_dir);\n    unlinkSync('/tmp/test3.json');\n  });\n\n  it('no file', () => {\n    mcp(base).should.equal(base + '_config.yml');\n    hexo.log.reader[0].type.should.eql('warning');\n    hexo.log.reader[0].msg.should.eql('No config file entered.');\n  });\n\n  it('not supported type', () => {\n    mcp(base, 'test1.xml,test1.json').should.equal(base + '_multiconfig.yml');\n    hexo.log.reader[0].type.should.eql('warning');\n    hexo.log.reader[0].msg.should.eql('Config file test1.xml not supported type.');\n  });\n\n  it('1 file', () => {\n    mcp(base, 'test1.yml').should.eql(\n      pathFn.resolve(base + 'test1.yml'));\n\n    mcp(base, 'test1.json').should.eql(\n      pathFn.resolve(base + 'test1.json'));\n\n    mcp(base, '/tmp/test3.json').should.eql('/tmp/test3.json');\n  });\n\n  it('1 not found file warning', () => {\n    const notFile = 'not_a_file.json';\n\n    mcp(base, notFile).should.eql(pathFn.join(base, '_config.yml'));\n    hexo.log.reader[0].type.should.eql('warning');\n    hexo.log.reader[0].msg.should.eql('Config file ' + notFile\n      + ' not found, using default.');\n  });\n\n  it('1 not found file warning absolute', () => {\n    const notFile = '/tmp/not_a_file.json';\n\n    mcp(base, notFile).should.eql(pathFn.join(base, '_config.yml'));\n    hexo.log.reader[0].type.should.eql('warning');\n    hexo.log.reader[0].msg.should.eql('Config file ' + notFile\n      + ' not found, using default.');\n  });\n\n  it('combined config output', () => {\n    const combinedPath = pathFn.join(base, '_multiconfig.yml');\n\n    mcp(base, 'test1.yml').should.not.eql(combinedPath);\n    mcp(base, 'test1.yml,test2.yml').should.eql(combinedPath);\n    mcp(base, 'test1.yml,test1.json').should.eql(combinedPath);\n    mcp(base, 'test1.json,test2.json').should.eql(combinedPath);\n    mcp(base, 'notafile.yml,test1.json').should.eql(combinedPath);\n\n    hexo.log.reader[0].type.should.eql('info');\n    hexo.log.reader[0].msg.should.eql('Config based on 2 files');\n    hexo.log.reader[6].type.should.eql('warning');\n    hexo.log.reader[6].msg.should.eql('Config file notafile.yml not found.');\n    hexo.log.reader[7].type.should.eql('info');\n    hexo.log.reader[7].msg.should.eql('Config based on 1 files');\n    // because who cares about grammar anyway?\n\n    mcp(base, 'notafile.yml,alsonotafile.json').should.not.eql(combinedPath);\n    hexo.log.reader[11].type.should.eql('error');\n    hexo.log.reader[11].msg.should.eql('No config files found. Using _config.yml.');\n  });\n\n  it('combine config output with absolute paths', () => {\n    const combinedPath = pathFn.join(base, '_multiconfig.yml');\n\n    mcp(base, 'test1.json,/tmp/test3.json').should.eql(combinedPath);\n    hexo.log.reader[0].type.should.eql('info');\n    hexo.log.reader[0].msg.should.eql('Config based on 2 files');\n  });\n\n  it('2 YAML overwrite', () => {\n    const configFile = mcp(base, 'test1.yml,test2.yml');\n    let config: any = readFileSync(configFile);\n    config = yml.load(config);\n\n    config.author.should.eql('bar');\n    config.favorites.food.should.eql('candy');\n    config.type.should.eql('dinosaur');\n\n    config = readFileSync(mcp(base, 'test2.yml,test1.yml'));\n    config = yml.load(config);\n\n    config.author.should.eql('foo');\n    config.favorites.food.should.eql('sushi');\n    config.type.should.eql('dinosaur');\n  });\n\n  it('2 JSON overwrite', () => {\n    let config: any = readFileSync(mcp(base, 'test1.json,test2.json'));\n    config = yml.load(config);\n\n    config.author.should.eql('waldo');\n    config.favorites.food.should.eql('ice cream');\n    config.type.should.eql('elephant');\n\n    config = readFileSync(mcp(base, 'test2.json,test1.json'));\n    config = yml.load(config);\n\n    config.author.should.eql('dinosaur');\n    config.favorites.food.should.eql('burgers');\n    config.type.should.eql('elephant');\n  });\n\n  it('JSON & YAML overwrite', () => {\n    let config: any = readFileSync(mcp(base, 'test1.yml,test1.json'));\n    config = yml.load(config);\n\n    config.author.should.eql('dinosaur');\n    config.favorites.food.should.eql('burgers');\n    config.type.should.eql('elephant');\n\n    config = readFileSync(mcp(base, 'test1.json,test1.yml'));\n    config = yml.load(config);\n\n    config.author.should.eql('foo');\n    config.favorites.food.should.eql('sushi');\n    config.type.should.eql('dinosaur');\n  });\n\n  it('write multiconfig to specified path', () => {\n    const outputPath = osFn.tmpdir();\n    const combinedPath = pathFn.join(outputPath, '_multiconfig.yml');\n\n    mcp(base, 'test1.yml', outputPath).should.not.eql(combinedPath);\n    mcp(base, 'test1.yml,test2.yml', outputPath).should.eql(combinedPath);\n    mcp(base, 'test1.yml,test1.json', outputPath).should.eql(combinedPath);\n    mcp(base, 'test1.json,test2.json', outputPath).should.eql(combinedPath);\n    mcp(base, 'notafile.yml,test1.json', outputPath).should.eql(combinedPath);\n    mcp(base, 'notafile.yml,alsonotafile.json', outputPath).should.not.eql(combinedPath);\n\n    // delete /tmp/_multiconfig.yml\n    unlinkSync(combinedPath);\n\n    hexo.log.reader[1].type.should.eql('debug');\n    hexo.log.reader[1].msg.should.eql(`Writing _multiconfig.yml to ${combinedPath}`);\n    hexo.log.reader[2].type.should.eql('info');\n    hexo.log.reader[2].msg.should.eql('Config based on 2 files');\n    hexo.log.reader[6].type.should.eql('warning');\n    hexo.log.reader[6].msg.should.eql('Config file notafile.yml not found.');\n    hexo.log.reader[7].type.should.eql('info');\n    hexo.log.reader[7].msg.should.eql('Config based on 1 files');\n    hexo.log.reader[11].type.should.eql('error');\n    hexo.log.reader[11].msg.should.eql('No config files found. Using _config.yml.');\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/post.ts",
    "content": "import { join } from 'path';\nimport moment from 'moment';\nimport { readFile, mkdirs, unlink, rmdir, writeFile, exists, stat, listDir } from 'hexo-fs';\nimport { spy, useFakeTimers } from 'sinon';\nimport { parse as yfm } from 'hexo-front-matter';\nimport { expected, content, expected_disable_nunjucks, content_for_issue_3346, expected_for_issue_3346, content_for_issue_4460 } from '../../fixtures/post_render';\nimport { highlight, deepMerge } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\nconst escapeSwigTag = str => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');\n\ndescribe('Post', () => {\n  const hexo = new Hexo(join(__dirname, 'post_test'));\n  require('../../../lib/plugins/highlight/')(hexo);\n  const { post } = hexo;\n  const now = Date.now();\n  let clock;\n  let defaultCfg = {};\n\n  before(async () => {\n    clock = useFakeTimers(now);\n\n    await mkdirs(hexo.base_dir);\n    await hexo.init();\n\n    // Load marked renderer for testing\n    await hexo.loadPlugin(require.resolve('hexo-renderer-marked'));\n    await hexo.scaffold.set('post', [\n      '---',\n      'title: {{ title }}',\n      'date: {{ date }}',\n      'tags:',\n      '---'\n    ].join('\\n'));\n    await hexo.scaffold.set('draft', [\n      '---',\n      'title: {{ title }}',\n      'tags:',\n      '---'\n    ].join('\\n'));\n\n    defaultCfg = JSON.parse(JSON.stringify(hexo.config));\n  });\n\n  after(() => {\n    clock.restore();\n    return rmdir(hexo.base_dir);\n  });\n\n  afterEach(() => {\n    hexo.config = JSON.parse(JSON.stringify(defaultCfg));\n  });\n\n  it('create()', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n    const listener = spy();\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    hexo.once('new', listener);\n\n    const result = await post.create({\n      title: 'Hello World'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n    listener.calledOnce.should.be.true;\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - slug', async () => {\n    const path = join(hexo.source_dir, '_posts', 'foo.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const result = await post.create({\n      title: 'Hello World',\n      slug: 'foo'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - filename_case', async () => {\n    hexo.config.filename_case = 1;\n\n    const path = join(hexo.source_dir, '_posts', 'hello-world.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const result = await post.create({\n      title: 'Hello World'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - layout', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'layout: photo',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const result = await post.create({\n      title: 'Hello World',\n      layout: 'photo'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - extra data', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'foo: bar',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const result = await post.create({\n      title: 'Hello World',\n      foo: 'bar'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - rename if target existed', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World-1.md');\n\n    await post.create({\n      title: 'Hello World'\n    });\n    const result = await post.create({\n      title: 'Hello World'\n    });\n    result.path.should.eql(path);\n    const exist = await exists(path);\n    exist.should.be.true;\n\n    await Promise.all([\n      unlink(path),\n      unlink(join(hexo.source_dir, '_posts', 'Hello-World.md'))\n    ]);\n  });\n\n  it('create() - replace existing files', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n\n    await post.create({\n      title: 'Hello World'\n    });\n    const result = await post.create({\n      title: 'Hello World'\n    }, true);\n    result.path.should.eql(path);\n    await unlink(path);\n  });\n\n  it('create() - asset folder', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World');\n\n    hexo.config.post_asset_folder = true;\n\n    await post.create({\n      title: 'Hello World'\n    });\n    const stats = await stat(path);\n    stats.isDirectory().should.be.true;\n    await unlink(path + '.md');\n  });\n\n  it('create() - page', async () => {\n    const path = join(hexo.source_dir, 'Hello-World/index.md');\n    hexo.config.post_asset_folder = true;\n    const result = await post.create({\n      title: 'Hello World',\n      layout: 'page'\n    });\n    result.path.should.eql(path);\n\n    try {\n      await stat(join(hexo.source_dir, 'Hello-World/index'));\n      should.fail();\n    } catch (err) {\n      err.code.should.eql('ENOENT');\n    } finally {\n      await unlink(path);\n    }\n  });\n\n  it('create() - follow the separator style in the scaffold', async () => {\n    const scaffold = [\n      '---',\n      'title: {{ title }}',\n      '---'\n    ].join('\\n');\n\n    await hexo.scaffold.set('test', scaffold);\n    const result = await post.create({\n      title: 'Hello World',\n      layout: 'test'\n    });\n    result.content.should.eql([\n      '---',\n      'title: Hello World',\n      '---'\n    ].join('\\n') + '\\n');\n\n    await Promise.all([\n      unlink(result.path),\n      hexo.scaffold.remove('test')\n    ]);\n  });\n\n  // #4511\n  it('create() - avoid quote if unnecessary', async () => {\n    const scaffold = [\n      '---',\n      'title: {{ title }}',\n      '---'\n    ].join('\\n');\n\n    await hexo.scaffold.set('test', scaffold);\n    const result = await post.create({\n      title: 'Hello World',\n      layout: 'test'\n    });\n\n    const data = await readFile(result.path);\n    data.should.eql([\n      '---',\n      'title: Hello World',\n      '---'\n    ].join('\\n') + '\\n');\n\n    await Promise.all([\n      unlink(result.path),\n      hexo.scaffold.remove('test')\n    ]);\n  });\n\n  // #4511\n  it('create() - wrap with quote when necessary', async () => {\n    const scaffold = [\n      '---',\n      'title: {{ title }}',\n      '---'\n    ].join('\\n');\n\n    await hexo.scaffold.set('test', scaffold);\n    const result = await post.create({\n      title: 'Hello: World',\n      layout: 'test'\n    });\n\n    const data = await readFile(result.path);\n    data.should.eql([\n      '---',\n      'title: \\'Hello: World\\'',\n      '---'\n    ].join('\\n') + '\\n');\n\n    await Promise.all([\n      unlink(result.path),\n      hexo.scaffold.remove('test')\n    ]);\n  });\n\n  // #4511\n  it('create() - wrap with quote when necessary - yaml tag', async () => {\n    const scaffold = [\n      '---',\n      'title: {{ title }}',\n      '---'\n    ].join('\\n');\n\n    await hexo.scaffold.set('test', scaffold);\n    const result = await post.create({\n      // https://github.com/nodeca/js-yaml#supported-yaml-types\n      title: '!!js/regexp /pattern/gim',\n      layout: 'test'\n    });\n\n    const data = await readFile(result.path);\n    data.should.eql([\n      '---',\n      'title: \\'!!js/regexp /pattern/gim\\'',\n      '---'\n    ].join('\\n') + '\\n');\n\n    await Promise.all([\n      unlink(result.path),\n      hexo.scaffold.remove('test')\n    ]);\n  });\n\n  it('create() - JSON front-matter', async () => {\n    const scaffold = [\n      '\"title\": {{ title }}',\n      ';;;'\n    ].join('\\n');\n\n    await hexo.scaffold.set('test', scaffold);\n    const result = await post.create({\n      title: 'Hello World',\n      layout: 'test',\n      lang: 'en'\n    });\n    result.content.should.eql([\n      '\"title\": \"Hello World\",',\n      '\"lang\": \"en\"',\n      ';;;'\n    ].join('\\n') + '\\n');\n\n    await Promise.all([\n      unlink(result.path),\n      hexo.scaffold.remove('test')\n    ]);\n  });\n\n  // #1100\n  it('create() - non-string title', async () => {\n    const path = join(hexo.source_dir, '_posts', '12345.md');\n\n    const result = await post.create({\n      title: 12345\n    });\n    result.path.should.eql(path);\n    await unlink(path);\n  });\n\n  it('create() - escape title', async () => {\n    const data = await post.create({\n      title: 'Foo: Bar'\n    });\n    data.content.should.eql([\n      // js-yaml use single-quotation for dumping since 3.3\n      '---',\n      'title: \\'Foo: Bar\\'',\n      'date: ' + moment(now).format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n');\n    await unlink(data.path);\n  });\n\n  it('create() - with content', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---',\n      '',\n      'Hello hexo'\n    ].join('\\n');\n\n    const result = await post.create({\n      title: 'Hello World',\n      content: 'Hello hexo'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n    await unlink(path);\n  });\n\n  it('create() - with callback', done => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    post.create({ title: 'Hello World' }, (err, post) => {\n      if (err) {\n        done(err);\n        return;\n      }\n      try {\n        post.path.should.eql(path);\n        post.content.should.eql(content);\n        readFile(path).asCallback((err, data: any) => {\n          if (err) {\n            done(err);\n            return;\n          }\n          try {\n            data.should.eql(content);\n            unlink(path).asCallback(done);\n          } catch (e) {\n            done(e);\n          }\n        });\n      } catch (e) {\n        done(e);\n      }\n    });\n  });\n\n  it('publish()', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const data = await post.create({\n      title: 'Hello World',\n      layout: 'draft'\n    });\n    const draftPath = data.path;\n    const result = await post.publish({\n      slug: 'Hello-World'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const exist = await exists(draftPath);\n    exist.should.be.false;\n\n    const newdata = await readFile(path);\n    newdata.should.eql(content);\n\n    await unlink(path);\n  });\n\n  it('publish() - layout', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'layout: photo',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    await post.create({\n      title: 'Hello World',\n      layout: 'draft'\n    });\n    const result = await post.publish({\n      slug: 'Hello-World',\n      layout: 'photo'\n    });\n    result.path.should.eql(path);\n    result.content.should.eql(content);\n\n    const data = await readFile(path);\n    data.should.eql(content);\n\n    await unlink(path);\n  });\n\n  it('publish() - rename if target existed', async () => {\n    const paths = [join(hexo.source_dir, '_posts', 'Hello-World-1.md')];\n\n    const result = await Promise.all([\n      post.create({ title: 'Hello World', layout: 'draft' }),\n      post.create({ title: 'Hello World' })\n    ]);\n    paths.push(result[1].path);\n\n    const data = await post.publish({\n      slug: 'Hello-World'\n    });\n    data.path.should.eql(paths[0]);\n\n    for (const path of paths) {\n      await unlink(path);\n    }\n  });\n\n  it('publish() - replace existing files', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n\n    await Promise.all([\n      post.create({ title: 'Hello World', layout: 'draft' }),\n      post.create({ title: 'Hello World' })\n    ]);\n    const data = await post.publish({\n      slug: 'Hello-World'\n    }, true);\n    data.path.should.eql(path);\n    await unlink(path);\n  });\n\n  it('publish() - asset folder', async () => {\n    const assetDir = join(hexo.source_dir, '_drafts', 'Hello-World');\n    const newAssetDir = join(hexo.source_dir, '_posts', 'Hello-World');\n    hexo.config.post_asset_folder = true;\n\n    await post.create({\n      title: 'Hello World',\n      layout: 'draft'\n    });\n    // Put some files into the asset folder\n    await Promise.all([\n      writeFile(join(assetDir, 'a.txt'), 'a'),\n      writeFile(join(assetDir, 'b.txt'), 'b')\n    ]);\n    const result = await post.publish({\n      slug: 'Hello-World'\n    });\n\n    const exist = await exists(assetDir);\n    exist.should.be.false;\n    const files = await listDir(newAssetDir);\n    files.should.have.members(['a.txt', 'b.txt']);\n\n    await unlink(result.path);\n\n    await rmdir(newAssetDir);\n  });\n\n  // #1100\n  it('publish() - non-string title', async () => {\n    const path = join(hexo.source_dir, '_posts', '12345.md');\n\n    await post.create({\n      title: 12345,\n      layout: 'draft'\n    });\n    const data = await post.publish({\n      slug: 12345\n    });\n    data.path.should.eql(path);\n    await unlink(path);\n  });\n\n  it('publish() - with callback', async () => {\n    const path = join(hexo.source_dir, '_posts', 'Hello-World.md');\n    const date = moment(now);\n\n    const content = [\n      '---',\n      'title: Hello World',\n      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),\n      'tags:',\n      '---'\n    ].join('\\n') + '\\n';\n\n    const callback = spy();\n\n    const data = await post.create({\n      title: 'Hello World',\n      layout: 'draft'\n    });\n    const draftPath = data.path;\n\n    await post.publish({\n      slug: 'Hello-World'\n    }, callback);\n    callback.calledOnce.should.be.true;\n    callback.calledWithMatch(null, { path, content }).should.true;\n\n    const exist = await exists(draftPath);\n    exist.should.be.false;\n\n    const newdata = await readFile(path);\n    newdata.should.eql(content);\n\n    await unlink(path);\n  });\n\n  // #1139\n  it('publish() - preserve non-null data in drafts', async () => {\n    await post.create({\n      title: 'foo',\n      layout: 'draft',\n      tags: ['tag', 'test']\n    });\n    const data = await post.publish({\n      slug: 'foo'\n    });\n    const meta = yfm(data.content);\n    meta.tags.should.eql(['tag', 'test']);\n    await unlink(data.path);\n  });\n\n  // https:// github.com/hexojs/hexo/issues/5155\n  it('publish() - merge front-matter', async () => {\n    const prefixTags = ['prefixTag1', 'fooo'];\n    const customTags = ['customTag', 'fooo'];\n\n    await hexo.scaffold.set('customscaff', [\n      '---',\n      'title: {{ title }}',\n      'date: {{ date }}',\n      `tags: ${JSON.stringify(prefixTags)}`,\n      'qwe: 123',\n      'zxc: zxc',\n      '---'\n    ].join('\\n'));\n\n    const path = join(hexo.source_dir, '_posts', 'fooo.md');\n    await post.create({\n      title: 'fooo',\n      layout: 'draft',\n      tags: customTags,\n      qwe: 456,\n      asd: 'asd'\n    });\n    const result = await post.publish({\n      slug: 'fooo',\n      layout: 'customscaff'\n    });\n\n    const fmt = yfm(result.content);\n    fmt.tags.sort().should.eql(deepMerge(prefixTags, customTags).sort());\n    fmt.qwe.should.eql(456);\n    fmt.asd.should.eql('asd');\n    fmt.zxc.should.eql('zxc');\n\n    await unlink(path);\n  });\n\n  it('render()', async () => {\n    // TODO: validate data\n    const beforeHook = spy();\n    const afterHook = spy();\n\n    hexo.extend.filter.register('before_post_render', beforeHook);\n    hexo.extend.filter.register('after_post_render', afterHook);\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql(expected);\n    beforeHook.calledOnce.should.be.true;\n    afterHook.calledOnce.should.be.true;\n  });\n\n  it('render() - callback', done => {\n    post.render('', {\n      content,\n      engine: 'markdown'\n    }, err => {\n      done(err);\n    });\n  });\n\n  it('render() - file', async () => {\n    const content = '**file test**';\n    const path = join(hexo.base_dir, 'render_test.md');\n\n    await writeFile(path, content);\n    const data = await post.render(path);\n    data.content.trim().should.eql('<p><strong>file test</strong></p>');\n    await unlink(path);\n  });\n\n  it('render() - skip js', async () => {\n    const content = 'let a = \"{{ 1 + 1 }}\"';\n\n    const data = await post.render('', {\n      content,\n      source: 'render_test.js'\n    });\n    data.content.trim().should.eql(content);\n  });\n\n  it('render() - toString', async () => {\n    const content = 'foo: 1';\n\n    const data = await post.render('', {\n      content,\n      engine: 'yaml'\n    });\n    data.content.should.eql('{\"foo\":1}');\n  });\n\n  it('render() - skip render phase if it\\'s nunjucks file', async () => {\n    const content = [\n      '{% quote Hello World %}',\n      'quote content',\n      '{% endquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'njk'\n    });\n    data.content.trim().should.eql([\n      '<blockquote><p>quote content</p>\\n',\n      '<footer><strong>Hello World</strong></footer></blockquote>'\n    ].join(''));\n  });\n\n  it('render() - escaping nunjucks blocks with similar names', async () => {\n    const code = 'alert(\"Hello world\")';\n    const highlighted = highlight(code);\n\n    const content = [\n      '{% codeblock %}',\n      code,\n      '{% endcodeblock %}',\n      '',\n      '{% code %}',\n      code,\n      '{% endcode %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content\n    });\n    data.content.trim().should.eql([\n      highlighted,\n      '',\n      highlighted\n    ].join('\\n'));\n  });\n\n  it('render() - recover escaped nunjucks blocks which is html escaped', async () => {\n    const content = '`{% raw %}{{ test }}{% endraw %}`, {%raw%}{{ test }}{%endraw%}';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql('<p><code>{{ test }}</code>, {{ test }}</p>');\n  });\n\n  it.skip('render() - recover escaped nunjucks blocks which is html escaped before post_render', async () => {\n    const content = '`{% raw %}{{ test }}{% endraw %}`';\n\n    const filter = spy();\n\n    hexo.extend.filter.register('after_render:html', filter);\n\n    await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    filter.calledOnce.should.be.true;\n    filter.firstCall.args[0].trim().should.eql('<p><code>{{ test }}</code></p>');\n    hexo.extend.filter.unregister('after_render:html', filter);\n  });\n\n  it('render() - callback - not path and file', callback => {\n    post.render('', {}, (err, result) => {\n      try {\n        err.should.be.exist;\n        err.should.be.instanceof(Error);\n        err.should.be.have.property('message', 'No input file or string!');\n        should.not.exist(result);\n      } catch (e) {\n        callback(e);\n        return;\n      }\n      callback();\n    });\n  });\n\n  // #3573\n  it('render() - (disableNunjucks === true)', async () => {\n    const renderer = hexo.render.renderer.get('markdown');\n    renderer.disableNunjucks = true;\n\n    try {\n      const data = await post.render('', {\n        content,\n        engine: 'markdown'\n      });\n      data.content.trim().should.eql(expected_disable_nunjucks);\n    } finally {\n      renderer.disableNunjucks = false;\n    }\n  });\n\n  // #3573\n  it('render() - (disableNunjucks === false)', async () => {\n    const renderer = hexo.render.renderer.get('markdown');\n    renderer.disableNunjucks = false;\n\n    try {\n      const data = await post.render('', {\n        content,\n        engine: 'markdown'\n      });\n      data.content.trim().should.eql(expected);\n    } finally {\n      renderer.disableNunjucks = false;\n    }\n  });\n\n  // #4498\n  it('render() - (disableNunjucks === true) - sync', async () => {\n    const content = '{% link foo http://bar.com %}';\n    const loremFn = data => { return data.text.toUpperCase(); };\n    loremFn.disableNunjucks = true;\n    hexo.extend.renderer.register('coffee', 'js', loremFn, true);\n\n    const data = await post.render('', { content, engine: 'coffee' });\n    data.content.should.eql(content.toUpperCase());\n  });\n\n  // #4498\n  it('render() - (disableNunjucks === false) - sync', async () => {\n    const content = '{% link foo http://bar.com %}';\n    const loremFn = data => { return data.text.toUpperCase(); };\n    loremFn.disableNunjucks = false;\n    hexo.extend.renderer.register('coffee', 'js', loremFn, true);\n\n    const data = await post.render('', { content, engine: 'coffee' });\n    data.content.should.not.eql(content.toUpperCase());\n  });\n\n  it('render() - (disableNunjucks === true) - front-matter', async () => {\n    const renderer = hexo.render.renderer.get('markdown');\n    renderer.disableNunjucks = true;\n\n    try {\n      const data = await post.render('', {\n        content,\n        engine: 'markdown',\n        disableNunjucks: false\n      });\n      data.content.trim().should.eql(expected);\n    } finally {\n      renderer.disableNunjucks = false;\n    }\n  });\n\n  it('render() - (disableNunjucks === false) - front-matter', async () => {\n    const renderer = hexo.render.renderer.get('markdown');\n    renderer.disableNunjucks = false;\n\n    try {\n      const data = await post.render('', {\n        content,\n        engine: 'markdown',\n        disableNunjucks: true\n      });\n      data.content.trim().should.eql(expected_disable_nunjucks);\n    } finally {\n      renderer.disableNunjucks = false;\n    }\n  });\n\n  // Only boolean type of front-matter's disableNunjucks is valid\n  it('render() - (disableNunjucks === null) - front-matter', async () => {\n    const renderer = hexo.render.renderer.get('markdown');\n    renderer.disableNunjucks = true;\n\n    try {\n      const data = await post.render('', {\n        content,\n        engine: 'markdown',\n        // @ts-ignore\n        disableNunjucks: null\n      });\n      data.content.trim().should.eql(expected_disable_nunjucks);\n    } finally {\n      renderer.disableNunjucks = false;\n    }\n  });\n\n  it('render() - nested swig tag', async () => {\n    const content = [\n      '{% blockquote %}',\n      'test1',\n      '{% quote test2 %}',\n      'test3',\n      '{% endquote %}',\n      'test4',\n      '{% endblockquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<blockquote><p>test1</p>',\n      '<blockquote><p>test3</p>',\n      '<footer><strong>test2</strong></footer></blockquote>',\n      'test4</blockquote>'\n    ].join('\\n'));\n  });\n\n  it('render() - swig comments', async () => {\n    const content = '{# blockquote #}';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql('');\n  });\n\n  it('render() - shouln\\'t break curly brackets', async () => {\n    hexo.config.syntax_highlighter = 'prismjs';\n\n    const content = [\n      '\\\\begin{equation}',\n      'E=h\\\\nu',\n      '\\\\end{equation}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.include('\\\\begin{equation}');\n    data.content.should.include('\\\\end{equation}');\n\n    hexo.config.syntax_highlighter = 'highlight.js';\n  });\n\n  // #2321\n  it('render() - allow backtick code block in \"blockquote\" tag plugin', async () => {\n    const code = 'alert(\"Hello world\")';\n    const highlighted = highlight(code);\n\n    const content = [\n      '{% blockquote %}',\n      '```',\n      code,\n      '```',\n      '{% endblockquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content\n    });\n    data.content.trim().should.eql([\n      '<blockquote>' + highlighted + '</blockquote>'\n    ].join('\\n'));\n  });\n\n  // #2969\n  it('render() - backtick cocde block in blockquote', async () => {\n    const code = 'alert(\"Hello world\")';\n    const highlighted = highlight(code);\n    const quotedContent = [\n      'This is a code-block',\n      '',\n      '```',\n      code,\n      '```'\n    ];\n\n    const content = [\n      'Hello',\n      '',\n      ...quotedContent.map(s => '> ' + s)\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<p>Hello</p>',\n      '<blockquote>',\n      '<p>This is a code-block</p>',\n      highlighted + '</blockquote>'\n    ].join('\\n'));\n  });\n\n  // #2969\n  it('render() - \"lang=dos\" backtick cocde block in blockquote', async () => {\n    const code = '> dir';\n    const highlighted = highlight(code);\n    const quotedContent = [\n      'This is a code-block',\n      '',\n      '```',\n      code,\n      '```'\n    ];\n\n    const content = [\n      'Hello',\n      '',\n      ...quotedContent.map(s => '> ' + s)\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<p>Hello</p>',\n      '<blockquote>',\n      '<p>This is a code-block</p>',\n      highlighted + '</blockquote>'\n    ].join('\\n'));\n  });\n\n  // #3767\n  it('render() - backtick cocde block (followed by a paragraph) in blockquote', async () => {\n    const code = 'alert(\"Hello world\")';\n    const highlighted = highlight(code);\n    const quotedContent = [\n      'This is a code-block',\n      '',\n      '```',\n      code,\n      '```',\n      '',\n      'This is a following paragraph'\n    ];\n\n    const content = [\n      'Hello',\n      '',\n      ...quotedContent.map(s => '> ' + s)\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<p>Hello</p>',\n      '<blockquote>',\n      '<p>This is a code-block</p>',\n      highlighted,\n      '',\n      '<p>This is a following paragraph</p>',\n      '</blockquote>'\n    ].join('\\n'));\n  });\n\n  // #3769\n  it('render() - blank lines in backtick cocde block in blockquote', async () => {\n    const code = [\n      '',\n      '',\n      '',\n      '{',\n      '  \"test\": 123',\n      '',\n      '',\n      '}',\n      ''\n    ];\n    const highlighted = highlight(code.join('\\n'));\n    const addQuote = s => '>' + (s ? ` ${s}` : '');\n    const code2 = code.map((s, i) => {\n      if (i === 0 || i === 2 || i === 6) return addQuote(s);\n      return s;\n    });\n    const quotedContent = [\n      'This is a code-block',\n      '',\n      '> ```',\n      ...code2,\n      '```',\n      '',\n      'This is a following paragraph'\n    ];\n    const content = [\n      'Hello',\n      '',\n      ...quotedContent.map(addQuote)\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<p>Hello</p>',\n      '<blockquote>',\n      '<p>This is a code-block</p>',\n      '<blockquote>',\n      highlighted.replace('{', '&#123;').replace('}', '&#125;'),\n      '</blockquote>',\n      '<p>This is a following paragraph</p>',\n      '</blockquote>'\n    ].join('\\n'));\n  });\n\n  // #4161\n  it('render() - adjacent tags', async () => {\n    const content = [\n      '{% pullquote %}content1{% endpullquote %}',\n      '',\n      'This is a following paragraph',\n      '',\n      '{% pullquote %}content2{% endpullquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<blockquote class=\"pullquote\"><p>content1</p>\\n</blockquote>\\n\\n',\n      '<p>This is a following paragraph</p>\\n',\n      '<blockquote class=\"pullquote\"><p>content2</p>\\n</blockquote>'\n    ].join(''));\n  });\n\n  // #4161\n  it('render() - adjacent tags with args', async () => {\n    const content = [\n      '{% pullquote center %}content1{% endpullquote %}',\n      '',\n      'This is a following paragraph',\n      '',\n      '{% pullquote center %}content2{% endpullquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.trim().should.eql([\n      '<blockquote class=\"pullquote center\"><p>content1</p>\\n</blockquote>\\n\\n',\n      '<p>This is a following paragraph</p>\\n',\n      '<blockquote class=\"pullquote center\"><p>content2</p>\\n</blockquote>'\n    ].join(''));\n  });\n\n  // #3346\n  it('render() - swig tag inside backtick code block', async () => {\n    const content = content_for_issue_3346;\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.trim().should.eql(expected_for_issue_3346);\n  });\n\n  // test for https://github.com/hexojs/hexo/pull/4171#issuecomment-594412367\n  it('render() - markdown content right after swig tag', async () => {\n    const content = [\n      '{% pullquote warning %}',\n      'Text',\n      '{% endpullquote %}',\n      '# Title 0',\n      '{% pullquote warning %}',\n      'Text',\n      '{% endpullquote %}',\n      '{% pullquote warning %}',\n      'Text',\n      '{% endpullquote %}',\n      '# Title 1',\n      '{% pullquote warning %}',\n      'Text',\n      '{% endpullquote %}',\n      '{% pullquote warning %}Text{% endpullquote %}',\n      '# Title 2',\n      '{% pullquote warning %}Text{% endpullquote %}',\n      '{% pullquote warning %}Text{% endpullquote %}',\n      '# Title 3',\n      '{% pullquote warning %}Text{% endpullquote %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    // We only to make sure markdown content is rendered correctly\n    data.content.trim().should.include('<h1 id=\"Title-0\"><a href=\"#Title-0\" class=\"headerlink\" title=\"Title 0\"></a>Title 0</h1>');\n    data.content.trim().should.include('<h1 id=\"Title-1\"><a href=\"#Title-1\" class=\"headerlink\" title=\"Title 1\"></a>Title 1</h1>');\n    data.content.trim().should.include('<h1 id=\"Title-2\"><a href=\"#Title-2\" class=\"headerlink\" title=\"Title 2\"></a>Title 2</h1>');\n    data.content.trim().should.include('<h1 id=\"Title-3\"><a href=\"#Title-3\" class=\"headerlink\" title=\"Title 3\"></a>Title 3</h1>');\n  });\n\n  // #3259\n  it('render() - \"{{\" & \"}}\" inside inline code', async () => {\n    const content = 'In Go\\'s templates, blocks look like this: `{{block \"template name\" .}} (content) {{end}}`.';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.trim().should.eql(`<p>In Go’s templates, blocks look like this: <code>${escapeSwigTag('{{block \"template name\" .}} (content) {{end}}')}</code>.</p>`);\n  });\n\n  // https://github.com/hexojs/hexo/issues/3346#issuecomment-595497849\n  it('render() - swig var inside inline code', async () => {\n    const content = '`{{ 1 + 1 }}` {{ 1 + 1 }}';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.trim().should.eql(`<p><code>${escapeSwigTag('{{ 1 + 1 }}')}</code> 2</p>`);\n  });\n\n  // #3543\n  it('render() - issue #3543', async () => {\n    // Adopted from #3459\n    const js = 'alert(\"Foo\")';\n    const html = '<div></div>';\n    const highlightedJs = highlight(js, { lang: 'js' });\n    const highlightedHtml = highlight(html, { lang: 'html' });\n\n    const content = [\n      '```js',\n      js,\n      '```',\n      '{% raw %}',\n      '<p>Foo</p>',\n      '{% endraw %}',\n      '```html',\n      html,\n      '```'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.trim().should.contains(highlightedJs);\n    data.content.trim().should.contains('<p>Foo</p>');\n    data.content.trim().should.not.contains('{% raw %}');\n    data.content.trim().should.not.contains('{% endraw %}');\n    data.content.trim().should.contains(highlightedHtml);\n  });\n\n  it('render() - escape & recover multi {% raw %} and backticks', async () => {\n    const content = [\n      '`{{ 1 + 1 }}` {{ 1 + 2 }} `{{ 2 + 2 }}`',\n      'Text',\n      '{% raw %}',\n      'Raw 1',\n      '{% endraw %}',\n      'Another Text',\n      '{% raw %}',\n      'Raw 2',\n      '{% endraw %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.trim().should.eql([\n      `<p><code>${escapeSwigTag('{{ 1 + 1 }}')}</code> 3 <code>${escapeSwigTag('{{ 2 + 2 }}')}</code><br>Text</p>`,\n      '',\n      'Raw 1',\n      '',\n      '<p>Another Text</p>',\n      '',\n      'Raw 2'\n    ].join('\\n'));\n  });\n\n  // #4087\n  it('render() - issue #4087', async () => {\n    // Adopted from https://github.com/hexojs/hexo/issues/4087#issuecomment-596999486\n    const content = [\n      '## Quote',\n      '',\n      '    {% pullquote %}foo foo foo{% endpullquote %}',\n      '',\n      'test001',\n      '',\n      '{% pullquote %}bar bar bar{% endpullquote %}',\n      '',\n      '## Insert',\n      '',\n      'test002',\n      ''\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    // indented pullquote\n    data.content.trim().should.contains(`<pre><code>${escapeSwigTag('{% pullquote %}foo foo foo{% endpullquote %}')}\\n</code></pre>`);\n    data.content.trim().should.contains('<p>test001</p>');\n    // pullquote tag\n    data.content.trim().should.contains('<blockquote class=\"pullquote\"><p>bar bar bar</p>\\n</blockquote>');\n    data.content.trim().should.contains('<p>test002</p>');\n  });\n\n  // #4385\n  it('render() - no double escape in code block (issue #4385)', async () => {\n    const content = [\n      '```rust',\n      'fn main() {',\n      '    println!(\"Hello, world!\");',\n      '}',\n      '```'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.contains('<figure class=\"highlight rust\">');\n    data.content.should.contains('&#123;');\n    data.content.should.contains('&#125;');\n    data.content.should.not.contains('&amp;#123');\n    data.content.should.not.contains('&amp;#125');\n  });\n\n  it('render() - issue #4460', async () => {\n    hexo.config.syntax_highlighter = 'prismjs';\n\n    const content = content_for_issue_4460;\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.not.include('hexoPostRenderEscape');\n\n    hexo.config.syntax_highlighter = 'highlight.js';\n  });\n\n  it('render() - empty tag name', async () => {\n    hexo.config.syntax_highlighter = 'prismjs';\n\n    const content = 'Disable rendering of Nunjucks tag `{{ }}` / `{% %}`';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.include(escapeSwigTag('{{ }}'));\n    data.content.should.include(escapeSwigTag('{% %}'));\n\n    hexo.config.syntax_highlighter = 'highlight.js';\n  });\n\n  // https://github.com/hexojs/hexo/issues/5301\n  it('render() - dont escape incomplete tags', async () => {\n    const content = 'dont drop `{% }` 11111 `{# }` 22222 `{{ }` 33333';\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.contains('11111');\n    data.content.should.contains('22222');\n    data.content.should.contains('33333');\n    data.content.should.not.contains('&#96;'); // `\n  });\n\n  it('render() - should support quotes in tags', async () => {\n    let content = '{{ \"{{ }\" }}';\n    let data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.eql('{{ }');\n\n    content = '{% blockquote \"{% }\"  %}test{% endblockquote %}';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.eql('<blockquote><p>test</p>\\n<footer><strong>{% }</strong></footer></blockquote>');\n  });\n\n  it('render() - dont escape incomplete tags with complete tags', async () => {\n    // lost one character\n    let content = '{{ 1 }} \\n `{% \"%}\" }` 22222';\n    let data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;% &quot;%&#125;&quot; &#125;');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{% \"%}\" %` 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;% &quot;%&#125;&quot; %');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{# }` 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;# &#125;');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{{ \"}}\" }` 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;&#123; &quot;&#125;&#125;&quot; &#125;');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{{ %}` 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;&#123; %&#125;');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{% custom %}` 22222  `{% endcustom }`';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('1');\n    data.content.should.contains('&#123;% custom %&#125;');\n    data.content.should.contains('22222');\n    data.content.should.contains('&#123;% endcustom &#125;');\n\n    // lost two characters\n    content = '{{ 1 }} \\n `{#` \\n 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;#');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{%` \\n 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('&#123;%');\n    data.content.should.contains('1');\n    data.content.should.contains('22222');\n\n    content = '{{ 1 }} \\n `{{ ` 22222';\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    data.content.should.contains('1');\n    data.content.should.contains('&#123;&#123; ');\n    data.content.should.contains('22222');\n  });\n\n  it('render() - tags with swig character', async () => {\n    const tagSpy = spy();\n    hexo.extend.tag.register('testTag', (args, content) => {\n      tagSpy(args, content);\n      return '';\n    }, {\n      ends: true\n    });\n    let content = '{% testTag 111 222 %}\\n3333\\n{% endtestTag %}';\n    await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    tagSpy.calledOnce.should.be.true;\n    tagSpy.firstCall.args[0].should.eql(['111', '222']);\n    tagSpy.firstCall.args[1].should.eql('3333');\n\n    content = '{% testTag 111% % 222 %}\\n333\\n{% endtestTag %}';\n    await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    tagSpy.calledTwice.should.be.true;\n    tagSpy.secondCall.args[0].should.eql(['111%', '%', '222']);\n    tagSpy.secondCall.args[1].should.eql('333');\n\n    content = '{% testTag 111 } 222} %}\\n333\\n{% endtestTag %}';\n    await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    tagSpy.calledThrice.should.be.true;\n    tagSpy.thirdCall.args[0].should.eql(['111', '}', '222}']);\n    tagSpy.thirdCall.args[1].should.eql('333');\n\n    content = '{% testTag 111 222 %}\\n333% % } %}\\n{% endtestTag %}';\n    await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n    tagSpy.callCount.should.eql(4);\n    tagSpy.getCall(3).args[0].should.eql(['111', '222']);\n    tagSpy.getCall(3).args[1].should.eql('333% % } %}');\n\n    hexo.extend.tag.unregister('testTag');\n  });\n\n  it('render() - incomplete tags throw error', async () => {\n    const content = 'nunjucks should throw {#  } error';\n\n    try {\n      await post.render('', {\n        content,\n        engine: 'markdown'\n      });\n      should.fail();\n    } catch {}\n  });\n\n  // https://github.com/hexojs/hexo/issues/5401\n  it('render() - tags in different lines', async () => {\n    const content = [\n      '{% link',\n      'foobar',\n      'https://hexo.io/',\n      'tttitle',\n      '%}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql('<a href=\"https://hexo.io/\" title=\"tttitle\" target=\"\">foobar</a>');\n  });\n\n  // https://github.com/hexojs/hexo/issues/5433\n  it('render() - nunjucks nesting in comments', async () => {\n    const content = [\n      'foo',\n      '<!--',\n      '{% raw %}',\n      'test',\n      '{% endraw %}',\n      '-->',\n      'bar'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql([\n      '<p>foo</p>',\n      '<!--',\n      '{% raw %}',\n      'test',\n      '{% endraw %}',\n      '-->',\n      '<p>bar</p>',\n      ''\n    ].join('\\n'));\n  });\n\n  it('render() - incomplete comments', async () => {\n    const content = [\n      'foo',\n      '<!--',\n      'test',\n      '{% raw %}',\n      'bar',\n      '{% endraw %}'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql([\n      '<p>foo</p>',\n      '<!--',\n      'test',\n      '{% raw %}',\n      'bar',\n      '{% endraw %}'\n    ].join('\\n'));\n  });\n\n  // https://github.com/hexojs/hexo/issues/5716\n  it('render() - comments nesting in nunjucks', async () => {\n    const tagSpy = spy();\n    hexo.extend.tag.register('testTag', (args, content) => {\n      tagSpy(args, content);\n      return '';\n    }, {\n      ends: true\n    });\n    let content = [\n      '{% testTag %}',\n      'foo',\n      '<!--',\n      'test',\n      '-->',\n      'bar',\n      '{% endtestTag %}'\n    ].join('\\n');\n\n    let data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql('');\n    tagSpy.calledOnce.should.be.true;\n    tagSpy.firstCall.args[1].should.eql([\n      'foo',\n      '<!--',\n      'test',\n      '-->',\n      'bar'\n    ].join('\\n'));\n\n    content = [\n      '{% testTag %}',\n      'foo',\n      '<!-- test -->',\n      'bar',\n      '{% endtestTag %}'\n    ].join('\\n');\n\n    data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql('');\n    tagSpy.calledTwice.should.be.true;\n    tagSpy.secondCall.args[1].should.eql([\n      'foo',\n      '<!-- test -->',\n      'bar'\n    ].join('\\n'));\n\n    hexo.extend.tag.unregister('testTag');\n  });\n\n  // https://github.com/hexojs/hexo/issues/5433\n  it('render() - code fence nesting in comments', async () => {\n    const code = 'alert(\"Hello world\")';\n    const content = [\n      'foo',\n      '<!--',\n      '```',\n      code,\n      '```',\n      '-->',\n      'bar'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql([\n      '<p>foo</p>',\n      '<!--',\n      '```',\n      code,\n      '```',\n      '-->',\n      '<p>bar</p>',\n      ''\n    ].join('\\n'));\n  });\n\n  // https://github.com/hexojs/hexo/issues/5715\n  it('render() - comment nesting in code fence', async () => {\n    const code = 'alert(\"Hello world\")';\n    const content = [\n      'foo',\n      '```',\n      '<!--',\n      code,\n      '-->',\n      '```',\n      'bar'\n    ].join('\\n');\n\n    const data = await post.render('', {\n      content,\n      engine: 'markdown'\n    });\n\n    data.content.should.eql([\n      '<p>foo</p>',\n      '<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">&lt;!--</span><br><span class=\"line\">alert(&quot;Hello world&quot;)</span><br><span class=\"line\">--&gt;</span><br></pre></td></tr></table></figure>',\n      '<p>bar</p>',\n      ''\n    ].join('\\n'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/render.ts",
    "content": "import { writeFile, rmdir } from 'hexo-fs';\nimport { join } from 'path';\nimport yaml from 'js-yaml';\nimport { spy, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Render', () => {\n  const hexo = new Hexo(join(__dirname, 'render_test'));\n\n  hexo.config.meta_generator = false;\n\n  const body = [\n    'name:',\n    '  first: John',\n    '  last: Doe',\n    '',\n    'age: 23',\n    '',\n    'list:',\n    '- Apple',\n    '- Banana'\n  ].join('\\n');\n\n  const obj = yaml.load(body);\n  const path = join(hexo.base_dir, 'test.yml');\n\n  before(async () => {\n    await writeFile(path, body);\n    await hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('isRenderable()', () => {\n    hexo.render.isRenderable('test.txt').should.be.false;\n\n    // html\n    hexo.render.isRenderable('test.htm').should.be.true;\n    hexo.render.isRenderable('test.html').should.be.true;\n\n    // swig\n    hexo.render.isRenderable('test.swig').should.be.false;\n    hexo.render.isRenderable('test.njk').should.be.true;\n\n    // yaml\n    hexo.render.isRenderable('test.yml').should.be.true;\n    hexo.render.isRenderable('test.yaml').should.be.true;\n  });\n\n  it('isRenderableSync()', () => {\n    hexo.render.isRenderableSync('test.txt').should.be.false;\n\n    // html\n    hexo.render.isRenderableSync('test.htm').should.be.true;\n    hexo.render.isRenderableSync('test.html').should.be.true;\n\n    // swig\n    hexo.render.isRenderableSync('test.swig').should.be.false;\n    hexo.render.isRenderableSync('test.njk').should.be.true;\n\n    // yaml\n    hexo.render.isRenderableSync('test.yml').should.be.true;\n    hexo.render.isRenderableSync('test.yaml').should.be.true;\n  });\n\n  it('getOutput()', () => {\n    hexo.render.getOutput('test.txt').should.not.ok;\n\n    // html\n    hexo.render.getOutput('test.htm').should.eql('html');\n    hexo.render.getOutput('test.html').should.eql('html');\n\n    // swig\n    hexo.render.getOutput('test.njk').should.eql('html');\n\n    // yaml\n    hexo.render.getOutput('test.yml').should.eql('json');\n    hexo.render.getOutput('test.yaml').should.eql('json');\n  });\n\n  it('render() - path', async () => {\n    const result = await hexo.render.render({path});\n    result.should.eql(obj);\n  });\n\n  it('render() - text (without engine)', async () => {\n    const result = await hexo.render.render({text: body});\n    result.should.eql(body);\n  });\n\n  it('render() - text (with engine)', async () => {\n    const result = await hexo.render.render({text: body, engine: 'yaml'});\n    result.should.eql(obj);\n  });\n\n  it('render() - no path and text', async () => {\n    try {\n      // @ts-expect-error\n      await hexo.render.render();\n      should.fail('Return value must be rejected');\n    } catch (err) {\n      err.message.should.eql('No input file or string!');\n    }\n  });\n\n  it('render() - null path and text', async () => {\n    try {\n      // @ts-ignore\n      await hexo.render.render({text: null, engine: null});\n      should.fail('Return value must be rejected');\n    } catch (err) {\n      err.message.should.eql('No input file or string!');\n    }\n  });\n\n  it('render() - options', async () => {\n    const result = await hexo.render.render({\n      text: [\n        '<title>{{ title }}</title>',\n        '<body>{{ content }}</body>'\n      ].join('\\n'),\n      engine: 'njk'\n    }, {\n      title: 'Hello world',\n      content: 'foobar'\n    });\n    result.should.eql([\n      '<title>Hello world</title>',\n      '<body>foobar</body>'\n    ].join('\\n'));\n  });\n\n  it('render() - toString', async () => {\n    const content = await hexo.render.render({\n      text: body,\n      engine: 'yaml',\n      toString: true\n    });\n    content.should.eql(JSON.stringify(obj));\n  });\n\n  it('render() - custom toString method', async () => {\n    const content = await hexo.render.render({\n      text: body,\n      engine: 'yaml',\n      toString(data) {\n        return JSON.stringify(data, null, '  ');\n      }\n    });\n    content.should.eql(JSON.stringify(obj, null, '  '));\n  });\n\n  it.skip('render() - after_render filter', async () => {\n    const data = {\n      text: '  <strong>123456</strong>  ',\n      engine: 'njk'\n    };\n\n    const filter = spy((result, obj) => {\n      result.should.eql(data.text);\n      obj.should.eql(data);\n      return result.trim();\n    });\n\n    hexo.extend.filter.register('after_render:html', filter);\n\n    const result = await hexo.render.render(data);\n    filter.calledOnce.should.be.true;\n    result.should.eql(data.text.trim());\n\n    hexo.extend.filter.unregister('after_render:html', filter);\n  });\n\n  it('render() - after_render filter: use the given output extension if not found', async () => {\n    const data = {\n      text: 'foo',\n      engine: 'txt'\n    };\n\n    const filter = spy();\n    hexo.extend.filter.register('after_render:txt', filter);\n\n    await hexo.render.render(data);\n    filter.calledOnce.should.be.true;\n    hexo.extend.filter.unregister('after_render:txt', filter);\n  });\n\n  it('render() - onRenderEnd method', async () => {\n    const onRenderEnd = spy(result => result + 'bar');\n\n    const data = {\n      text: 'foo',\n      engine: 'txt',\n      onRenderEnd\n    };\n\n    const filter = spy();\n\n    hexo.extend.filter.register('after_render:txt', filter);\n\n    await hexo.render.render(data);\n    onRenderEnd.calledOnce.should.be.true;\n    filter.calledOnce.should.be.true;\n    sinonAssert.calledWith(filter, 'foobar');\n\n    hexo.extend.filter.unregister('after_render:txt', filter);\n  });\n\n  it('render() - options as callback', async () => {\n    const cbSpy = spy();\n\n    const data = {\n      text: '  <strong>123456</strong>  ',\n      engine: 'njk'\n    };\n\n    await hexo.render.render(data, cbSpy);\n    cbSpy.calledOnce.should.be.true;\n  });\n\n  it('renderSync() - path', () => {\n    const result = hexo.render.renderSync({path});\n    result.should.eql(obj);\n  });\n\n  it('renderSync() - text (without engine)', () => {\n    const result = hexo.render.renderSync({text: body});\n    result.should.eql(body);\n  });\n\n  it('renderSync() - text (with engine)', () => {\n    const result = hexo.render.renderSync({text: body, engine: 'yaml'});\n    result.should.eql(obj);\n  });\n\n  it('renderSync() - no path and text', () => {\n    // @ts-expect-error\n    should.throw(() => hexo.render.renderSync(), 'No input file or string!');\n  });\n\n  it('renderSync() - null path and text', () => {\n    // @ts-ignore\n    should.throw(() => hexo.render.renderSync({text: null, engine: null}), 'No input file or string!');\n  });\n\n  it('renderSync() - options', () => {\n    const result = hexo.render.renderSync({\n      text: [\n        '<title>{{ title }}</title>',\n        '<body>{{ content }}</body>'\n      ].join('\\n'),\n      engine: 'njk'\n    }, {\n      title: 'Hello world',\n      content: 'foobar'\n    });\n\n    result.should.eql([\n      '<title>Hello world</title>',\n      '<body>foobar</body>'\n    ].join('\\n'));\n  });\n\n  it('renderSync() - toString', () => {\n    const result = hexo.render.renderSync({\n      text: body,\n      engine: 'yaml',\n      toString: true\n    });\n\n    result.should.eql(JSON.stringify(obj));\n  });\n\n  it('renderSync() - custom toString method', () => {\n    const result = hexo.render.renderSync({\n      text: body,\n      engine: 'yaml',\n      toString(data) {\n        return JSON.stringify(data, null, '  ');\n      }\n    });\n\n    result.should.eql(JSON.stringify(obj, null, '  '));\n  });\n\n  it.skip('renderSync() - after_render filter', () => {\n    const data = {\n      text: '  <strong>123456</strong>  ',\n      engine: 'njk'\n    };\n\n    const filter = spy(result => result.trim());\n\n    hexo.extend.filter.register('after_render:html', filter);\n\n    const result = hexo.render.renderSync(data);\n\n    filter.calledOnce.should.be.true;\n    // @ts-expect-error\n    sinonAssert.calledWith(filter, data.text, data);\n    result.should.eql(data.text.trim());\n\n    hexo.extend.filter.unregister('after_render:html', filter);\n  });\n\n  it('renderSync() - after_render filter: use the given output extension if not found', () => {\n    const data = {\n      text: 'foo',\n      engine: 'txt'\n    };\n\n    const filter = spy();\n    hexo.extend.filter.register('after_render:txt', filter);\n\n    hexo.render.renderSync(data);\n    filter.calledOnce.should.be.true;\n    hexo.extend.filter.unregister('after_render:txt', filter);\n  });\n\n  it('renderSync() - onRenderEnd', () => {\n    const onRenderEnd = spy(result => result + 'bar');\n\n    const data = {\n      text: 'foo',\n      engine: 'txt',\n      onRenderEnd\n    };\n\n    const filter = spy(result => {\n      result.should.eql('foobar');\n    });\n\n    hexo.extend.filter.register('after_render:txt', filter);\n\n    hexo.render.renderSync(data);\n    onRenderEnd.calledOnce.should.be.true;\n    filter.calledOnce.should.be.true;\n\n    hexo.extend.filter.unregister('after_render:txt', filter);\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/router.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport { Readable } from 'stream';\nimport { join } from 'path';\nimport crypto from 'crypto';\nimport { createReadStream } from 'hexo-fs';\nimport { spy, assert as sinonAssert } from 'sinon';\nimport { readStream } from '../../util';\nimport Router from '../../../lib/hexo/router';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Router', () => {\n  const router = new Router();\n\n  function checkStream(stream, expected) {\n    return readStream(stream).then(data => {\n      data.should.eql(expected);\n    });\n  }\n\n  function checksum(stream) {\n    return new BluebirdPromise((resolve, reject) => {\n      const hash = crypto.createHash('sha1');\n\n      stream.on('readable', () => {\n        let chunk;\n\n        while ((chunk = stream.read()) !== null) {\n          hash.update(chunk);\n        }\n      }).on('end', () => {\n        resolve(hash.digest('hex'));\n      }).on('error', reject);\n    });\n  }\n\n  it('format()', () => {\n    router.format('foo').should.eql('foo');\n\n    // Remove prefixed slashes\n    router.format('/foo').should.eql('foo');\n    router.format('///foo').should.eql('foo');\n\n    // Append `index.html` to the URL with trailing slash\n    router.format('foo/').should.eql('foo/index.html');\n\n    // '' => `index.html\n    router.format('').should.eql('index.html');\n    router.format().should.eql('index.html');\n\n    // Remove backslashes\n    router.format('foo\\\\bar').should.eql('foo/bar');\n    router.format('foo\\\\bar\\\\').should.eql('foo/bar/index.html');\n\n    // Remove query string\n    router.format('foo?a=1&b=2').should.eql('foo');\n  });\n\n  it('format() - path must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => router.format(() => {}), 'path must be a string!');\n  });\n\n  it('set() - string', () => {\n    const listener = spy();\n\n    router.once('update', listener);\n\n    router.set('test', 'foo');\n    const data = router.get('test');\n\n    data.modified.should.be.true;\n    listener.calledOnce.should.be.true;\n    sinonAssert.calledWith(listener, 'test');\n    return checkStream(data, 'foo');\n  });\n\n  it('set() - function', () => {\n    router.set('test', () => 'foo');\n\n    return checkStream(router.get('test'), 'foo');\n  });\n\n  it('set() - function (callback style)', () => {\n    router.set('test', callback => {\n      callback(null, 'foo');\n    });\n\n    return checkStream(router.get('test'), 'foo');\n  });\n\n  it('set() - readable stream', () => {\n    // Prepare a readable stream\n    const stream = new Readable();\n    stream.push('foo');\n    stream.push(null);\n\n    router.set('test', () => stream);\n\n    return checkStream(router.get('test'), 'foo');\n  });\n\n  it('set() - modified', () => {\n    router.set('test', {\n      data: '',\n      modified: false\n    });\n\n    router.isModified('test').should.be.false;\n  });\n\n  it('set() - path must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => router.set(), 'path must be a string!');\n  });\n\n  it('set() - data is required', () => {\n    // @ts-expect-error\n    should.throw(() => router.set('test'), 'data is required!');\n  });\n\n  it('get() - error handling', () => {\n    router.set('test', () => {\n      throw new Error('error test');\n    });\n\n    return readStream(router.get('test')).then(() => {\n      should.fail('Return value must be rejected');\n    }, err => {\n      err.should.have.property('message', 'error test');\n    });\n  });\n\n  it('get() - no data', () => {\n    router.set('test', () => {\n\n    });\n\n    return checkStream(router.get('test'), '');\n  });\n\n  it('get() - empty readable stream', () => {\n    const stream = new Readable();\n    stream.push(null);\n\n    router.set('test', () => stream);\n\n    return checkStream(router.get('test'), '');\n  });\n\n  it('get() - large readable stream (more than 65535 bits)', () => {\n    const path = join(__dirname, '../../fixtures/banner.jpg');\n\n    router.set('test', () => createReadStream(path));\n\n    return BluebirdPromise.all([\n      checksum(router.get('test')),\n      checksum(createReadStream(path))\n    ]).then((data: any) => {\n      data[0].should.eql(data[1]);\n    });\n  });\n\n  it('get() - path must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => router.get(), 'path must be a string!');\n  });\n\n  it('get() - export stringified JSON object', () => {\n    const obj = {foo: 1, bar: 2};\n\n    router.set('test', () => obj);\n\n    return checkStream(router.get('test'), JSON.stringify(obj));\n  });\n\n  it('list()', () => {\n    const router = new Router();\n\n    router.set('foo', 'foo');\n    router.set('bar', 'bar');\n    router.set('baz', 'baz');\n    router.remove('bar');\n\n    router.list().should.eql(['foo', 'baz']);\n  });\n\n  it('isModified()', () => {\n    router.set('test', 'foo');\n    router.isModified('test').should.be.true;\n  });\n\n  it('isModified() - path must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => router.isModified(), 'path must be a string!');\n  });\n\n  it('remove()', () => {\n    const listener = spy();\n\n    router.once('remove', listener);\n\n    router.set('test', 'foo');\n    router.remove('test');\n    should.not.exist(router.get('test'));\n    sinonAssert.calledWith(listener, 'test');\n    listener.calledOnce.should.be.true;\n  });\n\n  it('remove() - path must be a string', () => {\n    // @ts-expect-error\n    should.throw(() => router.remove(), 'path must be a string!');\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/scaffold.ts",
    "content": "import { join } from 'path';\nimport { exists, readFile, rmdir, unlink, writeFile } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Scaffold', () => {\n  const hexo = new Hexo(__dirname);\n  const scaffold = hexo.scaffold;\n  const scaffoldDir = hexo.scaffold_dir;\n\n  const testContent = [\n    '---',\n    'title: {{ title }}',\n    '---',\n    'test scaffold'\n  ].join('\\n');\n\n  const testPath = join(scaffoldDir, 'test.md');\n\n  before(async () => {\n    await hexo.init();\n    await writeFile(testPath, testContent);\n  });\n\n  after(() => rmdir(scaffoldDir));\n\n  it('get() - file exists', async () => {\n    const data = await scaffold.get('test');\n    data.should.eql(testContent);\n  });\n\n  it('get() - normal scaffold', async () => {\n    const data = await scaffold.get('normal');\n    data.should.eql(scaffold.defaults.normal);\n  });\n\n  it('set() - file exists', async () => {\n    await scaffold.set('test', 'foo');\n\n    const file = await readFile(testPath);\n    const data = await scaffold.get('test');\n    file.should.eql('foo');\n    data.should.eql('foo');\n\n    await writeFile(testPath, testContent);\n  });\n\n  it('set() - file does not exist', async () => {\n    const testPath = join(scaffoldDir, 'foo.md');\n\n    await scaffold.set('foo', 'bar');\n    const file = await readFile(testPath);\n    const data = await scaffold.get('foo');\n    file.should.eql('bar');\n    data.should.eql('bar');\n    await unlink(testPath);\n  });\n\n  it('remove() - file exist', async () => {\n    await scaffold.remove('test');\n    const exist = await exists(testPath);\n    const data = await scaffold.get('test');\n    exist.should.be.false;\n    should.not.exist(data);\n\n    await writeFile(testPath, testContent);\n  });\n\n  it('remove() - file does not exist', () => scaffold.remove('foo'));\n});\n"
  },
  {
    "path": "test/scripts/hexo/update_package.ts",
    "content": "import { join } from 'path';\nimport { readFile, unlink, writeFile } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport updatePkg from '../../../lib/hexo/update_package';\n\ndescribe('Update package.json', () => {\n  const hexo = new Hexo(__dirname, {silent: true});\n  const packagePath = join(hexo.base_dir, 'package.json');\n\n  beforeEach(() => {\n    hexo.env.init = false;\n  });\n\n  it('package.json does not exist', async () => {\n    await updatePkg(hexo);\n    hexo.env.init.should.be.false;\n  });\n\n  it('package.json exists, but the version doesn\\'t match', async () => {\n    const pkg = {\n      hexo: {\n        version: '0.0.1'\n      }\n    };\n\n    await writeFile(packagePath, JSON.stringify(pkg));\n    await updatePkg(hexo);\n    const content = await readFile(packagePath);\n    JSON.parse(content).hexo.version.should.eql(hexo.version);\n    hexo.env.init.should.be.true;\n\n    await unlink(packagePath);\n  });\n\n  it('package.json exists, but don\\'t have hexo data', async () => {\n    const pkg = {\n      name: 'hexo',\n      version: '0.0.1'\n    };\n\n    await writeFile(packagePath, JSON.stringify(pkg));\n    await updatePkg(hexo);\n    const content = await readFile(packagePath);\n    // Don't change the original package.json\n    JSON.parse(content).should.eql(pkg);\n    hexo.env.init.should.be.false;\n\n    await unlink(packagePath);\n  });\n\n  it('package.json exists and everything is ok', async () => {\n    const pkg = {\n      hexo: {\n        version: hexo.version\n      }\n    };\n\n    await writeFile(packagePath, JSON.stringify(pkg));\n    await updatePkg(hexo);\n    const content = await readFile(packagePath);\n    JSON.parse(content).should.eql(pkg);\n    hexo.env.init.should.be.true;\n\n    await unlink(packagePath);\n  });\n});\n"
  },
  {
    "path": "test/scripts/hexo/validate_config.ts",
    "content": "import { spy } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport validateConfig from '../../../lib/hexo/validate_config';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Validate config', () => {\n  const hexo = new Hexo();\n  let logSpy;\n\n  beforeEach(() => {\n    logSpy = spy();\n    hexo.config = JSON.parse(JSON.stringify(defaultConfig));\n    hexo.log.warn = logSpy;\n    hexo.log.info = spy();\n  });\n\n  it('config.url - undefined', () => {\n    delete(hexo.config as any).url;\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"url\" should be string, not undefined!');\n    }\n  });\n\n  it('config.url - wrong type', () => {\n    // @ts-expect-error\n    hexo.config.url = true;\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"url\" should be string, not boolean!');\n    }\n  });\n\n  it('config.url - empty', () => {\n    hexo.config.url = ' ';\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"url\" should be a valid URL!');\n    }\n  });\n\n\n  it('config.url - not start with xx://', () => {\n    // @ts-ignore\n    hexo.config.url = 'localhost:4000';\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"url\" should be a valid URL!');\n    }\n  });\n\n  // #4510\n  it('config.url - slash', () => {\n    hexo.config.url = '/';\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"url\" should be a valid URL!');\n    }\n  });\n\n  it('config.root - undefined', () => {\n    delete(hexo.config as any).root;\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"root\" should be string, not undefined!');\n    }\n  });\n\n  it('config.root - wrong type', () => {\n    // @ts-expect-error\n    hexo.config.root = true;\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"root\" should be string, not boolean!');\n    }\n  });\n\n  it('config.root - empty', () => {\n    hexo.config.root = ' ';\n\n    try {\n      validateConfig(hexo);\n      should.fail();\n    } catch (e) {\n      e.name.should.eql('TypeError');\n      e.message.should.eql('Invalid config detected: \"root\" should not be empty!');\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/asset.ts",
    "content": "import { join } from 'path';\nimport Hexo from '../../../lib/hexo';\n\ndescribe('Asset', () => {\n  const hexo = new Hexo();\n  const Asset = hexo.model('Asset');\n\n  it('default values', async () => {\n    const data = await Asset.insert({\n      _id: 'foo',\n      path: 'bar'\n    });\n    data.modified.should.be.true;\n\n    Asset.removeById(data._id);\n  });\n\n  it('_id - required', async () => {\n    try {\n      await Asset.insert({});\n    } catch (err) {\n      err.message.should.eql('ID is not defined');\n    }\n  });\n\n  it('path - required', async () => {\n    try {\n      await Asset.insert({\n        _id: 'foo'\n      });\n    } catch (err) {\n      err.message.should.eql('`path` is required!');\n    }\n  });\n\n  it('source - virtual', async () => {\n    const data = await Asset.insert({\n      _id: 'foo',\n      path: 'bar'\n    });\n    data.source.should.eql(join(hexo.base_dir, data._id));\n\n    Asset.removeById(data._id);\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/cache.ts",
    "content": "import Hexo from '../../../lib/hexo';\n\ndescribe('Cache', () => {\n  const hexo = new Hexo();\n  const Cache = hexo.model('Cache');\n\n  it('_id - required', async () => {\n    try {\n      await Cache.insert({});\n    } catch (err) {\n      err.message.should.eql('ID is not defined');\n    }\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/category.ts",
    "content": "import { deepMerge, full_url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport defaults from '../../../lib/hexo/default_config';\n\ndescribe('Category', () => {\n  const hexo = new Hexo();\n  const Category = hexo.model('Category');\n  const Post = hexo.model('Post');\n  const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category;\n  const PostCategory = hexo.model('PostCategory');\n\n  before(() => hexo.init());\n\n  beforeEach(() => {\n    hexo.config = deepMerge({}, defaults);\n  });\n\n  it('name - required', async () => {\n    try {\n      await Category.insert({});\n    } catch (err) {\n      err.message.should.eql('`name` is required!');\n    }\n  });\n\n  // it('parent - reference'); missing-unit-test\n\n  it('slug - virtual', async () => {\n    const data = await Category.insert({\n      name: 'foo'\n    });\n    data.slug.should.eql('foo');\n\n    Category.removeById(data._id);\n  });\n\n  it('slug - category_map', async () => {\n    hexo.config.category_map = {\n      test: 'wat'\n    };\n\n    const data = await Category.insert({\n      name: 'test'\n    });\n    data.slug.should.eql('wat');\n\n    Category.removeById(data._id);\n  });\n\n  it('slug - filename_case: 0', async () => {\n    const data = await Category.insert({\n      name: 'WahAHa'\n    });\n    data.slug.should.eql('WahAHa');\n\n    Category.removeById(data._id);\n  });\n\n  it('slug - filename_case: 1', async () => {\n    hexo.config.filename_case = 1;\n\n    const data = await Category.insert({\n      name: 'WahAHa'\n    });\n    data.slug.should.eql('wahaha');\n\n    Category.removeById(data._id);\n  });\n\n  it('slug - filename_case: 2', async () => {\n    hexo.config.filename_case = 2;\n\n    const data = await Category.insert({\n      name: 'WahAHa'\n    });\n\n    data.slug.should.eql('WAHAHA');\n\n    Category.removeById(data._id);\n  });\n\n  it('slug - parent', async () => {\n    let cat = await Category.insert({\n      name: 'parent'\n    });\n    cat = await Category.insert({\n      name: 'child',\n      parent: cat._id\n    });\n    cat.slug.should.eql('parent/child');\n\n    await Promise.all([\n      Category.removeById(cat._id),\n      Category.removeById(cat.parent)\n    ]);\n  });\n\n  it('path - virtual', async () => {\n    const data = await Category.insert({\n      name: 'foo'\n    });\n    data.path.should.eql(hexo.config.category_dir + '/' + data.slug + '/');\n\n    Category.removeById(data._id);\n  });\n\n  it('permalink - virtual', async () => {\n    const data = await Category.insert({\n      name: 'foo'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path);\n\n    Category.removeById(data._id);\n  });\n\n  it('permalink - trailing_index', async () => {\n    hexo.config.pretty_urls.trailing_index = false;\n\n    const data = await Category.insert({\n      name: 'foo'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\\.html$/, ''));\n\n    Category.removeById(data._id);\n  });\n\n  it('permalink - trailing_html', async () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    const data = await Category.insert({\n      name: 'foo'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\\.html$/, ''));\n\n    Category.removeById(data._id);\n  });\n\n  it('permalink - should be encoded', async () => {\n    hexo.config.url = 'http://fôo.com';\n    const data = await Category.insert({\n      name: '字'\n    });\n    data.permalink.should.eql(full_url_for.call(hexo, data.path));\n\n    Category.removeById(data._id);\n  });\n\n  it('posts - virtual', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo'},\n      {source: 'bar.md', slug: 'bar'},\n      {source: 'baz.md', slug: 'baz'}\n    ]);\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n\n    const cat = await Category.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    hexo.locals.invalidate();\n    cat.posts.map(mapper).should.eql(posts.map(mapper));\n    cat.should.have.lengthOf(posts.length);\n\n    await cat.remove();\n    await Promise.all(posts.map(post => post.remove()));\n  });\n\n  it('posts - draft', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo', published: true},\n      {source: 'bar.md', slug: 'bar', published: false},\n      {source: 'baz.md', slug: 'baz', published: true}\n    ]);\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n\n    let cat = Category.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    // draft off\n    hexo.locals.invalidate();\n    cat.posts.eq(0)._id.should.eql(posts[0]._id);\n    cat.posts.eq(1)._id.should.eql(posts[2]._id);\n    cat.should.have.lengthOf(2);\n\n    // draft on\n    hexo.config.render_drafts = true;\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n    hexo.locals.invalidate();\n    cat = Category.findOne({name: 'foo'});\n    cat.posts.map(mapper).should.eql(posts.map(mapper));\n    cat.should.have.lengthOf(posts.length);\n    hexo.config.render_drafts = false;\n\n    await cat.remove();\n    await Promise.all(posts.map(post => post.remove()));\n\n  });\n\n  it('posts - future', async () => {\n    const now = Date.now();\n\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo', date: now - 3600},\n      {source: 'bar.md', slug: 'bar', date: now + 3600},\n      {source: 'baz.md', slug: 'baz', date: now}\n    ]);\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n\n    let cat = Category.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    // future on\n    hexo.config.future = true;\n    hexo.locals.invalidate();\n    cat.posts.map(mapper).should.eql(posts.map(mapper));\n    cat.should.have.lengthOf(posts.length);\n\n    // future off\n    hexo.config.future = false;\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n    hexo.locals.invalidate();\n    cat = Category.findOne({name: 'foo'});\n    cat.posts.eq(0)._id.should.eql(posts[0]._id);\n    cat.posts.eq(1)._id.should.eql(posts[2]._id);\n    cat.should.have.lengthOf(2);\n\n    await cat.remove();\n    await Promise.all(posts.map(post => post.remove()));\n\n  });\n\n  it('check whether a category exists', async () => {\n    const data = await Category.insert({\n      name: 'foo'\n    });\n\n    try {\n      await Category.insert({\n        name: 'foo'\n      });\n    } catch (err) {\n      err.message.should.eql('Category `foo` has already existed!');\n    }\n\n    Category.removeById(data._id);\n  });\n\n  it('check whether a category exists (with parent)', async () => {\n    let data = await Category.insert({\n      name: 'foo'\n    });\n    data = await Category.insert({\n      name: 'foo',\n      parent: data._id\n    });\n    await Promise.all([\n      Category.removeById(data._id),\n      Category.removeById(data.parent)\n    ]);\n  });\n\n  it('remove PostCategory references when a category is removed', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo'},\n      {source: 'bar.md', slug: 'bar'},\n      {source: 'baz.md', slug: 'baz'}\n    ]);\n\n    await Promise.all(posts.map(post => post.setCategories(['foo'])));\n\n    const cat = Category.findOne({name: 'foo'});\n    await Category.removeById(cat._id!);\n\n    PostCategory.find({category_id: cat._id}).should.have.lengthOf(0);\n    ReadOnlyPostCategory.find({category_id: cat._id}).should.have.lengthOf(0);\n\n    await Promise.all(posts.map(post => post.remove()));\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/moment.ts",
    "content": "import moment from 'moment-timezone';\nimport SchemaTypeMoment from '../../../lib/models/types/moment';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('SchemaTypeMoment', () => {\n  const type = new SchemaTypeMoment('test');\n\n  it('cast()', () => {\n    type.cast(1e8).should.eql(moment(1e8));\n    type.cast(new Date(2014, 1, 1)).should.eql(moment(new Date(2014, 1, 1)));\n    type.cast('2014-11-03T07:45:41.237Z').should.eql(moment('2014-11-03T07:45:41.237Z'));\n    type.cast(moment(1e8)).valueOf().should.eql(1e8);\n  });\n\n  it('cast() - default', () => {\n    const type = new SchemaTypeMoment('test', {default: moment});\n    moment.isMoment(type.cast()).should.be.true;\n  });\n\n  function shouldThrowError(value) {\n    should.throw(\n      () => type.validate(value),\n      '`' + value + '` is not a valid date!'\n    );\n  }\n\n  it('validate()', () => {\n    type.validate(moment(1e8)).valueOf().should.eql(1e8);\n    shouldThrowError(moment.invalid());\n  });\n\n  it('validate() - required', () => {\n    const type = new SchemaTypeMoment('test', {required: true});\n    // @ts-expect-error\n    should.throw(() => type.validate(), '`test` is required!');\n  });\n\n  it('match()', () => {\n    type.match(moment(1e8), moment(1e8)).should.be.true;\n    type.match(moment(1e8), moment(1e8 + 1)).should.be.false;\n    type.match(undefined, moment()).should.be.false;\n  });\n\n  it('compare()', () => {\n    type.compare(moment([2014, 1, 3]), moment([2014, 1, 2])).should.gt(0);\n    type.compare(moment([2014, 1, 1]), moment([2014, 1, 2])).should.lt(0);\n    type.compare(moment([2014, 1, 2]), moment([2014, 1, 2])).should.eql(0);\n    type.compare(moment()).should.eql(1);\n    type.compare(undefined, moment()).should.eql(-1);\n    type.compare().should.eql(0);\n  });\n\n  it('parse()', () => {\n    type.parse('2014-11-03T07:45:41.237Z')!.should.eql(moment('2014-11-03T07:45:41.237Z'));\n    should.not.exist(type.parse());\n  });\n\n  it('value()', () => {\n    type.value(moment('2014-11-03T07:45:41.237Z')).should.eql('2014-11-03T07:45:41.237Z');\n    should.not.exist(type.value());\n  });\n\n  it('q$day()', () => {\n    type.q$day(moment([2014, 1, 1]), 1).should.be.true;\n    type.q$day(moment([2014, 1, 1]), 5).should.be.false;\n    type.q$day(undefined, 1).should.be.false;\n  });\n\n  it('q$month()', () => {\n    type.q$month(moment([2014, 1, 1]), 1).should.be.true;\n    type.q$month(moment([2014, 1, 1]), 5).should.be.false;\n    type.q$month(undefined, 1).should.be.false;\n  });\n\n  it('q$year()', () => {\n    type.q$year(moment([2014, 1, 1]), 2014).should.be.true;\n    type.q$year(moment([2014, 1, 1]), 1999).should.be.false;\n    type.q$year(undefined, 1).should.be.false;\n  });\n\n  it('u$inc()', () => {\n    type.u$inc(moment(1e8), 1).valueOf().should.eql(1e8 + 1);\n    // @ts-expect-error\n    should.not.exist(undefined, 1);\n  });\n\n  it('u$dec()', () => {\n    type.u$dec(moment(1e8), 1).valueOf().should.eql(1e8 - 1);\n    // @ts-expect-error\n    should.not.exist(undefined, 1);\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/page.ts",
    "content": "import { join } from 'path';\nimport { deepMerge, full_url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport defaults from '../../../lib/hexo/default_config';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Page', () => {\n  const hexo = new Hexo();\n  const Page = hexo.model('Page');\n\n  beforeEach(() => { hexo.config = deepMerge({}, defaults); });\n\n  it('default values', async () => {\n    const now = Date.now();\n\n    const data = await Page.insert({\n      source: 'foo',\n      path: 'bar'\n    });\n\n    data.title.should.eql('');\n    data.date.valueOf().should.gte(now);\n    data.comments.should.be.true;\n    data.layout.should.eql('page');\n    data._content.should.eql('');\n    data.raw.should.eql('');\n    should.not.exist(data.updated);\n    should.not.exist(data.content);\n    should.not.exist(data.excerpt);\n    should.not.exist(data.more);\n\n    Page.removeById(data._id);\n  });\n\n  it('source - required', async () => {\n    try {\n      await Page.insert({});\n    } catch (err) {\n      err.message.should.eql('`source` is required!');\n    }\n  });\n\n  it('path - required', async () => {\n    try {\n      await Page.insert({\n        source: 'foo'\n      });\n    } catch (err) {\n      err.message.should.eql('`path` is required!');\n    }\n  });\n\n  it('permalink - virtual', async () => {\n    const data = await Page.insert({\n      source: 'foo',\n      path: 'bar'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path);\n\n    Page.removeById(data._id);\n  });\n\n  it('permalink - trailing_index', async () => {\n    hexo.config.pretty_urls.trailing_index = false;\n    const data = await Page.insert({\n      source: 'foo.md',\n      path: 'bar/index.html'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\\.html$/, ''));\n\n    Page.removeById(data._id);\n  });\n\n  it('permalink - trailing_html', async () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    const data = await Page.insert({\n      source: 'foo.md',\n      path: 'bar/foo.html'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\\.html$/, ''));\n\n    Page.removeById(data._id);\n  });\n\n  it('permalink - trailing_html - index.html', async () => {\n    hexo.config.pretty_urls.trailing_html = false;\n\n    const data = await Page.insert({\n      source: 'foo.md',\n      path: 'bar/foo/index.html'\n    });\n    data.permalink.should.eql(hexo.config.url + '/' + data.path);\n\n    Page.removeById(data._id);\n  });\n\n  it('permalink - should be encoded', async () => {\n    hexo.config.url = 'http://fôo.com';\n    const path = 'bár';\n    const data = await Page.insert({\n      source: 'foo',\n      path\n    });\n    data.permalink.should.eql(full_url_for.call(hexo, data.path));\n\n    Page.removeById(data._id);\n  });\n\n  it('full_source - virtual', async () => {\n    const data = await Page.insert({\n      source: 'foo',\n      path: 'bar'\n    });\n    data.full_source.should.eql(join(hexo.source_dir, data.source));\n\n    Page.removeById(data._id);\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/post.ts",
    "content": "import { join, sep } from 'path';\nimport BluebirdPromise from 'bluebird';\nimport { full_url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Post', () => {\n  const hexo = new Hexo();\n  const Post = hexo.model('Post');\n  const Tag = hexo.model('Tag');\n  const Category = hexo.model('Category');\n  const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag;\n  const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category;\n  const PostTag = hexo.model('PostTag');\n  const PostCategory = hexo.model('PostCategory');\n  const Asset = hexo.model('Asset');\n\n  before(() => {\n    hexo.config.permalink = ':title';\n    return hexo.init();\n  });\n\n  it('default values', () => {\n    const now = Date.now();\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    }).then(data => {\n      data.title.should.eql('');\n      data.date.valueOf().should.gte(now);\n      data.comments.should.be.true;\n      data.layout.should.eql('post');\n      data._content.should.eql('');\n      data.raw.should.eql('');\n      data.published.should.be.true;\n      should.not.exist(data.updated);\n      should.not.exist(data.content);\n      should.not.exist(data.excerpt);\n      should.not.exist(data.more);\n\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('source - required', () => {\n    return Post.insert({}).then(() => {\n      should.fail('Return value must be rejected');\n    }, err => {\n      err.should.have.property('message', '`source` is required!');\n    });\n  });\n\n  it('slug - required', () => {\n    return Post.insert({\n      source: 'foo.md'\n    }).then(() => {\n      should.fail('Return value must be rejected');\n    }, err => {\n      err.should.have.property('message', '`slug` is required!');\n    });\n  });\n\n  it('path - virtual', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(data => {\n    data.path.should.eql(data.slug);\n    return Post.removeById(data._id);\n  }));\n\n  it('permalink - virtual', () => {\n    hexo.config.root = '/';\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path);\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink - should be encoded', () => {\n    const slug = 'bár';\n    hexo.config.url = 'http://fôo.com';\n    return Post.insert({\n      source: 'foo.md',\n      slug\n    }).then(data => {\n      data.permalink.should.eql(full_url_for.call(hexo, slug));\n      hexo.config.url = 'http://example.com';\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink - virtual - when set relative_link', () => {\n    hexo.config.root = '/';\n    hexo.config.relative_link = true;\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path);\n      hexo.config.relative_link = false;\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink_root_prefix - virtual', () => {\n    hexo.config.url = 'http://example.com/root';\n    hexo.config.root = '/root/';\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    }).then(data => {\n      data.permalink.should.eql('http://example.com/root/' + data.path);\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink_root_prefix - virtual - when set relative_link', () => {\n    hexo.config.url = 'http://example.com/root';\n    hexo.config.root = '/root/';\n    hexo.config.relative_link = true;\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path);\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink - trailing_index', () => {\n    hexo.config.pretty_urls.trailing_index = false;\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar/index.html'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\\.html$/, ''));\n      hexo.config.pretty_urls.trailing_index = true;\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink - trailing_html', () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar/foo.html'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\\.html$/, ''));\n      hexo.config.pretty_urls.trailing_html = true;\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('permalink - trailing_html - index.html', () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'bar/index.html'\n    }).then(data => {\n      data.permalink.should.eql(hexo.config.url + '/' + data.path);\n      hexo.config.pretty_urls.trailing_html = true;\n      return Post.removeById(data._id);\n    });\n  });\n\n  it('full_source - virtual', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(data => {\n    data.full_source.should.eql(join(hexo.source_dir, data.source));\n    return Post.removeById(data._id);\n  }));\n\n  it('asset_dir - virtual', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(data => {\n    data.asset_dir.should.eql(join(hexo.source_dir, 'foo') + sep);\n    return Post.removeById(data._id);\n  }));\n\n  it('tags - virtual', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setTags(['foo', 'bar', 'baz'])\n    .thenReturn(Post.findById(post._id))).then(post => {\n    post.tags.map(tag => tag.name).should.have.members(['bar', 'baz', 'foo']);\n\n    return Post.removeById(post._id);\n  }));\n\n  it('categories - virtual', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setCategories(['foo', 'bar', 'baz'])\n    .thenReturn(Post.findById(post._id))).then(post => {\n    const cats = post.categories;\n\n    // Make sure the order of categories is correct\n    cats.map((cat, i) => {\n      // Make sure the parent reference is correct\n      if (i) {\n        cat.parent.should.eql(cats.eq(i - 1)._id);\n      } else {\n        should.not.exist(cat.parent);\n      }\n\n      return cat.name;\n    }).should.eql(['foo', 'bar', 'baz']);\n\n    return Post.removeById(post._id);\n  }));\n\n  it('setTags() - old tags should be removed', () => {\n    let id;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      id = post._id;\n      return post.setTags(['foo', 'bar']);\n    }).then(() => {\n      const post = Post.findById(id);\n      return post.setTags(['bar', 'baz']);\n    }).then(() => {\n      const post = Post.findById(id);\n\n      post.tags.map(tag => tag.name).should.eql(['bar', 'baz']);\n\n      return Post.removeById(id);\n    });\n  });\n\n  it('setTags() - sync problem', () => Post.insert([\n    {source: 'foo.md', slug: 'foo'},\n    {source: 'bar.md', slug: 'bar'}\n  ]).then(posts => BluebirdPromise.all([\n    posts[0].setTags(['foo', 'bar']),\n    posts[1].setTags(['bar', 'baz'])\n  ]).thenReturn(posts)).then(posts => {\n    Tag.map(tag => tag.name).should.have.members(['foo', 'bar', 'baz']);\n\n    return posts;\n  }).map((post: any) => Post.removeById(post._id)));\n\n  it('setTags() - empty tag', () => {\n    let id;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      id = post._id;\n      return post.setTags(['', undefined, null, false, 0, 'normal']);\n    }).then(() => {\n      const post = Post.findById(id);\n\n      post.tags.map(tag => tag.name).should.eql(['false', '0', 'normal']);\n    }).finally(() => Post.removeById(id));\n  });\n\n  it('setCategories() - old categories should be removed', () => {\n    let id;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      id = post._id;\n      return post.setCategories(['foo', 'bar']);\n    }).then(() => {\n      const post = Post.findById(id);\n      return post.setCategories(['foo', 'baz']);\n    }).then(() => {\n      const post = Post.findById(id);\n\n      post.categories.map(cat => cat.name).should.eql(['foo', 'baz']);\n\n      return Post.removeById(id);\n    });\n  });\n\n  it('setCategories() - shared category should be same', () => {\n    let postIdA, postIdB;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      postIdA = post._id;\n      return post.setCategories(['foo', 'bar']);\n    }).then(() => Post.insert({\n      source: 'bar.md',\n      slug: 'bar'\n    }).then(post => {\n      postIdB = post._id;\n      return post.setCategories(['foo', 'bar']);\n    })).then(() => {\n      const postA = Post.findById(postIdA);\n      const postB = Post.findById(postIdB);\n\n      postA.categories.map(cat => cat._id).should.eql(postB.categories.map(cat => cat._id));\n\n      return BluebirdPromise.all([\n        Post.removeById(postIdA),\n        Post.removeById(postIdB)\n      ]);\n    });\n  });\n\n  it('setCategories() - category not shared should be different', () => {\n    let postIdA, postIdB;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      postIdA = post._id;\n      return post.setCategories(['foo', 'bar']);\n    }).then(() => Post.insert({\n      source: 'bar.md',\n      slug: 'bar'\n    }).then(post => {\n      postIdB = post._id;\n      return post.setCategories(['baz', 'bar']);\n    })).then(() => {\n      const postA = Post.findById(postIdA);\n      const postB = Post.findById(postIdB);\n\n      const postCategoriesA = postA.categories.map(cat => cat._id);\n\n      const postCategoriesB = postB.categories.map(cat => cat._id);\n\n      postCategoriesA.forEach(catId => {\n        postCategoriesB.should.not.include(catId);\n      });\n\n      postCategoriesB.forEach(catId => {\n        postCategoriesA.should.not.include(catId);\n      });\n\n      return BluebirdPromise.all([\n        Post.removeById(postIdA),\n        Post.removeById(postIdB)\n      ]);\n    });\n  });\n\n  it('setCategories() - empty category', () => {\n    let id;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      id = post._id;\n      return post.setCategories(['test', null]);\n    }).then(() => {\n      const post = Post.findById(id);\n\n      post.categories.map(cat => cat.name).should.eql(['test']);\n    }).finally(() => Post.removeById(id));\n  });\n\n  it('setCategories() - empty category in middle', () => {\n    let id;\n\n    return Post.insert({\n      source: 'foo.md',\n      slug: 'foo'\n    }).then(post => {\n      id = post._id;\n      return post.setCategories(['foo', null, 'bar']);\n    }).then(() => {\n      const post = Post.findById(id);\n\n      post.categories.map(cat => cat.name).should.eql(['foo', 'bar']);\n    }).finally(() => Post.removeById(id));\n  });\n\n  it('setCategories() - multiple hierarchies', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setCategories([['foo', '', 'bar'], '', 'baz'])\n    .thenReturn(Post.findById(post._id))).then(post => {\n    const cats = post.categories.toArray();\n\n    // There should have been 3 categories set; blanks eliminated\n    cats.should.have.lengthOf(3);\n\n    // Category 1 should be foo, no parent\n    cats[0].name.should.eql('foo');\n    should.not.exist(cats[0].parent);\n\n    // Category 2 should be bar, foo as parent\n    cats[1].name.should.eql('bar');\n    cats[1].parent.should.eql(cats[0]._id);\n\n    // Category 3 should be baz, no parent\n    cats[2].name.should.eql('baz');\n    should.not.exist(cats[2].parent);\n\n    return Post.removeById(post._id);\n  }));\n\n  it('setCategories() - multiple hierarchies (dedupes repeated parent)', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setCategories([['foo', 'bar'], ['foo', 'baz']])\n    .thenReturn(Post.findById(post._id))).then(post => {\n    const cats = post.categories.toArray();\n\n    // There should have been 3 categories set (foo is dupe)\n    cats.should.have.lengthOf(3);\n\n    return Post.removeById(post._id);\n  }));\n\n  it('remove PostTag references when a post is removed', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setTags(['foo', 'bar', 'baz'])\n    .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => {\n    PostTag.find({post_id: post._id}).should.have.lengthOf(0);\n    ReadOnlyPostTag.find({post_id: post._id}).should.have.lengthOf(0);\n    Tag.findOne({name: 'foo'}).posts.should.have.lengthOf(0);\n    Tag.findOne({name: 'bar'}).posts.should.have.lengthOf(0);\n    Tag.findOne({name: 'baz'}).posts.should.have.lengthOf(0);\n  }));\n\n  it('remove PostCategory references when a post is removed', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => post.setCategories(['foo', 'bar', 'baz'])\n    .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => {\n    PostCategory.find({post_id: post._id}).should.have.lengthOf(0);\n    ReadOnlyPostCategory.find({post_id: post._id}).should.have.lengthOf(0);\n    Category.findOne({name: 'foo'}).posts.should.have.lengthOf(0);\n    Category.findOne({name: 'bar'}).posts.should.have.lengthOf(0);\n    Category.findOne({name: 'baz'}).posts.should.have.lengthOf(0);\n  }));\n\n  it('remove related assets when a post is removed', () => Post.insert({\n    source: 'foo.md',\n    slug: 'bar'\n  }).then(post => BluebirdPromise.all([\n    Asset.insert({_id: 'foo', path: 'foo'}),\n    Asset.insert({_id: 'bar', path: 'bar'}),\n    Asset.insert({_id: 'baz', path: 'bar'})\n  ]).thenReturn(post)).then(post => Post.removeById(post._id)).then(post => {\n    Asset.find({post: post._id}).should.have.lengthOf(0);\n  }));\n});\n"
  },
  {
    "path": "test/scripts/models/post_asset.ts",
    "content": "import { join, posix } from 'path';\nimport Hexo from '../../../lib/hexo';\nimport defaults from '../../../lib/hexo/default_config';\n\ndescribe('PostAsset', () => {\n  const hexo = new Hexo();\n  const PostAsset = hexo.model('PostAsset');\n  const Post = hexo.model('Post');\n  let post;\n\n  before(async () => {\n    await hexo.init();\n    post = await Post.insert({\n      source: 'foo.md',\n      slug: 'bar'\n    });\n  });\n\n  beforeEach(() => {\n    hexo.config = Object.assign({}, defaults);\n  });\n\n  it('default values', async () => {\n    const data = await PostAsset.insert({\n      _id: 'foo',\n      slug: 'foo',\n      post: post._id\n    });\n    data.modified.should.be.true;\n    PostAsset.removeById(data._id);\n  });\n\n  it('_id - required', async () => {\n    try {\n      await PostAsset.insert({});\n    } catch (err) {\n      err.message.should.eql('ID is not defined');\n    }\n  });\n\n  it('slug - required', async () => {\n    try {\n      await PostAsset.insert({\n        _id: 'foo'\n      });\n    } catch (err) {\n      err.message.should.eql('`slug` is required!');\n    }\n  });\n\n  it('path - virtual', async () => {\n    const data = await PostAsset.insert({\n      _id: 'source/_posts/test/foo.jpg',\n      slug: 'foo.jpg',\n      post: post._id\n    });\n    data.path.should.eql(posix.join(post.path, data.slug));\n\n    PostAsset.removeById(data._id);\n  });\n\n  it('path - virtual - when permalink is .html', async () => {\n    hexo.config.permalink = ':year/:month/:day/:title.html';\n    const data = await PostAsset.insert({\n      _id: 'source/_posts/test/foo.html',\n      slug: 'foo.htm',\n      post: post._id\n    });\n    data.path.should.eql(posix.join(post.path, data.slug));\n\n    PostAsset.removeById(data._id);\n  });\n\n  it('path - virtual - when permalink is .htm', async () => {\n    hexo.config.permalink = ':year/:month/:day/:title.htm';\n    const data = await PostAsset.insert({\n      _id: 'source/_posts/test/foo.htm',\n      slug: 'foo.htm',\n      post: post._id\n    });\n    data.path.should.eql(posix.join(post.path, data.slug));\n\n    PostAsset.removeById(data._id);\n  });\n\n  it('path - virtual - when permalink contains .htm not in the end', async () => {\n    hexo.config.permalink = ':year/:month/:day/:title/.htm-foo/';\n    const data = await PostAsset.insert({\n      _id: 'source/_posts/test/foo.html',\n      slug: 'foo.html',\n      post: post._id\n    });\n    data.path.should.eql(posix.join(post.path + '.htm-foo/', data.slug));\n\n    PostAsset.removeById(data._id);\n  });\n\n  it('source - virtual', async () => {\n    const data = await PostAsset.insert({\n      _id: 'source/_posts/test/foo.jpg',\n      slug: 'foo.jpg',\n      post: post._id\n    });\n    data.source.should.eql(join(hexo.base_dir, data._id));\n\n    PostAsset.removeById(data._id);\n  });\n});\n"
  },
  {
    "path": "test/scripts/models/tag.ts",
    "content": "import { deepMerge, full_url_for } from 'hexo-util';\nimport Hexo from '../../../lib/hexo';\nimport defaults from '../../../lib/hexo/default_config';\n\ndescribe('Tag', () => {\n  const hexo = new Hexo();\n  const Tag = hexo.model('Tag');\n  const Post = hexo.model('Post');\n  const PostTag = hexo.model('PostTag');\n  const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag;\n\n  before(() => hexo.init());\n\n  beforeEach(() => { hexo.config = deepMerge({}, defaults); });\n\n  it('name - required', async () => {\n    try {\n      await Tag.insert({});\n    } catch (err) {\n      err.message.should.be.eql('`name` is required!');\n    }\n  });\n\n  it('slug - virtual', async () => {\n    const data = await Tag.insert({\n      name: 'foo'\n    });\n    data.slug.should.eql('foo');\n\n    Tag.removeById(data._id);\n  });\n\n  it('slug - tag_map', async () => {\n    hexo.config.tag_map = {\n      test: 'wat'\n    };\n\n    const data = await Tag.insert({\n      name: 'test'\n    });\n    data.slug.should.eql('wat');\n    Tag.removeById(data._id);\n  });\n\n  it('slug - filename_case: 0', async () => {\n    const data = await Tag.insert({\n      name: 'WahAHa'\n    });\n    data.slug.should.eql('WahAHa');\n\n    Tag.removeById(data._id);\n  });\n\n  it('slug - filename_case: 1', async () => {\n    hexo.config.filename_case = 1;\n\n    const data = await Tag.insert({\n      name: 'WahAHa'\n    });\n    data.slug.should.eql('wahaha');\n\n    Tag.removeById(data._id);\n  });\n\n  it('slug - filename_case: 2', async () => {\n    hexo.config.filename_case = 2;\n\n    const data = await Tag.insert({\n      name: 'WahAHa'\n    });\n\n    data.slug.should.eql('WAHAHA');\n\n    Tag.removeById(data._id);\n  });\n\n  it('path - virtual', async () => {\n    const data = await Tag.insert({\n      name: 'foo'\n    });\n\n    data.path.should.eql(hexo.config.tag_dir + '/' + data.slug + '/');\n\n    Tag.removeById(data._id);\n  });\n\n  it('permalink - virtual', async () => {\n    const data = await Tag.insert({\n      name: 'foo'\n    });\n\n    data.permalink.should.eql(hexo.config.url + '/' + data.path);\n\n    Tag.removeById(data._id);\n  });\n\n  it('permalink - trailing_index', async () => {\n    hexo.config.pretty_urls.trailing_index = false;\n    const data = await Tag.insert({\n      name: 'foo'\n    });\n\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\\.html$/, ''));\n\n    Tag.removeById(data._id);\n  });\n\n  it('permalink - trailing_html', async () => {\n    hexo.config.pretty_urls.trailing_html = false;\n    const data = await Tag.insert({\n      name: 'foo'\n    });\n\n    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\\.html$/, ''));\n\n    Tag.removeById(data._id);\n  });\n\n  it('permalink - should be encoded', async () => {\n    hexo.config.url = 'http://fôo.com';\n    const data = await Tag.insert({\n      name: '字'\n    });\n\n    data.permalink.should.eql(full_url_for.call(hexo, data.path));\n\n    Tag.removeById(data._id);\n  });\n\n  it('posts - virtual', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo'},\n      {source: 'bar.md', slug: 'bar'},\n      {source: 'baz.md', slug: 'baz'}\n    ]);\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n\n    const tag = Tag.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    hexo.locals.invalidate();\n    tag.posts.map(mapper).should.eql(posts.map(mapper));\n    tag.should.have.lengthOf(posts.length);\n\n    await tag.remove();\n    await Promise.all(posts.map(post => post.remove()));\n  });\n\n  it('posts - draft', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo', published: true},\n      {source: 'bar.md', slug: 'bar', published: false},\n      {source: 'baz.md', slug: 'baz', published: true}\n    ]);\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n\n    let tag = Tag.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    // draft off\n    hexo.locals.invalidate();\n    tag.posts.eq(0)._id.should.eql(posts[0]._id);\n    tag.posts.eq(1)._id.should.eql(posts[2]._id);\n    tag.should.have.lengthOf(2);\n\n    // draft on\n    hexo.config.render_drafts = true;\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n    tag = Tag.findOne({name: 'foo'});\n    hexo.locals.invalidate();\n    tag.posts.map(mapper).should.eql(posts.map(mapper));\n    tag.should.have.lengthOf(posts.length);\n    hexo.config.render_drafts = false;\n\n    await tag.remove();\n    await Promise.all(posts.map(post => post.remove()));\n  });\n\n  it('posts - future', async () => {\n    const now = Date.now();\n\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo', date: now - 3600},\n      {source: 'bar.md', slug: 'bar', date: now + 3600},\n      {source: 'baz.md', slug: 'baz', date: now}\n    ]);\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n\n    let tag = Tag.findOne({name: 'foo'});\n\n    function mapper(post) {\n      return post._id;\n    }\n\n    // future on\n    hexo.config.future = true;\n    hexo.locals.invalidate();\n    tag.posts.map(mapper).should.eql(posts.map(mapper));\n    tag.should.have.lengthOf(posts.length);\n\n    // future off\n    hexo.config.future = false;\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n    hexo.locals.invalidate();\n    tag = Tag.findOne({name: 'foo'});\n    tag.posts.eq(0)._id.should.eql(posts[0]._id);\n    tag.posts.eq(1)._id.should.eql(posts[2]._id);\n    tag.should.have.lengthOf(2);\n\n    await tag.remove();\n    await Promise.all(posts.map(post => post.remove()));\n  });\n\n  it('check whether a tag exists', async () => {\n    let data = await Tag.insert({\n      name: 'foo'\n    });\n\n    try {\n      data = await Tag.insert({\n        name: 'foo'\n      });\n    } catch (err) {\n      err.message.should.eql('Tag `foo` has already existed!');\n    }\n\n    Tag.removeById(data._id);\n  });\n\n  it('remove PostTag references when a tag is removed', async () => {\n    const posts = await Post.insert([\n      {source: 'foo.md', slug: 'foo'},\n      {source: 'bar.md', slug: 'bar'},\n      {source: 'baz.md', slug: 'baz'}\n    ]);\n    await Promise.all(posts.map(post => post.setTags(['foo'])));\n\n    const tag = Tag.findOne({name: 'foo'});\n    await Tag.removeById(tag._id!);\n\n    PostTag.find({tag_id: tag._id}).should.have.lengthOf(0);\n    ReadOnlyPostTag.find({tag_id: tag._id}).should.have.lengthOf(0);\n\n    await Promise.all(posts.map(post => Post.removeById(post._id)));\n  });\n});\n"
  },
  {
    "path": "test/scripts/processors/asset.ts",
    "content": "import { dirname, join } from 'path';\nimport { mkdirs, rmdir, stat, unlink, writeFile } from 'hexo-fs';\nimport { spy } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport defaults from '../../../lib/hexo/default_config';\nimport assets from '../../../lib/plugins/processor/asset';\nimport chai from 'chai';\nconst should = chai.should();\n\nconst dateFormat = 'YYYY-MM-DD HH:mm:ss';\n\ndescribe('asset', () => {\n  const baseDir = join(__dirname, 'asset_test');\n  const hexo = new Hexo(baseDir);\n  const asset = assets(hexo);\n  const process = asset.process.bind(hexo);\n  const { pattern } = asset;\n  const { source } = hexo;\n  const { File } = source;\n  const Asset = hexo.model('Asset');\n  const Page = hexo.model('Page');\n\n  function newFile(options) {\n    options.source = join(source.base, options.path);\n    options.params = {\n      renderable: options.renderable\n    };\n\n    return new File(options);\n  }\n\n  before(async () => {\n    await mkdirs(baseDir);\n    await hexo.init();\n  });\n\n  beforeEach(() => { hexo.config = Object.assign({}, defaults); });\n\n  after(() => rmdir(baseDir));\n\n  it('pattern', () => {\n    // Renderable files\n    pattern.match('foo.json').should.have.property('renderable', true);\n\n    // Non-renderable files\n    pattern.match('foo.txt').should.have.property('renderable', false);\n\n    // Tmp files\n    should.not.exist(pattern.match('foo.txt~'));\n    should.not.exist(pattern.match('foo.txt%'));\n\n    // Hidden files\n    should.not.exist(pattern.match('_foo.txt'));\n    should.not.exist(pattern.match('test/_foo.txt'));\n    should.not.exist(pattern.match('.foo.txt'));\n    should.not.exist(pattern.match('test/.foo.txt'));\n\n    // Include files\n    hexo.config.include = ['fff/**'];\n    pattern.match('fff/_foo.txt').should.exist;\n    hexo.config.include = [];\n\n    // Exclude files\n    hexo.config.exclude = ['fff/**'];\n    should.not.exist(pattern.match('fff/foo.txt'));\n    hexo.config.exclude = [];\n\n    // Skip render files\n    hexo.config.skip_render = ['fff/**'];\n    pattern.match('fff/foo.json').should.have.property('renderable', false);\n    hexo.config.skip_render = [];\n  });\n\n  it('asset - type: create', async () => {\n    const file = newFile({\n      path: 'foo.jpg',\n      type: 'create',\n      renderable: false\n    });\n\n    await writeFile(file.source, 'foo');\n    await process(file);\n    const id = 'source/' + file.path;\n    const asset = Asset.findById(id);\n\n    asset._id.should.eql(id);\n    asset.path.should.eql(file.path);\n    asset.modified.should.be.true;\n    asset.renderable.should.be.false;\n\n    return Promise.all([\n      asset.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: create (when source path is configured to parent directory)', async () => {\n    const file = newFile({\n      path: '../../source/foo.jpg',\n      type: 'create',\n      renderable: false\n    });\n\n    await writeFile(file.source, 'foo');\n    await process(file);\n    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.\n    const asset = Asset.findById(id);\n    asset._id.should.eql(id);\n    asset.path.should.eql(file.path);\n    asset.modified.should.be.true;\n    asset.renderable.should.be.false;\n\n    asset.remove();\n    await unlink(file.source);\n    rmdir(dirname(file.source));\n  });\n\n  it('asset - type: update', async () => {\n    const file = newFile({\n      path: 'foo.jpg',\n      type: 'update',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    await Promise.all([\n      writeFile(file.source, 'test'),\n      Asset.insert({\n        _id: id,\n        path: file.path,\n        modified: false\n      })\n    ]);\n    await process(file);\n    const asset = Asset.findById(id);\n\n    asset._id.should.eql(id);\n    asset.path.should.eql(file.path);\n    asset.modified.should.be.true;\n    asset.renderable.should.be.false;\n\n    return Promise.all([\n      asset.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: skip', async () => {\n    const file = newFile({\n      path: 'foo.jpg',\n      type: 'skip',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    await Promise.all([\n      writeFile(file.source, 'test'),\n      Asset.insert({\n        _id: id,\n        path: file.path,\n        modified: false\n      })\n    ]);\n    await process(file);\n    const asset = Asset.findById(id);\n    asset.modified.should.be.false;\n    await Promise.all([\n      Asset.removeById(id),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: delete', async () => {\n    const file = newFile({\n      path: 'foo.jpg',\n      type: 'delete',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    await Asset.insert({\n      _id: id,\n      path: file.path\n    });\n    await process(file);\n\n    should.not.exist(Asset.findById(id));\n  });\n\n  it('asset - type: delete - not exist', async () => {\n    const file = newFile({\n      path: 'foo.jpg',\n      type: 'delete',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n    await process(file);\n\n    should.not.exist(Asset.findById(id));\n  });\n\n  it('page - type: create', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: 2006-01-02 15:04:05',\n      'updated: 2014-12-13 01:02:03',\n      '---',\n      'The quick brown fox jumps over the lazy dog'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n\n    const page = Page.findOne({ source: file.path });\n    page.title.should.eql('Hello world');\n    page.date.format(dateFormat).should.eql('2006-01-02 15:04:05');\n    page.updated.format(dateFormat).should.eql('2014-12-13 01:02:03');\n    page._content.should.eql('The quick brown fox jumps over the lazy dog');\n    page.source.should.eql(file.path);\n    page.raw.should.eql(body);\n    page.path.should.eql('hello.html');\n    page.layout.should.eql('page');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - type: create - exist', async () => {\n    const logSpy = spy();\n    hexo.log.warn = logSpy;\n\n    const body = [\n      'title: \"Hello world\"',\n      'date: 2006-01-02 15:04:05',\n      'updated: 2014-12-13 01:02:03',\n      '---',\n      'The quick brown fox jumps over the lazy dog'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    await process(file);\n\n    const page = Page.findOne({ source: file.path });\n    page.title.should.eql('Hello world');\n    page.date.format(dateFormat).should.eql('2006-01-02 15:04:05');\n    page.updated.format(dateFormat).should.eql('2014-12-13 01:02:03');\n    page._content.should.eql('The quick brown fox jumps over the lazy dog');\n    page.source.should.eql(file.path);\n    page.raw.should.eql(body);\n    page.path.should.eql('hello.html');\n    page.layout.should.eql('page');\n\n    logSpy.called.should.be.true;\n    logSpy.args[0][0].should.contains('Trying to \"create\" \\x1B[35mhello.njk\\x1B[39m, but the file already exists!');\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - type: update', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'update',\n      renderable: true\n    });\n\n\n    const doc = await Page.insert({ source: file.path, path: 'hello.html' });\n    await writeFile(file.source, body);\n    const id = doc._id;\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page._id!.should.eql(id);\n    page.title.should.eql('Hello world');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - type: skip', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'skip',\n      renderable: true\n    });\n\n    await Page.insert({\n      source: file.path,\n      path: 'hello.html'\n    });\n    const page = Page.findOne({source: file.path});\n    await process(file);\n    should.exist(page);\n    await Promise.all([\n      page.remove()\n    ]);\n  });\n\n  it('page - type: delete', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'delete',\n      renderable: true\n    });\n\n    await Page.insert({\n      source: file.path,\n      path: 'hello.html'\n    });\n    await process(file);\n    should.not.exist(Page.findOne({ source: file.path }));\n  });\n\n  it('page - type: delete - not exist', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'delete',\n      renderable: true\n    });\n\n    await process(file);\n    should.not.exist(Page.findOne({ source: file.path }));\n  });\n\n  it('page - use the status of the source file if date not set', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, '');\n    await process(file);\n    const stats = await stat(file.source);\n    const page = Page.findOne({source: file.path});\n\n    page.date.toDate().should.eql(stats.ctime);\n    page.updated.toDate().should.eql(stats.mtime);\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - use the date for updated if updated_option = date', async () => {\n    const body = [\n      'date: 2011-4-5 14:19:19',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'date';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const stats = await stat(file.source);\n    const page = Page.findOne({source: file.path});\n\n    page.updated.toDate().should.eql(page.date.toDate());\n    page.updated.toDate().should.not.eql(stats.mtime);\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - use the status if updated_option = mtime', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'mtime';\n\n    await writeFile(file.source, '');\n    await process(file);\n    const stats = await stat(file.source);\n    const page = Page.findOne({source: file.path});\n\n    page.date.toDate().should.eql(stats.ctime);\n    page.updated.toDate().should.eql(stats.mtime);\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - updated shouldn\\'t exists if updated_option = empty', async () => {\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'empty';\n\n    await writeFile(file.source, '');\n    await process(file);\n    const stats = await stat(file.source);\n    const page = Page.findOne({source: file.path});\n\n    page.date.toDate().should.eql(stats.ctime);\n    should.not.exist(page.updated);\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - permalink', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'permalink: foo.html',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.path.should.eql('foo.html');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - permalink (without extension name)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'permalink: foo',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.path.should.eql('foo.html');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - permalink (with trailing slash)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'permalink: foo/',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.path.should.eql('foo/index.html');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - set layout to false if output is not html', async () => {\n    const body = 'foo: 1';\n\n    const file = newFile({\n      path: 'test.yml',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.layout.should.eql('false');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - don\\'t set layout to false if layout is set but output is not html', async () => {\n    const body = [\n      'layout: something',\n      '---',\n      'foo: 1'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'test.yml',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.layout.should.eql('something');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - parse date', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: Apr 24 2014',\n      'updated: May 5 2015',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.date.format(dateFormat).should.eql('2014-04-24 00:00:00');\n    page.updated.format(dateFormat).should.eql('2015-05-05 00:00:00');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - use file stats instead if date is invalid', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: yomama',\n      'updated: isfat',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const stats = await file.stat();\n    const page = Page.findOne({source: file.path});\n\n    page.date.toDate().should.eql(stats.ctime);\n    page.updated.toDate().should.eql(page.date.toDate());\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - don\\'t remove extension name', async () => {\n    const body = '';\n\n    const file = newFile({\n      path: 'test.min.js',\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({ source: file.path });\n    page.path.should.eql('test.min.js');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('page - timezone', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: Apr 24 2014',\n      'updated: May 5 2015',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'hello.njk',\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.timezone = 'UTC';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const page = Page.findOne({source: file.path});\n\n    page.date.utc().format(dateFormat).should.eql('2014-04-24 00:00:00');\n    page.updated.utc().format(dateFormat).should.eql('2015-05-05 00:00:00');\n\n    await Promise.all([\n      page.remove(),\n      unlink(file.source)\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/scripts/processors/common.ts",
    "content": "import moment from 'moment';\nimport { isTmpFile, isHiddenFile, toDate, adjustDateForTimezone, isMatch } from '../../../lib/plugins/processor/common';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('common', () => {\n  it('isTmpFile()', () => {\n    isTmpFile('foo').should.be.false;\n    isTmpFile('foo%').should.be.true;\n    isTmpFile('foo~').should.be.true;\n  });\n\n  it('isHiddenFile()', () => {\n    isHiddenFile('foo').should.be.false;\n    isHiddenFile('_foo').should.be.true;\n    isHiddenFile('foo/_bar').should.be.true;\n    isHiddenFile('.foo').should.be.true;\n    isHiddenFile('foo/.bar').should.be.true;\n  });\n\n  it('toDate()', () => {\n    const m = moment();\n    const d = new Date();\n\n    should.not.exist(toDate());\n    toDate(m)!.should.eql(m);\n    toDate(d)!.should.eql(d);\n    toDate(1e8)!.should.eql(new Date(1e8));\n    toDate('2014-04-25T01:32:21.196Z')!.should.eql(new Date('2014-04-25T01:32:21.196Z'));\n    toDate('Apr 24 2014')!.should.eql(new Date(2014, 3, 24));\n    should.not.exist(toDate('foo'));\n  });\n\n  it('timezone() - date', () => {\n    const d = new Date(Date.UTC(1972, 2, 29, 0, 0, 0));\n    const d_timezone_UTC = adjustDateForTimezone(d, 'UTC').getTime();\n    (adjustDateForTimezone(d, 'Asia/Shanghai').getTime() - d_timezone_UTC).should.eql(-8 * 3600 * 1000);\n    (adjustDateForTimezone(d, 'Asia/Bangkok').getTime() - d_timezone_UTC).should.eql(-7 * 3600 * 1000);\n    (adjustDateForTimezone(d, 'America/Los_Angeles').getTime() - d_timezone_UTC).should.eql(8 * 3600 * 1000);\n  });\n\n  it('timezone() - moment', () => {\n    const d = moment(new Date(Date.UTC(1972, 2, 29, 0, 0, 0)));\n    const d_timezone_UTC = adjustDateForTimezone(d, 'UTC').getTime();\n    (adjustDateForTimezone(d, 'Europe/Moscow').getTime() - d_timezone_UTC).should.eql(-3 * 3600 * 1000);\n  });\n\n  it('isMatch() - string', () => {\n    // String\n    isMatch('foo/test.html', 'foo/*.html').should.be.true;\n    isMatch('foo/test.html', 'bar/*.html').should.be.false;\n\n    // Array\n    isMatch('foo/test.html', []).should.be.false;\n    isMatch('foo/test.html', ['foo/*.html']).should.be.true;\n    isMatch('foo/test.html', ['bar/*.html', 'foo/*.html']).should.be.true;\n    isMatch('foo/test.html', ['bar/*.html', 'baz/*.html']).should.be.false;\n\n    // Undefined\n    isMatch('foo/test.html').should.be.false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/processors/data.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport { join } from 'path';\nimport Hexo from '../../../lib/hexo';\nimport data from '../../../lib/plugins/processor/data';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('data', () => {\n  const baseDir = join(__dirname, 'data_test');\n  const hexo = new Hexo(baseDir);\n  const processor = data(hexo);\n  const process = BluebirdPromise.method(processor.process).bind(hexo);\n  const { source } = hexo;\n  const { File } = source;\n  const Data = hexo.model('Data');\n\n  function newFile(options) {\n    const path = options.path;\n\n    options.params = {\n      path\n    };\n\n    options.path = '_data/' + path;\n    options.source = join(source.base, options.path);\n\n    return new File(options);\n  }\n\n  before(async () => {\n    await mkdirs(baseDir);\n    hexo.init();\n  });\n\n  after(() => rmdir(baseDir));\n\n  it('pattern', () => {\n    const pattern = processor.pattern;\n\n    pattern.match('_data/users.json').should.eql({\n      0: '_data/users.json',\n      1: 'users.json',\n      path: 'users.json'\n    });\n\n    pattern.match('_data/users.yaml').should.eql({\n      0: '_data/users.yaml',\n      1: 'users.yaml',\n      path: 'users.yaml'\n    });\n\n    should.not.exist(pattern.match('users.json'));\n  });\n\n  it('type: create - yaml', async () => {\n    const body = 'foo: bar';\n\n    const file = newFile({\n      path: 'users.yml',\n      type: 'create'\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const data = Data.findById('users');\n\n    data.data.should.eql({foo: 'bar'});\n\n    data.remove();\n    unlink(file.source);\n  });\n\n  it('type: create - json', async () => {\n    const body = '{\"foo\": 1}';\n\n    const file = newFile({\n      path: 'users.json',\n      type: 'create'\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const data = Data.findById('users');\n\n    data.data.should.eql({foo: 1});\n\n    data.remove();\n    unlink(file.source);\n  });\n\n  it('type: create - others', async () => {\n    const file = newFile({\n      path: 'users.txt',\n      type: 'create'\n    });\n\n    await writeFile(file.source, 'text');\n    await process(file);\n    const data = Data.findById('users');\n\n    data.data.should.eql('text');\n\n    data.remove();\n    unlink(file.source);\n  });\n\n  it('type: update', async () => {\n    const body = 'foo: bar';\n\n    const file = newFile({\n      path: 'users.yml',\n      type: 'update'\n    });\n\n    await BluebirdPromise.all([\n      writeFile(file.source, body),\n      Data.insert({\n        _id: 'users',\n        data: {}\n      })\n    ]);\n    await process(file);\n    const data = Data.findById('users');\n\n    data.data.should.eql({foo: 'bar'});\n\n    data.remove();\n    unlink(file.source);\n  });\n\n  it('type: skip', async () => {\n    const file = newFile({\n      path: 'users.yml',\n      type: 'skip'\n    });\n\n    await Data.insert({\n      _id: 'users',\n      data: {foo: 'bar'}\n    });\n    const data = Data.findById('users');\n    await process(file);\n    should.exist(data);\n    data.remove();\n  });\n\n  it('type: delete', async () => {\n    const file = newFile({\n      path: 'users.yml',\n      type: 'delete'\n    });\n\n    await Data.insert({\n      _id: 'users',\n      data: {foo: 'bar'}\n    });\n    await process(file);\n    should.not.exist(Data.findById('users'));\n  });\n\n  it('type: delete - not exist', async () => {\n    const file = newFile({\n      path: 'users.yml',\n      type: 'delete'\n    });\n\n    await process(file);\n    should.not.exist(Data.findById('users'));\n  });\n\n});\n"
  },
  {
    "path": "test/scripts/processors/post.ts",
    "content": "\nimport { join } from 'path';\nimport { exists, mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport defaultConfig from '../../../lib/hexo/default_config';\nimport Hexo from '../../../lib/hexo';\nimport posts from '../../../lib/plugins/processor/post';\nimport chai from 'chai';\nconst should = chai.should();\ntype PostParams = Parameters<ReturnType<typeof posts>['process']>\ntype PostReturn = ReturnType<ReturnType<typeof posts>['process']>\n\nconst dateFormat = 'YYYY-MM-DD HH:mm:ss';\n\ndescribe('post', () => {\n  const baseDir = join(__dirname, 'post_test');\n  const hexo = new Hexo(baseDir);\n  const post = posts(hexo);\n  const process: (...args: PostParams) => BluebirdPromise<PostReturn> = BluebirdPromise.method(post.process.bind(hexo));\n  const { pattern } = post;\n  const { source } = hexo;\n  const { File } = source;\n  const PostAsset = hexo.model('PostAsset');\n  const Post = hexo.model('Post');\n\n  function newFile(options) {\n    const { path } = options;\n\n    options.path = (options.published ? '_posts' : '_drafts') + '/' + path;\n    options.source = join(source.base, options.path);\n\n    options.params = {\n      published: options.published,\n      path,\n      renderable: options.renderable\n    };\n\n    return new File(options);\n  }\n\n  before(async () => {\n    await mkdirs(baseDir);\n    hexo.init();\n  });\n\n  beforeEach(() => { hexo.config = Object.assign({}, defaultConfig); });\n\n  after(() => rmdir(baseDir));\n\n  it('pattern', () => {\n    // Renderable files\n    pattern.match('_posts/foo.html').should.eql({\n      published: true,\n      path: 'foo.html',\n      renderable: true\n    });\n\n    pattern.match('_drafts/bar.html').should.eql({\n      published: false,\n      path: 'bar.html',\n      renderable: true\n    });\n\n    // Non-renderable files\n    pattern.match('_posts/foo.txt').should.eql({\n      published: true,\n      path: 'foo.txt',\n      renderable: false\n    });\n\n    pattern.match('_drafts/bar.txt').should.eql({\n      published: false,\n      path: 'bar.txt',\n      renderable: false\n    });\n\n    // Tmp files\n    should.not.exist(pattern.match('_posts/foo.html~'));\n    should.not.exist(pattern.match('_posts/foo.html%'));\n\n    // Hidden files\n    should.not.exist(pattern.match('_posts/_foo.html'));\n    should.not.exist(pattern.match('_posts/foo/_bar.html'));\n    should.not.exist(pattern.match('_posts/.foo.html'));\n    should.not.exist(pattern.match('_posts/foo/.bar.html'));\n\n    // Outside \"_posts\" and \"_drafts\" folder\n    should.not.exist(pattern.match('_foo/bar.html'));\n    should.not.exist(pattern.match('baz.html'));\n\n    // Skip render files\n    hexo.config.skip_render = ['_posts/foo/**'];\n    pattern.match('_posts/foo/bar.html').should.have.property('renderable', false);\n    hexo.config.skip_render = [];\n\n    // Skip render in the subdir assets if post_asset_folder is enabled\n    hexo.config.post_asset_folder = true;\n    pattern.match('_posts/foo/subdir/bar.html').should.have.property('renderable', false);\n    pattern.match('_posts/foo/subdir/bar.css').should.have.property('renderable', false);\n    pattern.match('_posts/foo/subdir/bar.js').should.have.property('renderable', false);\n    hexo.config.post_asset_folder = false;\n\n    // Render in the subdir assets if post_asset_folder is disabled\n    pattern.match('_posts/foo/subdir/bar.html').should.have.property('renderable', true);\n    pattern.match('_posts/foo/subdir/bar.css').should.have.property('renderable', true);\n    pattern.match('_posts/foo/subdir/bar.js').should.have.property('renderable', true);\n  });\n\n  it('asset - post_asset_folder disabled', async () => {\n    hexo.config.post_asset_folder = false;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'create',\n      renderable: false\n    });\n\n    await process(file);\n    const id = 'source/' + file.path;\n    should.not.exist(PostAsset.findById(id));\n  });\n\n  it('asset - type: create', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'create',\n      renderable: false\n    });\n\n\n    const doc = await Post.insert({\n      source: '_posts/foo.html',\n      slug: 'foo'\n    });\n    await writeFile(file.source, 'test');\n    const postId = doc._id;\n    await process(file);\n\n    const id = 'source/' + file.path;\n    const asset = PostAsset.findById(id);\n\n    asset._id.should.eql(id);\n    asset.post.should.eql(postId);\n    asset.modified.should.be.true;\n    asset.renderable.should.be.false;\n\n    await BluebirdPromise.all([\n      Post.removeById(postId),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: update', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'update',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    const post = await Post.insert({\n      source: '_posts/foo.html',\n      slug: 'foo'\n    });\n    await writeFile(file.source, 'test');\n    const postId = post._id;\n\n    await PostAsset.insert({\n      _id: id,\n      slug: file.path,\n      modified: false,\n      post: postId\n    });\n    await process(file);\n    const asset = PostAsset.findById(id);\n    asset.modified.should.be.true;\n\n    await BluebirdPromise.all([\n      Post.removeById(postId),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: skip', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'skip',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    const post = await Post.insert({\n      source: '_posts/foo.html',\n      slug: 'foo'\n    });\n    await writeFile(file.source, 'test');\n    const postId = post._id;\n\n    await PostAsset.insert({\n      _id: id,\n      slug: file.path,\n      modified: false,\n      post: postId\n    });\n    await process(file);\n    const asset = PostAsset.findById(id);\n    asset.modified.should.be.false;\n\n    await BluebirdPromise.all([\n      Post.removeById(postId),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - type: delete', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'delete',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    const post = await Post.insert({\n      source: '_posts/foo.html',\n      slug: 'foo'\n    });\n    const postId = post._id;\n\n    await PostAsset.insert({\n      _id: id,\n      slug: file.path,\n      modified: false,\n      post: postId\n    });\n    await process(file);\n    should.not.exist(PostAsset.findById(id));\n\n    Post.removeById(postId);\n  });\n\n  it('asset - type: delete - not exist', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'delete',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    const post = await Post.insert({\n      source: '_posts/foo.html',\n      slug: 'foo'\n    });\n    const postId = post._id;\n\n    await process(file);\n    should.not.exist(PostAsset.findById(id));\n\n    Post.removeById(postId);\n  });\n\n\n  it('asset - skip if can\\'t find a matching post', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'create',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    await writeFile(file.source, 'test');\n    await process(file);\n    should.not.exist(PostAsset.findById(id));\n\n    unlink(file.source);\n  });\n\n  it('asset - the related post has been deleted', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo/bar.jpg',\n      published: true,\n      type: 'update',\n      renderable: false\n    });\n\n    const id = 'source/' + file.path;\n\n    await BluebirdPromise.all([\n      writeFile(file.source, 'test'),\n      PostAsset.insert({\n        _id: id,\n        slug: file.path\n      })\n    ]);\n    await process(file);\n    should.not.exist(PostAsset.findById(id));\n\n    unlink(file.source);\n  });\n\n  it('post - type: create', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: 2006-01-02 15:04:05',\n      'updated: 2014-12-13 01:02:03',\n      '---',\n      'The quick brown fox jumps over the lazy dog'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.title.should.eql('Hello world');\n    post.date.format(dateFormat).should.eql('2006-01-02 15:04:05');\n    post.updated.format(dateFormat).should.eql('2014-12-13 01:02:03');\n    post._content.should.eql('The quick brown fox jumps over the lazy dog');\n    post.source.should.eql(file.path);\n    post.raw.should.eql(body);\n    post.slug.should.eql('foo');\n    post.published.should.be.true;\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - type: create - post_asset_folder enabled without asset', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const fooPath = join(hexo.source_dir, '_posts', 'foo');\n    if (await exists(fooPath)) {\n      await rmdir(fooPath);\n    }\n\n    const body = [\n      'title: \"Hello world\"',\n      'date: 2006-01-02 15:04:05',\n      'updated: 2014-12-13 01:02:03',\n      '---',\n      'The quick brown fox jumps over the lazy dog'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.title.should.eql('Hello world');\n    post.date.format(dateFormat).should.eql('2006-01-02 15:04:05');\n    post.updated.format(dateFormat).should.eql('2014-12-13 01:02:03');\n    post._content.should.eql('The quick brown fox jumps over the lazy dog');\n    post.source.should.eql(file.path);\n    post.raw.should.eql(body);\n    post.slug.should.eql('foo');\n    post.published.should.be.true;\n\n    hexo.config.post_asset_folder = false;\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - type: update', async () => {\n    const body = [\n      'title: \"New world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'update',\n      renderable: true\n    });\n\n\n    const doc = await Post.insert({ source: file.path, slug: 'foo' });\n    await writeFile(file.source, body);\n    const id = doc._id;\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post._id!.should.eql(id);\n    post.title.should.eql('New world');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - type: skip', async () => {\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'skip',\n      renderable: true\n    });\n\n    await Post.insert({\n      source: file.path,\n      slug: 'foo'\n    });\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n    should.exist(post);\n    post.remove();\n  });\n\n  it('post - type: delete - not exist', async () => {\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'delete',\n      renderable: true\n    });\n\n    await process(file);\n    should.not.exist(Post.findOne({ source: file.path }));\n  });\n\n  it('post - type: delete', async () => {\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'delete',\n      renderable: true\n    });\n\n    await Post.insert({\n      source: file.path,\n      slug: 'foo'\n    });\n    await process(file);\n    should.not.exist(Post.findOne({ source: file.path }));\n  });\n\n  it('post - type: delete - not exist', async () => {\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'delete',\n      renderable: true\n    });\n\n    await process(file);\n    should.not.exist(Post.findOne({ source: file.path }));\n  });\n\n  it('post - parse file name', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: '2006/01/02/foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.new_post_name = ':year/:month/:day/:title';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.slug.should.eql('foo');\n    post.date.format('YYYY-MM-DD').should.eql('2006-01-02');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - parse unusual file name', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: '20060102.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.new_post_name = ':year:month:day';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.slug.should.eql('20060102');\n    post.date.format('YYYY-MM-DD').should.eql('2006-01-02');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - extra data in file name', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'zh/foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.new_post_name = ':lang/:title';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.lang.should.eql('zh');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - file name does not match to the config', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.new_post_name = ':year/:month/:day/:title';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.slug.should.eql('foo');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - published', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'published: false',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'zh/foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.published.should.be.false;\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - always set published: false for drafts', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'published: true',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: false,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.published.should.be.false;\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - use the status of the source file if date not set', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    const stats = await file.stat();\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.date.toDate().setMilliseconds(0).should.eql(stats.birthtime.setMilliseconds(0));\n    post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0));\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - use the date for updated if updated_option = date', async () => {\n    const body = [\n      'date: 2011-4-5 14:19:19',\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'date';\n\n    await writeFile(file.source, body);\n    const stats = await file.stat();\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.updated.toDate().setMilliseconds(0).should.eql(post.date.toDate().setMilliseconds(0));\n    post.updated.toDate().setMilliseconds(0).should.not.eql(stats.mtime.setMilliseconds(0));\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - use the status of the source file if updated_option = mtime', async () => {\n    const body = [\n      'date: 2011-4-5 14:19:19',\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'mtime';\n\n    await writeFile(file.source, body);\n    const stats = await file.stat();\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0));\n    post.updated.toDate().setMilliseconds(0).should.not.eql(post.date.toDate().setMilliseconds(0));\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - updated shouldn\\'t exists if updated_option = empty', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.updated_option = 'empty';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    should.not.exist(post.updated);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - photo is an alias for photos', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'photo:',\n      '- https://hexo.io/foo.jpg',\n      '- https://hexo.io/bar.png',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.photos.should.eql([\n      'https://hexo.io/foo.jpg',\n      'https://hexo.io/bar.png'\n    ]);\n\n    should.not.exist(post.photo);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - photos (not array)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'photos: https://hexo.io/foo.jpg',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.photos.should.eql([\n      'https://hexo.io/foo.jpg'\n    ]);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - without title', async () => {\n    const body = '';\n\n    const file = newFile({\n      path: 'foo.md',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.title.should.eql('');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  // use `slug` as `title` of post when `title` is not specified.\n  // https://github.com/hexojs/hexo/issues/5372\n  it('post - without title - use filename', async () => {\n    hexo.config.use_slug_as_post_title = true;\n\n    const body = '';\n\n    const file = newFile({\n      path: 'bar.md',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.title.should.eql('bar');\n\n    return Promise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - category is an alias for categories', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'category:',\n      '- foo',\n      '- bar',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    should.not.exist(post.category);\n    post.categories.map(item => item.name).should.eql(['foo', 'bar']);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - categories (not array)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'categories: foo',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.categories.map(item => item.name).should.eql(['foo']);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - categories (multiple hierarchies)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'categories:',\n      '- foo',\n      '- [bar, baz]',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.categories.map(item => item.name).should.eql(['foo', 'bar', 'baz']);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - tag is an alias for tags', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'tag:',\n      '- foo',\n      '- bar',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    should.not.exist(post.tag);\n    post.tags.map(item => item.name).should.have.members(['foo', 'bar']);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - tags (not array)', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'tags: foo',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.tags.map(item => item.name).should.eql(['foo']);\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - post_asset_folder enabled', async () => {\n    hexo.config.post_asset_folder = true;\n    hexo.config.exclude = ['**.png'];\n    hexo.config.include = ['**/_fizz.*'];\n\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetFiles = [\n      'bar.jpg',\n      'baz.png',\n      '_fizz.jpg',\n      '_buzz.jpg'\n    ].map(filename => {\n      const id = `source/_posts/foo/${filename}`;\n      const path = join(hexo.base_dir, id);\n      const contents = filename.replace(/\\.\\w+$/, '');\n      return {\n        id,\n        path,\n        contents\n      };\n    });\n\n    await BluebirdPromise.all([\n      writeFile(file.source, body),\n      ...assetFiles.map(obj => writeFile(obj.path, obj.contents))\n    ]);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n    const assets = assetFiles.map(obj => PostAsset.findById(obj.id));\n\n    [assets[0]].should.not.eql([undefined]);\n    assets[0]._id.should.eql(assetFiles[0].id);\n    assets[0].post.should.eql(post._id);\n    assets[0].modified.should.be.true;\n\n    [assets[1]].should.eql([undefined]);\n\n    [assets[2]].should.not.eql([undefined]);\n    assets[2]._id.should.eql(assetFiles[2].id);\n    assets[2].post.should.eql(post._id);\n    assets[2].modified.should.be.true;\n\n    [assets[3]].should.eql([undefined]);\n\n    post.remove();\n\n    await BluebirdPromise.all([\n      unlink(file.source),\n      ...assetFiles.map(obj => unlink(obj.path))\n    ]);\n  });\n\n  it('post - post_asset_folder enabled with unpublished posts', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const body = [\n      'title: \"Hello world\"',\n      'published: false',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetId = 'source/_posts/foo/bar.jpg';\n    const assetPath = join(hexo.base_dir, assetId);\n\n    await BluebirdPromise.all([\n      writeFile(file.source, body),\n      writeFile(assetPath, '')\n    ]);\n\n    // drafts disabled - no draft assets should be generated\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.published.should.be.false;\n    should.not.exist(PostAsset.findById(assetId));\n\n    // drafts enabled - all assets should be generated\n    hexo.config.render_drafts = true;\n    await process(file);\n\n    should.exist(PostAsset.findById(assetId));\n\n    hexo.config.render_drafts = false;\n\n    await BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source),\n      unlink(assetPath)\n    ]);\n  });\n\n  it('asset - post_asset_folder enabled with hot processing', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const [post1, post2] = await Promise.all([\n      Post.insert({ source: '_posts/foo.html', slug: 'foo' }),\n      Post.insert({ source: '_posts/bar.html', slug: 'bar' })\n    ]);\n\n    const firstAsset = newFile({\n      path: 'bar/image1.jpg',\n      published: true,\n      type: 'create',\n      renderable: false\n    });\n\n    await writeFile(firstAsset.source, 'test1');\n    // cold processing\n    await process(firstAsset);\n\n    const firstAssetId = 'source/_posts/bar/image1.jpg';\n    const firstAssetRecord = PostAsset.findById(firstAssetId);\n    firstAssetRecord._id.should.eql(firstAssetId);\n    firstAssetRecord.post.should.eql(post2._id);\n    firstAssetRecord.slug.should.eql('image1.jpg');\n\n    const secondAsset = newFile({\n      path: 'bar/image2.jpg',\n      published: true,\n      type: 'create',\n      renderable: false\n    });\n\n    await writeFile(secondAsset.source, 'test2');\n    // hot processing\n    await process(secondAsset);\n\n    const secondAssetId = 'source/_posts/bar/image2.jpg';\n    const secondAssetRecord = PostAsset.findById(secondAssetId);\n\n    secondAssetRecord._id.should.eql(secondAssetId);\n    secondAssetRecord.post.should.eql(post2._id);\n    secondAssetRecord.slug.should.eql('image2.jpg');\n    secondAssetRecord.modified.should.be.true;\n\n    hexo.config.post_asset_folder = false;\n\n    await BluebirdPromise.all([\n      Post.removeById(post1._id),\n      Post.removeById(post2._id),\n      unlink(firstAsset.source),\n      unlink(secondAsset.source)\n    ]);\n  });\n\n  it('post - delete existing draft assets if draft posts are hidden', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const body = [\n      'title: \"Hello world\"',\n      'published: false',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetId = 'source/_posts/foo/bar.jpg';\n    const assetPath = join(hexo.base_dir, assetId);\n\n    await BluebirdPromise.all([\n      writeFile(file.source, body),\n      writeFile(assetPath, '')\n    ]);\n\n    // drafts disabled - no draft assets should be generated\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n    await PostAsset.insert({\n      _id: 'source/_posts/foo/bar.jpg',\n      slug: 'bar.jpg',\n      post: post._id\n    });\n    await process(file);\n\n    post.published.should.be.false;\n    should.not.exist(PostAsset.findById(assetId));\n\n    await BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source),\n      unlink(assetPath)\n    ]);\n  });\n\n  it('post - post_asset_folder disabled', async () => {\n    hexo.config.post_asset_folder = false;\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetId = 'source/_posts/foo/bar.jpg';\n    const assetPath = join(hexo.base_dir, assetId);\n\n    await BluebirdPromise.all([\n      writeFile(file.source, ''),\n      writeFile(assetPath, '')\n    ]);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n    should.not.exist(PostAsset.findById(assetId));\n\n    post.remove();\n    await BluebirdPromise.all([\n      unlink(file.source),\n      unlink(assetPath)\n    ]);\n  });\n\n  it('post - parse date', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: Apr 24 2014',\n      'updated: May 5 2015',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.date.format(dateFormat).should.eql('2014-04-24 00:00:00');\n    post.updated.format(dateFormat).should.eql('2015-05-05 00:00:00');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - use file stats instead if date is invalid', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: yomama',\n      'updated: isfat',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    const stats = await file.stat();\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.date.toDate().setMilliseconds(0).should.eql(stats.birthtime.setMilliseconds(0));\n    post.updated.toDate().setMilliseconds(0).should.eql(stats.mtime.setMilliseconds(0));\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('post - timezone', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'date: 2014-04-24',\n      'updated: 2015-05-05',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.timezone = 'UTC';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.date.utc().format(dateFormat).should.eql('2014-04-24 00:00:00');\n    post.updated.utc().format(dateFormat).should.eql('2015-05-05 00:00:00');\n\n    post.remove();\n    return unlink(file.source);\n  });\n\n  it('post - new_post_name timezone', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: '2006/01/02/foo.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    hexo.config.new_post_name = ':year/:month/:day/:title';\n    hexo.config.timezone = 'UTC';\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.date.utc().format(dateFormat).should.eql('2006-01-02 00:00:00');\n\n    post.remove();\n\n    unlink(file.source);\n  });\n\n  it('post - permalink', async () => {\n    const body = [\n      'title: \"Hello world\"',\n      'permalink: foooo',\n      '---'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'test.html',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const post = Post.findOne({ source: file.path });\n\n    post.__permalink.should.eql('foooo');\n\n    return BluebirdPromise.all([\n      post.remove(),\n      unlink(file.source)\n    ]);\n  });\n\n  it('asset - post - common render', async () => {\n    hexo.config.post_asset_folder = true;\n\n    const file = newFile({\n      path: 'foo.md',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetFile = newFile({\n      path: 'foo/test.yml',\n      published: true,\n      type: 'create'\n    });\n\n    await BluebirdPromise.all([\n      writeFile(file.source, 'test'),\n      writeFile(assetFile.source, 'test')\n    ]);\n    await process(file);\n    const id = 'source/' + assetFile.path;\n    const post = Post.findOne({ source: file.path });\n    PostAsset.findById(id).renderable.should.be.true;\n\n    hexo.config.post_asset_folder = false;\n\n    return BluebirdPromise.all([\n      unlink(file.source),\n      unlink(assetFile.source),\n      post.remove(),\n      PostAsset.removeById(id)\n    ]);\n  });\n\n  it('asset - post - skip render', async () => {\n    hexo.config.post_asset_folder = true;\n    hexo.config.skip_render = '**.yml' as any;\n\n    const file = newFile({\n      path: 'foo.md',\n      published: true,\n      type: 'create',\n      renderable: true\n    });\n\n    const assetFile = newFile({\n      path: 'foo/test.yml',\n      published: true,\n      type: 'create'\n    });\n\n    await BluebirdPromise.all([\n      writeFile(file.source, 'test'),\n      writeFile(assetFile.source, 'test')\n    ]);\n    await process(file);\n    const id = 'source/' + assetFile.path;\n    const post = Post.findOne({ source: file.path });\n    PostAsset.findById(id).renderable.should.be.false;\n\n    hexo.config.post_asset_folder = false;\n    hexo.config.skip_render = '' as any;\n\n    return BluebirdPromise.all([\n      unlink(file.source),\n      unlink(assetFile.source),\n      post.remove(),\n      PostAsset.removeById(id)\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/scripts/renderers/json.ts",
    "content": "import r from '../../../lib/plugins/renderer/json';\n\ndescribe('json', () => {\n  it('normal', () => {\n    const data = {\n      foo: 1,\n      bar: {\n        baz: 2\n      }\n    };\n\n    r({text: JSON.stringify(data)}).should.eql(data);\n  });\n});\n"
  },
  {
    "path": "test/scripts/renderers/nunjucks.ts",
    "content": "import r from '../../../lib/plugins/renderer/nunjucks';\nimport { dirname, join } from 'path';\nimport chai from 'chai';\nconst _should = chai.should();\n\n\ndescribe('nunjucks', () => {\n  const fixturePath = join(dirname(dirname(__dirname)), 'fixtures', 'hello.njk');\n\n  it('render from string', () => {\n    const body = [\n      'Hello {{ name }}!'\n    ].join('\\n');\n\n    r({ text: body }, {\n      name: 'world'\n    }).should.eql('Hello world!');\n  });\n\n  it('render from path', () => {\n    r({ path: fixturePath }, {\n      name: 'world'\n    }).should.matches(/^Hello world!\\s*$/);\n  });\n\n  it('compile from text', () => {\n    const body = [\n      'Hello {{ name }}!'\n    ].join('\\n');\n\n    const render = r.compile({\n      text: body\n    });\n\n    render({\n      name: 'world'\n    }).should.eql('Hello world!');\n  });\n\n  it('compile from an .njk file', () => {\n    const render = r.compile({\n      path: fixturePath\n    });\n\n    render({\n      name: 'world'\n    }).should.eql('Hello world!\\n');\n  });\n\n  describe('nunjucks filters', () => {\n    const forLoop = [\n      '{% for x in arr | toarray %}',\n      '{{ x }}',\n      '{% endfor %}'\n    ].join('');\n\n    it('toarray can iterate on Warehouse collections', () => {\n      const data = {\n        arr: {\n          toArray() {\n            return [1, 2, 3];\n          }\n        }\n      };\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray can iterate on plain array', () => {\n      const data = {\n        arr: [1, 2, 3]\n      };\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray can iterate on string', () => {\n      const data = {\n        arr: '123'\n      };\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    // https://github.com/lodash/lodash/blob/master/test/toarray.test.js\n    it('toarray can iterate on objects', () => {\n      const data = {\n        arr: { a: '1', b: '2', c: '3' }\n      };\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray can iterate on object string', () => {\n      const data = {\n        arr: Object('123')\n      };\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray can iterate on Map', () => {\n      const data = {\n        arr: new Map()\n      };\n\n      data.arr.set('a', 1);\n      data.arr.set('b', 2);\n      data.arr.set('c', 3);\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray can iterate on Set', () => {\n      const data = {\n        arr: new Set()\n      };\n\n      data.arr.add(1);\n      data.arr.add(2);\n      data.arr.add(3);\n\n      r({ text: forLoop }, data).should.eql('123');\n    });\n\n    it('toarray other case', () => {\n      const data = {\n        arr: 1\n      };\n\n      r({ text: forLoop }, data).should.eql('');\n    });\n\n    it('safedump undefined', () => {\n      const text = [\n        '{{ items | safedump }}'\n      ].join('\\n');\n\n      r({ text }).should.eql('\"\"');\n    });\n\n    it('safedump null', () => {\n      const text = [\n        '{% set items = null %}',\n        '{{ items | safedump }}'\n      ].join('\\n');\n\n      r({ text }).should.eql('\\n\"\"');\n    });\n\n    // Adapt from nunjucks test cases\n    // https://github.com/mozilla/nunjucks/blob/9a0ce364effd28fcdb3ab922fcffa9343b7b3630/tests/filters.js#L98\n    it('safedump default', () => {\n      const text = [\n        '{% set items = [\"a\", 1, { b : true}] %}',\n        '{{ items | safedump }}'\n      ].join('\\n');\n\n      r({ text }).should.eql('\\n[\"a\",1,{\"b\":true}]');\n    });\n\n    it('safedump spacer - 2', () => {\n      const text = [\n        '{% set items = [\"a\", 1, { b : true}] %}',\n        '{{ items | safedump(2) }}'\n      ].join('\\n');\n\n      r({ text }).should.eql([\n        '',\n        '[',\n        '  \"a\",',\n        '  1,',\n        '  {',\n        '    \"b\": true',\n        '  }',\n        ']'\n      ].join('\\n'));\n    });\n\n    it('safedump spacer - 2', () => {\n      const text = [\n        '{% set items = [\"a\", 1, { b : true}] %}',\n        '{{ items | safedump(2) }}'\n      ].join('\\n');\n\n      r({ text }).should.eql([\n        '',\n        '[',\n        '  \"a\",',\n        '  1,',\n        '  {',\n        '    \"b\": true',\n        '  }',\n        ']'\n      ].join('\\n'));\n    });\n\n    it('safedump spacer - 4', () => {\n      const text = [\n        '{% set items = [\"a\", 1, { b : true}] %}',\n        '{{ items | safedump(4) }}'\n      ].join('\\n');\n\n      r({ text }).should.eql([\n        '',\n        '[',\n        '    \"a\",',\n        '    1,',\n        '    {',\n        '        \"b\": true',\n        '    }',\n        ']'\n      ].join('\\n'));\n    });\n\n    it('safedump spacer - \\\\t', () => {\n      const text = [\n        '{% set items = [\"a\", 1, { b : true}] %}',\n        '{{ items | safedump(\\'\\t\\') }}'\n      ].join('\\n');\n\n      r({ text }).should.eql([\n        '',\n        '[',\n        '\\t\"a\",',\n        '\\t1,',\n        '\\t{',\n        '\\t\\t\"b\": true',\n        '\\t}',\n        ']'\n      ].join('\\n'));\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/renderers/plain.ts",
    "content": "import r from '../../../lib/plugins/renderer/plain';\n\ndescribe('plain', () => {\n  it('normal', () => {\n    r({text: '123'}).should.eql('123');\n  });\n});\n"
  },
  {
    "path": "test/scripts/renderers/yaml.ts",
    "content": "import r from '../../../lib/plugins/renderer/yaml';\n\ndescribe('yaml', () => {\n  it('normal', () => {\n    r({text: 'foo: 1'}).should.eql({foo: 1});\n  });\n\n  it('escape', () => {\n    const body = [\n      'foo: 1',\n      'bar:',\n      '\\tbaz: 3'\n    ].join('\\n');\n\n    r({text: body}).should.eql({\n      foo: 1,\n      bar: {\n        baz: 3\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/asset_img.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport tagAssetImg from '../../../lib/plugins/tag/asset_img';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('asset_img', () => {\n  const hexo = new Hexo(__dirname);\n  const assetImgTag = tagAssetImg(hexo);\n  const Post = hexo.model('Post');\n  const PostAsset = hexo.model('PostAsset');\n  let post;\n\n  hexo.config.permalink = ':title/';\n\n  function assetImg(args) {\n    return assetImgTag.call(post, args.split(' '));\n  }\n\n  before(() => hexo.init().then(() => Post.insert({\n    source: 'foo.md',\n    slug: 'foo'\n  })).then(post_ => {\n    post = post_;\n\n    return BluebirdPromise.all([\n      PostAsset.insert({\n        _id: 'bar',\n        slug: 'bar',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'bár',\n        slug: 'bár',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'spaced asset',\n        slug: 'spaced asset',\n        post: post._id\n      })\n    ]);\n  }));\n\n  it('default', () => {\n    assetImg('bar').should.eql('<img src=\"/foo/bar\" class=\"\">');\n  });\n\n  it('should encode path', () => {\n    assetImg('bár').should.eql('<img src=\"/foo/b%C3%A1r\" class=\"\">');\n  });\n\n  it('default', () => {\n    assetImg('bar \"a title\"').should.eql('<img src=\"/foo/bar\" class=\"\" title=\"a title\">');\n  });\n\n  it('with space', () => {\n    // {% asset_img \"spaced asset\" \"spaced title\" %}\n    assetImgTag.call(post, ['spaced asset', 'spaced title'])\n      .should.eql('<img src=\"/foo/spaced%20asset\" class=\"\" title=\"spaced title\">');\n  });\n\n  it('with alt and title', () => {\n    assetImgTag.call(post, ['bar', '\"a title\"', '\"an alt\"'])\n      .should.eql('<img src=\"/foo/bar\" class=\"\" title=\"a title\" alt=\"an alt\">');\n  });\n\n  it('with width height alt and title', () => {\n    assetImgTag.call(post, ['bar', '100', '200', '\"a title\"', '\"an alt\"'])\n      .should.eql('<img src=\"/foo/bar\" class=\"\" width=\"100\" height=\"200\" title=\"a title\" alt=\"an alt\">');\n  });\n\n  it('no slug', () => {\n    should.not.exist(assetImg(''));\n  });\n\n  it('asset not found', () => {\n    should.not.exist(assetImg('boo'));\n  });\n\n  it('with root path', () => {\n    hexo.config.root = '/root/';\n    assetImg('bar').should.eql('<img src=\"/root/foo/bar\" class=\"\">');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/asset_link.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport tagAssetLink from '../../../lib/plugins/tag/asset_link';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('asset_link', () => {\n  const hexo = new Hexo(__dirname);\n  const assetLinkTag = tagAssetLink(hexo);\n  const Post = hexo.model('Post');\n  const PostAsset = hexo.model('PostAsset');\n  let post;\n\n  hexo.config.permalink = ':title/';\n\n  function assetLink(args) {\n    return assetLinkTag.call(post, args.split(' '));\n  }\n\n  before(() => hexo.init().then(() => Post.insert({\n    source: 'foo.md',\n    slug: 'foo'\n  })).then(post_ => {\n    post = post_;\n\n    return BluebirdPromise.all([\n      PostAsset.insert({\n        _id: 'bar',\n        slug: 'bar',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'bár',\n        slug: 'bár',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'spaced asset',\n        slug: 'spaced asset',\n        post: post._id\n      })\n    ]);\n  }));\n\n  it('default', () => {\n    assetLink('bar').should.eql('<a href=\"/foo/bar\" title=\"bar\">bar</a>');\n  });\n\n  it('should encode path', () => {\n    assetLink('bár').should.eql('<a href=\"/foo/b%C3%A1r\" title=\"bár\">bár</a>');\n  });\n\n  it('title', () => {\n    assetLink('bar Hello world').should.eql('<a href=\"/foo/bar\" title=\"Hello world\">Hello world</a>');\n  });\n\n  it('should escape tag in title by default', () => {\n    assetLink('bar \"Hello\" <world>').should.eql('<a href=\"/foo/bar\" title=\"&quot;Hello&quot; &lt;world&gt;\">&quot;Hello&quot; &lt;world&gt;</a>');\n  });\n\n  it('should escape tag in title', () => {\n    assetLink('bar \"Hello\" <world> true').should.eql('<a href=\"/foo/bar\" title=\"&quot;Hello&quot; &lt;world&gt;\">&quot;Hello&quot; &lt;world&gt;</a>');\n  });\n\n  it('should not escape tag in title', () => {\n    assetLink('bar \"Hello\" <b>world</b> false').should.eql('<a href=\"/foo/bar\" title=\"&quot;Hello&quot; &lt;b&gt;world&lt;&#x2F;b&gt;\">\"Hello\" <b>world</b></a>');\n  });\n\n  it('with space', () => {\n    // {% asset_link \"spaced asset\" \"spaced title\" %}\n    assetLinkTag.call(post, ['spaced asset', 'spaced title'])\n      .should.eql('<a href=\"/foo/spaced%20asset\" title=\"spaced title\">spaced title</a>');\n  });\n\n  it('no slug', () => {\n    should.not.exist(assetLink(''));\n  });\n\n  it('asset not found', () => {\n    should.not.exist(assetLink('boo'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/asset_path.ts",
    "content": "import BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport tagAssetPath from '../../../lib/plugins/tag/asset_path';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('asset_path', () => {\n  const hexo = new Hexo(__dirname);\n  const assetPathTag = tagAssetPath(hexo);\n  const Post = hexo.model('Post');\n  const PostAsset = hexo.model('PostAsset');\n  let post;\n\n  hexo.config.permalink = ':title/';\n\n  function assetPath(args) {\n    return assetPathTag.call(post, args.split(' '));\n  }\n\n  before(() => hexo.init().then(() => Post.insert({\n    source: 'foo.md',\n    slug: 'foo'\n  })).then(post_ => {\n    post = post_;\n\n    return BluebirdPromise.all([\n      PostAsset.insert({\n        _id: 'bar',\n        slug: 'bar',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'bár',\n        slug: 'bár',\n        post: post._id\n      }),\n      PostAsset.insert({\n        _id: 'spaced asset',\n        slug: 'spaced asset',\n        post: post._id\n      })\n    ]);\n  }));\n\n  it('default', () => {\n    assetPath('bar').should.eql('/foo/bar');\n  });\n\n  it('should encode path', () => {\n    assetPath('bár').should.eql('/foo/b%C3%A1r');\n  });\n\n  it('with space', () => {\n    // {% asset_path \"spaced asset\" %}\n    assetPathTag.call(post, ['spaced asset'])\n      .should.eql('/foo/spaced%20asset');\n  });\n\n  it('no slug', () => {\n    should.not.exist(assetPath(''));\n  });\n\n  it('asset not found', () => {\n    should.not.exist(assetPath('boo'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/blockquote.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport tagBlockquote from '../../../lib/plugins/tag/blockquote';\n\ndescribe('blockquote', () => {\n  const hexo = new Hexo(__dirname);\n  const blockquote = tagBlockquote(hexo);\n\n  before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked'))));\n\n  const bq = (args, content?) => blockquote(args.split(' '), content || '');\n\n  it('default', () => {\n    const result = bq('', '123456 **bold** and *italic*');\n    result.should.eql('<blockquote><p>123456 <strong>bold</strong> and <em>italic</em></p>\\n</blockquote>');\n  });\n\n  it('author', () => {\n    const result = bq('John Doe', '');\n    result.should.eql('<blockquote><footer><strong>John Doe</strong></footer></blockquote>');\n  });\n\n  it('source', () => {\n    const result = bq('Jane Austen, Pride and Prejudice');\n    result.should.eql('<blockquote><footer><strong>Jane Austen</strong><cite>Pride and Prejudice</cite></footer></blockquote>');\n  });\n\n  it('link', () => {\n    const result = bq('John Doe https://hexo.io/');\n    result.should.eql('<blockquote><footer><strong>John Doe</strong><cite><a href=\"https://hexo.io/\">hexo.io</a></cite></footer></blockquote>');\n  });\n\n  it('link title', () => {\n    const result = bq('John Doe https://hexo.io/ Hexo');\n    result.should.eql('<blockquote><footer><strong>John Doe</strong><cite><a href=\"https://hexo.io/\">Hexo</a></cite></footer></blockquote>');\n  });\n\n  it('titlecase', () => {\n    hexo.config.titlecase = true;\n\n    const result = bq('Jane Austen, pride and prejudice');\n    result.should.eql('<blockquote><footer><strong>Jane Austen</strong><cite>Pride and Prejudice</cite></footer></blockquote>');\n\n    hexo.config.titlecase = false;\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/code.ts",
    "content": "import { escapeHTML, highlight as utilHighlight, prismHighlight } from 'hexo-util';\nimport * as cheerio from 'cheerio';\nimport Hexo from '../../../lib/hexo';\nimport tagCode from '../../../lib/plugins/tag/code';\n\ndescribe('code', () => {\n  const hexo = new Hexo();\n  require('../../../lib/plugins/highlight/')(hexo);\n  const codeTag = tagCode(hexo);\n\n  const fixture = [\n    'if (tired && night){',\n    '  sleep();',\n    '}'\n  ].join('\\n');\n\n  function code(args, content) {\n    return codeTag(args.split(' '), content);\n  }\n\n  function highlight(code, options?) {\n    return utilHighlight(code, options || {})\n      .replace(/{/g, '&#123;')\n      .replace(/}/g, '&#125;');\n  }\n\n  function prism(code, options?) {\n    return prismHighlight(code, options || {})\n      .replace(/{/g, '&#123;')\n      .replace(/}/g, '&#125;');\n  }\n\n  describe('highlightjs', () => {\n    it('default', () => {\n      const result = code('', fixture);\n      result.should.eql(highlight(fixture));\n    });\n\n    it('non standard indent', () => {\n      const nonStandardIndent = [\n        '  ',\n        '  return x;',\n        '}',\n        '',\n        fixture,\n        '  '\n      ].join('/n');\n      const result = code('', nonStandardIndent);\n      result.should.eql(highlight(nonStandardIndent));\n    });\n\n    it('lang', () => {\n      const result = code('lang:js', fixture);\n      result.should.eql(highlight(fixture, {\n        lang: 'js'\n      }));\n    });\n\n    it('line_number', () => {\n      let result = code('line_number:false', fixture);\n      result.should.eql(highlight(fixture, {\n        gutter: false\n      }));\n      result = code('line_number:true', fixture);\n      result.should.eql(highlight(fixture, {\n        gutter: true\n      }));\n    });\n\n    it('line_threshold', () => {\n      let result = code('line_number:false line_threshold:1', fixture);\n      result.should.eql(highlight(fixture, {\n        gutter: false\n      }));\n      result = code('line_number:true line_threshold:1', fixture);\n      result.should.eql(highlight(fixture, {\n        gutter: true\n      }));\n      result = code('line_number:true line_threshold:3', fixture);\n      result.should.eql(highlight(fixture, {\n        gutter: false\n      }));\n    });\n\n    it('highlight disable', () => {\n      const result = code('highlight:false', fixture);\n      result.should.eql('<pre><code>' + escapeHTML(fixture) + '</code></pre>');\n    });\n\n    it('title', () => {\n      const result = code('Hello world', fixture);\n      result.should.eql(highlight(fixture, {\n        caption: '<span>Hello world</span>'\n      }));\n    });\n\n    it('uses html tag in title', () => {\n      const result = code('<strong>Bold</strong>', fixture);\n      result.should.eql(highlight(fixture, {\n        caption: `<span>${escapeHTML('<strong>Bold</strong>')}</span>`\n      }));\n    });\n\n    it('link', () => {\n      const result = code('Hello world https://hexo.io/', fixture);\n      const expected = highlight(fixture, {\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">link</a>'\n      });\n\n      result.should.eql(expected);\n    });\n\n    it('link text', () => {\n      const result = code('Hello world https://hexo.io/ Hexo', fixture);\n      const expected = highlight(fixture, {\n        caption: '<span>Hello world</span><a href=\"https://hexo.io/\">Hexo</a>'\n      });\n\n      result.should.eql(expected);\n    });\n\n    it('uses html tag in link text', () => {\n      const result = code('Hello world https://hexo.io/ <strong>Bold</strong>', fixture);\n      const expected = highlight(fixture, {\n        caption: `<span>Hello world</span><a href=\"https://hexo.io/\">${escapeHTML('<strong>Bold</strong>')}</a>`\n      });\n\n      result.should.eql(expected);\n    });\n\n    it('disabled', () => {\n      hexo.config.syntax_highlighter = '';\n\n      const result = code('', fixture);\n      result.should.eql('<pre><code>' + escapeHTML(fixture) + '</code></pre>');\n\n      hexo.config.syntax_highlighter = 'highlight.js';\n    });\n\n    it('first_line', () => {\n      let result = code('first_line:1234', fixture);\n      result.should.eql(highlight(fixture, {\n        firstLine: 1234\n      }));\n      result = code('', fixture);\n      result.should.eql(highlight(fixture, {\n        firstLine: 1\n      }));\n    });\n\n    it('mark', () => {\n      const source = [\n        'const http = require(\\'http\\');',\n        '',\n        'const hostname = \\'127.0.0.1\\';',\n        'const port = 1337;',\n        '',\n        'http.createServer((req, res) => {',\n        '  res.writeHead(200, { \\'Content-Type\\': \\'text/plain\\' });',\n        '  res.end(\\'Hello World\\n\\');',\n        '}).listen(port, hostname, () => {',\n        '  console.log(`Server running at http://${hostname}:${port}/`);',\n        '});'\n      ].join('\\n');\n\n      code('mark:1,7-9,11', source).should.eql(highlight(source, {\n        mark: [1, 7, 8, 9, 11]\n      }));\n\n      code('mark:11,9-7,1', source).should.eql(highlight(source, {\n        mark: [1, 7, 8, 9, 11]\n      }));\n    });\n\n    it('# lines', () => {\n      const result = code('', fixture);\n      const $ = cheerio.load(result);\n      $('.gutter .line').should.have.lengthOf(3);\n    });\n\n    it('wrap', () => {\n      let result = code('wrap:false', fixture);\n      result.should.eql(highlight(fixture, {\n        wrap: false\n      }));\n      result = code('wrap:true', fixture);\n      result.should.eql(highlight(fixture, {\n        wrap: true\n      }));\n    });\n\n    it('language_attr', () => {\n      const result = code('lang:js language_attr:true', fixture);\n      result.should.eql(highlight(fixture, {\n        lang: 'js',\n        languageAttr: true\n      }));\n    });\n  });\n\n  describe('prismjs', () => {\n    beforeEach(() => {\n      hexo.config.syntax_highlighter = 'prismjs';\n    });\n\n    it('default', () => {\n      const result = code('', fixture);\n      result.should.eql(prism(fixture));\n    });\n\n    it('non standard indent', () => {\n      const nonStandardIndent = [\n        '  ',\n        '  return x;',\n        '}',\n        '',\n        fixture,\n        '  '\n      ].join('/n');\n      const result = code('', nonStandardIndent);\n      result.should.eql(prism(nonStandardIndent));\n    });\n\n    it('lang', () => {\n      const result = code('lang:js', fixture);\n      result.should.eql(prism(fixture, {\n        lang: 'js'\n      }));\n    });\n\n    it('line_number', () => {\n      let result = code('line_number:false', fixture);\n      result.should.eql(prism(fixture, {\n        lineNumber: false\n      }));\n      result = code('line_number:true', fixture);\n      result.should.eql(prism(fixture, {\n        lineNumber: true\n      }));\n    });\n\n    it('line_threshold', () => {\n      let result = code('line_number:false line_threshold:1', fixture);\n      result.should.eql(prism(fixture, {\n        lineNumber: false\n      }));\n      result = code('line_number:true line_threshold:1', fixture);\n      result.should.eql(prism(fixture, {\n        lineNumber: true\n      }));\n      result = code('line_number:true line_threshold:3', fixture);\n      result.should.eql(prism(fixture, {\n        lineNumber: false\n      }));\n    });\n\n    it('highlight disable', () => {\n      const result = code('highlight:false', fixture);\n      result.should.eql('<pre><code>' + escapeHTML(fixture) + '</code></pre>');\n    });\n\n    it('disabled', () => {\n      hexo.config.syntax_highlighter = '';\n\n      const result = code('', fixture);\n      result.should.eql('<pre><code>' + escapeHTML(fixture) + '</code></pre>');\n\n      hexo.config.syntax_highlighter = 'highlight.js';\n    });\n\n    it('first_line', () => {\n      let result = code('first_line:1234', fixture);\n      result.should.eql(prism(fixture, {\n        firstLine: 1234\n      }));\n      result = code('', fixture);\n      result.should.eql(prism(fixture, {\n        firstLine: 1\n      }));\n    });\n\n    it('mark', () => {\n      const source = [\n        'const http = require(\\'http\\');',\n        '',\n        'const hostname = \\'127.0.0.1\\';',\n        'const port = 1337;',\n        '',\n        'http.createServer((req, res) => {',\n        '  res.writeHead(200, { \\'Content-Type\\': \\'text/plain\\' });',\n        '  res.end(\\'Hello World\\n\\');',\n        '}).listen(port, hostname, () => {',\n        '  console.log(`Server running at http://${hostname}:${port}/`);',\n        '});'\n      ].join('\\n');\n\n      code('mark:1,7-9,11', source).should.eql(prism(source, {\n        mark: [1, 7, 8, 9, 11]\n      }));\n\n      code('mark:11,9-7,1', source).should.eql(prism(source, {\n        mark: [1, 7, 8, 9, 11]\n      }));\n    });\n\n    it('title', () => {\n      const result = code('Hello world', fixture);\n      result.should.eql(prism(fixture, {\n        caption: '<span>Hello world</span>'\n      }));\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/full_url_for.ts",
    "content": "import * as cheerio from 'cheerio';\nimport tagFullUrlFor from '../../../lib/plugins/tag/full_url_for';\n\ndescribe('full_url_for', () => {\n  const ctx: any = {\n    config: { url: 'https://example.com' }\n  };\n\n  const fullUrlForTag = tagFullUrlFor(ctx);\n  const fullUrlFor = args => fullUrlForTag(args.split(' '));\n\n  it('no path input', () => {\n    const $ = cheerio.load(fullUrlFor('nopath'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/');\n    $('a').html()!.should.eql('nopath');\n  });\n\n  it('internal url', () => {\n    let $ = cheerio.load(fullUrlFor('index index.html'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/index.html');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(fullUrlFor('index /'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(fullUrlFor('index /index.html'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/index.html');\n    $('a').html()!.should.eql('index');\n  });\n\n  it('internal url (pretty_urls.trailing_index disabled)', () => {\n    ctx.config.pretty_urls = { trailing_index: false };\n    let $ = cheerio.load(fullUrlFor('index index.html'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(fullUrlFor('index /index.html'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/');\n    $('a').html()!.should.eql('index');\n  });\n\n  it('external url', () => {\n    [\n      'https://hexo.io/',\n      '//google.com/',\n      // 'index.html' in external link should not be removed\n      '//google.com/index.html'\n    ].forEach(url => {\n      const $ = cheerio.load(fullUrlFor(`external ${url}`));\n      $('a').attr('href')!.should.eql(url);\n      $('a').html()!.should.eql('external');\n    });\n  });\n\n  it('only hash', () => {\n    const $ = cheerio.load(fullUrlFor('hash #test'));\n    $('a').attr('href')!.should.eql(ctx.config.url + '/#test');\n    $('a').html()!.should.eql('hash');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/iframe.ts",
    "content": "import * as cheerio from 'cheerio';\nimport iframe from '../../../lib/plugins/tag/iframe';\n\ndescribe('iframe', () => {\n  it('url', () => {\n    const $ = cheerio.load(iframe(['https://zespia.tw']));\n\n    $('iframe').attr('src')!.should.eql('https://zespia.tw/');\n    $('iframe').attr('width')!.should.eql('100%');\n    $('iframe').attr('height')!.should.eql('300');\n    $('iframe').attr('frameborder')!.should.eql('0');\n    $('iframe').attr('allowfullscreen')!.should.eql('');\n    $('iframe').attr('loading')!.should.eql('lazy');\n  });\n\n  it('width', () => {\n    const $ = cheerio.load(iframe(['https://zespia.tw', '500']));\n\n    $('iframe').attr('src')!.should.eql('https://zespia.tw/');\n    $('iframe').attr('width')!.should.eql('500');\n    $('iframe').attr('height')!.should.eql('300');\n    $('iframe').attr('frameborder')!.should.eql('0');\n    $('iframe').attr('allowfullscreen')!.should.eql('');\n    $('iframe').attr('loading')!.should.eql('lazy');\n  });\n\n  it('height', () => {\n    const $ = cheerio.load(iframe(['https://zespia.tw', '500', '600']));\n\n    $('iframe').attr('src')!.should.eql('https://zespia.tw/');\n    $('iframe').attr('width')!.should.eql('500');\n    $('iframe').attr('height')!.should.eql('600');\n    $('iframe').attr('frameborder')!.should.eql('0');\n    $('iframe').attr('allowfullscreen')!.should.eql('');\n    $('iframe').attr('loading')!.should.eql('lazy');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/img.ts",
    "content": "import * as cheerio from 'cheerio';\nimport pathFn from 'path';\nimport Hexo from '../../../lib/hexo';\nimport tagImg from '../../../lib/plugins/tag/img';\n\n\ndescribe('img', () => {\n  const hexo = new Hexo(pathFn.join(__dirname, 'img_test'));\n  const img = tagImg(hexo);\n\n  before(() => hexo.init());\n\n  it('src', () => {\n    const $ = cheerio.load(img(['https://placekitten.com/200/300']));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n  });\n\n  it('src //', () => {\n    const $ = cheerio.load(img(['//placekitten.com/200/300']));\n\n    $('img').attr('src')!.should.eql('//placekitten.com/200/300');\n  });\n\n  it('internal src', () => {\n    hexo.config.root = '/';\n    let $ = cheerio.load(img(['/images/test.jpg']));\n    $('img').attr('src')!.should.eql('/images/test.jpg');\n\n    hexo.config.url = 'http://example.com/root';\n    hexo.config.root = '/root/';\n    $ = cheerio.load(img(['/images/test.jpg']));\n    $('img').attr('src')!.should.eql('/root/images/test.jpg');\n  });\n\n  it('class + src', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n  });\n\n  it('class + internal src', () => {\n    hexo.config.root = '/';\n    let $ = cheerio.load(img('left /images/test.jpg'.split(' ')));\n    $('img').attr('src')!.should.eql('/images/test.jpg');\n    $('img').attr('class')!.should.eql('left');\n\n    hexo.config.url = 'http://example.com/root';\n    hexo.config.root = '/root/';\n    $ = cheerio.load(img('left /images/test.jpg'.split(' ')));\n    $('img').attr('src')!.should.eql('/root/images/test.jpg');\n    $('img').attr('class')!.should.eql('left');\n  });\n\n  it('multiple classes + src', () => {\n    const $ = cheerio.load(img('left top https://placekitten.com/200/300'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left top');\n  });\n\n  it('multiple classes + internal src', () => {\n    hexo.config.root = '/';\n    let $ = cheerio.load(img('left top /images/test.jpg'.split(' ')));\n    $('img').attr('src')!.should.eql('/images/test.jpg');\n    $('img').attr('class')!.should.eql('left top');\n\n    hexo.config.url = 'http://example.com/root';\n    hexo.config.root = '/root/';\n    $ = cheerio.load(img('left top /images/test.jpg'.split(' ')));\n    $('img').attr('src')!.should.eql('/root/images/test.jpg');\n    $('img').attr('class')!.should.eql('left top');\n  });\n\n  it('class + src + width', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n  });\n\n  it('class + src + width + height', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('height')!.should.eql('300');\n  });\n\n  it('class + src + title', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 Place Kitten'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('title')!.should.eql('Place Kitten');\n  });\n\n  it('class + src + width + title', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 Place Kitten'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('title')!.should.eql('Place Kitten');\n  });\n\n  it('class + src + width + height + title', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 Place Kitten'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('height')!.should.eql('300');\n    $('img').attr('title')!.should.eql('Place Kitten');\n  });\n\n  it('class + src + width + height + title + alt', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 \"Place Kitten\" \"A cute kitten\"'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('height')!.should.eql('300');\n    $('img').attr('title')!.should.eql('Place Kitten');\n    $('img').attr('alt')!.should.eql('A cute kitten');\n  });\n\n  it('single quote in double quote', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 \"Place Kitten\" \"A \\'cute\\' kitten\"'.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('height')!.should.eql('300');\n    $('img').attr('title')!.should.eql('Place Kitten');\n    $('img').attr('alt')!.should.eql('A \\'cute\\' kitten');\n  });\n\n  it('double quote in single quote', () => {\n    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 \"Place Kitten\" \\'A \"cute\" kitten\\''.split(' ')));\n\n    $('img').attr('src')!.should.eql('https://placekitten.com/200/300');\n    $('img').attr('class')!.should.eql('left');\n    $('img').attr('width')!.should.eql('200');\n    $('img').attr('height')!.should.eql('300');\n    $('img').attr('title')!.should.eql('Place Kitten');\n    $('img').attr('alt')!.should.eql('A \"cute\" kitten');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/include_code.ts",
    "content": "import { join } from 'path';\nimport { rmdir, writeFile } from 'hexo-fs';\nimport { escapeHTML, highlight, prismHighlight } from 'hexo-util';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport tagIncludeCode from '../../../lib/plugins/tag/include_code';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('include_code', () => {\n  const hexo = new Hexo(join(__dirname, 'include_code_test'));\n  require('../../../lib/plugins/highlight/')(hexo);\n  const includeCode = BluebirdPromise.method(tagIncludeCode(hexo)) as (arg1: string[]) => BluebirdPromise<string>;\n  const path = join(hexo.source_dir, hexo.config.code_dir, 'test.js');\n  const defaultCfg = JSON.parse(JSON.stringify(hexo.config));\n\n  const fixture = [\n    'if (tired && night) {',\n    '  sleep();',\n    '}'\n  ].join('\\n');\n\n  const code = args => includeCode(args.split(' '));\n\n  before(async () => {\n    await writeFile(path, fixture);\n    await hexo.init();\n    await hexo.load();\n  });\n\n  beforeEach(() => {\n    hexo.config = JSON.parse(JSON.stringify(defaultCfg));\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  describe('highlightjs', () => {\n    it('default', async () => {\n      hexo.config.syntax_highlighter = 'highlight.js';\n\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>test.js</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('test.js');\n      result.should.eql(expected);\n    });\n\n    it('title', async () => {\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world test.js');\n      result.should.eql(expected);\n    });\n\n    it('uses html tag in title', async () => {\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: `<span>${escapeHTML('<strong>Bold</strong>')}</span><a href=\"/downloads/code/test.js\">view raw</a>`\n      });\n\n      const result = await code('<strong>Bold</strong> test.js');\n      result.should.eql(expected);\n    });\n\n    it('lang', async () => {\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js test.js');\n      result.should.eql(expected);\n    });\n\n    it('language_attr', async () => {\n      const original = hexo.config.highlight.language_attr;\n      hexo.config.highlight.language_attr = true;\n\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>',\n        languageAttr: true\n      });\n\n      const result = await code('Hello world lang:js test.js');\n      result.should.eql(expected);\n\n      hexo.config.highlight.language_attr = original;\n    });\n\n    it('from', async () => {\n      const fixture = [\n        '}'\n      ].join('\\n');\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js from:3 test.js');\n      result.should.eql(expected);\n    });\n\n    it('to', async () => {\n      const fixture = [\n        'if (tired && night) {',\n        '  sleep();'\n      ].join('\\n');\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js to:2 test.js');\n      result.should.eql(expected);\n    });\n\n    it('from and to', async () => {\n      const fixture = [\n        'sleep();'\n      ].join('\\n');\n      const expected = highlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js from:2 to:2 test.js');\n      result.should.eql(expected);\n    });\n\n    it('file not found', async () => {\n      const result = await code('nothing');\n      should.not.exist(result);\n    });\n\n    it('disabled', async () => {\n      hexo.config.syntax_highlighter = '';\n\n      const result = await code('test.js');\n      result.should.eql('<pre><code>' + fixture + '</code></pre>');\n    });\n  });\n\n  describe('prismjs', () => {\n    beforeEach(() => {\n      hexo.config.syntax_highlighter = 'prismjs';\n    });\n\n    it('default', async () => {\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>test.js</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('test.js');\n      result.should.eql(expected);\n    });\n\n    it('lang', async () => {\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js test.js');\n      result.should.eql(expected);\n    });\n\n    it('from', async () => {\n      const fixture = [\n        '}'\n      ].join('\\n');\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js from:3 test.js');\n      result.should.eql(expected);\n    });\n\n    it('to', async () => {\n      const fixture = [\n        'if (tired && night) {',\n        '  sleep();'\n      ].join('\\n');\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js to:2 test.js');\n      result.should.eql(expected);\n    });\n\n    it('from and to', async () => {\n      const fixture = [\n        'sleep();'\n      ].join('\\n');\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world lang:js from:2 to:2 test.js');\n      result.should.eql(expected);\n    });\n\n    it('title', async () => {\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: '<span>Hello world</span><a href=\"/downloads/code/test.js\">view raw</a>'\n      });\n\n      const result = await code('Hello world test.js');\n      result.should.eql(expected);\n    });\n\n    it('uses html tag in title', async () => {\n      const expected = prismHighlight(fixture, {\n        lang: 'js',\n        caption: `<span>${escapeHTML('<strong>Bold</strong>')}</span><a href=\"/downloads/code/test.js\">view raw</a>`\n      });\n\n      const result = await code('<strong>Bold</strong> test.js');\n      result.should.eql(expected);\n    });\n\n    it('file not found', async () => {\n      const result = await code('nothing');\n      should.not.exist(result);\n    });\n\n    it('disabled', async () => {\n      hexo.config.syntax_highlighter = '';\n\n      const result = await code('test.js');\n      result.should.eql('<pre><code>' + fixture + '</code></pre>');\n    });\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/link.ts",
    "content": "import * as cheerio from 'cheerio';\nimport link from '../../../lib/plugins/tag/link';\n\ndescribe('link', () => {\n  it('text + url', () => {\n    const $ = cheerio.load(link('Click here to Google https://google.com'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n  });\n\n  it('text + url + external', () => {\n    let $ = cheerio.load(link('Click here to Google https://google.com true'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n    $('a').attr('target')!.should.eql('_blank');\n\n    $ = cheerio.load(link('Click here to Google https://google.com false'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n    $('a').attr('title')!.should.eql('');\n    $('a').attr('target')!.should.eql('');\n  });\n\n  it('text + url + title', () => {\n    const $ = cheerio.load(link('Click here to Google https://google.com Google link'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n    $('a').attr('title')!.should.eql('Google link');\n  });\n\n  it('text + url + external + title', () => {\n    let $ = cheerio.load(link('Click here to Google https://google.com true Google link'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n    $('a').attr('target')!.should.eql('_blank');\n    $('a').attr('title')!.should.eql('Google link');\n\n    $ = cheerio.load(link('Click here to Google https://google.com false Google link'.split(' ')));\n\n    $('a').attr('href')!.should.eql('https://google.com/');\n    $('a').html()!.should.eql('Click here to Google');\n    $('a').attr('target')!.should.eql('');\n    $('a').attr('title')!.should.eql('Google link');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/post_link.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport tagPostLink from '../../../lib/plugins/tag/post_link';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('post_link', () => {\n  const hexo = new Hexo(__dirname);\n  const postLink = tagPostLink(hexo);\n  const Post = hexo.model('Post');\n\n  hexo.config.permalink = ':title/';\n\n  before(() => hexo.init().then(() => Post.insert([{\n    source: 'foo',\n    slug: 'foo',\n    title: 'Hello world'\n  },\n  {\n    source: 'title-with-tag',\n    slug: 'title-with-tag',\n    title: '\"Hello\" <new world>!'\n  },\n  {\n    source: 'fôo',\n    slug: 'fôo',\n    title: 'Hello world'\n  },\n  {\n    source: 'no-title',\n    slug: 'no-title',\n    title: ''\n  }])));\n\n  it('default', () => {\n    postLink(['foo']).should.eql('<a href=\"/foo/\" title=\"Hello world\">Hello world</a>');\n  });\n\n  it('should encode path', () => {\n    postLink(['fôo']).should.eql('<a href=\"/f%C3%B4o/\" title=\"Hello world\">Hello world</a>');\n  });\n\n  it('title', () => {\n    postLink(['foo', 'test']).should.eql('<a href=\"/foo/\" title=\"Hello world\">test</a>');\n  });\n\n  it('no title', () => {\n    postLink(['no-title']).should.eql('<a href=\"/no-title/\" title=\"no-title\">no-title</a>');\n  });\n\n  it('should escape tag in title by default', () => {\n    postLink(['title-with-tag']).should.eql('<a href=\"/title-with-tag/\" title=\"&quot;Hello&quot; &lt;new world&gt;!\">&quot;Hello&quot; &lt;new world&gt;!</a>');\n  });\n\n  it('should escape tag in title', () => {\n    postLink(['title-with-tag', 'true']).should.eql('<a href=\"/title-with-tag/\" title=\"&quot;Hello&quot; &lt;new world&gt;!\">&quot;Hello&quot; &lt;new world&gt;!</a>');\n  });\n\n  it('should escape tag in custom title', () => {\n    postLink(['title-with-tag', '<test>', 'title', 'true']).should.eql('<a href=\"/title-with-tag/\" title=\"&quot;Hello&quot; &lt;new world&gt;!\">&lt;test&gt; title</a>');\n  });\n\n  it('should not escape tag in title', () => {\n    postLink(['title-with-tag', 'false']).should.eql('<a href=\"/title-with-tag/\" title=\"&quot;Hello&quot; &lt;new world&gt;!\">\"Hello\" <new world>!</a>');\n  });\n\n  it('should not escape tag in custom title', () => {\n    postLink(['title-with-tag', 'This is a <b>Bold</b> \"statement\"', 'false'])\n      .should.eql('<a href=\"/title-with-tag/\" title=\"&quot;Hello&quot; &lt;new world&gt;!\">This is a <b>Bold</b> \"statement\"</a>');\n  });\n\n  it('should throw if no slug', () => {\n    should.throw(() => postLink([]), Error, /Post not found: \"undefined\" doesn't exist for \\{% post_link %\\}/);\n  });\n\n  it('should throw if post not found', () => {\n    should.throw(() => postLink(['bar']), Error, /Post not found: post_link bar\\./);\n  });\n\n  it('should keep hash', () => {\n    postLink(['foo#bar']).should.eql('<a href=\"/foo/#bar\" title=\"Hello world\">Hello world</a>');\n  });\n\n  it('should keep subdir', () => {\n    hexo.config.root = '/subdir/';\n    postLink(['foo']).should.eql('<a href=\"/subdir/foo/\" title=\"Hello world\">Hello world</a>');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/post_path.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport tagPostPath from '../../../lib/plugins/tag/post_path';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('post_path', () => {\n  const hexo = new Hexo(__dirname);\n  const postPath = tagPostPath(hexo);\n  const Post = hexo.model('Post');\n\n  hexo.config.permalink = ':title/';\n\n  before(() => hexo.init().then(() => Post.insert([{\n    source: 'foo',\n    slug: 'foo'\n  }, {\n    source: 'fôo',\n    slug: 'fôo'\n  }])));\n\n  it('default', () => {\n    postPath(['foo']).should.eql('/foo/');\n  });\n\n  it('should encode path', () => {\n    postPath(['fôo']).should.eql('/f%C3%B4o/');\n  });\n\n  it('no slug', () => {\n    should.not.exist(postPath([]));\n  });\n\n  it('post not found', () => {\n    should.not.exist(postPath(['bar']));\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/pullquote.ts",
    "content": "import Hexo from '../../../lib/hexo';\nimport tagPullquote from '../../../lib/plugins/tag/pullquote';\n\ndescribe('pullquote', () => {\n  const hexo = new Hexo(__dirname);\n  const pullquote = tagPullquote(hexo);\n\n  before(() => hexo.init().then(() => hexo.loadPlugin(require.resolve('hexo-renderer-marked'))));\n\n  it('default', () => {\n    const result = pullquote([], '123456 **bold** and *italic*');\n    result.should.eql('<blockquote class=\"pullquote\"><p>123456 <strong>bold</strong> and <em>italic</em></p>\\n</blockquote>');\n  });\n\n  it('class', () => {\n    const result = pullquote(['foo', 'bar'], '');\n    result.should.eql('<blockquote class=\"pullquote foo bar\"></blockquote>');\n  });\n});\n"
  },
  {
    "path": "test/scripts/tags/url_for.ts",
    "content": "import * as cheerio from 'cheerio';\nimport tagUrlFor from '../../../lib/plugins/tag/url_for';\n\ndescribe('url_for', () => {\n  const ctx: any = {\n    config: { url: 'https://example.com' }\n  };\n\n  const urlForTag = tagUrlFor(ctx);\n  const urlFor = args => urlForTag(args.split(' '));\n\n  it('should encode path', () => {\n    ctx.config.root = '/';\n    let $ = cheerio.load(urlFor('foo fôo.html'));\n    $('a').attr('href')!.should.eql('/f%C3%B4o.html');\n    $('a').html()!.should.eql('foo');\n\n    ctx.config.root = '/fôo/';\n    $ = cheerio.load(urlFor('foo bár.html'));\n    $('a').attr('href')!.should.eql('/f%C3%B4o/b%C3%A1r.html');\n    $('a').html()!.should.eql('foo');\n  });\n\n  it('internal url (relative off)', () => {\n    ctx.config.root = '/';\n    let $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('/index.html');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(urlFor('index /'));\n    $('a').attr('href')!.should.eql('/');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(urlFor('index /index.html'));\n    $('a').attr('href')!.should.eql('/index.html');\n    $('a').html()!.should.eql('index');\n\n    ctx.config.root = '/blog/';\n    $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('/blog/index.html');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(urlFor('index /'));\n    $('a').attr('href')!.should.eql('/blog/');\n    $('a').html()!.should.eql('index');\n\n    $ = cheerio.load(urlFor('index /index.html'));\n    $('a').attr('href')!.should.eql('/blog/index.html');\n    $('a').html()!.should.eql('index');\n  });\n\n  it('internal url (relative on)', () => {\n    ctx.config.relative_link = true;\n    ctx.config.root = '/';\n\n    ctx.path = '';\n    let $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('index.html');\n    $('a').html()!.should.eql('index');\n\n    ctx.path = 'foo/bar/';\n    $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('../../index.html');\n    $('a').html()!.should.eql('index');\n\n    ctx.config.relative_link = false;\n  });\n\n  it('internal url (options.relative)', () => {\n    ctx.path = '';\n    let $ = cheerio.load(urlFor('index index.html true'));\n    $('a').attr('href')!.should.eql('index.html');\n    $('a').html()!.should.eql('index');\n\n    ctx.config.relative_link = true;\n    $ = cheerio.load(urlFor('index index.html false'));\n    $('a').attr('href')!.should.eql('/index.html');\n    $('a').html()!.should.eql('index');\n    ctx.config.relative_link = false;\n  });\n\n  it('internal url (pretty_urls.trailing_index disabled)', () => {\n    ctx.config.pretty_urls = { trailing_index: false };\n    ctx.path = '';\n    ctx.config.root = '/';\n    let $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('/');\n    $('a').html()!.should.eql('index');\n    $ = cheerio.load(urlFor('index /index.html'));\n    $('a').attr('href')!.should.eql('/');\n    $('a').html()!.should.eql('index');\n\n    ctx.config.root = '/blog/';\n    $ = cheerio.load(urlFor('index index.html'));\n    $('a').attr('href')!.should.eql('/blog/');\n    $('a').html()!.should.eql('index');\n    $ = cheerio.load(urlFor('index /index.html'));\n    $('a').attr('href')!.should.eql('/blog/');\n    $('a').html()!.should.eql('index');\n  });\n\n  it('external url', () => {\n    [\n      'https://hexo.io/',\n      '//google.com/',\n      // 'index.html' in external link should not be removed\n      '//google.com/index.html'\n    ].forEach(url => {\n      const $ = cheerio.load(urlFor(`external ${url}`));\n      $('a').attr('href')!.should.eql(url);\n      $('a').html()!.should.eql('external');\n    });\n  });\n\n  it('only hash', () => {\n    const $ = cheerio.load(urlFor('hash #test'));\n    $('a').attr('href')!.should.eql('#test');\n    $('a').html()!.should.eql('hash');\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme/theme.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, writeFile } from 'hexo-fs';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('Theme', () => {\n  const hexo = new Hexo(join(__dirname, 'theme_test'), {silent: true});\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n\n  before(async () => {\n    await Promise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('getView()', () => {\n    hexo.theme.setView('test.njk', '');\n\n    // With extension name\n    hexo.theme.getView('test.njk').should.have.property('path', 'test.njk');\n\n    // Without extension name\n    hexo.theme.getView('test').should.have.property('path', 'test.njk');\n\n    // not exist\n    should.not.exist(hexo.theme.getView('abc.njk'));\n\n    hexo.theme.removeView('test.njk');\n  });\n\n  it('getView() - escape backslashes', () => {\n    hexo.theme.setView('foo/bar.njk', '');\n\n    hexo.theme.getView('foo\\\\bar.njk').should.have.property('path', 'foo/bar.njk');\n\n    hexo.theme.removeView('foo/bar.njk');\n  });\n\n  it('setView()', () => {\n    hexo.theme.setView('test.njk', '');\n\n    const view = hexo.theme.getView('test.njk');\n    view.path.should.eql('test.njk');\n\n    hexo.theme.removeView('test.njk');\n  });\n\n  it('removeView()', () => {\n    hexo.theme.setView('test.njk', '');\n    hexo.theme.removeView('test.njk');\n\n    should.not.exist(hexo.theme.getView('test.njk'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme/view.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, writeFile } from 'hexo-fs';\nimport moment from 'moment';\nimport { fake, assert as sinonAssert } from 'sinon';\nimport Hexo from '../../../lib/hexo';\nimport chai from 'chai';\nconst should = chai.should();\n\ndescribe('View', () => {\n  const hexo = new Hexo(join(__dirname, 'theme_test'));\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n  const { compile } = Object.assign({}, hexo.extend.renderer.store.njk);\n\n  hexo.env.init = true;\n\n  function newView(path, data) {\n    return new hexo.theme.View(path, data);\n  }\n\n  before(async () => {\n    await Promise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    await hexo.init();\n    // Setup layout\n    hexo.theme.setView('layout.njk', [\n      'pre',\n      '{{ body }}',\n      'post'\n    ].join('\\n'));\n  });\n\n  beforeEach(() => {\n    // Restore compile function\n    hexo.extend.renderer.store.njk.compile = compile;\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('constructor', () => {\n    const data = {\n      _content: ''\n    };\n    const view = newView('index.njk', data);\n\n    view.path.should.eql('index.njk');\n    view.source.should.eql(join(themeDir, 'layout', 'index.njk'));\n    view.data.should.eql(data);\n  });\n\n  it('parse front-matter', () => {\n    const body = [\n      'layout: false',\n      '---',\n      'content'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    view.data.should.eql({\n      layout: false,\n      _content: 'content'\n    });\n  });\n\n  it('precompile view if possible', async () => {\n    const body = 'Hello {{ name }}';\n    const view = newView('index.njk', body);\n\n    view._compiledSync({\n      name: 'Hexo'\n    }).should.eql('Hello Hexo');\n\n    const result = await view._compiled({\n      name: 'Hexo'\n    });\n    result.should.eql('Hello Hexo');\n  });\n\n  it('generate precompiled function even if renderer does not provide compile function', async () => {\n    // Remove compile function\n    delete hexo.extend.renderer.store.njk.compile;\n\n    const body = 'Hello {{ name }}';\n    const view = newView('index.njk', body);\n\n    view._compiledSync({\n      name: 'Hexo'\n    }).should.eql('Hello Hexo');\n\n    const result = await view._compiled({\n      name: 'Hexo'\n    });\n    result.should.eql('Hello Hexo');\n  });\n\n  it('render()', async () => {\n    const body = [\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    const content = await view.render({\n      test: 'foo'\n    });\n    content.should.eql('foo');\n  });\n\n  it('render() - front-matter', async () => {\n    // The priority of front-matter is higher\n    const body = [\n      'foo: bar',\n      '---',\n      '{{ foo }}',\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    const content = await view.render({\n      foo: 'foo',\n      test: 'test'\n    });\n    content.should.eql('bar\\ntest');\n  });\n\n  it('render() - helper', async () => {\n    const body = [\n      '{{ date() }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    const content = await view.render({\n      config: hexo.config,\n      page: {}\n    });\n    content.should.eql(moment().format(hexo.config.date_format));\n  });\n\n  it('render() - layout', async () => {\n    const body = 'content';\n    const view = newView('index.njk', body);\n\n    const content = await view.render({\n      layout: 'layout'\n    });\n    content.should.eql('pre\\n' + body + '\\npost');\n  });\n\n  it('render() - layout not found', async () => {\n    const body = 'content';\n    const view = newView('index.njk', body);\n\n    const content = await view.render({\n      layout: 'wtf'\n    });\n    content.should.eql(body);\n  });\n\n  it('render() - callback', callback => {\n    const body = [\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    view.render({\n      test: 'foo'\n    }, (err, content) => {\n      should.not.exist(err);\n      content.should.eql('foo');\n      callback();\n    });\n  });\n\n  it('render() - callback (without options)', callback => {\n    const body = [\n      'test: foo',\n      '---',\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    view.render((err, content) => {\n      should.not.exist(err);\n      content.should.eql('foo');\n      callback();\n    });\n  });\n\n  it.skip('render() - execute after_render:html', async () => {\n    const body = [\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    const filter = fake.returns('bar');\n\n    hexo.extend.filter.register('after_render:html', filter);\n\n    const content = await view.render({\n      test: 'foo'\n    });\n    content.should.eql('bar');\n\n    hexo.extend.filter.unregister('after_render:html', filter);\n    sinonAssert.alwaysCalledWith(filter, 'foo');\n  });\n\n  it('renderSync()', () => {\n    const body = [\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n    view.renderSync({test: 'foo'}).should.eql('foo');\n  });\n\n  it('renderSync() - front-matter', () => {\n    // The priority of front-matter is higher\n    const body = [\n      'foo: bar',\n      '---',\n      '{{ foo }}',\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    view.renderSync({\n      foo: 'foo',\n      test: 'test'\n    }).should.eql('bar\\ntest');\n  });\n\n  it('renderSync() - helper', () => {\n    const body = [\n      '{{ date() }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    view.renderSync({\n      config: hexo.config,\n      page: {}\n    }).should.eql(moment().format(hexo.config.date_format));\n  });\n\n  it('renderSync() - layout', () => {\n    const body = 'content';\n    const view = newView('index.njk', body);\n\n    view.renderSync({\n      layout: 'layout'\n    }).should.eql('pre\\n' + body + '\\npost');\n  });\n\n  it('renderSync() - layout not found', () => {\n    const body = 'content';\n    const view = newView('index.njk', body);\n\n    view.renderSync({\n      layout: 'wtf'\n    }).should.eql(body);\n  });\n\n  it.skip('renderSync() - execute after_render:html', () => {\n    const body = [\n      '{{ test }}'\n    ].join('\\n');\n\n    const view = newView('index.njk', body);\n\n    const filter = fake.returns('bar');\n\n    hexo.extend.filter.register('after_render:html', filter);\n    view.renderSync({test: 'foo'}).should.eql('bar');\n    hexo.extend.filter.unregister('after_render:html', filter);\n    sinonAssert.alwaysCalledWith(filter, 'foo');\n  });\n\n  it('_resolveLayout()', () => {\n    const view = newView('partials/header.njk', 'header');\n\n    // Relative path\n    view._resolveLayout('../layout').should.have.property('path', 'layout.njk');\n\n    // Absolute path\n    view._resolveLayout('layout').should.have.property('path', 'layout.njk');\n\n    // Can't be itself\n    should.not.exist(view._resolveLayout('header'));\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme_processors/config.ts",
    "content": "import { spy, assert as sinonAssert } from 'sinon';\nimport { join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile} from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport { config } from '../../../lib/theme/processors/config';\nimport chai from 'chai';\nconst should = chai.should();\ntype ConfigParams = Parameters<typeof config['process']>\ntype ConfigReturn = ReturnType<typeof config['process']>\n\ndescribe('config', () => {\n  const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true});\n  const process: (...args: ConfigParams) => BluebirdPromise<ConfigReturn> = BluebirdPromise.method(config.process.bind(hexo));\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n\n  function newFile(options) {\n    options.source = join(themeDir, options.path);\n    return new hexo.theme.File(options);\n  }\n\n  before(async () => {\n    await BluebirdPromise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    hexo.init();\n  });\n\n  beforeEach(() => { hexo.theme.config = {}; });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('pattern', () => {\n    const pattern = config.pattern;\n\n    pattern.match('_config.yml').should.be.ok;\n    pattern.match('_config.json').should.be.ok;\n    should.not.exist(pattern.match('_config/foo.yml'));\n    should.not.exist(pattern.match('foo.yml'));\n  });\n\n  it('type: create', async () => {\n    const body = [\n      'name:',\n      '  first: John',\n      '  last: Doe'\n    ].join('\\n');\n\n    const file = newFile({\n      path: '_config.yml',\n      type: 'create',\n      content: body\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    hexo.theme.config.should.eql({\n      name: {first: 'John', last: 'Doe'}\n    });\n\n    unlink(file.source);\n  });\n\n  it('type: delete', async () => {\n    const file = newFile({\n      path: '_config.yml',\n      type: 'delete'\n    });\n\n    hexo.theme.config = {foo: 'bar'};\n\n    await process(file);\n    hexo.theme.config.should.eql({});\n  });\n\n  it('load failed', () => {\n    const file = newFile({\n      path: '_config.yml',\n      type: 'create'\n    });\n\n    const logSpy = spy(hexo.log, 'error');\n\n    return process(file).then(() => {\n      should.fail('Return value must be rejected');\n    }, () => {\n      sinonAssert.calledWith(logSpy, 'Theme config load failed.');\n    }).finally(() => logSpy.restore());\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme_processors/i18n.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport { i18n } from '../../../lib/theme/processors/i18n';\nimport chai from 'chai';\nconst should = chai.should();\ntype I18nParams = Parameters<typeof i18n['process']>\ntype I18nReturn = ReturnType<typeof i18n['process']>\n\ndescribe('i18n', () => {\n  const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true});\n  const process: (...args: I18nParams) => BluebirdPromise<I18nReturn> = BluebirdPromise.method(i18n.process.bind(hexo));\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n\n  function newFile(options) {\n    const { path } = options;\n\n    options.params = { path };\n\n    options.path = 'languages/' + path;\n    options.source = join(themeDir, options.path);\n\n    return new hexo.theme.File(options);\n  }\n\n  before(async () => {\n    await BluebirdPromise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('pattern', () => {\n    const pattern = i18n.pattern;\n\n    pattern.match('languages/default.yml').should.be.ok;\n    pattern.match('languages/zh-TW.yml').should.be.ok;\n    should.not.exist(pattern.match('default.yml'));\n  });\n\n  it('type: create', async () => {\n    const body = [\n      'ok: OK',\n      'index:',\n      '  title: Home'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'en.yml',\n      type: 'create'\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const __ = hexo.theme.i18n.__('en' as any);\n\n    __('ok').should.eql('OK');\n    __('index.title').should.eql('Home');\n    unlink(file.source);\n  });\n\n  it('type: delete', async () => {\n    hexo.theme.i18n.set('en', {\n      foo: 'foo',\n      bar: 'bar'\n    });\n\n    const file = newFile({\n      path: 'en.yml',\n      type: 'delete'\n    });\n\n    await process(file);\n    hexo.theme.i18n.get('en').should.eql({});\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme_processors/source.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport { source } from '../../../lib/theme/processors/source';\nimport chai from 'chai';\nconst should = chai.should();\ntype SourceParams = Parameters<typeof source['process']>\ntype SourceReturn = ReturnType<typeof source['process']>\n\ndescribe('source', () => {\n  const hexo = new Hexo(join(__dirname, 'source_test'), {silent: true});\n  const process: (...args: SourceParams) => BluebirdPromise<SourceReturn> = BluebirdPromise.method(source.process.bind(hexo));\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n  const Asset = hexo.model('Asset');\n\n  function newFile(options) {\n    const { path } = options;\n\n    options.params = {path};\n    options.path = 'source/' + path;\n    options.source = join(themeDir, options.path);\n\n    return new hexo.theme.File(options);\n  }\n\n  before(async () => {\n    await BluebirdPromise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    await hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('pattern', () => {\n    const { pattern } = source;\n\n    pattern.match('source/foo.jpg').should.eql({path: 'foo.jpg'});\n    pattern.match('source/_foo.jpg').should.be.false;\n    pattern.match('source/foo/_bar.jpg').should.be.false;\n    pattern.match('source/foo.jpg~').should.be.false;\n    pattern.match('source/foo.jpg%').should.be.false;\n    pattern.match('layout/foo.swig').should.be.false;\n    pattern.match('layout/foo.njk').should.be.false;\n    pattern.match('package.json').should.be.false;\n    pattern.match('node_modules/test/test.js').should.be.false;\n    pattern.match('source/node_modules/test/test.js').should.be.false;\n  });\n\n  it('type: create', async () => {\n    const file = newFile({\n      path: 'style.css',\n      type: 'create'\n    });\n\n    const id = 'themes/test/' + file.path;\n\n    await writeFile(file.source, 'test');\n    await process(file);\n    const asset = Asset.findById(id);\n\n    asset._id.should.eql(id);\n    asset.path.should.eql(file.params.path);\n    asset.modified.should.be.true;\n\n    asset.remove();\n\n    unlink(file.source);\n  });\n\n  it('type: update', async () => {\n    const file = newFile({\n      path: 'style.css',\n      type: 'update'\n    });\n\n    const id = 'themes/test/' + file.path;\n\n    await BluebirdPromise.all([\n      writeFile(file.source, 'test'),\n      Asset.insert({\n        _id: id,\n        path: file.params.path,\n        modified: false\n      })\n    ]);\n    await process(file);\n    const asset = Asset.findById(id);\n\n    asset.modified.should.be.true;\n\n    await BluebirdPromise.all([\n      unlink(file.source),\n      Asset.removeById(id)\n    ]);\n  });\n\n  it('type: skip', async () => {\n    const file = newFile({\n      path: 'style.css',\n      type: 'skip'\n    });\n\n    const id = 'themes/test/' + file.path;\n\n    await BluebirdPromise.all([\n      writeFile(file.source, 'test'),\n      Asset.insert({\n        _id: id,\n        path: file.params.path,\n        modified: false\n      })\n    ]);\n    await process(file);\n    const asset = Asset.findById(id);\n\n    asset.modified.should.be.false;\n    await BluebirdPromise.all([\n      unlink(file.source),\n      Asset.removeById(id)\n    ]);\n  });\n\n  it('type: delete', async () => {\n    const file = newFile({\n      path: 'style.css',\n      type: 'delete'\n    });\n\n    const id = 'themes/test/' + file.path;\n\n    await Asset.insert({\n      _id: id,\n      path: file.params.path\n    });\n    await process(file);\n    should.not.exist(Asset.findById(id));\n  });\n\n  it('type: delete - not -exist', async () => {\n    const file = newFile({\n      path: 'style.css',\n      type: 'delete'\n    });\n\n    const id = 'themes/test/' + file.path;\n\n    await process(file);\n    should.not.exist(Asset.findById(id));\n  });\n});\n"
  },
  {
    "path": "test/scripts/theme_processors/view.ts",
    "content": "import { join } from 'path';\nimport { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs';\nimport BluebirdPromise from 'bluebird';\nimport Hexo from '../../../lib/hexo';\nimport { view } from '../../../lib/theme/processors/view';\nimport chai from 'chai';\nconst should = chai.should();\ntype ViewParams = Parameters<typeof view['process']>\ntype ViewReturn = ReturnType<typeof view['process']>\n\n\ndescribe('view', () => {\n  const hexo = new Hexo(join(__dirname, 'view_test'), {silent: true});\n  const process: (...args: ViewParams) => BluebirdPromise<ViewReturn> = BluebirdPromise.method(view.process.bind(hexo));\n  const themeDir = join(hexo.base_dir, 'themes', 'test');\n\n  hexo.env.init = true;\n\n  function newFile(options) {\n    const { path } = options;\n\n    options.params = {path};\n    options.path = 'layout/' + path;\n    options.source = join(themeDir, options.path);\n\n    return new hexo.theme.File(options);\n  }\n\n  before(async () => {\n    await BluebirdPromise.all([\n      mkdirs(themeDir),\n      writeFile(hexo.config_path, 'theme: test')\n    ]);\n    await hexo.init();\n  });\n\n  after(() => rmdir(hexo.base_dir));\n\n  it('pattern', () => {\n    const { pattern } = view;\n\n    pattern.match('layout/index.njk').path.should.eql('index.njk');\n    should.not.exist(pattern.match('index.njk'));\n    should.not.exist(pattern.match('view/index.njk'));\n  });\n\n  it('type: create', async () => {\n    const body = [\n      'foo: bar',\n      '---',\n      'test'\n    ].join('\\n');\n\n    const file = newFile({\n      path: 'index.njk',\n      type: 'create'\n    });\n\n    await writeFile(file.source, body);\n    await process(file);\n    const view = hexo.theme.getView('index.njk');\n\n    view.path.should.eql('index.njk');\n    view.source.should.eql(join(themeDir, 'layout', 'index.njk'));\n    view.data.should.eql({\n      foo: 'bar',\n      _content: 'test'\n    });\n    hexo.theme.removeView('index.njk');\n    unlink(file.source);\n  });\n\n  it('type: delete', async () => {\n    const file = newFile({\n      path: 'index.njk',\n      type: 'delete'\n    });\n\n    await process(file);\n    should.not.exist(hexo.theme.getView('index.njk'));\n  });\n});\n"
  },
  {
    "path": "test/util/index.ts",
    "content": "export { readStream } from './stream';\n"
  },
  {
    "path": "test/util/stream.ts",
    "content": "import Promise from 'bluebird';\n\nexport function readStream(stream): Promise<string> {\n  return new Promise((resolve, reject) => {\n    let data = '';\n\n    stream.on('data', chunk => {\n      data += chunk.toString();\n    }).on('end', () => {\n      resolve(data);\n    }).on('error', reject);\n  });\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es2020\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"types\": [\n      \"node\",\n      \"mocha\"\n    ]\n  },\n  \"include\": [\n    \"lib/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]