it("commits changes with a custom message", () =>

run(testDir)("--message", "chore: Release %s :rocket:").then(() => {

expect(GitUtilities.commit).lastCalledWith("chore: Release v1.0.1 :rocket:", execOpts(testDir));

keep keep keep keep replace keep keep keep

it("commits changes with a custom message using %s", () =>

import { EOL } from "os";
import async from "async";
import chalk from "chalk";
import dedent from "dedent";
import minimatch from "minimatch";
import path from "path";
import semver from "semver";
import writeJsonFile from "write-json-file";
import writePkg from "write-pkg";

import Command from "../Command";
import ConventionalCommitUtilities from "../ConventionalCommitUtilities";
import FileSystemUtilities from "../FileSystemUtilities";
import GitUtilities from "../GitUtilities";
import NpmUtilities from "../NpmUtilities";
import output from "../utils/output";
import PackageUtilities from "../PackageUtilities";
import PromptUtilities from "../PromptUtilities";
import UpdatedPackagesCollector from "../UpdatedPackagesCollector";
import ValidationError from "../utils/ValidationError";

export function handler(argv) {
  // eslint-disable-next-line no-use-before-define
  const cmd = new PublishCommand(argv._, argv, argv._cwd);
  return cmd.run().then(argv._onResolved, argv._onRejected);
}

export const command = "publish";

export const describe = "Publish packages in the current project.";

const cdVersionOptions = ["major", "minor", "patch", "premajor", "preminor", "prepatch", "prerelease"];

const cdVersionOptionString = `'${cdVersionOptions.slice(0, -1).join("', '")}', or '${
  cdVersionOptions[cdVersionOptions.length - 1]
}'.`;

export const builder = {
  canary: {
    group: "Command Options:",
    defaultDescription: "alpha",
    describe: "Publish packages after every successful merge using the sha as part of the tag.",
    alias: "c",
    // NOTE: this type must remain undefined, as it is too overloaded to make sense
    // type: "string",
  },
  "cd-version": {
    group: "Command Options:",
    describe: `Skip the version selection prompt and increment semver: ${cdVersionOptionString}`,
    type: "string",
    requiresArg: true,
    coerce: choice => {
      if (cdVersionOptions.indexOf(choice) === -1) {
        throw new Error(`--cd-version must be one of: ${cdVersionOptionString}`);
      }
      return choice;
    },
  },
  "conventional-commits": {
    group: "Command Options:",
    describe: "Use angular conventional-commit format to determine version bump and generate CHANGELOG.",
    type: "boolean",
    default: undefined,
  },
  "changelog-preset": {
    group: "Command Options:",
    describe: "Use another conventional-changelog preset rather than angular.",
    type: "string",
    default: undefined,
  },
  exact: {
    group: "Command Options:",
    describe: "Specify cross-dependency version numbers exactly rather than with a caret (^).",
    type: "boolean",
    default: undefined,
  },
  "git-remote": {
    group: "Command Options:",
    defaultDescription: "origin",
    describe: "Push git changes to the specified remote instead of 'origin'.",
    type: "string",
    requiresArg: true,
  },
  yes: {
    group: "Command Options:",
    describe: "Skip all confirmation prompts.",
    type: "boolean",
    default: undefined,
  },
  message: {
    group: "Command Options:",
    describe: "Use a custom commit message when creating the publish commit.",
    alias: "m",
    type: "string",
    requiresArg: true,
  },
  "npm-tag": {
    group: "Command Options:",
    describe: "Publish packages with the specified npm dist-tag",
    type: "string",
    requiresArg: true,
  },
  "npm-client": {
    group: "Command Options:",
    describe: "Executable used to publish dependencies (npm, yarn, pnpm, ...)",
    type: "string",
    requiresArg: true,
  },
  preid: {
    group: "Command Options:",
    describe: "Specify the prerelease identifier (major.minor.patch-pre).",
    type: "string",
    requiresArg: true,
  },
  "repo-version": {
    group: "Command Options:",
    describe: "Specify repo version to publish.",
    type: "string",
    requiresArg: true,
  },
  "skip-git": {
    group: "Command Options:",
    describe: "Skip commiting, tagging, and pushing git changes.",
    type: "boolean",
    default: undefined,
  },
  "skip-npm": {
    group: "Command Options:",
    describe: "Stop before actually publishing change to npm.",
    type: "boolean",
    default: undefined,
  },
  "temp-tag": {
    group: "Command Options:",
    describe: "Create a temporary tag while publishing.",
    type: "boolean",
    default: undefined,
  },
  "allow-branch": {
    group: "Command Options:",
    describe: "Specify which branches to allow publishing from.",
    type: "array",
  },
};

export default class PublishCommand extends Command {
  get defaultOptions() {
    return Object.assign({}, super.defaultOptions, {
      conventionalCommits: false,
      exact: false,
      skipGit: false,
      skipNpm: false,
      tempTag: false,
      yes: false,
      allowBranch: false,
    });
  }

  get changelogOpts() {
    if (!this._changelogOpts) {
      const { changelogPreset } = this.options;

      this._changelogOpts = Object.assign({}, this.execOpts, {
        changelogPreset,
      });
    }

    return this._changelogOpts;
  }

  initialize(callback) {
    this.gitRemote = this.options.gitRemote || "origin";
    this.gitEnabled = !(this.options.canary || this.options.skipGit);

    this.npmConfig = {
      npmClient: this.options.npmClient || "npm",
      registry: this.npmRegistry,
    };

    if (this.options.useGitVersion && !this.options.exact) {
      throw new Error(dedent`
        Using git version without 'exact' option is not recommended.
        Please make sure you publish with --exact.
      `);
    }

    if (this.options.canary) {
      this.logger.info("canary", "enabled");
    }

    if (!this.repository.isIndependent()) {
      this.globalVersion = this.repository.version;
      this.logger.info("current version", this.globalVersion);
    }

    // git validation, if enabled, should happen before updates are calculated and versions picked
    if (this.gitEnabled) {
      if (GitUtilities.isDetachedHead(this.execOpts)) {
        throw new ValidationError(
          "ENOGIT",
          "Detached git HEAD, please checkout a branch to publish changes.",
        );
      }

      const currentBranch = GitUtilities.getCurrentBranch(this.execOpts);
      if (
        this.options.allowBranch &&
        ![].concat(this.options.allowBranch).some(x => minimatch(currentBranch, x))
      ) {
        throw new ValidationError(
          "ENOTALLOWED",
          dedent`
            Branch '${currentBranch}' is restricted from publishing due to allowBranch config.
            Please consider the reasons for this restriction before overriding the option.
          `,
        );
      }
    }

    this.updates = new UpdatedPackagesCollector(this).getUpdates();

    this.packagesToPublish = this.updates.map(update => update.package).filter(pkg => !pkg.isPrivate());

    this.packagesToPublishCount = this.packagesToPublish.length;
    try {
      this.batchedPackagesToPublish = this.toposort
        ? PackageUtilities.topologicallyBatchPackages(this.packagesToPublish, {
            // Don't sort based on devDependencies because that would increase the chance of dependency cycles
            // causing less-than-ideal a publishing order.
            depsOnly: true,
            rejectCycles: this.options.rejectCycles,
          })
        : [this.packagesToPublish];
    } catch (e) {
      return callback(e);
    }

    if (!this.updates.length) {
      this.logger.info("No updated packages to publish.");
      callback(null, false);
      return;
    }

    this.getVersionsForUpdates((err, { version, versions }) => {
      if (err) {
        callback(err);
        return;
      }

      this.masterVersion = version;
      this.updatesVersions =
        versions ||
        this.updates.reduce((acc, update) => {
          acc[update.package.name] = version;
          return acc;
        }, {});

      this.confirmVersions(callback);
    });
  }

  execute(callback) {
    if (!this.repository.isIndependent() && !this.options.canary) {
      this.updateVersionInLernaJson();
    }

    this.updateUpdatedPackages();

    if (this.gitEnabled) {
      this.commitAndTagUpdates();
    }

    if (this.options.skipNpm) {
      callback(null, true);
    } else {
      this.publishPackagesToNpm(callback);
    }
  }

  publishPackagesToNpm(callback) {
    this.logger.info("publish", "Publishing packages to npm...");

    this.npmPublish(publishError => {
      if (publishError) {
        callback(publishError);
        return;
      }

      if (this.options.canary) {
        this.logger.info("canary", "Resetting git state");
        // reset since the package.json files are changed
        this.repository.packageConfigs.forEach(pkgGlob => {
          GitUtilities.checkoutChanges(`${pkgGlob}/package.json`, this.execOpts);
        });
      }

      this.npmUpdateAsLatest(updateError => {
        if (updateError) {
          callback(updateError);
          return;
        }

        if (this.gitEnabled) {
          this.logger.info("git", "Pushing tags...");
          GitUtilities.pushWithTags(this.gitRemote, this.tags, this.execOpts);
        }

        const message = this.packagesToPublish.map(pkg => ` - ${pkg.name}@${pkg.version}`);

        output("Successfully published:");
        output(message.join(EOL));

        this.logger.success("publish", "finished");
        callback(null, true);
      });
    });
  }

  getVersionsForUpdates(callback) {
    const { canary, cdVersion, conventionalCommits, preid, repoVersion } = this.options;
    const independentVersions = this.repository.isIndependent();

    if (cdVersion && !canary) {
      if (independentVersions) {
        // Independent Semver Keyword Mode
        const versions = {};

        this.updates.forEach(update => {
          const { name, version } = update.package;
          versions[name] = semver.inc(version, cdVersion, preid);
        });

        return callback(null, { versions });
      }

      // Non-Independent Semver Keyword Mode
      const version = semver.inc(this.globalVersion, cdVersion, preid);
      return callback(null, { version });
    }

    if (repoVersion) {
      return callback(null, {
        version: repoVersion,
      });
    }

    if (canary) {
      if (independentVersions) {
        // Independent Canary Mode
        const versions = {};
        this.updates.forEach(update => {
          const { name, version } = update.package;
          versions[name] = this.getCanaryVersion(version, canary);
        });

        return callback(null, { versions });
      }

      // Non-Independent Canary Mode
      const version = this.getCanaryVersion(this.globalVersion, canary);
      return callback(null, { version });
    }

    if (conventionalCommits) {
      if (independentVersions) {
        // Independent Conventional-Commits Mode
        const versions = {};
        this.recommendVersions(
          this.updates,
          ConventionalCommitUtilities.recommendIndependentVersion,
          versionBump => {
            versions[versionBump.pkg.name] = versionBump.recommendedVersion;
          },
        );

        return callback(null, { versions });
      }

      // Non-Independent Conventional-Commits Mode
      const currentFixedVersion = this.repository.lernaJson.version;

      this.updates.forEach(update => {
        const pkg = update.package;
        if (semver.lt(pkg.version, currentFixedVersion)) {
          this.logger.verbose(
            "publish",
            `Overriding version of ${pkg.name} from  ${pkg.version} to ${currentFixedVersion}`,
          );
          pkg.version = currentFixedVersion;
        }
      });

      let version = "0.0.0";
      this.recommendVersions(this.updates, ConventionalCommitUtilities.recommendFixedVersion, versionBump => {
        if (semver.gt(versionBump.recommendedVersion, version)) {
          version = versionBump.recommendedVersion;
        }
      });

      return callback(null, { version });
    }

    if (independentVersions) {
      // Independent Non-Canary Mode
      async.mapLimit(
        this.updates,
        1,
        (update, cb) => {
          this.promptVersion(update.package.name, update.package.version, cb);
        },
        (err, versions) => {
          if (err) {
            return callback(err);
          }

          this.updates.forEach((update, index) => {
            versions[update.package.name] = versions[index];
          });

          return callback(null, { versions });
        },
      );
    } else {
      // Non-Independent Non-Canary Mode
      this.promptVersion(null, this.globalVersion, (err, version) => {
        if (err) {
          return callback(err);
        }
        return callback(null, { version });
      });
    }
  }

  recommendVersions(updates, recommendVersionFn, callback) {
    updates.forEach(update => {
      const pkg = {
        name: update.package.name,
        version: update.package.version,
        location: update.package.location,
      };

      const recommendedVersion = recommendVersionFn(pkg, this.changelogOpts);
      callback({ pkg, recommendedVersion });
    });
  }

  getCanaryVersion(version, _preid) {
    // FIXME: this complicated defaulting should be done in yargs option.coerce()
    const preid = _preid == null || typeof _preid !== "string" ? "alpha" : _preid;

    const release = this.options.cdVersion || "minor";
    const nextVersion = semver.inc(version, release);
    const hash = GitUtilities.getCurrentSHA(this.execOpts).slice(0, 8);
    return `${nextVersion}-${preid}.${hash}`;
  }

  promptVersion(packageName, currentVersion, callback) {
    const patch = semver.inc(currentVersion, "patch");
    const minor = semver.inc(currentVersion, "minor");
    const major = semver.inc(currentVersion, "major");
    const prepatch = semver.inc(currentVersion, "prepatch");
    const preminor = semver.inc(currentVersion, "preminor");
    const premajor = semver.inc(currentVersion, "premajor");

    let message = "Select a new version";
    if (packageName) {
      message += ` for ${packageName}`;
    }
    message += ` (currently ${currentVersion})`;

    PromptUtilities.select(
      message,
      {
        choices: [
          { value: patch, name: `Patch (${patch})` },
          { value: minor, name: `Minor (${minor})` },
          { value: major, name: `Major (${major})` },
          { value: prepatch, name: `Prepatch (${prepatch})` },
          { value: preminor, name: `Preminor (${preminor})` },
          { value: premajor, name: `Premajor (${premajor})` },
          { value: "PRERELEASE", name: "Prerelease" },
          { value: "CUSTOM", name: "Custom" },
        ],
      },
      choice => {
        switch (choice) {
          case "CUSTOM": {
            PromptUtilities.input(
              "Enter a custom version",
              {
                filter: semver.valid,
                validate: v => v !== null || "Must be a valid semver version",
              },
              input => {
                callback(null, input);
              },
            );
            break;
          }

          case "PRERELEASE": {
            const [existingId] = semver.prerelease(currentVersion) || [];
            const defaultVersion = semver.inc(currentVersion, "prerelease", existingId);
            const prompt = `(default: ${
              existingId ? `"${existingId}"` : "none"
            }, yielding ${defaultVersion})`;

            // TODO: allow specifying prerelease identifier as CLI option to skip the prompt
            PromptUtilities.input(
              `Enter a prerelease identifier ${prompt}`,
              {
                filter: v => {
                  const preid = v || existingId;
                  return semver.inc(currentVersion, "prerelease", preid);
                },
              },
              input => {
                callback(null, input);
              },
            );
            break;
          }

          default: {
            callback(null, choice);
            break;
          }
        }
      },
    );
  }

  confirmVersions(callback) {
    const changes = this.updates.map(update => {
      const pkg = update.package;
      let line = ` - ${pkg.name}: ${pkg.version} => ${this.updatesVersions[pkg.name]}`;
      if (pkg.isPrivate()) {
        line += ` (${chalk.red("private")})`;
      }
      return line;
    });

    output("");
    output("Changes:");
    output(changes.join(EOL));
    output("");

    if (this.options.yes) {
      this.logger.info("auto-confirmed");
      callback(null, true);
    } else {
      PromptUtilities.confirm("Are you sure you want to publish the above changes?", confirm => {
        callback(null, confirm);
      });
    }
  }

  updateVersionInLernaJson() {
    this.repository.lernaJson.version = this.masterVersion;
    writeJsonFile.sync(this.repository.lernaJsonLocation, this.repository.lernaJson, { indent: 2 });

    if (!this.options.skipGit) {
      GitUtilities.addFile(this.repository.lernaJsonLocation, this.execOpts);
    }
  }

  runSyncScriptInPackage(pkg, scriptName) {
    pkg.runScriptSync(scriptName, err => {
      if (err) {
        this.logger.error("publish", `error running ${scriptName} in ${pkg.name}\n`, err.stack || err);
      }
    });
  }

  updateUpdatedPackages() {
    const { exact, conventionalCommits } = this.options;
    const independentVersions = this.repository.isIndependent();
    const changedFiles = [];

    // exec preversion lifecycle in root (before all updates)
    this.runSyncScriptInPackage(this.repository.package, "preversion");

    this.updates.forEach(update => {
      const pkg = update.package;
      const packageJsonLocation = path.join(pkg.location, "package.json");

      // set new version
      pkg.version = this.updatesVersions[pkg.name] || pkg.version;

      // update pkg dependencies
      this.updatePackageDepsObject(pkg, "dependencies", exact);
      this.updatePackageDepsObject(pkg, "devDependencies", exact);

      // exec preversion script
      this.runSyncScriptInPackage(pkg, "preversion");

      // write new package
      writePkg.sync(packageJsonLocation, pkg.toJSON());
      // NOTE: Object.prototype.toJSON() is normally called when passed to
      // JSON.stringify(), but write-pkg iterates Object.keys() before serializing
      // so it has to be explicit here (otherwise it mangles the instance properties)

      // exec version script
      this.runSyncScriptInPackage(pkg, "version");

      // we can now generate the Changelog, based on the
      // the updated version that we're about to release.
      if (conventionalCommits) {
        if (independentVersions) {
          ConventionalCommitUtilities.updateIndependentChangelog(pkg, this.changelogOpts);
        } else {
          ConventionalCommitUtilities.updateFixedChangelog(pkg, this.changelogOpts);
        }

        changedFiles.push(ConventionalCommitUtilities.changelogLocation(pkg));
      }

      // push to be git committed
      changedFiles.push(packageJsonLocation);
    });

    if (conventionalCommits && !independentVersions) {
      const rootPkg = this.repository.packageJson;

      ConventionalCommitUtilities.updateFixedRootChangelog(
        {
          name: rootPkg && rootPkg.name ? rootPkg.name : "root",
          location: this.repository.rootPath,
        },
        this.changelogOpts,
      );

      changedFiles.push(
        ConventionalCommitUtilities.changelogLocation({
          location: this.repository.rootPath,
        }),
      );
    }

    // exec version lifecycle in root (after all updates)
    this.runSyncScriptInPackage(this.repository.package, "version");

    if (this.gitEnabled) {
      changedFiles.forEach(file => GitUtilities.addFile(file, this.execOpts));
    }
  }

  updatePackageDepsObject(pkg, depsKey, exact) {
    const deps = pkg[depsKey];

    if (!deps) {
      return;
    }

    this.packageGraph.get(pkg.name).dependencies.forEach(depName => {
      const version = this.updatesVersions[depName];

      if (deps[depName] && version) {
        deps[depName] = exact ? version : `^${version}`;
      }
    });
  }

  commitAndTagUpdates() {
    if (this.repository.isIndependent()) {
      this.tags = this.gitCommitAndTagVersionForUpdates();
    } else {
      this.tags = [this.gitCommitAndTagVersion(this.masterVersion)];
    }

    // run the postversion script for each update
    this.updates.forEach(update => {
      this.runSyncScriptInPackage(update.package, "postversion");
    });

    // run postversion, if set in the root directory
    this.runSyncScriptInPackage(this.repository.package, "postversion");
  }

  gitCommitAndTagVersionForUpdates() {
    const tags = this.updates.map(({ package: { name } }) => `${name}@${this.updatesVersions[name]}`);
    const subject = this.options.message || "Publish";
    const message = tags.reduce((msg, tag) => `${msg}${EOL} - ${tag}`, `${subject}${EOL}`);

    GitUtilities.commit(message, this.execOpts);
    tags.forEach(tag => GitUtilities.addTag(tag, this.execOpts));

    return tags;
  }

  gitCommitAndTagVersion(version) {
    const tag = `v${version}`;
    const message = (this.options.message && this.options.message.replace(/%s/g, tag)) || tag;

    GitUtilities.commit(message, this.execOpts);
    GitUtilities.addTag(tag, this.execOpts);

    return tag;
  }

  execScript(pkg, script) {
    const scriptLocation = path.join(pkg.location, "scripts", `${script}.js`);

    if (FileSystemUtilities.existsSync(scriptLocation)) {
      // eslint-disable-next-line import/no-dynamic-require, global-require
      require(scriptLocation);
    } else {
      this.logger.verbose("execScript", `No ${script} script found at ${scriptLocation}`);
    }
  }

  npmPublish(callback) {
    const tracker = this.logger.newItem("npmPublish");

    // if we skip temp tags we should tag with the proper value immediately
    // therefore no updates will be needed
    const tag = this.options.tempTag ? "lerna-temp" : this.getDistTag();

    this.updates.forEach(update => {
      this.execScript(update.package, "prepublish");
    });

    tracker.addWork(this.packagesToPublishCount);

    PackageUtilities.runParallelBatches(
      this.batchedPackagesToPublish,
      pkg => {
        let attempts = 0;

        const run = cb => {
          tracker.verbose("publishing", pkg.name);

          NpmUtilities.publishTaggedInDir(tag, pkg, this.npmConfig, err => {
            // FIXME: this err.stack conditional is too cute
            err = (err && err.stack) || err; // eslint-disable-line no-param-reassign

            if (
              !err ||
              // publishing over an existing package which is likely due to a timeout or something
              err.indexOf("You cannot publish over the previously published version") > -1
            ) {
              tracker.info("published", pkg.name);
              tracker.completeWork(1);
              this.execScript(pkg, "postpublish");
              cb();
              return;
            }

            attempts += 1;

            if (attempts < 5) {
              this.logger.error("publish", "Retrying failed publish:", pkg.name);
              this.logger.verbose("publish error", err.message);
              run(cb);
            } else {
              this.logger.error("publish", "Ran out of retries while publishing", pkg.name, err.stack || err);
              cb(err);
            }
          });
        };

        return run;
      },
      this.concurrency,
      err => {
        tracker.finish();
        callback(err);
      },
    );
  }

  npmUpdateAsLatest(callback) {
    if (!this.options.tempTag) {
      return callback();
    }

    const tracker = this.logger.newItem("npmUpdateAsLatest");
    tracker.addWork(this.packagesToPublishCount);

    PackageUtilities.runParallelBatches(
      this.batchedPackagesToPublish,
      pkg => cb => {
        let attempts = 0;

        // eslint-disable-next-line no-constant-condition
        while (true) {
          attempts += 1;

          try {
            this.updateTag(pkg);
            tracker.info("latest", pkg.name);
            tracker.completeWork(1);
            cb();
            break;
          } catch (err) {
            if (attempts < 5) {
              this.logger.error("publish", "Error updating version as latest", err.stack || err);
              continue; // eslint-disable-line no-continue
            } else {
              cb(err);
              return;
            }
          }
        }
      },
      4,
      err => {
        tracker.finish();
        callback(err);
      },
    );
  }

  updateTag(pkg) {
    const distTag = this.getDistTag();

    if (NpmUtilities.checkDistTag(pkg.location, pkg.name, "lerna-temp", this.npmRegistry)) {
      NpmUtilities.removeDistTag(pkg.location, pkg.name, "lerna-temp", this.npmRegistry);
    }

    /* eslint-disable max-len */
    // TODO: fix this API to be less verbose with parameters
    if (this.options.npmTag) {
      NpmUtilities.addDistTag(
        pkg.location,
        pkg.name,
        this.updatesVersions[pkg.name],
        distTag,
        this.npmRegistry,
      );
    } else if (this.options.canary) {
      NpmUtilities.addDistTag(pkg.location, pkg.name, pkg.version, distTag, this.npmRegistry);
    } else {
      NpmUtilities.addDistTag(
        pkg.location,
        pkg.name,
        this.updatesVersions[pkg.name],
        distTag,
        this.npmRegistry,
      );
    }
    /* eslint-enable max-len */
  }

  getDistTag() {
    return this.options.npmTag || (this.options.canary && "canary") || "latest";
  }
}


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`commit: first feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
"v1.1.0
"
`;

exports[`commit: first feature added in fixed mode --conventional-commits 1`] = `
"v1.1.0
"
`;

exports[`commit: initial commit in fixed mode --conventional-commits --force-publish=* 1`] = `
"v1.0.1
"
`;

exports[`commit: initial commit in fixed mode --conventional-commits 1`] = `
"v1.0.1
"
`;

exports[`commit: second feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
"v1.2.0
"
`;

exports[`commit: second feature added in fixed mode --conventional-commits 1`] = `
"v1.2.0
"
`;

exports[`commit: updates fixed versions 1`] = `
"v1.0.1
"
`;

exports[`commit: updates independent versions 1`] = `
"Publish

 - package-1@2.0.0
 - package-2@3.0.0
 - package-3@4.0.0
 - package-4@5.0.0
 - package-5@6.0.0
"
`;

exports[`packages: first feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
Array [
  Object {
    "foo": true,
    "name": "package-1",
    "version": "1.1.0",
  },
  Object {
    "name": "package-2",
    "version": "1.1.0",
  },
]
`;

exports[`packages: first feature added in fixed mode --conventional-commits 1`] = `
Array [
  Object {
    "foobar": true,
    "name": "package-1",
    "version": "1.1.0",
  },
  Object {
    "name": "package-2",
    "version": "1.0.1",
  },
]
`;

exports[`packages: initial commit in fixed mode --conventional-commits --force-publish=* 1`] = `
Array [
  Object {
    "name": "package-1",
    "version": "1.0.1",
  },
  Object {
    "name": "package-2",
    "version": "1.0.1",
  },
]
`;

exports[`packages: initial commit in fixed mode --conventional-commits 1`] = `
Array [
  Object {
    "name": "package-1",
    "version": "1.0.1",
  },
  Object {
    "name": "package-2",
    "version": "1.0.1",
  },
]
`;

exports[`packages: second feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
Array [
  Object {
    "foo": true,
    "name": "package-1",
    "version": "1.2.0",
  },
  Object {
    "bar": true,
    "name": "package-2",
    "version": "1.2.0",
  },
]
`;

exports[`packages: second feature added in fixed mode --conventional-commits 1`] = `
Array [
  Object {
    "foobar": true,
    "name": "package-1",
    "version": "1.1.0",
  },
  Object {
    "baz": true,
    "name": "package-2",
    "version": "1.2.0",
  },
]
`;

exports[`packages: updates fixed versions 1`] = `
Array [
  Object {
    "name": "package-1",
    "version": "1.0.1",
  },
  Object {
    "dependencies": Object {
      "package-1": "^1.0.1",
    },
    "name": "package-2",
    "version": "1.0.1",
  },
  Object {
    "devDependencies": Object {
      "package-2": "^1.0.1",
    },
    "name": "package-3",
    "peerDependencies": Object {
      "package-2": "^1.0.0",
    },
    "version": "1.0.1",
  },
  Object {
    "dependencies": Object {
      "package-1": "^0.0.0",
    },
    "name": "package-4",
    "version": "1.0.1",
  },
  Object {
    "dependencies": Object {
      "package-1": "^1.0.1",
    },
    "name": "package-5",
    "private": true,
    "version": "1.0.1",
  },
]
`;

exports[`packages: updates independent versions 1`] = `
Array [
  Object {
    "name": "package-1",
    "version": "2.0.0",
  },
  Object {
    "dependencies": Object {
      "package-1": "^2.0.0",
    },
    "name": "package-2",
    "version": "3.0.0",
  },
  Object {
    "devDependencies": Object {
      "package-2": "^3.0.0",
    },
    "name": "package-3",
    "version": "4.0.0",
  },
  Object {
    "dependencies": Object {
      "package-1": "^0.0.0",
    },
    "name": "package-4",
    "version": "5.0.0",
  },
  Object {
    "dependencies": Object {
      "package-3": "^4.0.0",
    },
    "name": "package-5",
    "private": true,
    "version": "6.0.0",
  },
]
`;

exports[`stderr: canary beta version 1`] = `
"lerna info version __TEST_VERSION__
lerna info canary enabled
lerna info current version 1.0.0
lerna info Checking for updated packages...
lerna info Comparing with initial commit.
lerna info Checking for prereleased packages...
lerna info auto-confirmed "
`;

exports[`stderr: canary default version 1`] = `
"lerna info version __TEST_VERSION__
lerna info canary enabled
lerna info current version 1.0.0
lerna info Checking for updated packages...
lerna info Comparing with initial commit.
lerna info Checking for prereleased packages...
lerna info auto-confirmed "
`;

exports[`stderr: exit 0 when no updates 1`] = `
"lerna info version __TEST_VERSION__
lerna info current version 1.0.0
lerna info Checking for updated packages...
lerna info Comparing with v1.0.0.
lerna info Checking for prereleased packages...
lerna info No updated packages to publish. "
`;

exports[`stderr: updates fixed versions 1`] = `
"lerna info version __TEST_VERSION__
lerna info current version 1.0.0
lerna info Checking for updated packages...
lerna info Comparing with initial commit.
lerna info Checking for prereleased packages...
lerna info auto-confirmed "
`;

exports[`stderr: updates independent versions 1`] = `
"lerna info version __TEST_VERSION__
lerna info versioning independent
lerna info Checking for updated packages...
lerna info Comparing with initial commit.
lerna info Checking for prereleased packages...
lerna info auto-confirmed "
`;

exports[`stdout: canary beta version 1`] = `
"
Changes:
 - package-1: 1.0.0 => 1.1.0-beta.hash
 - package-2: 1.0.0 => 1.1.0-beta.hash
 - package-3: 1.0.0 => 1.1.0-beta.hash
 - package-4: 1.0.0 => 1.1.0-beta.hash
 - package-5: 1.0.0 => 1.1.0-beta.hash (private)
"
`;

exports[`stdout: canary default version 1`] = `
"
Changes:
 - package-1: 1.0.0 => 1.1.0-alpha.hash
 - package-2: 1.0.0 => 1.1.0-alpha.hash
 - package-3: 1.0.0 => 1.1.0-alpha.hash
 - package-4: 1.0.0 => 1.1.0-alpha.hash
 - package-5: 1.0.0 => 1.1.0-alpha.hash (private)
"
`;

exports[`stdout: exit 0 when no updates 1`] = `""`;

exports[`stdout: first feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
"
Changes:
 - package-1: 1.0.1 => 1.1.0
 - package-2: 1.0.1 => 1.1.0
"
`;

exports[`stdout: first feature added in fixed mode --conventional-commits 1`] = `
"
Changes:
 - package-1: 1.0.1 => 1.1.0
"
`;

exports[`stdout: initial commit in fixed mode --conventional-commits --force-publish=* 1`] = `
"
Changes:
 - package-1: 1.0.0 => 1.0.1
 - package-2: 1.0.0 => 1.0.1
"
`;

exports[`stdout: initial commit in fixed mode --conventional-commits 1`] = `
"
Changes:
 - package-1: 1.0.0 => 1.0.1
 - package-2: 1.0.0 => 1.0.1
"
`;

exports[`stdout: second feature added in fixed mode --conventional-commits --force-publish=* 1`] = `
"
Changes:
 - package-1: 1.1.0 => 1.2.0
 - package-2: 1.1.0 => 1.2.0
"
`;

exports[`stdout: second feature added in fixed mode --conventional-commits 1`] = `
"
Changes:
 - package-2: 1.1.0 => 1.2.0
"
`;

exports[`stdout: updates fixed versions 1`] = `
"
Changes:
 - package-1: 1.0.0 => 1.0.1
 - package-2: 1.0.0 => 1.0.1
 - package-3: 1.0.0 => 1.0.1
 - package-4: 1.0.0 => 1.0.1
 - package-5: 1.0.0 => 1.0.1 (private)
"
`;

exports[`stdout: updates independent versions 1`] = `
"
Changes:
 - package-1: 1.0.0 => 2.0.0
 - package-2: 2.0.0 => 3.0.0
 - package-3: 3.0.0 => 4.0.0
 - package-4: 4.0.0 => 5.0.0
 - package-5: 5.0.0 => 6.0.0 (private)
"
`;


it("commits changes with a custom message using %v", () =>

run(testDir)("--message", "chore: Release %v :rocket:").then(() => {

expect(GitUtilities.commit).lastCalledWith("chore: Release 1.0.1 :rocket:", execOpts(testDir));


import _ from "lodash";

import ChildProcessUtilities from "../ChildProcessUtilities";
import Command from "../Command";
import GitUtilities from "../GitUtilities";

export function handler(argv) {
  // eslint-disable-next-line no-use-before-define
  const cmd = new DiffCommand([argv.pkg], argv, argv._cwd);
  return cmd.run().then(argv._onResolved, argv._onRejected);
}

export const command = "diff [pkg]";

export const describe = "Diff all packages or a single package since the last release.";

export const builder = {};

function getLastCommit(execOpts) {
  if (GitUtilities.hasTags(execOpts)) {
    return GitUtilities.getLastTaggedCommit(execOpts);
  }

  return GitUtilities.getFirstCommit(execOpts);
}

export default class DiffCommand extends Command {
  initialize(callback) {
    const packageName = this.input[0];

    // don't interrupt spawned or streaming stdio
    this.logger.disableProgress();

    let targetPackage;

    if (packageName) {
      targetPackage = _.find(this.packages, pkg => pkg.name === packageName);

      if (!targetPackage) {
        callback(new Error(`Package '${packageName}' does not exist.`));
        return;
      }
    }

    if (!GitUtilities.hasCommit(this.execOpts)) {
      callback(new Error("Can't diff. There are no commits in this repository, yet."));
      return;
    }

    this.args = ["diff", getLastCommit(this.execOpts), "--color=auto"];

    if (targetPackage) {
      this.args.push("--", targetPackage.location);
    } else {
      this.args.push("--", ...this.repository.packageParentDirs);
    }

    callback(null, true);
  }

  execute(callback) {
    ChildProcessUtilities.spawn("git", this.args, this.execOpts, err => {
      if (err && err.code) {
        callback(err);
      } else {
        callback(null, true);
      }
    });
  }
}


import { htmlTag, url_for } from 'hexo-util';
import moize from 'moize';

function jsHelper(...args) {
  let result = '\n';

  args.flat(Infinity).forEach(item => {
    if (typeof item === 'string' || item instanceof String) {
      let path = item;
      if (!path.endsWith('.js')) {
        path += '.js';
      }
      result += `<script src="${url_for.call(this, path)}"></script>\n`;
    } else {
      // Custom attributes
      item.src = url_for.call(this, item.src);
      if (!item.src.endsWith('.js')) item.src += '.js';
      result += htmlTag('script', { ...item }, '') + '\n';
    }
  });
  return result;
}

export = moize(jsHelper, {
  maxSize: 10,
  isDeepEqual: true
});


import { encodeURL } from 'hexo-util';

/**
 * Asset path tag
 *
 * Syntax:
 *   {% asset_path slug %}
 */
export = ctx => {
  const PostAsset = ctx.model('PostAsset');

  return function assetPathTag(args) {
    const slug = args.shift();
    if (!slug) return;

    const asset = PostAsset.findOne({post: this._id, slug});
    if (!asset) return;

    const path = encodeURL(new URL(asset.path, ctx.config.url).pathname);

    return path;
  };
};


'use strict';

describe('Theme', () => {
  require('./theme');
  require('./view');
});


'use strict';

const decache = require('decache');

describe('Meta Generator', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo();
  let metaGenerator;
  const cheerio = require('cheerio');

  beforeEach(() => {
    decache('../../../dist/plugins/filter/after_render/meta_generator');
    metaGenerator = require('../../../dist/plugins/filter/after_render/meta_generator').bind(hexo);
  });

  it('default', () => {
    const content = '<link>';
    const result = metaGenerator(content);

    const $ = cheerio.load(result);
    $('meta[name="generator"]').should.have.lengthOf(1);
    $('meta[name="generator"]').attr('content').should.eql(`Hexo ${hexo.version}`);
  });

  it('disable meta_generator', () => {
    const content = '<link>';
    hexo.config.meta_generator = false;
    const result = metaGenerator(content);

    should.not.exist(result);
  });

  it('no duplicate generator tag', () => {
    hexo.config.meta_generator = true;

    should.not.exist(metaGenerator('<link><meta name="generator" content="foo">'));
    should.not.exist(metaGenerator('<link><meta content="foo" name="generator">'));
  });

  // Test for Issue #3777
  it('multi-line head', () => {
    const content = '\n<link>\n';
    hexo.config.meta_generator = true;
    const result = metaGenerator(content);

    const $ = cheerio.load(result);
    $('meta[name="generator"]').should.have.lengthOf(1);

    const expected = '\n<link>\n<meta name="generator" content="Hexo ' + hexo.version + '">';

    result.should.eql(expected);
  });
});


if (typeof item === 'string' || item instanceof String) {


import { stripIndent } from 'hexo-util';
import { cyan, magenta, red, bold } from 'picocolors';
import { Environment } from 'nunjucks';
import Promise from 'bluebird';
const rSwigRawFullBlock = /{% *raw *%}/;
const rCodeTag = /<code[^<>]*>[\s\S]+?<\/code>/g;
const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}');

interface TagFunction {
  (args: any[], content: string): string;
}
interface AsyncTagFunction {
  (args: any[], content: string): Promise<string>;
}

class NunjucksTag {
  public tags: string[];
  public fn: TagFunction | AsyncTagFunction;

  constructor(name: string, fn: TagFunction | AsyncTagFunction) {
    this.tags = [name];
    this.fn = fn;
  }

  parse(parser, nodes, lexer) {
    const node = this._parseArgs(parser, nodes, lexer);

    return new nodes.CallExtension(this, 'run', node, []);
  }

  _parseArgs(parser, nodes, lexer) {
    const tag = parser.nextToken();
    const node = new nodes.NodeList(tag.lineno, tag.colno);
    const argarray = new nodes.Array(tag.lineno, tag.colno);

    let token;
    let argitem = '';

    while ((token = parser.nextToken(true))) {
      if (token.type === lexer.TOKEN_WHITESPACE || token.type === lexer.TOKEN_BLOCK_END) {
        if (argitem !== '') {
          const argnode = new nodes.Literal(tag.lineno, tag.colno, argitem.trim());
          argarray.addChild(argnode);
          argitem = '';
        }

        if (token.type === lexer.TOKEN_BLOCK_END) {
          break;
        }
      } else {
        argitem += token.value;
      }
    }

    node.addChild(argarray);

    return node;
  }

  run(context, args, body, callback) {
    return this._run(context, args, '');
  }

  _run(context, args, body): any {
    return Reflect.apply(this.fn, context.ctx, [args, body]);
  }
}

const trimBody = body => {
  return stripIndent(body()).replace(/^\n?|\n?$/g, '');
};

class NunjucksBlock extends NunjucksTag {
  parse(parser, nodes, lexer) {
    const node = this._parseArgs(parser, nodes, lexer);
    const body = this._parseBody(parser, nodes, lexer);

    return new nodes.CallExtension(this, 'run', node, [body]);
  }

  _parseBody(parser, nodes, lexer) {
    const body = parser.parseUntilBlocks(`end${this.tags[0]}`);

    parser.advanceAfterBlockEnd();
    return body;
  }

  run(context, args, body, callback) {
    return this._run(context, args, trimBody(body));
  }
}

class NunjucksAsyncTag extends NunjucksTag {
  parse(parser, nodes, lexer) {
    const node = this._parseArgs(parser, nodes, lexer);

    return new nodes.CallExtensionAsync(this, 'run', node, []);
  }

  run(context, args, callback) {
    return this._run(context, args, '').then(result => {
      callback(null, result);
    }, callback);
  }
}

class NunjucksAsyncBlock extends NunjucksBlock {
  parse(parser, nodes, lexer) {
    const node = this._parseArgs(parser, nodes, lexer);
    const body = this._parseBody(parser, nodes, lexer);

    return new nodes.CallExtensionAsync(this, 'run', node, [body]);
  }

  run(context, args, body, callback) {
    // enable async tag nesting
    body((err, result) => {
      // wrapper for trimBody expecting
      // body to be a function
      body = () => result || '';

      this._run(context, args, trimBody(body)).then(result => {
        callback(err, result);
      });
    });
  }
}

const getContextLineNums = (min, max, center, amplitude) => {
  const result = [];
  let lbound = Math.max(min, center - amplitude);
  const hbound = Math.min(max, center + amplitude);
  while (lbound <= hbound) result.push(lbound++);
  return result;
};

const LINES_OF_CONTEXT = 5;

const getContext = (lines, errLine, location, type) => {
  const message = [
    location + ' ' + red(type),
    cyan('    =====               Context Dump               ====='),
    cyan('    === (line number probably different from source) ===')
  ];

  message.push(
    // get LINES_OF_CONTEXT lines surrounding `errLine`
    ...getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT)
      .map(lnNum => {
        const line = '  ' + lnNum + ' | ' + lines[lnNum - 1];
        if (lnNum === errLine) {
          return cyan(bold(line));
        }

        return cyan(line);
      })
  );
  message.push(cyan(
    '    =====             Context Dump Ends            ====='));

  return message;
};

class NunjucksError extends Error {
  line?: number;
  location?: string;
  type?: string;
}

/**
 * Provide context for Nunjucks error
 * @param  {Error}    err Nunjucks error
 * @param  {string}   str string input for Nunjucks
 * @return {Error}    New error object with embedded context
 */
const formatNunjucksError = (err, input, source = '') => {
  err.message = err.message.replace('(unknown path)', source ? magenta(source) : '');

  const match = err.message.match(/Line (\d+), Column \d+/);
  if (!match) return err;
  const errLine = parseInt(match[1], 10);
  if (isNaN(errLine)) return err;

  // trim useless info from Nunjucks Error
  const splited = err.message.split('\n');

  const e = new NunjucksError();
  e.name = 'Nunjucks Error';
  e.line = errLine;
  e.location = splited[0];
  e.type = splited[1].trim();
  e.message = getContext(input.split(/\r?\n/), errLine, e.location, e.type).join('\n');
  return e;
};

type RegisterOptions = {
  async?: boolean;
  ends?: boolean;
}

class Tag {
  public env: any;
  public source: any;

  constructor() {
    this.env = new Environment(null, {
      autoescape: false
    });
  }

  register(name: string, fn: TagFunction): void
  register(name: string, fn: TagFunction, ends: boolean): void
  register(name: string, fn: TagFunction, options: RegisterOptions): void
  register(name: string, fn: TagFunction, options?: RegisterOptions | boolean) {
    if (!name) throw new TypeError('name is required');
    if (typeof fn !== 'function') throw new TypeError('fn must be a function');

    if (options == null || typeof options === 'boolean') {
      options = { ends: options as boolean };
    }

    let tag: NunjucksTag;

    if (options.async) {
      let asyncFn: AsyncTagFunction;
      if (fn.length > 2) {
        asyncFn = Promise.promisify(fn);
      } else {
        asyncFn = Promise.method(fn);
      }

      if (options.ends) {
        tag = new NunjucksAsyncBlock(name, asyncFn);
      } else {
        tag = new NunjucksAsyncTag(name, asyncFn);
      }
    } else if (options.ends) {
      tag = new NunjucksBlock(name, fn);
    } else {
      tag = new NunjucksTag(name, fn);
    }

    this.env.addExtension(name, tag);
  }

  unregister(name) {
    if (!name) throw new TypeError('name is required');

    const { env } = this;

    if (env.hasExtension(name)) env.removeExtension(name);
  }

  render(str, options: { source?: string } = {}, callback) {
    if (!callback && typeof options === 'function') {
      callback = options;
      options = {};
    }

    // Get path of post from source
    const { source = '' } = options;

    return Promise.fromCallback(cb => {
      this.env.renderString(
        str.replace(rCodeTag, s => {
          // https://hexo.io/docs/tag-plugins#Raw
          // https://mozilla.github.io/nunjucks/templating.html#raw
          // Only escape code block when there is no raw tag included
          return s.match(rSwigRawFullBlock) ? s : escapeSwigTag(s);
        }),
        options,
        cb
      );
    }).catch(err => {
      return Promise.reject(formatNunjucksError(err, str, source));
    })
      .asCallback(callback);
  }
}

export = Tag;


import type Hexo from '../../hexo';

export = (ctx: Hexo) => {
  const { helper } = ctx.extend;

  const date = require('./date');

  helper.register('date', date.date);
  helper.register('date_xml', date.date_xml);
  helper.register('time', date.time);
  helper.register('full_date', date.full_date);
  helper.register('relative_date', date.relative_date);
  helper.register('time_tag', date.time_tag);
  helper.register('moment', date.moment);

  helper.register('search_form', require('./search_form'));

  const { strip_html, trim, titlecase, word_wrap, truncate, escape_html } = require('./format');

  helper.register('strip_html', strip_html);
  helper.register('trim', trim);
  helper.register('titlecase', titlecase);
  helper.register('word_wrap', word_wrap);
  helper.register('truncate', truncate);
  helper.register('escape_html', escape_html);

  helper.register('fragment_cache', require('./fragment_cache')(ctx));

  helper.register('gravatar', require('./gravatar'));

  const is = require('./is');
  helper.register('is_current', is.current);
  helper.register('is_home', is.home);
  helper.register('is_home_first_page', is.home_first_page);
  helper.register('is_post', is.post);
  helper.register('is_page', is.page);
  helper.register('is_archive', is.archive);
  helper.register('is_year', is.year);
  helper.register('is_month', is.month);
  helper.register('is_category', is.category);
  helper.register('is_tag', is.tag);

  helper.register('list_archives', require('./list_archives'));
  helper.register('list_categories', require('./list_categories'));
  helper.register('list_tags', require('./list_tags'));
  helper.register('list_posts', require('./list_posts'));

  helper.register('meta_generator', require('./meta_generator'));

  helper.register('open_graph', require('./open_graph'));

  helper.register('number_format', require('./number_format'));

  helper.register('paginator', require('./paginator'));

  helper.register('partial', require('./partial')(ctx));

  helper.register('markdown', require('./markdown'));
  helper.register('render', require('./render')(ctx));

  helper.register('css', require('./css'));
  helper.register('js', require('./js'));
  helper.register('link_to', require('./link_to'));
  helper.register('mail_to', require('./mail_to'));
  helper.register('image_tag', require('./image_tag'));
  helper.register('favicon_tag', require('./favicon_tag'));
  helper.register('feed_tag', require('./feed_tag'));

  const tagcloud = require('./tagcloud');
  helper.register('tagcloud', tagcloud);
  helper.register('tag_cloud', tagcloud);

  helper.register('toc', require('./toc'));

  helper.register('relative_url', require('./relative_url'));
  helper.register('url_for', require('./url_for'));
  helper.register('full_url_for', require('./full_url_for'));

  const debug = require('./debug');
  helper.register('inspect', debug.inspectObject);
  helper.register('log', debug.log);
};


import { Pattern } from 'hexo-util';

function i18nLocalsFilter(locals) {
  const { i18n } = this.theme;
  const { config } = this;
  const i18nDir = config.i18n_dir;
  const { page } = locals;
  let lang = page.lang || page.language;
  const i18nLanguages = i18n.list();
  const i18nConfigLanguages = i18n.languages;

  if (!lang) {
    const pattern = new Pattern(`${i18nDir}/*path`);
    const data = pattern.match(locals.path);

    if (data && data.lang && i18nLanguages.includes(data.lang)) {
      lang = data.lang;
      page.canonical_path = data.path;
    } else {
      // i18n.languages is always an array with at least one argument ('default')
      lang = i18nConfigLanguages[0];
    }
  }

  page.lang = lang;
  page.canonical_path = page.canonical_path || locals.path;

  const languages = [...new Set([].concat(lang, i18nConfigLanguages, i18nLanguages).filter(Boolean))];

  locals.__ = i18n.__(languages);
  locals._p = i18n._p(languages);
}

export = i18nLocalsFilter;


export = moize(cssHelper, {
maxSize: 10,
isDeepEqual: true
});

isDeepEqual: true,
updateCacheForKey() {
return relative_link;
}


'use strict';

const { url_for } = require('hexo-util');

describe('paginator', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo(__dirname);

  const ctx = {
    page: {
      base: '',
      total: 10
    },
    site: hexo.locals,
    config: hexo.config
  };

  const paginator = require('../../../dist/plugins/helper/paginator').bind(ctx);

  function link(i) {
    return url_for.call(ctx, i === 1 ? '' : 'page/' + i + '/');
  }

  function checkResult(result, data) {
    let expected = '';
    const current = data.current;
    const total = data.total;
    const pages = data.pages;
    const space = data.space || '…';
    const prevNext = Object.prototype.hasOwnProperty.call(data, 'prev_next') ? data.prev_next : true;
    let num;

    if (prevNext && current > 1) {
      expected += 'Prev';
    }

    for (let i = 0, len = pages.length; i < len; i++) {
      num = pages[i];

      if (!num) {
        expected += '' + space + '';
      } else if (num === current) {
        expected += '' + current + '';
      } else {
        expected += '' + num + '';
      }
    }

    if (prevNext && current < total) {
      expected += 'Next';
    }

    result.should.eql(expected);
  }

  [
    [1, 2, 3, 0, 10],
    [1, 2, 3, 4, 0, 10],
    [1, 2, 3, 4, 5, 0, 10],
    [1, 2, 3, 4, 5, 6, 0, 10],
    [1, 0, 3, 4, 5, 6, 7, 0, 10],
    [1, 0, 4, 5, 6, 7, 8, 0, 10],
    [1, 0, 5, 6, 7, 8, 9, 10],
    [1, 0, 6, 7, 8, 9, 10],
    [1, 0, 7, 8, 9, 10],
    [1, 0, 8, 9, 10]
  ].forEach((pages, i, arr) => {
    const current = i + 1;
    const total = arr.length;

    it('current = ' + current, () => {
      const result = paginator({
        current,
        total
      });

      checkResult(result, {
        current,
        total,
        pages
      });
    });
  });

  it('show_all', () => {
    const result = paginator({
      current: 5,
      show_all: true
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    });
  });

  it('end_size', () => {
    const result = paginator({
      current: 5,
      end_size: 2
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 2, 3, 4, 5, 6, 7, 0, 9, 10]
    });
  });

  it('end_size = 0', () => {
    const result = paginator({
      current: 5,
      end_size: 0
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [0, 3, 4, 5, 6, 7, 0]
    });
  });

  it('mid_size', () => {
    const result = paginator({
      current: 5,
      mid_size: 1
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 0, 4, 5, 6, 0, 10]
    });
  });

  it('mid_size = 0', () => {
    const result = paginator({
      current: 5,
      mid_size: 0
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 0, 5, 0, 10]
    });
  });

  it('space', () => {
    const result = paginator({
      current: 5,
      space: '~'
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 0, 3, 4, 5, 6, 7, 0, 10],
      space: '~'
    });
  });

  it('no space', () => {
    const result = paginator({
      current: 5,
      space: ''
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 3, 4, 5, 6, 7, 10]
    });
  });

  it('base', () => {
    const result = paginator({
      current: 1,
      base: 'archives/'
    });

    result.should.eql([
      '1',
      '2',
      '3',
      '…',
      '10',
      'Next'
    ].join(''));
  });

  it('format', () => {
    const result = paginator({
      current: 1,
      format: 'index-%d.html'
    });

    result.should.eql([
      '1',
      '2',
      '3',
      '…',
      '10',
      'Next'
    ].join(''));
  });

  it('prev_text / next_text', () => {
    const result = paginator({
      current: 2,
      prev_text: 'Newer',
      next_text: 'Older'
    });

    result.should.eql([
      'Newer',
      '1',
      '2',
      '3',
      '4',
      '…',
      '10',
      'Older'
    ].join(''));
  });

  it('prev_next', () => {
    const result = paginator({
      current: 2,
      prev_next: false
    });

    result.should.eql([
      '1',
      '2',
      '3',
      '4',
      '…',
      '10'
    ].join(''));
  });

  it('transform', () => {
    const result = paginator({
      current: 2,
      transform(page) {
        return 'Page ' + page;
      }
    });

    result.should.eql([
      'Prev',
      'Page 1',
      'Page 2',
      'Page 3',
      'Page 4',
      '…',
      'Page 10',
      'Next'
    ].join(''));
  });

  it('context', () => {
    ctx.page.current = 5;
    const result = paginator({
      space: ''
    });

    checkResult(result, {
      current: 5,
      total: 10,
      pages: [1, 3, 4, 5, 6, 7, 10]
    });
  });

  it('current = 0', () => {
    ctx.page.current = 0;
    const result = paginator({});

    result.should.eql('');
  });

  it('escape', () => {
    const result = paginator({
      current: 2,
      prev_text: '<foo>',
      next_text: '<bar>',
      escape: false
    });

    result.should.eql([
      '',
      '<foo>',
      '1',
      '2',
      '3',
      '4',
      '…',
      '10',
      '',
      '<bar>'
    ].join(''));
  });

  it('custom_class', () => {
    const result = paginator({
      current: 2,
      current_class: 'current-class',
      space_class: 'space-class',
      page_class: 'page-class',
      prev_class: 'prev-class',
      next_class: 'next-class'
    });

    result.should.eql([
      'Prev',
      '1',
      '2',
      '3',
      '4',
      '…',
      '10',
      'Next'
    ].join(''));
  });

  it('force_prev_next', () => {
    const result = paginator({
      current: 1,
      force_prev_next: true
    });

    result.should.eql([
      'Prev',
      '1',
      '2',
      '3',
      '…',
      '10',
      'Next'
    ].join(''));
  });

  it('force_prev_next - 2', () => {
    const result = paginator({
      current: 1,
      prev_next: false,
      force_prev_next: true
    });

    result.should.eql([
      'Prev',
      '1',
      '2',
      '3',
      '…',
      '10',
      'Next'
    ].join(''));
  });
});


'use strict';

const { join } = require('path');
const { exists, readFile, rmdir, unlink, writeFile } = require('hexo-fs');

describe('Scaffold', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo(__dirname);
  const scaffold = hexo.scaffold;
  const scaffoldDir = hexo.scaffold_dir;

  const testContent = [
    '---',
    'title: {{ title }}',
    '---',
    'test scaffold'
  ].join('\n');

  const testPath = join(scaffoldDir, 'test.md');

  before(async () => {
    await hexo.init();
    await writeFile(testPath, testContent);
  });

  after(() => rmdir(scaffoldDir));

  it('get() - file exists', async () => {
    const data = await scaffold.get('test');
    data.should.eql(testContent);
  });

  it('get() - normal scaffold', async () => {
    const data = await scaffold.get('normal');
    data.should.eql(scaffold.defaults.normal);
  });

  it('set() - file exists', async () => {
    await scaffold.set('test', 'foo');

    const file = await readFile(testPath);
    const data = await scaffold.get('test');
    file.should.eql('foo');
    data.should.eql('foo');

    await writeFile(testPath, testContent);
  });

  it('set() - file does not exist', async () => {
    const testPath = join(scaffoldDir, 'foo.md');

    await scaffold.set('foo', 'bar');
    const file = await readFile(testPath);
    const data = await scaffold.get('foo');
    file.should.eql('bar');
    data.should.eql('bar');
    await unlink(testPath);
  });

  it('remove() - file exist', async () => {
    await scaffold.remove('test');
    const exist = await exists(testPath);
    const data = await scaffold.get('test');
    exist.should.be.false;
    should.not.exist(data);

    await writeFile(testPath, testContent);
  });

  it('remove() - file does not exist', () => scaffold.remove('foo'));
});


import type Hexo from '../../hexo';

export = (ctx: Hexo) => {
  const { renderer } = ctx.extend;

  const plain = require('./plain');

  renderer.register('htm', 'html', plain, true);
  renderer.register('html', 'html', plain, true);
  renderer.register('css', 'css', plain, true);
  renderer.register('js', 'js', plain, true);

  renderer.register('json', 'json', require('./json'), true);

  const yaml = require('./yaml');

  renderer.register('yml', 'json', yaml, true);
  renderer.register('yaml', 'json', yaml, true);

  const nunjucks = require('./nunjucks');

  renderer.register('njk', 'html', nunjucks, true);
  renderer.register('j2', 'html', nunjucks, true);
};


import tildify from 'tildify';
import { magenta } from 'picocolors';

function publishConsole(args) {
  // Display help message if user didn't input any arguments
  if (!args._.length) {
    return this.call('help', {_: ['publish']});
  }

  return this.post.publish({
    slug: args._.pop(),
    layout: args._.length ? args._[0] : this.config.default_layout
  }, args.r || args.replace).then(post => {
    this.log.info('Published: %s', magenta(tildify(post.path)));
  });
}

export = publishConsole;


name: Linter

on: [push, pull_request]

permissions:
  contents: read

jobs:
  linter:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js 14.x
        uses: actions/setup-node@v3
        with:
          node-version: '14.x'
      - name: Install Dependencies
        run: npm install
      - name: Lint
        run: |
          npm run eslint
        env:
          CI: true


function metaGeneratorHelper() {
  return `<meta name="generator" content="Hexo ${this.env.version}">`;
}

export = metaGeneratorHelper;


import { htmlTag, url_for } from 'hexo-util';
import moize from 'moize';

function cssHelper(...args) {
  let result = '\n';

  args.flat(Infinity).forEach(item => {
    if (typeof item === 'string' || item instanceof String) {
      let path = item;
      if (!path.endsWith('.css')) {
        path += '.css';
      }
      result += `<link rel="stylesheet" href="${url_for.call(this, path)}">\n`;
    } else {
      // Custom attributes
      item.href = url_for.call(this, item.href);
      if (!item.href.endsWith('.css')) item.href += '.css';
      result += htmlTag('link', { rel: 'stylesheet', ...item }) + '\n';
    }
  });
  return result;
}

export = moize(cssHelper, {
  maxSize: 10,
  isDeepEqual: true
});


import { htmlTag, url_for } from 'hexo-util';

const createLink = (options, ctx) => {
  const { base, format } = options;

  return i => url_for.call(ctx, i === 1 ? base : base + format.replace('%d', i));
};

const createPageTag = (options, ctx) => {
  const link = createLink(options, ctx);
  const {
    current,
    escape,
    transform,
    page_class: pageClass,
    current_class: currentClass
  } = options;

  return i => {
    if (i === current) {
      return htmlTag('span', { class: pageClass + ' ' + currentClass }, transform ? transform(i) : i, escape);
    }
    return htmlTag('a', { class: pageClass, href: link(i) }, transform ? transform(i) : i, escape);
  };
};

const showAll = (tags, options, ctx) => {
  const { total } = options;

  const pageLink = createPageTag(options, ctx);

  for (let i = 1; i <= total; i++) {
    tags.push(pageLink(i));
  }
};

const pagenasionPartShow = (tags, options, ctx) => {
  const {
    current,
    total,
    space,
    end_size: endSize,
    mid_size: midSize,
    space_class: spaceClass
  } = options;

  const leftEnd = Math.min(endSize, current - 1);
  const rightEnd = Math.max(total - endSize + 1, current + 1);
  const leftMid = Math.max(leftEnd + 1, current - midSize);
  const rightMid = Math.min(rightEnd - 1, current + midSize);
  const spaceHtml = htmlTag('span', { class: spaceClass }, space, false);

  const pageTag = createPageTag(options, ctx);

  // Display pages on the left edge
  for (let i = 1; i <= leftEnd; i++) {
    tags.push(pageTag(i));
  }

  // Display spaces between edges and middle pages
  if (space && leftMid - leftEnd > 1) {
    tags.push(spaceHtml);
  }

  // Display left middle pages
  for (let i = leftMid; i < current; i++) {
    tags.push(pageTag(i));
  }

  // Display the current page
  tags.push(pageTag(current));

  // Display right middle pages
  for (let i = current + 1; i <= rightMid; i++) {
    tags.push(pageTag(i));
  }

  // Display spaces between edges and middle pages
  if (space && rightEnd - rightMid > 1) {
    tags.push(spaceHtml);
  }

  // Display pages on the right edge
  for (let i = rightEnd; i <= total; i++) {
    tags.push(pageTag(i));
  }
};

interface Options {
  base?: string;
  current?: number;
  format?: string;
  total?: number;
  end_size?: number;
  mid_size?: number;
  space?: string;
  next_text?: string;
  prev_text?: string;
  prev_next?: boolean;
  escape?: boolean;
  page_class?: string;
  current_class?: string;
  space_class?: string;
  prev_class?: string;
  next_class?: string;
  force_prev_next?: boolean;
  show_all?: boolean;
}

function paginatorHelper(options: Options = {}) {
  options = Object.assign({
    base: this.page.base || '',
    current: this.page.current || 0,
    format: `${this.config.pagination_dir}/%d/`,
    total: this.page.total || 1,
    end_size: 1,
    mid_size: 2,
    space: '…',
    next_text: 'Next',
    prev_text: 'Prev',
    prev_next: true,
    escape: true,
    page_class: 'page-number',
    current_class: 'current',
    space_class: 'space',
    prev_class: 'extend prev',
    next_class: 'extend next',
    force_prev_next: false
  }, options);

  const {
    current,
    total,
    prev_text: prevText,
    next_text: nextText,
    prev_next: prevNext,
    escape,
    prev_class: prevClass,
    next_class: nextClass,
    force_prev_next: forcePrevNext
  } = options;

  if (!current) return '';

  const link = createLink(options, this);

  const tags = [];

  // Display the link to the previous page
  if (prevNext && current > 1) {
    tags.push(htmlTag('a', { class: prevClass, rel: 'prev', href: link(current - 1)}, prevText, escape));
  } else if (forcePrevNext) {
    tags.push(htmlTag('span', { class: prevClass, rel: 'prev' }, prevText, escape));
  }

  if (options.show_all) {
    showAll(tags, options, this);
  } else {
    pagenasionPartShow(tags, options, this);
  }

  // Display the link to the next page
  if (prevNext && current < total) {
    tags.push(htmlTag('a', { class: nextClass, rel: 'next', href: link(current + 1) }, nextText, escape));
  } else if (forcePrevNext) {
    tags.push(htmlTag('span', { class: nextClass, rel: 'next' }, nextText, escape));
  }

  return tags.join('');
}

export = paginatorHelper;


'use strict';

const pathFn = require('path');
const cheerio = require('cheerio');

describe('img', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo(pathFn.join(__dirname, 'img_test'));
  const img = require('../../../dist/plugins/tag/img')(hexo);

  before(() => hexo.init());

  it('src', () => {
    const $ = cheerio.load(img(['https://placekitten.com/200/300']));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
  });

  it('src //', () => {
    const $ = cheerio.load(img(['//placekitten.com/200/300']));

    $('img').attr('src').should.eql('//placekitten.com/200/300');
  });

  it('internal src', () => {
    hexo.config.root = '/';
    let $ = cheerio.load(img(['/images/test.jpg']));
    $('img').attr('src').should.eql('/images/test.jpg');

    hexo.config.url = 'http://example.com/root';
    hexo.config.root = '/root/';
    $ = cheerio.load(img(['/images/test.jpg']));
    $('img').attr('src').should.eql('/root/images/test.jpg');
  });

  it('class + src', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
  });

  it('class + internal src', () => {
    hexo.config.root = '/';
    let $ = cheerio.load(img('left /images/test.jpg'.split(' ')));
    $('img').attr('src').should.eql('/images/test.jpg');
    $('img').attr('class').should.eql('left');

    hexo.config.url = 'http://example.com/root';
    hexo.config.root = '/root/';
    $ = cheerio.load(img('left /images/test.jpg'.split(' ')));
    $('img').attr('src').should.eql('/root/images/test.jpg');
    $('img').attr('class').should.eql('left');
  });

  it('multiple classes + src', () => {
    const $ = cheerio.load(img('left top https://placekitten.com/200/300'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left top');
  });

  it('multiple classes + internal src', () => {
    hexo.config.root = '/';
    let $ = cheerio.load(img('left top /images/test.jpg'.split(' ')));
    $('img').attr('src').should.eql('/images/test.jpg');
    $('img').attr('class').should.eql('left top');

    hexo.config.url = 'http://example.com/root';
    hexo.config.root = '/root/';
    $ = cheerio.load(img('left top /images/test.jpg'.split(' ')));
    $('img').attr('src').should.eql('/root/images/test.jpg');
    $('img').attr('class').should.eql('left top');
  });

  it('class + src + width', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
  });

  it('class + src + width + height', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('height').should.eql('300');
  });

  it('class + src + title', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 Place Kitten'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('title').should.eql('Place Kitten');
  });

  it('class + src + width + title', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 Place Kitten'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('title').should.eql('Place Kitten');
  });

  it('class + src + width + height + title', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 Place Kitten'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('height').should.eql('300');
    $('img').attr('title').should.eql('Place Kitten');
  });

  it('class + src + width + height + title + alt', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" "A cute kitten"'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('height').should.eql('300');
    $('img').attr('title').should.eql('Place Kitten');
    $('img').attr('alt').should.eql('A cute kitten');
  });

  it('single quote in double quote', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" "A \'cute\' kitten"'.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('height').should.eql('300');
    $('img').attr('title').should.eql('Place Kitten');
    $('img').attr('alt').should.eql('A \'cute\' kitten');
  });

  it('double quote in single quote', () => {
    const $ = cheerio.load(img('left https://placekitten.com/200/300 200 300 "Place Kitten" \'A "cute" kitten\''.split(' ')));

    $('img').attr('src').should.eql('https://placekitten.com/200/300');
    $('img').attr('class').should.eql('left');
    $('img').attr('width').should.eql('200');
    $('img').attr('height').should.eql('300');
    $('img').attr('title').should.eql('Place Kitten');
    $('img').attr('alt').should.eql('A "cute" kitten');
  });
});


'use strict';

describe('feed_tag', () => {
  const ctx = {
    config: {
      title: 'Hexo',
      url: 'http://example.com',
      root: '/',
      feed: {}
    }
  };

  beforeEach(() => { ctx.config.feed = {}; });

  const feed = require('../../../dist/plugins/helper/feed_tag').bind(ctx);

  it('path - atom', () => {
    feed('atom.xml').should.eql('<link rel="alternate" href="/atom.xml" title="Hexo" type="application/atom+xml">');
  });

  it('path - rss', () => {
    feed('rss2.xml').should.eql('<link rel="alternate" href="/rss2.xml" title="Hexo" type="application/rss+xml">');
  });

  it('title', () => {
    feed('atom.xml', {title: 'RSS Feed'}).should.eql('<link rel="alternate" href="/atom.xml" title="RSS Feed" type="application/atom+xml">');
  });

  it('type', () => {
    feed('rss.xml', {type: 'rss'}).should.eql('<link rel="alternate" href="/rss.xml" title="Hexo" type="application/rss+xml">');
  });

  it('type - null', () => {
    feed('foo.xml', {type: null}).should.eql('<link rel="alternate" href="/foo.xml" title="Hexo" >');
  });

  it('invalid input - number', () => {
    should.throw(() => feed(123), 'path must be a string!');
  });

  it('invalid input - undefined', () => {
    delete ctx.config.feed;
    feed().should.eql('');
  });

  it('invalid input - empty', () => {
    ctx.config.feed = {};
    feed().should.eql('');
  });

  it('feed - parse argument if available', () => {
    ctx.config.feed = {
      type: 'atom',
      path: 'atom.xml'
    };

    feed('atomic.xml').should.eql('<link rel="alternate" href="/atomic.xml" title="Hexo" type="application/atom+xml">');
  });

  it('feed - atom', () => {
    ctx.config.feed = {
      type: 'atom',
      path: 'atom.xml'
    };

    feed().should.eql('<link rel="alternate" href="/atom.xml" title="Hexo" type="application/atom+xml">');
  });

  it('feed - rss2', () => {
    ctx.config.feed = {
      type: 'rss2',
      path: 'rss.xml'
    };

    feed().should.eql('<link rel="alternate" href="/rss.xml" title="Hexo" type="application/rss+xml">');
  });

  it('feed - rss2', () => {
    ctx.config.feed = {
      type: ['atom', 'rss2'],
      path: ['atom.xml', 'rss.xml']
    };

    feed().should.eql([
      '<link rel="alternate" href="/atom.xml" title="Hexo" type="application/atom+xml">',
      '<link rel="alternate" href="/rss.xml" title="Hexo" type="application/rss+xml">'
    ].join(''));
  });
});


}
export = moize(jsHelper, {
maxSize: 10,
isDeepEqual: true
});

'use strict';

const cheerio = require('cheerio');

describe('js', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo(__dirname);

  const ctx = {
    config: hexo.config
  };

  const js = require('../../../dist/plugins/helper/js').bind(ctx);

  function assertResult(result, expected) {
    const $ = cheerio.load(result);

    if (!Array.isArray(expected)) {
      expected = [expected];
    }

    expected.forEach((item, index) => {
      if (typeof item === 'string' || item instanceof String) {
        $('script').eq(index).attr('src').should.eql(item);
      } else {
        for (const attribute in item) {
          if (item[attribute] === true) {
            $('script').eq(index).attr(attribute).should.eql(attribute);
          } else {
            $('script').eq(index).attr(attribute).should.eql(item[attribute]);
          }
        }
      }
    });
  }

  it('a string', () => {
    assertResult(js('script'), '/script.js');
    assertResult(js('script.js'), '/script.js');
    assertResult(js('https://hexo.io/script.js'), 'https://hexo.io/script.js');
    assertResult(js('//hexo.io/script.js'), '//hexo.io/script.js');
  });

  it('an array', () => {
    assertResult(js(['//hexo.io/script.js']), '//hexo.io/script.js');

    assertResult(js(['foo', 'bar', 'baz']), ['/foo.js', '/bar.js', '/baz.js']);
  });

  it('multiple strings', () => {
    assertResult(js('foo', 'bar', 'baz'), ['/foo.js', '/bar.js', '/baz.js']);
  });

  it('multiple arrays', () => {
    assertResult(js(['foo', 'bar'], ['baz']), ['/foo.js', '/bar.js', '/baz.js']);
  });

  it('mixed', () => {
    assertResult(js(['foo', 'bar'], 'baz'), ['/foo.js', '/bar.js', '/baz.js']);
  });

  it('an object', () => {
    assertResult(js({src: 'script.js'}), {src: '/script.js'});
    assertResult(js({src: '/script.js'}), {src: '/script.js'});
    assertResult(js({src: 'script'}), {src: '/script.js'});
    assertResult(js({src: '/script.js', foo: 'bar'}), {src: '/script.js', foo: 'bar'});
  });

  it('mulitple objects', () => {
    assertResult(js({src: '/foo.js'}, {src: '/bar.js'}), [{src: '/foo.js'}, {src: '/bar.js'}]);
    assertResult(js({src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}),
      [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]);
  });

  it('an array of objects', () => {
    assertResult(js([{src: '/foo.js'}, {src: '/bar.js'}]), [{src: '/foo.js'}, {src: '/bar.js'}]);
    assertResult(js([{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]),
      [{src: '/aaa.js', bbb: 'ccc'}, {src: '/ddd.js', eee: 'fff'}]);
  });

  it('async and defer attributes', () => {
    assertResult(js({src: '/foo.js', 'async': true}), {src: '/foo.js', 'async': true});
    assertResult(js({src: '/bar.js', 'defer': true}), {src: '/bar.js', 'defer': true});
  });
});


'use strict';

const { join } = require('path');

describe('PostAsset', () => {
  const Hexo = require('../../../dist/hexo');
  const hexo = new Hexo();
  const PostAsset = hexo.model('PostAsset');
  const Post = hexo.model('Post');
  let post;
  const defaults = require('../../../dist/hexo/default_config');

  before(async () => {
    await hexo.init();
    post = await Post.insert({
      source: 'foo.md',
      slug: 'bar'
    });
  });

  beforeEach(() => {
    hexo.config = Object.assign({}, defaults);
  });

  it('default values', async () => {
    const data = await PostAsset.insert({
      _id: 'foo',
      slug: 'foo',
      post: post._id
    });
    data.modified.should.be.true;
    PostAsset.removeById(data._id);
  });

  it('_id - required', async () => {
    try {
      await PostAsset.insert({});
    } catch (err) {
      err.message.should.eql('ID is not defined');
    }
  });

  it('slug - required', async () => {
    try {
      await PostAsset.insert({
        _id: 'foo'
      });
    } catch (err) {
      err.message.should.eql('`slug` is required!');
    }
  });

  it('path - virtual', async () => {
    const data = await PostAsset.insert({
      _id: 'source/_posts/test/foo.jpg',
      slug: 'foo.jpg',
      post: post._id
    });
    data.path.should.eql(join(post.path, data.slug));

    PostAsset.removeById(data._id);
  });

  it('path - virtual - when permalink is .html', async () => {
    hexo.config.permalink = ':year/:month/:day/:title.html';
    const data = await PostAsset.insert({
      _id: 'source/_posts/test/foo.html',
      slug: 'foo.htm',
      post: post._id
    });
    data.path.should.eql(join(post.path, data.slug));

    PostAsset.removeById(data._id);
  });

  it('path - virtual - when permalink is .htm', async () => {
    hexo.config.permalink = ':year/:month/:day/:title.htm';
    const data = await PostAsset.insert({
      _id: 'source/_posts/test/foo.htm',
      slug: 'foo.htm',
      post: post._id
    });
    data.path.should.eql(join(post.path, data.slug));

    PostAsset.removeById(data._id);
  });

  it('path - virtual - when permalink contains .htm not in the end', async () => {
    hexo.config.permalink = ':year/:month/:day/:title/.htm-foo/';
    const data = await PostAsset.insert({
      _id: 'source/_posts/test/foo.html',
      slug: 'foo.html',
      post: post._id
    });
    data.path.should.eql(join(post.path + '.htm-foo/', data.slug));

    PostAsset.removeById(data._id);
  });

  it('source - virtual', async () => {
    const data = await PostAsset.insert({
      _id: 'source/_posts/test/foo.jpg',
      slug: 'foo.jpg',
      post: post._id
    });
    data.source.should.eql(join(hexo.base_dir, data._id));

    PostAsset.removeById(data._id);
  });
});


import { encodeURL } from 'hexo-util';
import { postFindOneFactory } from './';

/**
 * Post path tag
 *
 * Syntax:
 *   {% post_path slug | title %}
 */
export = ctx => {
  return function postPathTag(args) {
    const slug = args.shift();
    if (!slug) return;

    const factory = postFindOneFactory(ctx);
    const post = factory({ slug }) || factory({ title: slug });
    if (!post) return;

    const link = encodeURL(new URL(post.path, ctx.config.url).pathname);

    return link;
  };
};


import { Color, url_for } from 'hexo-util';
import moize from 'moize';

function tagcloudHelper(tags, options?) {
  if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {
    options = tags;
    tags = this.site.tags;
  }

  if (!tags || !tags.length) return '';
  options = options || {};

  const min = options.min_font || 10;
  const max = options.max_font || 20;
  const orderby = options.orderby || 'name';
  const order = options.order || 1;
  const unit = options.unit || 'px';
  const color = options.color;
  const className = options.class;
  const showCount = options.show_count;
  const countClassName = options.count_class || 'count';
  const level = options.level || 10;
  const { transform } = options;
  const separator = options.separator || ' ';
  const result = [];
  let startColor, endColor;

  if (color) {
    if (!options.start_color) throw new TypeError('start_color is required!');
    if (!options.end_color) throw new TypeError('end_color is required!');

    startColor = new Color(options.start_color);
    endColor = new Color(options.end_color);
  }

  // Sort the tags
  if (orderby === 'random' || orderby === 'rand') {
    tags = tags.random();
  } else {
    tags = tags.sort(orderby, order);
  }

  // Limit the number of tags
  if (options.amount) {
    tags = tags.limit(options.amount);
  }

  const sizes = [];

  tags.sort('length').forEach(tag => {
    const { length } = tag;
    if (sizes.includes(length)) return;

    sizes.push(length);
  });

  const length = sizes.length - 1;

  tags.forEach(tag => {
    const ratio = length ? sizes.indexOf(tag.length) / length : 0;
    const size = min + ((max - min) * ratio);
    let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};`;
    const attr = className ? ` class="${className}-${Math.round(ratio * level)}"` : '';

    if (color) {
      const midColor = startColor.mix(endColor, ratio);
      style += ` color: ${midColor.toString()}`;
    }

    result.push(
      `<a href="${url_for.call(this, tag.path)}" style="${style}"${attr}>${transform ? transform(tag.name) : tag.name}${showCount ? `${tag.length}` : ''}</a>`
    );
  });

  return result.join(separator);
}

function tagcloudHelperFactory(tags, options) {
  const transformArgs = () => {
    if (!options && (!tags || !Object.prototype.hasOwnProperty.call(tags, 'length'))) {
      options = tags;
      tags = this.site.tags;
    }

    return [tags.toArray(), options];
  };

  return moize(tagcloudHelper.bind(this), {
    maxSize: 5,
    isDeepEqual: true,
    transformArgs
  }).call(this, tags, options);
}

export = tagcloudHelperFactory;


assertResult(css([{href: '/foo.css'}, {href: '/bar.css'}]), [{href: '/foo.css'}, {href: '/bar.css'}]);

assertResult(css([{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]),

[{href: '/aaa.css', bbb: 'ccc'}, {href: '/ddd.css', eee: 'fff'}]);


'use strict';

describe('url_for', () => {
  const ctx = {
    config: { url: 'https://example.com' },
    relative_url: require('../../../dist/plugins/helper/relative_url')
  };

  const urlFor = require('../../../dist/plugins/helper/url_for').bind(ctx);

  it('should encode path', () => {
    ctx.config.root = '/';
    urlFor('fôo.html').should.eql('/f%C3%B4o.html');

    ctx.config.root = '/fôo/';
    urlFor('bár.html').should.eql('/f%C3%B4o/b%C3%A1r.html');
  });

  it('internal url (relative off)', () => {
    ctx.config.root = '/';
    urlFor('index.html').should.eql('/index.html');
    urlFor('/').should.eql('/');
    urlFor('/index.html').should.eql('/index.html');

    ctx.config.root = '/blog/';
    urlFor('index.html').should.eql('/blog/index.html');
    urlFor('/').should.eql('/blog/');
    urlFor('/index.html').should.eql('/blog/index.html');
  });

  it('internal url (relative on)', () => {
    ctx.config.relative_link = true;
    ctx.config.root = '/';

    ctx.path = '';
    urlFor('index.html').should.eql('index.html');

    ctx.path = 'foo/bar/';
    urlFor('index.html').should.eql('../../index.html');

    ctx.config.relative_link = false;
  });

  it('internal url (options.relative)', () => {
    ctx.path = '';
    urlFor('index.html', {relative: true}).should.eql('index.html');

    ctx.config.relative_link = true;
    urlFor('index.html', {relative: false}).should.eql('/index.html');
    ctx.config.relative_link = false;
  });

  it('internel url (pretty_urls.trailing_index disabled)', () => {
    ctx.config.pretty_urls = { trailing_index: false };
    ctx.path = '';
    ctx.config.root = '/';
    urlFor('index.html').should.eql('/');
    urlFor('/index.html').should.eql('/');

    ctx.config.root = '/blog/';
    urlFor('index.html').should.eql('/blog/');
    urlFor('/index.html').should.eql('/blog/');
  });

  it('external url', () => {
    [
      'https://hexo.io/',
      '//google.com/',
      // 'index.html' in external link should not be removed
      '//google.com/index.html'
    ].forEach(url => {
      urlFor(url).should.eql(url);
    });
  });

  it('only hash', () => {
    urlFor('#test').should.eql('#test');
  });
});


export = ctx => {
  const { filter } = ctx.extend;

  filter.register('before_post_render', require('./backtick_code_block')(ctx));
  filter.register('before_post_render', require('./titlecase'));
};


'use strict';

describe('Migrator', () => {
  const Migrator = require('../../../dist/extend/migrator');

  it('register()', () => {
    const d = new Migrator();

    // name, fn
    d.register('test', () => {});

    d.get('test').should.exist;

    // no name
    should.throw(() => d.register(), TypeError, 'name is required');

    // no fn
    should.throw(() => d.register('test'), TypeError, 'fn must be a function');
  });

  it('register() - promisify', () => {
    const d = new Migrator();

    d.register('test', (args, callback) => {
      args.should.eql({foo: 'bar'});
      callback(null, 'foo');
    });

    d.get('test')({
      foo: 'bar'
    }).then(result => {
      result.should.eql('foo');
    });
  });

  it('register() - Promise.method', async () => {
    const d = new Migrator();

    d.register('test', args => {
      args.should.eql({foo: 'bar'});
      return 'foo';
    });

    const result = await d.get('test')({
      foo: 'bar'
    });

    result.should.eql('foo');
  });

  it('list()', () => {
    const d = new Migrator();

    d.register('test', () => {});

    d.list().should.have.all.keys(['test']);
  });

  it('get()', () => {
    const d = new Migrator();

    d.register('test', () => {});

    d.get('test').should.exist;
  });
});


it('async and defer attributes', () => {

assertResult(js({src: '/foo.js', 'async': true}), {src: '/foo.js', 'async': true});

assertResult(js({src: '/bar.js', 'defer': true}), {src: '/bar.js', 'defer': true});

import { htmlTag, url_for } from 'hexo-util';
import moize from 'moize';

function jsHelper(...args) {
  let result = '\n';

  args.flat(Infinity).forEach(item => {
    if (typeof item === 'string' || item instanceof String) {
      let path = item;
      if (!path.endsWith('.js')) {
        path += '.js';
      }
      result += `<script src="${url_for.call(this, path)}"></script>\n`;
    } else {
      // Custom attributes
      item.src = url_for.call(this, item.src);
      if (!item.src.endsWith('.js')) item.src += '.js';
      result += htmlTag('script', { ...item }, '') + '\n';
    }
  });
  return result;
}

export = moize(jsHelper, {
  maxSize: 10,
  isDeepEqual: true
});


'use strict';

describe('yaml', () => {
  const r = require('../../../dist/plugins/renderer/yaml');

  it('normal', () => {
    r({text: 'foo: 1'}).should.eql({foo: 1});
  });

  it('escape', () => {
    const body = [
      'foo: 1',
      'bar:',
      '\tbaz: 3'
    ].join('\n');

    r({text: body}).should.eql({
      foo: 1,
      bar: {
        baz: 3
      }
    });
  });
});


function plainRenderer(data) {
  return data.text;
}

export = plainRenderer;


keep keep keep keep keep replace keep keep keep

"use strict";

const { EOL } = require("os");
const log = require("npmlog");
const path = require("path");
const tempWrite = require("temp-write");
const slash = require("slash");

const ChildProcessUtilities = require("./ChildProcessUtilities");

function isDetachedHead(opts) {
  log.silly("isDetachedHead");

  const branchName = exports.getCurrentBranch(opts);
  const isDetached = branchName === "HEAD";
  log.verbose("isDetachedHead", isDetached);

  return isDetached;
}

function isInitialized(opts) {
  log.silly("isInitialized");
  let initialized;

  try {
    // we only want the return code, so ignore stdout/stderr
    ChildProcessUtilities.execSync(
      "git",
      ["rev-parse"],
      Object.assign({}, opts, {
        stdio: "ignore",
      })
    );
    initialized = true;
  } catch (err) {
    log.verbose("isInitialized", "swallowed error", err);
    initialized = false;
  }

  // this does not need to be verbose
  log.silly("isInitialized", initialized);
  return initialized;
}

function addFile(file, opts) {
  log.silly("addFile", file);
  const relativePath = path.relative(opts.cwd, path.resolve(opts.cwd, file));
  const portablePath = slash(relativePath);
  ChildProcessUtilities.execSync("git", ["add", portablePath], opts);
}

function commit(message, opts) {
  log.silly("commit", message);
  const args = ["commit"];

  if (message.indexOf(EOL) > -1) {
    // Use tempfile to allow multi\nline strings.
    args.push("-F", tempWrite.sync(message, "lerna-commit.txt"));
  } else {
    args.push("-m", message);
  }

  log.verbose("commit", args);
  ChildProcessUtilities.execSync("git", args, opts);
}

function addTag(tag, opts) {
  log.silly("addTag", tag);
  ChildProcessUtilities.execSync("git", ["tag", tag, "-m", tag], opts);
}

function removeTag(tag, opts) {
  log.silly("removeTag", tag);
  ChildProcessUtilities.execSync("git", ["tag", "-d", tag], opts);
}

function hasTags(opts) {
  log.silly("hasTags");

  const yes = !!ChildProcessUtilities.execSync("git", ["tag"], opts);
  log.verbose("hasTags", yes);

  return yes;
}

function getLastTaggedCommit(opts) {
  log.silly("getLastTaggedCommit");

  const taggedCommit = ChildProcessUtilities.execSync("git", ["rev-list", "--tags", "--max-count=1"], opts);
  log.verbose("getLastTaggedCommit", taggedCommit);

  return taggedCommit;
}

function getLastTaggedCommitInBranch(opts) {
  log.silly("getLastTaggedCommitInBranch");

  const tagName = exports.getLastTag(opts);
  const commitInBranch = ChildProcessUtilities.execSync("git", ["rev-list", "-n", "1", tagName], opts);
  log.verbose("getLastTaggedCommitInBranch", commitInBranch);

  return commitInBranch;
}

function getFirstCommit(opts) {
  log.silly("getFirstCommit");

  const firstCommit = ChildProcessUtilities.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], opts);
  log.verbose("getFirstCommit", firstCommit);

  return firstCommit;
}

function pushWithTags(remote, tags, opts) {
  log.silly("pushWithTags", [remote, tags]);

  const branch = exports.getCurrentBranch(opts);
  ChildProcessUtilities.execSync("git", ["push", remote, branch], opts);
  ChildProcessUtilities.execSync("git", ["push", remote].concat(tags), opts);
}

function getLastTag(opts) {
  log.silly("getLastTag");

  const lastTag = ChildProcessUtilities.execSync("git", ["describe", "--tags", "--abbrev=0"], opts);
  log.verbose("getLastTag", lastTag);

  return lastTag;
}

function describeTag(ref, opts) {
  log.silly("describeTag", ref);

  const description = ChildProcessUtilities.execSync("git", ["describe", "--tags", ref], opts);
  log.silly("describeTag", description);

  return description;
}

function diffSinceIn(since, location, opts) {
  const formattedLocation = path.relative(opts.cwd, location).replace(/\\/g, "/");
  log.silly("diffSinceIn", since, formattedLocation);

  const diff = ChildProcessUtilities.execSync(
    "git",
    ["diff", "--name-only", since, "--", formattedLocation],
    opts
  );
  log.silly("diff", diff);

  return diff;
}

function getWorkspaceRoot(opts) {
  log.silly("getWorkspaceRoot");

  const workRoot = ChildProcessUtilities.execSync("git", ["rev-parse", "--show-toplevel"], opts);
  log.verbose("getWorkspaceRoot", workRoot);

  return workRoot;
}

function getCurrentBranch(opts) {
  log.silly("getCurrentBranch");

  const currentBranch = ChildProcessUtilities.execSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], opts);
  log.verbose("getCurrentBranch", currentBranch);

  return currentBranch;
}

function getCurrentSHA(opts) {
  log.silly("getCurrentSHA");

  const sha = ChildProcessUtilities.execSync("git", ["rev-parse", "HEAD"], opts);
  log.verbose("getCurrentSHA", sha);

  return sha;
}

function checkoutChanges(fileGlob, opts) {
  log.silly("checkoutChanges", fileGlob);
  ChildProcessUtilities.execSync("git", ["checkout", "--", fileGlob], opts);
}

function init(opts) {
  log.silly("git init");
  ChildProcessUtilities.execSync("git", ["init"], opts);
}

function hasCommit(opts) {
  log.silly("hasCommit");
  let retVal;

  try {
    ChildProcessUtilities.execSync("git", ["log"], opts);
    retVal = true;
  } catch (e) {
    retVal = false;
  }

  log.verbose("hasCommit", retVal);
  return retVal;
}

exports.isDetachedHead = isDetachedHead;
exports.isInitialized = isInitialized;
exports.addFile = addFile;
exports.commit = commit;
exports.addTag = addTag;
exports.removeTag = removeTag;
exports.hasTags = hasTags;
exports.getLastTaggedCommit = getLastTaggedCommit;
exports.getLastTaggedCommitInBranch = getLastTaggedCommitInBranch;
exports.getFirstCommit = getFirstCommit;
exports.pushWithTags = pushWithTags;
exports.getLastTag = getLastTag;
exports.describeTag = describeTag;
exports.diffSinceIn = diffSinceIn;
exports.getWorkspaceRoot = getWorkspaceRoot;
exports.getCurrentBranch = getCurrentBranch;
exports.getCurrentSHA = getCurrentSHA;
exports.checkoutChanges = checkoutChanges;
exports.init = init;
exports.hasCommit = hasCommit;


#!/usr/bin/env node
console.log("Hello, world!");