diff --git a/packages/cli/src/cmd/init.js b/packages/cli/src/cmd/init.js index 068f62ee69..9b68980ede 100755 --- a/packages/cli/src/cmd/init.js +++ b/packages/cli/src/cmd/init.js @@ -2,11 +2,10 @@ const fs = require('fs-extra'); const path = require('path'); const { Template } = require('@markbind/core'); -const { Site } = require('@markbind/core').Site; const logger = require('../util/logger'); -function init(root, options) { +async function init(root, options) { const rootFolder = path.resolve(root || process.cwd()); if (options.convert) { @@ -17,28 +16,25 @@ function init(root, options) { } const template = new Template(rootFolder, options.template); - template.init() - .then(() => { - logger.info('Initialization success.'); - }) - .then(() => { - if (options.convert) { - logger.info('Converting to MarkBind website.'); - const outputRoot = path.join(rootFolder, '_site'); - new Site(rootFolder, outputRoot).convert() - .then(() => { - logger.info('Conversion success.'); - }) - .catch((error) => { - logger.error(error.message); - process.exitCode = 1; - }); - } - }) - .catch((error) => { - logger.error(`Failed to initialize site with given template with error: ${error.message}`); + + try { + await template.init(); + logger.info('Initialization success.'); + } catch (error) { + logger.error(`Failed to initialize site with given template with error: ${error.message}`); + process.exitCode = 1; + } + + if (options.convert) { + logger.info('Converting to MarkBind website.'); + try { + await template.convert(); + logger.info('Conversion success.'); + } catch (error) { + logger.error(error.message); process.exitCode = 1; - }); + } + } } module.exports = { diff --git a/packages/core/src/Site/SiteConfig.ts b/packages/core/src/Site/SiteConfig.ts index 6815adb2ac..f7a4bcb60a 100644 --- a/packages/core/src/Site/SiteConfig.ts +++ b/packages/core/src/Site/SiteConfig.ts @@ -1,3 +1,5 @@ +import fs from 'fs-extra'; +import path from 'path'; import { FrontMatter } from '../plugins/Plugin'; const HEADING_INDEXING_LEVEL_DEFAULT = 3; @@ -99,4 +101,25 @@ export class SiteConfig { this.plantumlCheck = siteConfigJson.plantumlCheck !== undefined ? siteConfigJson.plantumlCheck : true; // check PlantUML's prerequisite by default } + + /** + * Read and returns the site config from site.json, overwrites the default base URL + * if it's specified by the user. + * + * @param rootPath The absolute path to the site folder + * @param siteConfigPath The relative path to the siteConfig + * @param baseUrl user defined base URL (if exists) + */ + static async readSiteConfig(rootPath: string, siteConfigPath: string, baseUrl?: string): Promise { + try { + const absoluteSiteConfigPath = path.join(rootPath, siteConfigPath); + const siteConfigJson = fs.readJsonSync(absoluteSiteConfigPath); + const siteConfig = new SiteConfig(siteConfigJson, baseUrl); + + return siteConfig; + } catch (err) { + throw (new Error(`Failed to read the site config file '${siteConfigPath}' at` + + `${rootPath}:\n${(err as Error).message}\nPlease ensure the file exist or is valid`)); + } + } } diff --git a/packages/core/src/Site/constants.ts b/packages/core/src/Site/constants.ts index f5ae87a6ad..f9d7d7eaa3 100644 --- a/packages/core/src/Site/constants.ts +++ b/packages/core/src/Site/constants.ts @@ -1,3 +1,32 @@ +import difference from 'lodash/difference'; +import differenceWith from 'lodash/differenceWith'; +import flatMap from 'lodash/flatMap'; +import has from 'lodash/has'; +import isBoolean from 'lodash/isBoolean'; +import isEmpty from 'lodash/isEmpty'; +import isEqual from 'lodash/isEqual'; +import isUndefined from 'lodash/isUndefined'; +import noop from 'lodash/noop'; +import omitBy from 'lodash/omitBy'; +import startCase from 'lodash/startCase'; +import union from 'lodash/union'; +import uniq from 'lodash/uniq'; + export const INDEX_MARKDOWN_FILE = 'index.md'; export const SITE_CONFIG_NAME = 'site.json'; export const LAZY_LOADING_SITE_FILE_NAME = 'LazyLiveReloadLoadingSite.html'; +export const _ = { + difference, + differenceWith, + flatMap, + has, + isUndefined, + isEqual, + isEmpty, + isBoolean, + noop, + omitBy, + startCase, + union, + uniq, +}; diff --git a/packages/core/src/Site/index.ts b/packages/core/src/Site/index.ts index 02105a2a6e..5609d16ce7 100644 --- a/packages/core/src/Site/index.ts +++ b/packages/core/src/Site/index.ts @@ -6,19 +6,6 @@ import walkSync from 'walk-sync'; import simpleGit, { SimpleGit } from 'simple-git'; import Bluebird from 'bluebird'; import ghpages from 'gh-pages'; -import difference from 'lodash/difference'; -import differenceWith from 'lodash/differenceWith'; -import flatMap from 'lodash/flatMap'; -import has from 'lodash/has'; -import isBoolean from 'lodash/isBoolean'; -import isEmpty from 'lodash/isEmpty'; -import isEqual from 'lodash/isEqual'; -import isUndefined from 'lodash/isUndefined'; -import noop from 'lodash/noop'; -import omitBy from 'lodash/omitBy'; -import startCase from 'lodash/startCase'; -import union from 'lodash/union'; -import uniq from 'lodash/uniq'; import { Template as NunjucksTemplate } from 'nunjucks'; import { SiteConfig, SiteConfigPage, SiteConfigStyle } from './SiteConfig'; @@ -35,29 +22,13 @@ import { delay } from '../utils/delay'; import * as fsUtil from '../utils/fsUtil'; import * as gitUtil from '../utils/git'; import * as logger from '../utils/logger'; -import { SITE_CONFIG_NAME, INDEX_MARKDOWN_FILE, LAZY_LOADING_SITE_FILE_NAME } from './constants'; +import { SITE_CONFIG_NAME, LAZY_LOADING_SITE_FILE_NAME, _ } from './constants'; // Change when they are migrated to TypeScript const ProgressBar = require('../lib/progress'); -const { LayoutManager, LAYOUT_DEFAULT_NAME, LAYOUT_FOLDER_PATH } = require('../Layout'); +const { LayoutManager } = require('../Layout'); require('../patches/htmlparser2'); -const _ = { - difference, - differenceWith, - flatMap, - has, - isUndefined, - isEqual, - isEmpty, - isBoolean, - noop, - omitBy, - startCase, - union, - uniq, -}; - const url = { join: path.posix.join, }; @@ -70,16 +41,12 @@ const TEMP_FOLDER_NAME = '.temp'; const TEMPLATE_SITE_ASSET_FOLDER_NAME = 'markbind'; const LAYOUT_SITE_FOLDER_NAME = 'layouts'; -const ABOUT_MARKDOWN_FILE = 'about.md'; 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 WIKI_SITE_NAV_PATH = '_Sidebar.md'; -const WIKI_FOOTER_PATH = '_Footer.md'; - const MAX_CONCURRENT_PAGE_GENERATION_PROMISES = 4; const LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT = 30000; @@ -114,9 +81,6 @@ const HIGHLIGHT_ASSETS = { light: 'codeblock-light.min.css', }; -const ABOUT_MARKDOWN_DEFAULT = '# About\n' - + 'Welcome to your **About Us** page.\n'; - const MARKBIND_WEBSITE_URL = 'https://markbind.org/'; const MARKBIND_LINK_HTML = `MarkBind ${MARKBIND_VERSION}`; @@ -301,17 +265,9 @@ export class Site { * if it's specified by the user. * @param baseUrl user defined base URL (if exists) */ - async readSiteConfig(baseUrl?: string): Promise { - try { - const siteConfigPath = path.join(this.rootPath, this.siteConfigPath); - const siteConfigJson = fs.readJsonSync(siteConfigPath); - this.siteConfig = new SiteConfig(siteConfigJson, baseUrl); - - return this.siteConfig; - } catch (err) { - throw (new Error(`Failed to read the site config file '${this.siteConfigPath}' at` - + `${this.rootPath}:\n${(err as Error).message}\nPlease ensure the file exist or is valid`)); - } + async readSiteConfig(baseUrl?: string) { + this.siteConfig = await SiteConfig.readSiteConfig(this.rootPath, this.siteConfigPath, baseUrl); + return this.siteConfig; } listAssets(fileIgnore: Ignore) { @@ -375,120 +331,6 @@ export class Site { return new Page(pageConfig, this.siteConfig); } - /** - * Converts an existing GitHub wiki or docs folder to a MarkBind website. - */ - async convert() { - await this.readSiteConfig(); - this.collectAddressablePages(); - await this.addIndexPage(); - await this.addAboutPage(); - this.addDefaultLayoutFiles(); - await this.addDefaultLayoutToSiteConfig(); - Site.printBaseUrlMessage(); - } - - /** - * Copies over README.md or Home.md to default index.md if present. - */ - async addIndexPage() { - const indexPagePath = path.join(this.rootPath, INDEX_MARKDOWN_FILE); - const fileNames = ['README.md', 'Home.md']; - const filePath = fileNames.find(fileName => fs.existsSync(path.join(this.rootPath, fileName))); - // if none of the files exist, do nothing - if (_.isUndefined(filePath)) return; - try { - await fs.copy(path.join(this.rootPath, filePath), indexPagePath); - } catch (error) { - throw new Error(`Failed to copy over ${filePath}`); - } - } - - /** - * Adds an about page to site if not present. - */ - async addAboutPage() { - const aboutPath = path.join(this.rootPath, ABOUT_MARKDOWN_FILE); - try { - await fs.access(aboutPath); - } catch (error) { - if (fs.existsSync(aboutPath)) { - return; - } - await fs.outputFile(aboutPath, ABOUT_MARKDOWN_DEFAULT); - } - } - - /** - * Adds a footer to default layout of site. - */ - addDefaultLayoutFiles() { - const wikiFooterPath = path.join(this.rootPath, WIKI_FOOTER_PATH); - let footer; - if (fs.existsSync(wikiFooterPath)) { - logger.info(`Copied over the existing ${WIKI_FOOTER_PATH} file to the converted layout`); - footer = `\n${fs.readFileSync(wikiFooterPath, 'utf8')}`; - } - - const wikiSiteNavPath = path.join(this.rootPath, WIKI_SITE_NAV_PATH); - let siteNav; - if (fs.existsSync(wikiSiteNavPath)) { - logger.info(`Copied over the existing ${WIKI_SITE_NAV_PATH} file to the converted layout\n` - + 'Check https://markbind.org/userGuide/tweakingThePageStructure.html#site-navigation-menus\n' - + 'for information on site navigation menus.'); - siteNav = fs.readFileSync(wikiSiteNavPath, 'utf8'); - } else { - siteNav = this.buildSiteNav(); - } - - const convertedLayoutTemplate = VariableRenderer.compile( - fs.readFileSync(path.join(__dirname, 'siteConvertLayout.njk'), 'utf8')); - const renderedLayout = convertedLayoutTemplate.render({ - footer, - siteNav, - }); - const layoutOutputPath = path.join(this.rootPath, LAYOUT_FOLDER_PATH, LAYOUT_DEFAULT_NAME); - - fs.writeFileSync(layoutOutputPath, renderedLayout, 'utf-8'); - } - - /** - * Builds a site navigation file from the directory structure of the site. - */ - buildSiteNav() { - let siteNavContent = ''; - this.addressablePages - .filter(addressablePage => !addressablePage.src.startsWith('_')) - .forEach((page) => { - const addressablePagePath = path.join(this.rootPath, page.src); - const relativePagePathWithoutExt = fsUtil.removeExtensionPosix( - path.relative(this.rootPath, addressablePagePath)); - const pageName = _.startCase(fsUtil.removeExtension(path.basename(addressablePagePath))); - const pageUrl = `{{ baseUrl }}/${relativePagePathWithoutExt}.html`; - siteNavContent += `* [${pageName}](${pageUrl})\n`; - }); - - return siteNavContent.trimEnd(); - } - - /** - * Applies the default layout to all addressable pages by modifying the site config file. - */ - async addDefaultLayoutToSiteConfig() { - const configPath = path.join(this.rootPath, SITE_CONFIG_NAME); - const config = await fs.readJson(configPath); - await Site.writeToSiteConfig(config, configPath); - } - - /** - * Helper function for addDefaultLayoutToSiteConfig(). - */ - static async writeToSiteConfig(config: SiteConfig, configPath: string) { - const layoutObj: SiteConfigPage = { glob: '**/*.md', layout: LAYOUT_DEFAULT_NAME }; - config.pages.push(layoutObj); - await fs.outputJson(configPath, config); - } - 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' diff --git a/packages/core/src/Site/template.ts b/packages/core/src/Site/template.ts index f2c8a308d3..0d2bb6a2cc 100644 --- a/packages/core/src/Site/template.ts +++ b/packages/core/src/Site/template.ts @@ -1,24 +1,46 @@ import fs from 'fs-extra'; import path from 'path'; +import walkSync from 'walk-sync'; import * as fsUtil from '../utils/fsUtil'; +import { INDEX_MARKDOWN_FILE, SITE_CONFIG_NAME, _ } from './constants'; +import { SiteConfig, SiteConfigPage } from './SiteConfig'; +import { VariableRenderer } from '../variables/VariableRenderer'; +import * as logger from '../utils/logger'; + +const { LAYOUT_DEFAULT_NAME, LAYOUT_FOLDER_PATH } = require('../Layout'); const requiredFiles = ['index.md', 'site.json', '_markbind/']; const PATH_TO_TEMPLATE = '../../template'; +const ABOUT_MARKDOWN_FILE = 'about.md'; +const ABOUT_MARKDOWN_DEFAULT = '# About\n' + + 'Welcome to your **About Us** page.\n'; +const CONFIG_FOLDER_NAME = '_markbind'; +const SITE_FOLDER_NAME = '_site'; +const WIKI_SITE_NAV_PATH = '_Sidebar.md'; +const WIKI_FOOTER_PATH = '_Footer.md'; + +type NaviagablePage = { + src: string, + title?: string, +}; export class Template { - root: string; - template: string; + rootPath: string; + templatePath: string; + siteConfig!: SiteConfig; + siteConfigPath: string = SITE_CONFIG_NAME; + navigablePages!: NaviagablePage[]; constructor(rootPath: string, templatePath: string) { - this.root = rootPath; - this.template = path.join(__dirname, PATH_TO_TEMPLATE, templatePath); + this.rootPath = rootPath; + this.templatePath = path.join(__dirname, PATH_TO_TEMPLATE, templatePath); } validateTemplateFromPath() { for (let i = 0; i < requiredFiles.length; i += 1) { const requiredFile = requiredFiles[i]; - const requiredFilePath = path.join(this.template, requiredFile); + const requiredFilePath = path.join(this.templatePath, requiredFile); if (!fs.existsSync(requiredFilePath)) { return false; @@ -30,9 +52,9 @@ export class Template { generateSiteWithTemplate() { return new Promise((resolve, reject) => { - fs.access(this.root) - .catch(() => fs.mkdirSync(this.root)) - .then(() => fsUtil.copySyncWithOptions(this.template, this.root, { overwrite: false })) + fs.access(this.rootPath) + .catch(() => fs.mkdirSync(this.rootPath)) + .then(() => fsUtil.copySyncWithOptions(this.templatePath, this.rootPath, { overwrite: false })) .then(resolve) .catch(reject); }); @@ -53,4 +75,145 @@ export class Template { .catch(reject); }); } + + /** + * Converts an existing GitHub wiki or docs folder to a MarkBind website. + */ + async convert() { + this.siteConfig = await SiteConfig.readSiteConfig(this.rootPath, this.siteConfigPath); + this.collectNavigablePages(); + await this.addIndexPage(); + await this.addAboutPage(); + this.addDefaultLayoutFiles(); + await this.addDefaultLayoutToSiteConfig(); + } + + getPageGlobPaths(page: SiteConfigPage, pagesExclude: string[]) { + const pageGlobs = page.glob ?? []; + return walkSync(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 navigable pages + */ + collectNavigablePages() { + const { pages, pagesExclude } = this.siteConfig; + const pagesFromGlobs = _.flatMap(pages.filter(page => page.glob), + page => this.getPageGlobPaths(page, pagesExclude) + .map(filePath => ({ + src: filePath, + title: page.title, + }))) as NaviagablePage[]; + + this.navigablePages = pagesFromGlobs; + } + + /** + * Copies over README.md or Home.md to default index.md if present. + */ + async addIndexPage() { + const indexPagePath = path.join(this.rootPath, INDEX_MARKDOWN_FILE); + const fileNames = ['README.md', 'Home.md']; + const filePath = fileNames.find(fileName => fs.existsSync(path.join(this.rootPath, fileName))); + // if none of the files exist, do nothing + if (_.isUndefined(filePath)) return; + try { + await fs.copy(path.join(this.rootPath, filePath), indexPagePath); + } catch (error) { + throw new Error(`Failed to copy over ${filePath}`); + } + } + + /** + * Adds an about page to site if not present. + */ + async addAboutPage() { + const aboutPath = path.join(this.rootPath, ABOUT_MARKDOWN_FILE); + try { + await fs.access(aboutPath); + } catch (error) { + if (fs.existsSync(aboutPath)) { + return; + } + await fs.outputFile(aboutPath, ABOUT_MARKDOWN_DEFAULT); + } + } + + /** + * Adds a footer to default layout of site. + */ + addDefaultLayoutFiles() { + const wikiFooterPath = path.join(this.rootPath, WIKI_FOOTER_PATH); + let footer; + if (fs.existsSync(wikiFooterPath)) { + logger.info(`Copied over the existing ${WIKI_FOOTER_PATH} file to the converted layout`); + footer = `\n${fs.readFileSync(wikiFooterPath, 'utf8')}`; + } + + const wikiSiteNavPath = path.join(this.rootPath, WIKI_SITE_NAV_PATH); + let siteNav; + if (fs.existsSync(wikiSiteNavPath)) { + logger.info(`Copied over the existing ${WIKI_SITE_NAV_PATH} file to the converted layout\n` + + 'Check https://markbind.org/userGuide/tweakingThePageStructure.html#site-navigation-menus\n' + + 'for information on site navigation menus.'); + siteNav = fs.readFileSync(wikiSiteNavPath, 'utf8'); + } else { + siteNav = this.buildSiteNav(); + } + + const convertedLayoutTemplate = VariableRenderer.compile( + fs.readFileSync(path.join(__dirname, 'siteConvertLayout.njk'), 'utf8')); + const renderedLayout = convertedLayoutTemplate.render({ + footer, + siteNav, + }); + const layoutOutputPath = path.join(this.rootPath, LAYOUT_FOLDER_PATH, LAYOUT_DEFAULT_NAME); + + fs.writeFileSync(layoutOutputPath, renderedLayout, 'utf-8'); + } + + /** + * Builds a site navigation file from the directory structure of the site. + */ + buildSiteNav() { + let siteNavContent = ''; + this.navigablePages + .filter(navigablePage => !navigablePage.src.startsWith('_')) + .forEach((page) => { + const navigablePagePath = path.join(this.rootPath, page.src); + const relativePagePathWithoutExt = fsUtil.removeExtensionPosix( + path.relative(this.rootPath, navigablePagePath)); + const pageName = _.startCase(fsUtil.removeExtension(path.basename(navigablePagePath))); + const pageUrl = `{{ baseUrl }}/${relativePagePathWithoutExt}.html`; + siteNavContent += `* [${pageName}](${pageUrl})\n`; + }); + + return siteNavContent.trimEnd(); + } + + /** + * Applies the default layout to all addressable pages by modifying the site config file. + */ + async addDefaultLayoutToSiteConfig() { + const configPath = path.join(this.rootPath, SITE_CONFIG_NAME); + const config = await fs.readJson(configPath); + await Template.writeToSiteConfig(config, configPath); + } + + /** + * Helper function for addDefaultLayoutToSiteConfig(). + */ + static async writeToSiteConfig(config: SiteConfig, configPath: string) { + const layoutObj: SiteConfigPage = { glob: '**/*.md', layout: LAYOUT_DEFAULT_NAME }; + config.pages.push(layoutObj); + await fs.outputJson(configPath, config); + } }