context.js

/**
 * @module context
 */
import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import YAML from 'yaml';
import util from './util.js';
import logger from './logger.js';
import config from './config.js';
import { Meta, LocalPage } from './models/index.js';

const MKDOCS_YML = 'mkdocs.yml';
const README_MD = 'README.md';
/**
 * Loads an parses the 'mkdocs.yml' file 
 * 
 * @param {string} basePath - the basepath to look for 'mkdocs.yml'
 * @returns {object} with nav, repo_url, site_name attributes
 */
function loadConfig(basePath) {
    const mkDocsFile = path.resolve(basePath, MKDOCS_YML);
    const yml = readFileSync(mkDocsFile, 'utf8');
    const json = YAML.parse(yml);
    const { nav, repo_url, site_name } = json;
    if (!Array.isArray(nav)) {
        throw new Error(`nav is missing from your ${MKDOCS_YML} file`);
    }
    if (typeof repo_url !== 'string' || repo_url.trim().length === 0) {
        throw new Error(`repo_url is missing from your ${MKDOCS_YML} file`);
    }

    return { nav, repo_url, site_name };
}

/**
 * Recursively traverses the `nav` object and adds `LocalPage`s to `pages` array
 *  
 * @param {string} repo_url - repo_url from 'mkdocs.yml'
 * @param {*} nav - nav object from 'mkdocs.yml'
 * @param {*} basePath - the basepath to resolve files
 * @param {Array<LocalPage>} pages 
 * @returns {Array<LocalPage>} The array with all pages from `nav`
 */
function traverse(repo_url, nav, basePath, pages = []) {
    nav.forEach((item) => {
        if (typeof item === 'string') {
            throw new Error(`No title for ${item}`);
        }
        const pageTitle = Object.keys(item)[0];
        const pagePath = Object.values(item)[0];
        if (Array.isArray(pagePath)) {
            traverse(repo_url, pagePath, basePath, pages);
        } else {
            const page = getPage(repo_url, pageTitle, path.resolve(basePath, 'docs', pagePath));
            if (page) {
                pages.push(page);
            }
        }
    });
    return pages;
}

/**
 * Creates `LocalPage` instances from the parameters
 * 
 * @param {string} repo_url - Repository url 
 * @param {string} title - Page title
 * @param {string} pagePath - Page path
 * @param {string} titlePrefix - Page title prefix
 * @returns {LocalPage} The page created from the parameters
 */
function getPage(repo_url, title, pagePath, titlePrefix = config.confluence.titlePrefix) {
    const safe = pagePath.startsWith(process.cwd());
    const exists = safe && existsSync(pagePath);
    const relPath = path.relative(process.cwd(), pagePath);
    if (!exists) {
        logger.warn(`Page "${title}" not found at "${relPath}"`);
        return;
    }
    const sha = util.fileHash(pagePath);
    const prefixedTitle = `${titlePrefix} ${title}`.trim();
    return new LocalPage(prefixedTitle, new Meta(repo_url, relPath, sha));
}

/**
 * Create a context object with all information needed for the sync 
 * 
 * @param {string} basePath - Base path to resolve files
 * @returns {object} The context object
 */
function getContext(basePath = '.') {
    const { nav, repo_url, site_name } = loadConfig(basePath);
    const pages = traverse(repo_url, nav, basePath);
    const readMe = getPage(repo_url, site_name, path.resolve(basePath, README_MD), '');
    const pageRefs = pages.reduce((obj, page) => {
        obj[page.meta.path] = page.title;
        return obj;
    }, readMe ? { [readMe.meta.path]: readMe.title } : {});
    const context = { siteName: site_name, repo: repo_url, pages, pageRefs };
    if (readMe) {
        context.readMe = readMe;
    }

    if (logger.isDebug()) {
        logger.debug(`Context:\n${JSON.stringify(context, null, 2)}`);
    }
    return context;
}

export default { getContext };