recital / core / src / pipeline / 03-fragment-objects / index.ts
index.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 moo from 'moo'
import eol from 'eol'
import {
	FragmentedSceneObject,
	RawEmptyObject,
	RawFragmentObject,
	RawSceneObject,
} from '../../types'
import { exposeMeta } from '../../tools/expose-meta'
import { parseTOMLMeta } from '../../tools/parse-toml-meta'
import { parseInlineMeta } from '../../tools/parse-inline-meta'

const FRAGMENT_START_TOKEN = '<>'
const FRAGMENT_END_TOKEN = '</>'

export const genFragmentsForScenes = (scenes: RawSceneObject[]): FragmentedSceneObject[] => {
	const newScenes: FragmentedSceneObject[] = []
	for (let scene of scenes) {
		const fragments = genFragmentObjects(scene.raw)
		const newScene: FragmentedSceneObject = {
			type: 'scene',
			meta: scene.meta,
			id: scene.id,
			classes: scene.classes,
			primary: scene.primary,
			title: scene.title,
			fragments,
		}

		newScenes.push(newScene)
	}

	return newScenes
}

export const genFragmentObjects = (raw: string): (RawFragmentObject | RawEmptyObject)[] => {
	const fragments: (RawFragmentObject | RawEmptyObject)[] = []

	let currentFragment = ''
	const lines = eol.split(raw)

	for (let line of lines) {
		if (line.trim().startsWith(FRAGMENT_START_TOKEN)) {
			// we start a new fragment
			let trimmedFragment = currentFragment.trim()
			if (trimmedFragment.length) {
				fragments.push(genFragmentObject(currentFragment))
			}
			currentFragment = line + '\n'
			continue
		}

		if (line.trim() === FRAGMENT_END_TOKEN) {
			// we end the fragment
			currentFragment += line + '\n'
			fragments.push(genFragmentObject(currentFragment))
			currentFragment = ''
			continue
		}

		// otherwise, we just add the line to the fragment
		currentFragment += line + '\n'
	}

	// make sure we finish off the last fragment
	let trimmedFragment = currentFragment.trim()
	if (trimmedFragment.length) {
		fragments.push(genFragmentObject(currentFragment))
	}

	return fragments
}

/**
 * Creates a single fragment out of the given text.
 * @param text text of the fragment. Should start with the opening <> and end with the </>, if available. If we pass in an object without those tags,
 * we assume that we are outside of a fragment.
 */
export const genFragmentObject = (text: string): RawFragmentObject | RawEmptyObject => {
	// if we
	if (!text.startsWith(FRAGMENT_START_TOKEN)) {
		return {
			type: 'empty',
			raw: text,
		}
	}

	const fragmentObject: RawFragmentObject = {
		type: 'fragment',
		raw: '',
	}

	const lexer = moo.states({
		start: {
			fragDefStart: { match: FRAGMENT_START_TOKEN, push: 'fragInlineMeta' },
		},
		fragInlineMeta: {
			fragInlineEnd: { match: /\n/, lineBreaks: true, push: 'fragDefinition' },
			inlineMeta: { match: /[^]+?/, lineBreaks: true },
		},
		fragDefinition: {
			tomlStart: { match: '---', push: 'fragToml' },
			tomlStartPlus: { match: '+++', push: 'fragTomlPlus' },
			fragDefEnd: { match: /\n/, lineBreaks: true, push: 'main' },
			text: { match: /[^]+?/, lineBreaks: true },
		},
		fragToml: {
			tomlEnd: { match: '---', push: 'fragDefinition' },
			toml: { match: /[^]+?(?=---)/, lineBreaks: true },
		},
		fragTomlPlus: {
			tomlEnd: { match: '+++', push: 'fragDefinition' },
			toml: { match: /[^]+?(?=\+\+\+)/, lineBreaks: true },
		},
		main: {
			fragTagEnd: { match: new RegExp(`^\s*${FRAGMENT_END_TOKEN.replace('/', '\\/')}\s*$`), push: 'endFragTag' },
			text: { match: /[^]+?/, lineBreaks: true },
		},
		endFragTag: {
			misc: { match: /[^]+?/, lineBreaks: true },
		},
	})

	lexer.reset(eol.lf(text))
	let inlineMeta = ''
	for (let token of Array.from(lexer) as any[]) {
		switch (token.type) {
			case 'toml':
				const tomlObj = parseTOMLMeta(token.value)
				if (tomlObj === undefined) {
					throw new Error(lexer.formatError(token, 'empty TOML frontmatter in scene.'))
				}
				if (!fragmentObject.meta) {
					fragmentObject.meta = {}
				}
				fragmentObject.meta = { ...fragmentObject.meta, ...tomlObj }

				exposeMeta(fragmentObject)

				break
			case 'text':
				fragmentObject.raw += token.value
				break
			case 'inlineMeta':
				inlineMeta += token.value
		}
	}

	// Process any inline meta
	////////////////////////////
	inlineMeta = inlineMeta.trim()

	if (inlineMeta.length) {
		let inlineMetaObject = parseInlineMeta(inlineMeta)

		fragmentObject.meta = { ...inlineMetaObject, ...fragmentObject.meta }
		exposeMeta(fragmentObject)
	}

	return fragmentObject
}