recital / packages / ext-common-commands / src / lib / include-command.ts
include-command.ts
Raw
/**
 * Copyright (c) 2022 Amorphous
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

import { createCommandObject } from '@a-morphous/recital/dist/lib/tools/command-shorthand'

import eol from 'eol'

/**
 * Parses a recital file for the `$include` tag, which link to a separate `.stage` file whose contents are inserted at the location of the original `$include` command.
 * 
 * The include command's usage is:
 * $include <file to include> <raw: boolean>
 * 'raw' determines whether we process the included stage file for more includes, or just leave the text unprocessed.
 * 
 * @param fs The node 'fs' module - or an API equivalent in the browser
 * @param path The node 'path' module - or an API equivalent in the browser
 * @param filePath Path to file that serves as the entrypoint. Advised for this to be the path to the file provided for `stageString`.
 * @param stageString The entry point's file contents. This is separate from filePath in case you want the 'working directory' to be different
 * @param layer Unused. This is mostly as a guard against infinite recursion.
 * @returns a new string representing the .stage file with all its includes added in.
 */
export const resolveIncludes = (
	fs,
	path,
	filePath: string,
	stageString: string,
	layer: number = 0
): string => {
	// infinite recursion guard
	if (layer > 20) {
		throw new Error(
			'Include command recursed more than 20 layers deep. This is a lot. Is there an infinite loop somewhere?'
		)
	}
	const tokens = eol.split(stageString)
	let finalString = ''

	for (let token of tokens) {
		if (!token.startsWith('$include')) {
			finalString += token + '\n'
			continue
		}

		let commandObj = createCommandObject(token)
		if (!commandObj.args) {
			console.warn('empty $include command found.')
			continue
		}

		let givenFilepath = commandObj.args[0].toString()
		let targetFilepath = path.resolve(path.dirname(filePath), givenFilepath)

		if (!fs.existsSync(targetFilepath)) {
			console.log(targetFilepath)
			console.warn("File '" + givenFilepath + "' used in $include command does not exist")
			continue
		}

		const newFileContents = fs.readFileSync(targetFilepath, { encoding: 'utf-8' })

		// by default, we recursively process includes
		// processing just means that we continue to look for $includes
		let shouldProcessInclude = true

		// ...but if we specifically say that it should be raw, then do not
		if (
			shouldProcessInclude == true &&
			commandObj.args.length > 1 &&
			commandObj.args[1] === 'raw'
		) {
			shouldProcessInclude = false
		}

		if (shouldProcessInclude) {
			const processedContents = resolveIncludes(
				fs,
				path,
				targetFilepath,
				newFileContents,
				layer + 1
			)
			finalString += processedContents + '\n'
			continue
		}

		finalString += newFileContents + '\n'
	}

	return finalString.trim()
}