"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Site = void 0;
const cheerio_1 = __importDefault(require("cheerio"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const ignore_1 = __importDefault(require("ignore"));
const path_1 = __importDefault(require("path"));
const walk_sync_1 = __importDefault(require("walk-sync"));
const simple_git_1 = __importDefault(require("simple-git"));
const bluebird_1 = __importDefault(require("bluebird"));
const gh_pages_1 = __importDefault(require("gh-pages"));
const SiteConfig_1 = require("./SiteConfig");
const Page_1 = require("../Page");
const PageConfig_1 = require("../Page/PageConfig");
const VariableProcessor_1 = require("../variables/VariableProcessor");
const VariableRenderer_1 = require("../variables/VariableRenderer");
const ExternalManager_1 = require("../External/ExternalManager");
const SiteLinkManager_1 = require("../html/SiteLinkManager");
const PluginManager_1 = require("../plugins/PluginManager");
const async_1 = require("../utils/async");
const delay_1 = require("../utils/delay");
const fsUtil = __importStar(require("../utils/fsUtil"));
const gitUtil = __importStar(require("../utils/git"));
const logger = __importStar(require("../utils/logger"));
const constants_1 = require("./constants");
// Change when they are migrated to TypeScript
const ProgressBar = require('../lib/progress');
const { LayoutManager } = require('../Layout');
require('../patches/htmlparser2');
const url = {
    join: path_1.default.posix.join,
};
const MARKBIND_VERSION = require('../../package.json').version;
const CONFIG_FOLDER_NAME = '_markbind';
const SITE_FOLDER_NAME = '_site';
const TEMP_FOLDER_NAME = '.temp';
const TEMPLATE_SITE_ASSET_FOLDER_NAME = 'markbind';
const LAYOUT_SITE_FOLDER_NAME = 'layouts';
const FAVICON_DEFAULT_PATH = 'favicon.ico';
const USER_VARIABLES_PATH = '_markbind/variables.md';
const PAGE_TEMPLATE_NAME = 'page.njk';
const SITE_DATA_NAME = 'siteData.json';
const MAX_CONCURRENT_PAGE_GENERATION_PROMISES = 4;
const LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT = 30000;
const LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT = 5000;
function getBootswatchThemePath(theme) {
    return require.resolve(`bootswatch/dist/${theme}/bootstrap.min.css`);
}
const SUPPORTED_THEMES_PATHS = {
    'bootswatch-cerulean': getBootswatchThemePath('cerulean'),
    'bootswatch-cosmo': getBootswatchThemePath('cosmo'),
    'bootswatch-flatly': getBootswatchThemePath('flatly'),
    'bootswatch-journal': getBootswatchThemePath('journal'),
    'bootswatch-litera': getBootswatchThemePath('litera'),
    'bootswatch-lumen': getBootswatchThemePath('lumen'),
    'bootswatch-lux': getBootswatchThemePath('lux'),
    'bootswatch-materia': getBootswatchThemePath('materia'),
    'bootswatch-minty': getBootswatchThemePath('minty'),
    'bootswatch-pulse': getBootswatchThemePath('pulse'),
    'bootswatch-sandstone': getBootswatchThemePath('sandstone'),
    'bootswatch-simplex': getBootswatchThemePath('simplex'),
    'bootswatch-sketchy': getBootswatchThemePath('sketchy'),
    'bootswatch-spacelab': getBootswatchThemePath('spacelab'),
    'bootswatch-united': getBootswatchThemePath('united'),
    'bootswatch-yeti': getBootswatchThemePath('yeti'),
    'bootswatch-zephyr': getBootswatchThemePath('zephyr'),
};
const HIGHLIGHT_ASSETS = {
    dark: 'codeblock-dark.min.css',
    light: 'codeblock-light.min.css',
};
const MARKBIND_WEBSITE_URL = 'https://markbind.org/';
const MARKBIND_LINK_HTML = `<a href='${MARKBIND_WEBSITE_URL}'>MarkBind ${MARKBIND_VERSION}</a>`;
class Site {
    constructor(rootPath, outputPath, onePagePath, forceReload = false, siteConfigPath = constants_1.SITE_CONFIG_NAME, dev, backgroundBuildMode, postBackgroundBuildFunc) {
        /**
         * Build/copy assets that are specified in filePaths
         * @param filePaths a single path or an array of paths corresponding to the assets to build
         */
        this.buildAsset = (0, delay_1.delay)(this._buildMultipleAssets, 1000);
        /**
         * Remove assets that are specified in filePaths
         * @param filePaths a single path or an array of paths corresponding to the assets to remove
         */
        this.removeAsset = (0, delay_1.delay)(this._removeMultipleAssets, 1000);
        this.rebuildPagesBeingViewed = (0, delay_1.delay)(this._rebuildPagesBeingViewed, 1000);
        /**
         * Rebuild pages that are affected by changes in filePaths
         * @param filePaths a single path or an array of paths corresponding to the files that have changed
         */
        this.rebuildAffectedSourceFiles = (0, delay_1.delay)(this._rebuildAffectedSourceFiles, 1000);
        /**
         * Rebuild all pages
         */
        this.rebuildSourceFiles = (0, delay_1.delay)(this._rebuildSourceFiles, 1000);
        /**
         * Builds pages that are yet to build/rebuild in the background
         */
        this.backgroundBuildNotViewedFiles = (0, delay_1.delay)(this._backgroundBuildNotViewedFiles, 1000);
        this.dev = !!dev;
        this.rootPath = rootPath;
        this.outputPath = outputPath;
        this.tempPath = path_1.default.join(rootPath, TEMP_FOLDER_NAME);
        // MarkBind assets to be copied
        this.siteAssetsDestPath = path_1.default.join(outputPath, TEMPLATE_SITE_ASSET_FOLDER_NAME);
        // Page template path
        this.pageTemplatePath = path_1.default.join(__dirname, '../Page', PAGE_TEMPLATE_NAME);
        this.pageTemplate = VariableRenderer_1.VariableRenderer.compile(fs_extra_1.default.readFileSync(this.pageTemplatePath, 'utf8'));
        this.pages = [];
        // Other properties
        this.addressablePages = [];
        this.addressablePagesSource = [];
        this.baseUrlMap = new Set();
        this.forceReload = forceReload;
        this.siteConfigPath = siteConfigPath;
        // Background build properties
        this.backgroundBuildMode = onePagePath && backgroundBuildMode;
        this.stopGenerationTimeThreshold = new Date();
        this.postBackgroundBuildFunc = postBackgroundBuildFunc || (() => { });
        // Lazy reload properties
        this.onePagePath = onePagePath;
        this.currentPageViewed = onePagePath
            ? path_1.default.resolve(this.rootPath, fsUtil.removeExtension(onePagePath))
            : '';
        this.currentOpenedPages = [];
        this.toRebuild = new Set();
    }
    /**
     * Util Methods
     */
    static rejectHandler(error, removeFolders) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.warn(error);
            try {
                yield Promise.all(removeFolders.map(folder => fs_extra_1.default.remove(folder)));
            }
            catch (err) {
                logger.error(`Failed to remove generated files after error!\n${err.message}`);
            }
        });
    }
    beforeSiteGenerate() {
        this.variableProcessor.invalidateCache();
        this.externalManager.reset();
        this.pluginManager.beforeSiteGenerate();
    }
    /**
     * Changes the site variable of the current page being viewed, building it if necessary.
     * @param normalizedUrl BaseUrl-less and extension-less url of the page
     * @return Boolean of whether the page needed to be rebuilt
     */
    changeCurrentPage(normalizedUrl) {
        this.currentPageViewed = path_1.default.join(this.rootPath, normalizedUrl);
        if (this.toRebuild.has(this.currentPageViewed)) {
            this.beforeSiteGenerate();
            /*
             Lazy loading only builds the page being viewed, but the user may be quick enough
             to trigger multiple page builds before the first one has finished building,
             hence we need to take this into account by using the delayed variant of the method.
             */
            this.rebuildPagesBeingViewed(this.currentPageViewed);
            return true;
        }
        return false;
    }
    /**
     * Changes the list of current opened pages
     * @param normalizedUrls Collection of normalized url of pages taken from the clients
     * ordered from most-to-least recently opened
     */
    changeCurrentOpenedPages(normalizedUrls) {
        if (!this.onePagePath) {
            return;
        }
        const openedPages = normalizedUrls.map(normalizedUrl => path_1.default.join(this.rootPath, normalizedUrl));
        this.currentOpenedPages = constants_1._.uniq(openedPages);
        if (this.currentOpenedPages.length > 0) {
            logger.info('Current opened pages, from most-to-least recent:');
            this.currentOpenedPages.forEach((pagePath, idx) => {
                logger.info(`${idx + 1}. ${fsUtil.ensurePosix(path_1.default.relative(this.rootPath, pagePath))}`);
            });
        }
        else {
            logger.info('No pages are currently opened');
        }
    }
    /**
     * Read and store the site config from site.json, overwrite the default base URL
     * if it's specified by the user.
     * @param baseUrl user defined base URL (if exists)
     */
    readSiteConfig(baseUrl) {
        return __awaiter(this, void 0, void 0, function* () {
            this.siteConfig = yield SiteConfig_1.SiteConfig.readSiteConfig(this.rootPath, this.siteConfigPath, baseUrl);
            return this.siteConfig;
        });
    }
    listAssets(fileIgnore) {
        const files = (0, walk_sync_1.default)(this.rootPath, { directories: false });
        return fileIgnore.filter(files);
    }
    /**
     * Create a Page object from the site and page creation config.
     */
    createPage(config) {
        const sourcePath = path_1.default.join(this.rootPath, config.pageSrc);
        const resultPath = path_1.default.join(this.outputPath, fsUtil.setExtension(config.pageSrc, '.html'));
        const baseAssetsPath = path_1.default.posix.join(this.siteConfig.baseUrl || '/', TEMPLATE_SITE_ASSET_FOLDER_NAME);
        const pageConfig = new PageConfig_1.PageConfig({
            asset: {
                bootstrap: path_1.default.posix.join(baseAssetsPath, 'css', 'bootstrap.min.css'),
                externalScripts: constants_1._.union(this.siteConfig.externalScripts, config.externalScripts),
                fontAwesome: path_1.default.posix.join(baseAssetsPath, 'fontawesome', 'css', 'all.min.css'),
                glyphicons: path_1.default.posix.join(baseAssetsPath, 'glyphicons', 'css', 'bootstrap-glyphicons.min.css'),
                octicons: path_1.default.posix.join(baseAssetsPath, 'css', 'octicons.css'),
                materialIcons: path_1.default.posix.join(baseAssetsPath, 'material-icons', 'material-icons.css'),
                highlight: path_1.default.posix.join(baseAssetsPath, 'css', HIGHLIGHT_ASSETS[this.siteConfig.style.codeTheme]),
                markBindCss: path_1.default.posix.join(baseAssetsPath, 'css', 'markbind.min.css'),
                markBindJs: path_1.default.posix.join(baseAssetsPath, 'js', 'markbind.min.js'),
                pageNavCss: path_1.default.posix.join(baseAssetsPath, 'css', 'page-nav.css'),
                siteNavCss: path_1.default.posix.join(baseAssetsPath, 'css', 'site-nav.css'),
                bootstrapUtilityJs: path_1.default.posix.join(baseAssetsPath, 'js', 'bootstrap-utility.min.js'),
                polyfillJs: path_1.default.posix.join(baseAssetsPath, 'js', 'polyfill.min.js'),
                // We use development Vue when MarkBind is served in 'dev' mode so that hydration issues are reported
                vue: this.dev
                    ? 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js'
                    : path_1.default.posix.join(baseAssetsPath, 'js', 'vue.min.js'),
                layoutUserScriptsAndStyles: [],
            },
            baseUrlMap: this.baseUrlMap,
            dev: this.dev,
            faviconUrl: config.faviconUrl,
            frontmatterOverride: config.frontmatter,
            layout: config.layout,
            layoutsAssetPath: path_1.default.posix.join(baseAssetsPath, LAYOUT_SITE_FOLDER_NAME),
            pluginManager: this.pluginManager,
            resultPath,
            rootPath: this.rootPath,
            searchable: this.siteConfig.enableSearch && config.searchable,
            siteLinkManager: this.siteLinkManager,
            siteOutputPath: this.outputPath,
            sourcePath,
            src: config.pageSrc,
            title: config.title,
            template: this.pageTemplate,
            variableProcessor: this.variableProcessor,
            addressablePagesSource: this.addressablePagesSource,
            layoutManager: this.layoutManager,
        });
        return new Page_1.Page(pageConfig, this.siteConfig);
    }
    static printBaseUrlMessage() {
        logger.info('The default base URL of your site is set to /\n'
            + 'You can change the base URL of your site by editing site.json\n'
            + 'Check https://markbind.org/userGuide/siteConfiguration.html for more information.');
    }
    /**
     * Updates the paths to be traversed as addressable pages and returns a list of filepaths to be deleted
     */
    updateAddressablePages() {
        const oldAddressablePagesSources = this.addressablePages.slice().map(page => page.src);
        this.collectAddressablePages();
        const newAddressablePagesSources = this.addressablePages.map(page => page.src);
        return constants_1._.difference(oldAddressablePagesSources, newAddressablePagesSources)
            .map(filePath => fsUtil.setExtension(filePath, '.html'));
    }
    getPageGlobPaths(page, pagesExclude) {
        var _a;
        const pageGlobs = (_a = page.glob) !== null && _a !== void 0 ? _a : [];
        return (0, walk_sync_1.default)(this.rootPath, {
            directories: false,
            globs: Array.isArray(pageGlobs) ? pageGlobs : [pageGlobs],
            ignore: [
                CONFIG_FOLDER_NAME,
                SITE_FOLDER_NAME,
                ...pagesExclude.concat(page.globExclude || []),
            ],
        });
    }
    /**
     * Collects the paths to be traversed as addressable pages
     */
    collectAddressablePages() {
        const { pages, pagesExclude } = this.siteConfig;
        const pagesFromSrc = constants_1._.flatMap(pages.filter(page => page.src), page => (Array.isArray(page.src)
            ? page.src.map(pageSrc => (Object.assign(Object.assign({}, page), { src: pageSrc })))
            : [page]));
        const set = new Set();
        const duplicatePages = pagesFromSrc
            .filter(page => set.size === set.add(page.src).size)
            .map(page => page.src);
        if (duplicatePages.length > 0) {
            throw new Error(`Duplicate page entries found in site config: ${constants_1._.uniq(duplicatePages).join(', ')}`);
        }
        const pagesFromGlobs = constants_1._.flatMap(pages.filter(page => page.glob), page => this.getPageGlobPaths(page, pagesExclude)
            .map(filePath => ({
            src: filePath,
            searchable: page.searchable,
            layout: page.layout,
            frontmatter: page.frontmatter,
        })));
        /*
         Add pages collected from globs and merge properties for pages
         Page properties collected from src have priority over page properties from globs,
         while page properties from later entries take priority over earlier ones.
         */
        const filteredPages = {};
        pagesFromGlobs.concat(pagesFromSrc).forEach((page) => {
            const filteredPage = constants_1._.omitBy(page, constants_1._.isUndefined);
            filteredPages[page.src] = page.src in filteredPages
                ? Object.assign(Object.assign({}, filteredPages[page.src]), filteredPage) : filteredPage;
        });
        this.addressablePages = Object.values(filteredPages);
        this.addressablePagesSource.length = 0;
        this.addressablePages.forEach((page) => {
            this.addressablePagesSource.push(fsUtil.removeExtensionPosix(page.src));
        });
    }
    /**
     * Collects the base url map in the site/subsites
     */
    collectBaseUrl() {
        const candidates = (0, walk_sync_1.default)(this.rootPath, { directories: false })
            .filter(x => x.endsWith(this.siteConfigPath))
            .map(x => path_1.default.resolve(this.rootPath, x));
        this.baseUrlMap = new Set(candidates.map(candidate => path_1.default.dirname(candidate)));
        this.variableProcessor = new VariableProcessor_1.VariableProcessor(this.rootPath, this.baseUrlMap);
        this.buildManagers();
    }
    /**
     * Set up the managers used with the configurations.
     */
    buildManagers() {
        const config = {
            baseUrlMap: this.baseUrlMap,
            baseUrl: this.siteConfig.baseUrl,
            rootPath: this.rootPath,
            outputPath: this.outputPath,
            ignore: this.siteConfig.ignore,
            addressablePagesSource: this.addressablePagesSource,
            variableProcessor: this.variableProcessor,
            intrasiteLinkValidation: this.siteConfig.intrasiteLinkValidation,
            codeLineNumbers: this.siteConfig.style.codeLineNumbers,
            plantumlCheck: this.siteConfig.plantumlCheck,
            headerIdMap: {},
            siteLinkManager: this.siteLinkManager,
            pluginManager: this.pluginManager,
            externalManager: this.externalManager,
        };
        this.siteLinkManager = new SiteLinkManager_1.SiteLinkManager(config);
        config.siteLinkManager = this.siteLinkManager;
        this.pluginManager = new PluginManager_1.PluginManager(config, this.siteConfig.plugins, this.siteConfig.pluginsContext);
        config.pluginManager = this.pluginManager;
        this.externalManager = new ExternalManager_1.ExternalManager(config);
        config.externalManager = this.externalManager;
        this.layoutManager = new LayoutManager(config);
    }
    /**
     * Collects the user defined variables map in the site/subsites
     */
    collectUserDefinedVariablesMap() {
        this.variableProcessor.resetUserDefinedVariablesMap();
        this.baseUrlMap.forEach((base) => {
            const userDefinedVariablesPath = path_1.default.resolve(base, USER_VARIABLES_PATH);
            let content;
            try {
                content = fs_extra_1.default.readFileSync(userDefinedVariablesPath, 'utf8');
            }
            catch (e) {
                content = '';
                logger.warn(e.message);
            }
            /*
             We retrieve the baseUrl of the (sub)site by appending the relative to the configured base url
             i.e. We ignore the configured baseUrl of the sub sites.
             */
            const siteRelativePathFromRoot = fsUtil.ensurePosix(path_1.default.relative(this.rootPath, base));
            const siteBaseUrl = siteRelativePathFromRoot === ''
                ? this.siteConfig.baseUrl
                : path_1.default.posix.join(this.siteConfig.baseUrl || '/', siteRelativePathFromRoot);
            this.variableProcessor.addUserDefinedVariable(base, 'baseUrl', siteBaseUrl);
            this.variableProcessor.addUserDefinedVariable(base, 'MarkBind', MARKBIND_LINK_HTML);
            const $ = cheerio_1.default.load(content, { decodeEntities: false });
            $('variable,span').each((_index, element) => {
                const name = $(element).attr('name') || $(element).attr('id');
                this.variableProcessor.renderAndAddUserDefinedVariable(base, name, $(element).html());
            });
        });
    }
    /**
     * Collects the user defined variables map in the site/subsites
     * if there is a change in the variables file
     * @param filePaths array of paths corresponding to files that have changed
     */
    collectUserDefinedVariablesMapIfNeeded(filePaths) {
        const variablesPath = path_1.default.resolve(this.rootPath, USER_VARIABLES_PATH);
        if (filePaths.includes(variablesPath)) {
            this.collectUserDefinedVariablesMap();
            return true;
        }
        return false;
    }
    /**
     * Generate the website.
     * @param baseUrl user defined base URL (if exists)
     */
    generate(baseUrl) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            // Create the .tmp folder for storing intermediate results.
            fs_extra_1.default.emptydirSync(this.tempPath);
            // Clean the output folder; create it if not exist.
            fs_extra_1.default.emptydirSync(this.outputPath);
            const lazyWebsiteGenerationString = this.onePagePath ? '(lazy) ' : '';
            logger.info(`Website generation ${lazyWebsiteGenerationString}started at ${startTime.toLocaleTimeString()}`);
            try {
                yield this.readSiteConfig(baseUrl);
                this.collectAddressablePages();
                this.collectBaseUrl();
                this.collectUserDefinedVariablesMap();
                yield this.buildAssets();
                yield (this.onePagePath ? this.lazyBuildSourceFiles() : this.buildSourceFiles());
                yield this.copyCoreWebAsset();
                yield this.copyBootstrapTheme(false);
                yield this.copyFontAwesomeAsset();
                yield this.copyOcticonsAsset();
                yield this.copyMaterialIconsAsset();
                yield this.writeSiteData();
                this.calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString);
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for generate().
     */
    calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        logger.info(`Website generation ${lazyWebsiteGenerationString}complete! Total build time: ${totalBuildTime}s`);
        if (!this.onePagePath && totalBuildTime > LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT) {
            logger.info('Your site took quite a while to build...'
                + 'Have you considered using markbind serve -o when writing content to speed things up?');
        }
    }
    /**
     * Build all pages of the site
     */
    buildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            this.beforeSiteGenerate();
            logger.info('Generating pages...');
            try {
                yield this.generatePages();
                yield fs_extra_1.default.remove(this.tempPath);
                logger.info('Pages built');
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Adds all pages except the viewed pages to toRebuild, flagging them for lazy building later.
     */
    lazyBuildAllPagesNotViewed(viewedPages) {
        return __awaiter(this, void 0, void 0, function* () {
            const viewedPagesArray = Array.isArray(viewedPages) ? viewedPages : [viewedPages];
            this.pages.forEach((page) => {
                const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath);
                if (!viewedPagesArray.some(viewedPage => normalizedUrl === viewedPage)) {
                    this.toRebuild.add(normalizedUrl);
                }
            });
        });
    }
    /**
     * Only build landing page of the site, building more as the author goes to different links.
     */
    lazyBuildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            this.beforeSiteGenerate();
            logger.info('Generating landing page...');
            try {
                yield this.generateLandingPage();
                yield this.copyLazySourceFiles();
                yield fs_extra_1.default.remove(this.tempPath);
                yield this.lazyBuildAllPagesNotViewed(this.currentPageViewed);
                logger.info('Landing page built, other pages will be built as you navigate to them!');
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for lazyBuildSourceFiles().
     */
    copyLazySourceFiles() {
        const lazyLoadingSpinnerHtmlFilePath = path_1.default.join(__dirname, constants_1.LAZY_LOADING_SITE_FILE_NAME);
        const outputSpinnerHtmlFilePath = path_1.default.join(this.outputPath, constants_1.LAZY_LOADING_SITE_FILE_NAME);
        return fs_extra_1.default.copy(lazyLoadingSpinnerHtmlFilePath, outputSpinnerHtmlFilePath);
    }
    _rebuildAffectedSourceFiles(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
            const uniquePaths = constants_1._.uniq(filePathArray);
            this.beforeSiteGenerate();
            try {
                yield this.layoutManager.updateLayouts(filePaths);
                yield this.regenerateAffectedPages(uniquePaths);
                yield fs_extra_1.default.remove(this.tempPath);
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    _rebuildPagesBeingViewed(normalizedUrls) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            const normalizedUrlArray = Array.isArray(normalizedUrls) ? normalizedUrls : [normalizedUrls];
            const uniqueUrls = constants_1._.uniq(normalizedUrlArray);
            uniqueUrls.forEach(normalizedUrl => logger.info(`Building ${normalizedUrl} as some of its dependencies were changed since the last visit`));
            const pagesToRebuild = this.pages.filter(page => uniqueUrls.some(pageUrl => fsUtil.removeExtension(page.pageConfig.sourcePath) === pageUrl));
            const pageGenerationTask = {
                mode: 'async',
                pages: pagesToRebuild,
            };
            try {
                this._setTimestampVariable();
                yield this.runPageGenerationTasks([pageGenerationTask]);
                yield this.writeSiteData();
                Site.calculateBuildTimeForRebuildPagesBeingViewed(startTime);
            }
            catch (err) {
                yield Site.rejectHandler(err, [this.tempPath, this.outputPath]);
            }
            yield fs_extra_1.default.remove(this.tempPath);
        });
    }
    /**
     * Helper function for _rebuildPagesBeingViewed().
     */
    static calculateBuildTimeForRebuildPagesBeingViewed(startTime) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        return logger.info(`Lazy website regeneration complete! Total build time: ${totalBuildTime}s`);
    }
    _backgroundBuildNotViewedFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.toRebuild.size === 0) {
                return;
            }
            logger.info('Building files that are not viewed in the background...');
            const isCompleted = yield this.generatePagesMarkedToRebuild();
            if (isCompleted) {
                logger.info('Background building completed!');
                this.postBackgroundBuildFunc();
            }
        });
    }
    /**
     * Generates pages that are marked to be built/rebuilt.
     * @returns A Promise that resolves once all pages are generated.
     */
    generatePagesMarkedToRebuild() {
        return __awaiter(this, void 0, void 0, function* () {
            const pagesToRebuild = this.pages.filter((page) => {
                const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath);
                return this.toRebuild.has(normalizedUrl);
            });
            const pageRebuildTask = {
                mode: 'async',
                pages: pagesToRebuild,
            };
            return this.runPageGenerationTasks([pageRebuildTask]);
        });
    }
    _rebuildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            logger.info('Pages or site config modified, updating pages...');
            this.beforeSiteGenerate();
            this.layoutManager.removeLayouts();
            const removedPageFilePaths = this.updateAddressablePages();
            try {
                yield this.removeAsset(removedPageFilePaths);
                yield this.rebuildRequiredPages();
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for _rebuildSourceFiles().
     */
    rebuildRequiredPages() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.onePagePath) {
                this.mapAddressablePagesToPages(this.addressablePages || [], this.getFavIconUrl());
                yield this._rebuildPagesBeingViewed(this.currentOpenedPages);
                yield this.lazyBuildAllPagesNotViewed(this.currentOpenedPages);
                return;
            }
            logger.warn('Rebuilding all pages...');
            yield this.buildSourceFiles();
        });
    }
    _buildMultipleAssets(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
            const uniquePaths = constants_1._.uniq(filePathArray);
            const fileIgnore = (0, ignore_1.default)().add(this.siteConfig.ignore);
            const fileRelativePaths = uniquePaths.map(filePath => path_1.default.relative(this.rootPath, filePath));
            const copyAssets = fileIgnore.filter(fileRelativePaths)
                .map(asset => fs_extra_1.default.copy(path_1.default.join(this.rootPath, asset), path_1.default.join(this.outputPath, asset)));
            yield Promise.all(copyAssets);
            logger.info('Assets built');
        });
    }
    _removeMultipleAssets(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
            const uniquePaths = constants_1._.uniq(filePathArray);
            const fileRelativePaths = uniquePaths.map(filePath => path_1.default.relative(this.rootPath, filePath));
            const filesToRemove = fileRelativePaths.map(fileRelativePath => path_1.default.join(this.outputPath, fileRelativePath));
            const removeFiles = filesToRemove.map(asset => fs_extra_1.default.remove(asset));
            if (removeFiles.length !== 0) {
                yield Promise.all(removeFiles);
                logger.debug('Assets removed');
            }
        });
    }
    buildAssets() {
        return __awaiter(this, void 0, void 0, function* () {
            logger.info('Building assets...');
            const outputFolder = path_1.default.relative(this.rootPath, this.outputPath);
            const fileIgnore = (0, ignore_1.default)().add([...this.siteConfig.ignore, outputFolder]);
            // Scan and copy assets (excluding ignore files).
            try {
                const listOfAssets = this.listAssets(fileIgnore);
                const assetsToCopy = listOfAssets.map(asset => fs_extra_1.default.copy(path_1.default.join(this.rootPath, asset), path_1.default.join(this.outputPath, asset)));
                yield Promise.all(assetsToCopy);
                logger.info('Assets built');
            }
            catch (error) {
                yield Site.rejectHandler(error, []); // assets won't affect deletion
            }
        });
    }
    reloadSiteConfig() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            const oldSiteConfig = this.siteConfig;
            const oldAddressablePages = this.addressablePages.slice();
            const oldPagesSrc = oldAddressablePages.map(page => page.src);
            yield this.readSiteConfig();
            yield this.handleIgnoreReload(oldSiteConfig.ignore);
            yield this.handlePageReload(oldAddressablePages, oldPagesSrc, oldSiteConfig);
            yield this.handleStyleReload(oldSiteConfig.style);
            if (this.backgroundBuildMode) {
                this.backgroundBuildNotViewedFiles();
            }
        });
    }
    /**
     * Handles the rebuilding of modified pages
     */
    handlePageReload(oldAddressablePages, oldPagesSrc, oldSiteConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            this.collectAddressablePages();
            // Comparator for the _differenceWith comparison below
            const isNewPage = (newPage, oldPage) => constants_1._.isEqual(newPage, oldPage) || newPage.src === oldPage.src;
            const addedPages = constants_1._.differenceWith(this.addressablePages, oldAddressablePages, isNewPage);
            const removedPages = constants_1._.differenceWith(oldAddressablePages, this.addressablePages, isNewPage)
                .map(filePath => fsUtil.setExtension(filePath.src, '.html'));
            // Checks if any attributes of site.json requiring a global rebuild are modified
            const isGlobalConfigModified = () => !constants_1._.isEqual(oldSiteConfig.faviconPath, this.siteConfig.faviconPath)
                || !constants_1._.isEqual(oldSiteConfig.titlePrefix, this.siteConfig.titlePrefix)
                || !constants_1._.isEqual(oldSiteConfig.titleSuffix, this.siteConfig.titleSuffix)
                || !constants_1._.isEqual(oldSiteConfig.style, this.siteConfig.style)
                || !constants_1._.isEqual(oldSiteConfig.externalScripts, this.siteConfig.externalScripts)
                || !constants_1._.isEqual(oldSiteConfig.globalOverride, this.siteConfig.globalOverride)
                || !constants_1._.isEqual(oldSiteConfig.plugins, this.siteConfig.plugins)
                || !constants_1._.isEqual(oldSiteConfig.pluginsContext, this.siteConfig.pluginsContext)
                || !constants_1._.isEqual(oldSiteConfig.headingIndexingLevel, this.siteConfig.headingIndexingLevel)
                || !constants_1._.isEqual(oldSiteConfig.enableSearch, this.siteConfig.enableSearch)
                || !constants_1._.isEqual(oldSiteConfig.timeZone, this.siteConfig.timeZone)
                || !constants_1._.isEqual(oldSiteConfig.locale, this.siteConfig.locale)
                || !constants_1._.isEqual(oldSiteConfig.intrasiteLinkValidation, this.siteConfig.intrasiteLinkValidation)
                || !constants_1._.isEqual(oldSiteConfig.plantumlCheck, this.siteConfig.plantumlCheck);
            if (isGlobalConfigModified() || !constants_1._.isEmpty(addedPages) || !constants_1._.isEmpty(removedPages)) {
                yield this.removeAsset(removedPages);
                this.buildManagers();
                yield this._rebuildSourceFiles();
                yield this.writeSiteData();
            }
            else {
                // Get pages with edited attributes but with the same src
                const editedPages = constants_1._.differenceWith(this.addressablePages, oldAddressablePages, (newPage, oldPage) => constants_1._.isEqual(newPage, oldPage) || !oldPagesSrc.includes(newPage.src));
                this.updatePages(editedPages);
                const siteConfigDirectory = path_1.default.dirname(path_1.default.join(this.rootPath, this.siteConfigPath));
                this.regenerateAffectedPages(editedPages.map(page => path_1.default.join(siteConfigDirectory, page.src)));
            }
        });
    }
    /**
     * Creates new pages and replaces the original pages with the updated version
     */
    updatePages(pagesToUpdate) {
        pagesToUpdate.forEach((pageToUpdate) => {
            this.pages.forEach((page, index) => {
                if (page.pageConfig.src === pageToUpdate.src) {
                    const newPage = this.createNewPage(pageToUpdate, this.getFavIconUrl());
                    newPage.resetState();
                    this.pages[index] = newPage;
                }
            });
        });
    }
    /**
     * Handles the reloading of ignore attributes
     */
    handleIgnoreReload(oldIgnore) {
        return __awaiter(this, void 0, void 0, function* () {
            const assetsToRemove = constants_1._.difference(this.siteConfig.ignore, oldIgnore);
            if (!constants_1._.isEqual(oldIgnore, this.siteConfig.ignore)) {
                yield this._removeMultipleAssets(assetsToRemove);
                this.buildManagers();
                yield this.buildAssets();
            }
        });
    }
    /**
     * Handles the reloading of the style attribute if it has been modified
     */
    handleStyleReload(oldStyle) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!constants_1._.isEqual(oldStyle.bootstrapTheme, this.siteConfig.style.bootstrapTheme)) {
                yield this.copyBootstrapTheme(true);
                logger.info('Updated bootstrap theme');
            }
        });
    }
    /**
     * Checks if a specified file path is a dependency of a page
     * @param filePath file path to check
     * @returns whether the file path is a dependency of any of the site's pages
     */
    isDependencyOfPage(filePath) {
        return this.pages.some(page => page.isDependency(filePath))
            || fsUtil.ensurePosix(filePath).endsWith(USER_VARIABLES_PATH);
    }
    /**
     * Checks if a specified file path satisfies a src or glob in any of the page configurations.
     * @param filePath file path to check
     * @returns whether the file path is satisfies any glob
     */
    isFilepathAPage(filePath) {
        const { pages, pagesExclude } = this.siteConfig;
        const relativeFilePath = fsUtil.ensurePosix(path_1.default.relative(this.rootPath, filePath));
        const srcesFromPages = constants_1._.flatMap(pages.filter(page => page.src), page => (Array.isArray(page.src) ? page.src : [page.src]));
        if (srcesFromPages.includes(relativeFilePath)) {
            return true;
        }
        const filePathsFromGlobs = constants_1._.flatMap(pages.filter(page => page.glob), page => this.getPageGlobPaths(page, pagesExclude));
        return filePathsFromGlobs.some(fp => fp === relativeFilePath);
    }
    getFavIconUrl() {
        const { baseUrl, faviconPath } = this.siteConfig;
        if (faviconPath) {
            if (!fs_extra_1.default.existsSync(path_1.default.join(this.rootPath, faviconPath))) {
                logger.warn(`${faviconPath} does not exist`);
            }
            return url.join('/', baseUrl, faviconPath);
        }
        else if (fs_extra_1.default.existsSync(path_1.default.join(this.rootPath, FAVICON_DEFAULT_PATH))) {
            return url.join('/', baseUrl, FAVICON_DEFAULT_PATH);
        }
        return undefined;
    }
    /**
     * Maps an array of addressable pages to an array of Page object
     */
    mapAddressablePagesToPages(addressablePages, faviconUrl) {
        this.pages = addressablePages.map(page => this.createNewPage(page, faviconUrl));
    }
    /**
     * Creates and returns a new Page with the given page config details and favicon url
     * @param page config
     * @param faviconUrl of the page
     */
    createNewPage(page, faviconUrl) {
        return this.createPage({
            faviconUrl,
            pageSrc: page.src,
            title: page.title,
            layout: page.layout,
            frontmatter: page.frontmatter,
            searchable: page.searchable !== 'no',
            externalScripts: page.externalScripts || [],
        });
    }
    stopOngoingBuilds() {
        this.stopGenerationTimeThreshold = new Date();
    }
    /**
     * Runs the supplied page generation tasks according to the specified mode of each task.
     * A page generation task can be a sequential generation or an asynchronous generation.
     * @param pageGenerationTasks Array of page generation tasks
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    runPageGenerationTasks(pageGenerationTasks) {
        return __awaiter(this, void 0, void 0, function* () {
            const pagesCount = pageGenerationTasks.reduce((acc, task) => acc + task.pages.length, 0);
            const progressBar = new ProgressBar(`[:bar] :current / ${pagesCount} pages built`, { total: pagesCount });
            progressBar.render();
            logger.setProgressBar(progressBar);
            const startTime = new Date();
            let isCompleted = true;
            yield (0, async_1.sequentialAsyncForEach)(pageGenerationTasks, (task) => __awaiter(this, void 0, void 0, function* () {
                if (this.backgroundBuildMode && startTime < this.stopGenerationTimeThreshold) {
                    logger.info('Page generation stopped');
                    logger.debug('Page generation stopped at generation task queue');
                    isCompleted = false;
                    return;
                }
                if (task.mode === 'sequential') {
                    isCompleted = yield this.generatePagesSequential(task.pages, progressBar);
                }
                else {
                    isCompleted = (yield this.generatePagesAsyncThrottled(task.pages, progressBar));
                }
                logger.removeProgressBar();
                this.siteLinkManager.validateAllIntralinks();
            }));
            return isCompleted;
        });
    }
    /**
     * Generate pages sequentially. That is, the pages are generated
     * one-by-one in order.
     * @param pages Pages to be generated
     * @param progressBar Progress bar of the overall generation process
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    generatePagesSequential(pages, progressBar) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            let isCompleted = true;
            yield (0, async_1.sequentialAsyncForEach)(pages, (page) => __awaiter(this, void 0, void 0, function* () {
                if (this.backgroundBuildMode && startTime < this.stopGenerationTimeThreshold) {
                    logger.info('Page generation stopped');
                    logger.debug('Page generation stopped at sequential generation');
                    isCompleted = false;
                    return;
                }
                try {
                    yield page.generate(this.externalManager);
                    this.toRebuild.delete(fsUtil.removeExtension(page.pageConfig.sourcePath));
                    if (this.backgroundBuildMode) {
                        yield this.writeSiteData(false);
                    }
                    progressBar.tick();
                }
                catch (err) {
                    logger.error(err);
                    throw new Error(`Error while generating ${page.pageConfig.sourcePath}`);
                }
            }));
            return isCompleted;
        });
    }
    /**
     * Creates the supplied pages' page generation promises at a throttled rate.
     * This is done to avoid pushing too many callbacks into the event loop at once. (#1245)
     * @param pages Pages to be generated
     * @param progressBar Progress bar of the overall generation process
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    generatePagesAsyncThrottled(pages, progressBar) {
        return new Promise((resolve, reject) => {
            const context = {
                startTime: new Date(),
                numPagesGenerated: 0,
                numPagesToGenerate: pages.length,
                isCompleted: true,
            };
            // Map pages into array of callbacks for delayed execution
            const pageGenerationQueue = pages.map(page => () => __awaiter(this, void 0, void 0, function* () {
                // Pre-generate guard to ensure no newly executed callbacks start on stop
                if (this.backgroundBuildMode && context.startTime < this.stopGenerationTimeThreshold) {
                    if (context.isCompleted) {
                        logger.info('Page generation stopped');
                        logger.debug('Page generation stopped at asynchronous generation');
                        context.isCompleted = false;
                        resolve(false);
                    }
                    return;
                }
                try {
                    yield page.generate(this.externalManager);
                    this.toRebuild.delete(fsUtil.removeExtension(page.pageConfig.sourcePath));
                    if (this.backgroundBuildMode) {
                        yield this.writeSiteData(false);
                    }
                    this.generateProgressBarStatus(progressBar, context, pageGenerationQueue, resolve);
                }
                catch (err) {
                    logger.error(err);
                    reject(new Error(`Error while generating ${page.pageConfig.sourcePath}`));
                }
            }));
            /*
             Take the first MAX_CONCURRENT_PAGE_GENERATION_PROMISES callbacks and execute them.
             Whenever a page generation callback resolves,
             it pops the next unprocessed callback off pageGenerationQueue and executes it.
             */
            pageGenerationQueue.splice(0, MAX_CONCURRENT_PAGE_GENERATION_PROMISES)
                .forEach(generatePage => generatePage());
        });
    }
    /**
     * Helper function for generatePagesAsyncThrottled().
     */
    generateProgressBarStatus(progressBar, context, pageGenerationQueue, resolve) {
        // Post-generate guard to ensure no new callbacks are executed on stop
        if (this.backgroundBuildMode && context.startTime < this.stopGenerationTimeThreshold) {
            if (context.isCompleted) {
                logger.info('Page generation stopped');
                logger.debug('Page generation stopped at asynchronous generation');
                context.isCompleted = false;
                resolve(false);
            }
            return;
        }
        progressBar.tick();
        context.numPagesGenerated += 1;
        if (pageGenerationQueue.length) {
            pageGenerationQueue.pop()();
        }
        else if (context.numPagesGenerated === context.numPagesToGenerate) {
            resolve(true);
        }
    }
    /**
     * Renders all pages specified in site configuration file to the output folder
     */
    generatePages() {
        // Run MarkBind include and render on each source file.
        // Render the final rendered page to the output folder.
        const addressablePages = this.addressablePages || [];
        const faviconUrl = this.getFavIconUrl();
        this._setTimestampVariable();
        this.mapAddressablePagesToPages(addressablePages, faviconUrl);
        const pageGenerationTask = {
            mode: 'async',
            pages: this.pages,
        };
        return this.runPageGenerationTasks([pageGenerationTask]);
    }
    /**
     * Renders only the starting page for lazy loading to the output folder.
     */
    generateLandingPage() {
        return __awaiter(this, void 0, void 0, function* () {
            const addressablePages = this.addressablePages || [];
            const faviconUrl = this.getFavIconUrl();
            this._setTimestampVariable();
            this.mapAddressablePagesToPages(addressablePages, faviconUrl);
            const landingPage = this.pages.find(page => page.pageConfig.src === this.onePagePath);
            if (!landingPage) {
                throw new Error(`${this.onePagePath} is not specified in the site configuration.`);
            }
            yield landingPage.generate(this.externalManager);
        });
    }
    regenerateAffectedPages(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            const shouldRebuildAllPages = this.collectUserDefinedVariablesMapIfNeeded(filePaths) || this.forceReload;
            if (shouldRebuildAllPages) {
                logger.warn('Rebuilding all pages as variables file was changed, or the --force-reload flag was set');
            }
            this._setTimestampVariable();
            let openedPagesToRegenerate = [];
            const asyncPagesToRegenerate = this.pages.filter((page) => {
                const doFilePathsHaveSourceFiles = filePaths.some(filePath => page.isDependency(filePath));
                if (shouldRebuildAllPages || doFilePathsHaveSourceFiles) {
                    if (this.onePagePath) {
                        const normalizedSource = fsUtil.removeExtension(page.pageConfig.sourcePath);
                        const openIdx = this.currentOpenedPages.findIndex(pagePath => pagePath === normalizedSource);
                        const isRecentlyViewed = openIdx !== -1;
                        if (!isRecentlyViewed) {
                            this.toRebuild.add(normalizedSource);
                        }
                        else {
                            openedPagesToRegenerate[openIdx] = page;
                        }
                        return false;
                    }
                    return true;
                }
                return false;
            });
            /*
             * As a side effect of doing assignment to an empty array, some elements might be
             * undefined if it has not been assigned to anything. We filter those out here.
             */
            openedPagesToRegenerate = openedPagesToRegenerate.filter(page => page);
            const totalPagesToRegenerate = openedPagesToRegenerate.length + asyncPagesToRegenerate.length;
            if (totalPagesToRegenerate === 0) {
                logger.info('No pages needed to be rebuilt');
                return;
            }
            logger.info(`Rebuilding ${totalPagesToRegenerate} pages`);
            const pageGenerationTasks = [];
            if (openedPagesToRegenerate.length > 0) {
                const recentPagesGenerationTask = {
                    mode: 'sequential',
                    pages: openedPagesToRegenerate,
                };
                pageGenerationTasks.push(recentPagesGenerationTask);
            }
            if (asyncPagesToRegenerate.length > 0) {
                const asyncPagesGenerationTask = {
                    mode: 'async',
                    pages: asyncPagesToRegenerate,
                };
                pageGenerationTasks.push(asyncPagesGenerationTask);
            }
            try {
                yield this.runPageGenerationTasks(pageGenerationTasks);
                yield this.writeSiteData();
                logger.info('Pages rebuilt');
                this.calculateBuildTimeForRegenerateAffectedPages(startTime);
            }
            catch (error) {
                yield Site.rejectHandler(error, []);
            }
        });
    }
    /**
     * Helper function for regenerateAffectedPages().
     */
    calculateBuildTimeForRegenerateAffectedPages(startTime) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        logger.info(`Website regeneration complete! Total build time: ${totalBuildTime}s`);
        if (!this.onePagePath && totalBuildTime > LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT) {
            logger.info('Your pages took quite a while to rebuild...'
                + 'Have you considered using markbind serve -o when writing content to speed things up?');
        }
    }
    /**
     * Copies Font Awesome assets to the assets folder
     */
    copyFontAwesomeAsset() {
        return __awaiter(this, void 0, void 0, function* () {
            const faRootSrcPath = path_1.default.dirname(require.resolve('@fortawesome/fontawesome-free/package.json'));
            const faCssSrcPath = path_1.default.join(faRootSrcPath, 'css', 'all.min.css');
            const faCssDestPath = path_1.default.join(this.siteAssetsDestPath, 'fontawesome', 'css', 'all.min.css');
            const faFontsSrcPath = path_1.default.join(faRootSrcPath, 'webfonts');
            const faFontsDestPath = path_1.default.join(this.siteAssetsDestPath, 'fontawesome', 'webfonts');
            yield fs_extra_1.default.copy(faCssSrcPath, faCssDestPath);
            yield fs_extra_1.default.copy(faFontsSrcPath, faFontsDestPath);
        });
    }
    /**
     * Copies Octicon assets to the assets folder
     */
    copyOcticonsAsset() {
        const octiconsCssSrcPath = require.resolve('@primer/octicons/build/build.css');
        const octiconsCssDestPath = path_1.default.join(this.siteAssetsDestPath, 'css', 'octicons.css');
        return fs_extra_1.default.copy(octiconsCssSrcPath, octiconsCssDestPath);
    }
    /**
     * Copies Google Material Icons assets to the assets folder
     */
    copyMaterialIconsAsset() {
        const materialIconsRootSrcPath = path_1.default.dirname(require.resolve('material-icons/package.json'));
        const materialIconsCssAndFontsSrcPath = path_1.default.join(materialIconsRootSrcPath, 'iconfont');
        const materialIconsCssAndFontsDestPath = path_1.default.join(this.siteAssetsDestPath, 'material-icons');
        return fs_extra_1.default.copy(materialIconsCssAndFontsSrcPath, materialIconsCssAndFontsDestPath);
    }
    /**
     * Copies core-web bundles and external assets to the assets output folder
     */
    copyCoreWebAsset() {
        const coreWebRootPath = path_1.default.dirname(require.resolve('@markbind/core-web/package.json'));
        const coreWebAssetPath = path_1.default.join(coreWebRootPath, 'asset');
        fs_extra_1.default.copySync(coreWebAssetPath, this.siteAssetsDestPath);
        const dirsToCopy = ['fonts'];
        const filesToCopy = [
            'js/markbind.min.js',
            'css/markbind.min.css',
        ];
        const copyAllFiles = filesToCopy.map((file) => {
            const srcPath = path_1.default.join(coreWebRootPath, 'dist', file);
            const destPath = path_1.default.join(this.siteAssetsDestPath, file);
            return fs_extra_1.default.copy(srcPath, destPath);
        });
        const copyFontsDir = dirsToCopy.map((dir) => {
            const srcPath = path_1.default.join(coreWebRootPath, 'dist', dir);
            const destPath = path_1.default.join(this.siteAssetsDestPath, 'css', dir);
            return fs_extra_1.default.copy(srcPath, destPath);
        });
        return Promise.all([...copyAllFiles, ...copyFontsDir]);
    }
    /**
     * Copies bootstrapTheme to the assets folder if a valid bootstrapTheme is specified
     * @param isRebuild only true if it is a rebuild
     */
    copyBootstrapTheme(isRebuild) {
        const { bootstrapTheme } = this.siteConfig.style;
        /**
         * If it is the initial build using the default bootstrapTheme or if the bootstrapTheme specified
         * is not valid, then do nothing.
         */
        if ((!isRebuild && !bootstrapTheme)
            || (bootstrapTheme && !constants_1._.has(SUPPORTED_THEMES_PATHS, bootstrapTheme))) {
            return constants_1._.noop;
        }
        const themeSrcPath = !bootstrapTheme
            ? require.resolve('@markbind/core-web/asset/css/bootstrap.min.css')
            : SUPPORTED_THEMES_PATHS[bootstrapTheme];
        const themeDestPath = path_1.default.join(this.siteAssetsDestPath, 'css', 'bootstrap.min.css');
        return fs_extra_1.default.copy(themeSrcPath, themeDestPath);
    }
    /**
     * Writes the site data to siteData.json
     * @param verbose Flag to emit logs of the operation
     */
    writeSiteData(verbose = true) {
        return __awaiter(this, void 0, void 0, function* () {
            const siteDataPath = path_1.default.join(this.outputPath, SITE_DATA_NAME);
            const siteData = {
                enableSearch: this.siteConfig.enableSearch,
                pages: this.pages.filter(page => page.pageConfig.searchable && page.headings)
                    .map(page => ({
                    src: page.pageConfig.src,
                    title: page.title,
                    headings: page.headings,
                    headingKeywords: page.keywords,
                    frontmatterKeywords: page.frontmatter.keywords,
                })),
            };
            try {
                yield fs_extra_1.default.outputJson(siteDataPath, siteData, { spaces: 2 });
                if (verbose) {
                    logger.info('Site data built');
                }
            }
            catch (error) {
                yield Site.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    deploy(ciTokenVar) {
        const defaultDeployConfig = {
            branch: 'gh-pages',
            message: 'Site Update.',
            repo: '',
            remote: 'origin',
        };
        process.env.NODE_DEBUG = 'gh-pages';
        return this.generateDepUrl(ciTokenVar, defaultDeployConfig);
    }
    /**
     * Helper function for deploy(). Returns the ghpages link where the repo will be hosted.
     */
    generateDepUrl(ciTokenVar, defaultDeployConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            const publish = bluebird_1.default.promisify(gh_pages_1.default.publish);
            yield this.readSiteConfig();
            const depOptions = yield this.getDepOptions(ciTokenVar, defaultDeployConfig, publish);
            return Site.getDepUrl(depOptions);
        });
    }
    /**
     * Helper function for deploy(). Set the options needed to be used by ghpages.publish.
     */
    getDepOptions(ciTokenVar, defaultDeployConfig, publish) {
        return __awaiter(this, void 0, void 0, function* () {
            const basePath = this.siteConfig.deploy.baseDir || this.outputPath;
            if (!fs_extra_1.default.existsSync(basePath)) {
                throw new Error('The site directory does not exist. Please build the site first before deploy.');
            }
            const options = {
                branch: this.siteConfig.deploy.branch || defaultDeployConfig.branch,
                message: this.siteConfig.deploy.message || defaultDeployConfig.message,
                repo: this.siteConfig.deploy.repo || defaultDeployConfig.repo,
                remote: defaultDeployConfig.remote,
            };
            options.message = options.message.concat(' [skip ci]');
            if (ciTokenVar) {
                const ciToken = constants_1._.isBoolean(ciTokenVar) ? 'GITHUB_TOKEN' : ciTokenVar;
                if (!process.env[ciToken]) {
                    throw new Error(`The environment variable ${ciToken} does not exist.`);
                }
                const githubToken = process.env[ciToken];
                let repoSlug;
                if (process.env.TRAVIS) {
                    repoSlug = Site.extractRepoSlug(options.repo, process.env.TRAVIS_REPO_SLUG);
                    options.user = {
                        name: 'Deployment Bot',
                        email: 'deploy@travis-ci.org',
                    };
                }
                else if (process.env.APPVEYOR) {
                    repoSlug = Site.extractRepoSlug(options.repo, process.env.APPVEYOR_REPO_NAME);
                    options.user = {
                        name: 'AppVeyorBot',
                        email: 'deploy@appveyor.com',
                    };
                }
                else if (process.env.GITHUB_ACTIONS) {
                    repoSlug = Site.extractRepoSlug(options.repo, process.env.GITHUB_REPOSITORY);
                    options.user = {
                        name: 'github-actions',
                        email: 'github-actions@github.com',
                    };
                }
                else if (process.env.CIRCLECI) {
                    repoSlug = Site.extractRepoSlug(options.repo, `${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`);
                    options.user = {
                        name: 'circleci-bot',
                        email: 'deploy@circleci.com',
                    };
                }
                else {
                    throw new Error('-c/--ci should only be run in CI environments.');
                }
                options.repo = `https://x-access-token:${githubToken}@github.com/${repoSlug}.git`;
            }
            // Waits for the repo to be updated.
            yield publish(basePath, options);
            return options;
        });
    }
    /**
     * Extract repo slug from user-specified repo URL so that we can include the access token
     */
    static extractRepoSlug(repo, ciRepoSlug) {
        if (!repo) {
            return ciRepoSlug;
        }
        const repoSlugRegex = /github\.com[:/]([\w-]+\/[\w-.]+)\.git$/;
        const repoSlugMatch = repoSlugRegex.exec(repo);
        if (!repoSlugMatch) {
            throw new Error('-c/--ci expects a GitHub repository.\n'
                + `The specified repository ${repo} is not valid.`);
        }
        const [, repoSlug] = repoSlugMatch;
        return repoSlug;
    }
    /**
     * Helper function for deploy().
     */
    static getDepUrl(options) {
        const git = (0, simple_git_1.default)({ baseDir: process.cwd() });
        return Site.getDeploymentUrl(git, options);
    }
    /**
     * Gets the deployed website's url, returning null if there was an error retrieving it.
     */
    static getDeploymentUrl(git, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const HTTPS_PREAMBLE = 'https://';
            const SSH_PREAMBLE = 'git@github.com:';
            const GITHUB_IO_PART = 'github.io';
            // https://<name|org name>.github.io/<repo name>/
            function constructGhPagesUrl(remoteUrl) {
                if (!remoteUrl) {
                    return null;
                }
                const parts = remoteUrl.split('/');
                if (remoteUrl.startsWith(HTTPS_PREAMBLE)) {
                    // https://github.com/<name|org>/<repo>.git (HTTPS)
                    const repoNameWithExt = parts[parts.length - 1];
                    const repoName = repoNameWithExt.substring(0, repoNameWithExt.lastIndexOf('.'));
                    const name = parts[parts.length - 2].toLowerCase();
                    return `https://${name}.${GITHUB_IO_PART}/${repoName}`;
                }
                else if (remoteUrl.startsWith(SSH_PREAMBLE)) {
                    // git@github.com:<name|org>/<repo>.git (SSH)
                    const repoNameWithExt = parts[parts.length - 1];
                    const repoName = repoNameWithExt.substring(0, repoNameWithExt.lastIndexOf('.'));
                    const name = parts[0].substring(SSH_PREAMBLE.length);
                    return `https://${name}.${GITHUB_IO_PART}/${repoName}`;
                }
                return null;
            }
            const { remote, branch, repo } = options;
            const cnamePromise = gitUtil.getRemoteBranchFile(git, 'blob', remote, branch, 'CNAME');
            const remoteUrlPromise = gitUtil.getRemoteUrl(git, remote);
            const promises = [cnamePromise, remoteUrlPromise];
            try {
                const promiseResults = yield Promise.all(promises);
                const generateGhPagesUrl = (results) => {
                    const cname = results[0];
                    const remoteUrl = results[1];
                    if (cname) {
                        return cname.trim();
                    }
                    else if (repo) {
                        return constructGhPagesUrl(repo);
                    }
                    return constructGhPagesUrl(remoteUrl.trim());
                };
                return generateGhPagesUrl(promiseResults);
            }
            catch (err) {
                logger.error(err);
                return null;
            }
        });
    }
    _setTimestampVariable() {
        const options = {
            weekday: 'short',
            year: 'numeric',
            month: 'short',
            day: 'numeric',
            timeZone: this.siteConfig.timeZone,
            timeZoneName: 'short',
        };
        const time = new Date().toLocaleTimeString(this.siteConfig.locale, options);
        this.variableProcessor.addUserDefinedVariableForAllSites('timestamp', time);
    }
}
exports.Site = Site;
//# sourceMappingURL=index.js.map