const _ = require('lodash'); const fs = require("fs").promises; const fss = require("fs"); const glob = require("glob-promise"); const path = require("path"); const zlib = require('zlib'); const Terser = require("terser"); const child_process = require('child_process'); const { getLanguages } = require("./lib/language.js"); const { filter } = require("./lib/dependencies.js"); const config = require("./build_config.js"); const { install, installCleanCSS, mkdir, renderTemplate } = require("./lib/makestuff.js"); const log = (...args) => console.log(...args); const { rollupCode } = require("./lib/bundling.js"); const bundling = require('./lib/bundling.js'); const Table = require('cli-table'); const getDefaultHeader = () => ({ ...require('../package.json'), git_sha: child_process .execSync("git rev-parse --short=10 HEAD") .toString().trim() }); function buildHeader(args = getDefaultHeader()) { return "/*!\n" + ` Highlight.js v${args.version} (git: ${args.git_sha})\n` + ` (c) ${config.copyrightYears} ${args.author} and other contributors\n` + ` License: ${args.license}\n` + ` */`; } function sortByKey(array, key) { return array.sort(function(a, b) { const x = a[key]; const y = b[key]; return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }); } function detailedGrammarSizes(languages) { if (languages.length > 180) return; const resultTable = new Table({ head: ['lang', 'minified'], // colWidths: [20,20,10,20,10,20], chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, style: { head: ['grey'] } }); languages.map(async(lang) => { resultTable.push([lang.name, lang.data.length]); }); console.log(resultTable.sort((b, a) => a[1] - b[1]).toString()); } async function buildBrowser(options) { let languages = await getLanguages(); // filter languages for inclusion in the highlight.js bundle languages = filter(languages, options.languages); await installDocs(); await installDemo(languages, { minify: options.minify }); log("Preparing languages."); await Promise.all( languages.map(async(lang) => { // await lang.compile({ terser: config.terser }); process.stdout.write("."); }) ); log(""); detailedGrammarSizes(languages); const size = await buildCore("highlight", languages, { minify: options.minify, format: "cjs" }); log("-----"); log("Languages (raw) :", languages.map((el) => el.data.length).reduce((acc, curr) => acc + curr, 0), "bytes"); log("highlight.js :", size.fullSize, "bytes"); if (options.minify) { log("highlight.min.js :", size.minified, "bytes"); log("highlight.min.js.gz :", zlib.gzipSync(size.minifiedSrc).length, "bytes"); } else { log("highlight.js.gz :", zlib.gzipSync(size.fullSrc).length, "bytes"); } log("-----"); } async function installDemo(languages, { minify }) { log("Writing demo files."); mkdir("demo"); installDemoStyles(); const assets = await glob("./demo/*.{js,css}"); assets.forEach((file) => install(file)); renderIndex(languages, minify); } async function renderIndex(languages, minify) { languages = languages.filter((lang) => // hide a few languages lang.name !== "plaintext" && lang.name !== "c-like" // no sample means no demo && lang.sample ); languages.forEach((language) => { if (!language.categories.length) { language.categories.push("misc"); } language.categories.push("all"); }); const categoryCounter = languages .flatMap((language) => language.categories) .reduce((map, category) => map.set(category, (map.get(category) || 0) + 1), new Map()); const categories = [ "common", ...Array.from(categoryCounter.keys()) .filter((category) => !["common", "misc", "all"].includes(category)) .sort(), "misc", "all" ] .filter((category) => categoryCounter.has(category)) .map((category) => ({ category, count: categoryCounter.get(category) })); function nameForStyle(file) { let name = _.startCase(path.basename(file, ".css")); if (file.includes("base16/")) { name = `Base16 / ${name}`; } return name; } const css = await glob("styles/**/*.css", { cwd: "./src" }); let styles = css .map((el) => ({ name: nameForStyle(el), path: el })) .filter((style) => style.name !== "Default"); styles = sortByKey(styles, "name"); renderTemplate("./demo/index.html", "./demo/index.html", { categories, languages, minify, styles }); } async function installDocs() { log("Writing docs files."); mkdir("docs"); const docs = await glob("./docs/*.rst"); docs.forEach((file) => install(file)); } function installDemoStyles() { log("Writing style files."); mkdir("demo/styles"); mkdir("demo/styles/base16"); glob.sync("**", { cwd: "./src/styles" }).forEach((file) => { const stat = fss.statSync(`./src/styles/${file}`); if (stat.isDirectory()) return; if (file.endsWith(".css")) { installCleanCSS(`./src/styles/${file}`, `demo/styles/${file}`); } else { // images, backgrounds, etc install(`./src/styles/${file}`, `demo/styles/${file}`); } }); } const builtInLanguagesPlugin = (languages) => ({ name: "hljs-index", resolveId(source) { if (source === "builtInLanguages") { return source; // this signals that rollup should not ask other plugins or check the file system to find this id } return null; // other ids should be handled as usually }, load(id) { const escape = (s) => "grmr_" + s.replace("-", "_"); if (id === "builtInLanguages") { return languages.map((lang) => `export { default as ${escape(lang.name)} } from ${JSON.stringify(lang.path)};` ).join("\n"); } return null; } }); async function buildCore(name, languages, options) { const header = buildHeader(); let relativePath = ""; const input = { ...config.rollup.core.input, input: `src/stub.js` }; input.plugins = [ ...input.plugins, builtInLanguagesPlugin(languages) ]; const output = { ...(options.format === "es" ? config.rollup.node.output : config.rollup.browser_iife.output), file: `${process.env.BUILD_DIR}/${name}.js` }; // optimize for no languages by not including the language loading stub if (languages.length === 0) { input.input = "src/highlight.js"; } if (options.format === "es") { output.format = "es"; output.file = `${process.env.BUILD_DIR}/es/${name}.js`; relativePath = "es/"; } log(`Building ${relativePath}${name}.js.`); const index = await rollupCode(input, output); const sizeInfo = { shas: [] }; const writePromises = []; if (options.minify) { const { code } = await Terser.minify(index, { ...config.terser, module: (options.format === "es") }); const src = `${header}\n${code}`; writePromises.push(fs.writeFile(output.file.replace(/js$/, "min.js"), src)); sizeInfo.minified = src.length; sizeInfo.minifiedSrc = src; sizeInfo.shas[`${relativePath}${name}.min.js`] = bundling.sha384(src); } { const src = `${header}\n${index}`; writePromises.push(fs.writeFile(output.file, src)); sizeInfo.fullSize = src.length; sizeInfo.fullSrc = src; sizeInfo.shas[`${relativePath}${name}.js`] = bundling.sha384(src); } await Promise.all(writePromises); return sizeInfo; } // CDN build uses the exact same highlight.js distributable module.exports.buildCore = buildCore; module.exports.build = buildBrowser;