mono/packages/osrl/src/Engine.ts
2025-12-30 16:33:03 +01:00

239 lines
7.3 KiB
TypeScript

import * as path from 'path'
import * as _ from './liquid/underscore'
import { Liquid, Context as LContext } from './liquidjs/liquid'
import { engineDefault } from './liquid'
import { exists, existsSync, readFile, readFileSync, resolve } from './fs'
import { LiquidOptions } from './liquidjs/liquid-options'
import { stringify } from 'yaml'
import { IObjectLiteral } from '@plastichub/core'
export { sync as dir } from '@plastichub/fs/dir'
import { sync as read } from '@plastichub/fs/read'
import { sync as write } from '@plastichub/fs/write'
import { sync as rm } from '@plastichub/fs/remove'
import { resolve as resolve_path } from "@plastichub/osr-commons"
import { runJS, runJSExpression, runJSExpressionEx } from './plugins/js'
import { register as registerDSTag } from './plugins/ds'
import { register as registerJS } from './plugins/js'
import { register as registerI18n, i18n } from './plugins/i18n'
import { register as registerPrint } from './plugins/print'
import { register as registerOpenAI, openAI } from './plugins/osr-ai'
import { markdown } from './plugins/turndown'
import { html, pretty } from './plugins/html'
import { register as registerMInclude } from './plugins/minclude'
import { register as registerGET } from './plugins/get'
import { Context } from './conf/bootstrap'
import { IOptions } from './types'
import { defaultFS } from './fs'
import { md2html } from './filters'
import { substitute, logger } from './index'
import { html_beautify } from 'js-beautify'
const fm = require('front-matter')
let _engine: Engine
export const getEngine = (): Engine => _engine
export const getContext = (): Context => _engine.context
export class LiquidEx extends Liquid {
owner: Engine
}
export class Engine {
constructor(options: IOptions) {
this.engine = engineDefault(options) as LiquidEx
this.options = options || {} as IOptions
this.expressionCache = {}
this.global = {}
this.stats = {
imports: []
}
_engine = this
}
engine: LiquidEx
options: IOptions
variables: IObjectLiteral
expressionCache: IObjectLiteral
global: any
context: Context
stats: any
async render(sourceFile: string, vars: IObjectLiteral) {
vars = {
targetLanguage: this.options.targetLanguage || 'en',
sourceLanguage: this.options.sourceLanguage || 'en',
i18n: '${OSR_ROOT}/i18n-store/store-en.json',
...this.options.profile.variables,
...vars
}
let owner = this
let _engine = this.engine
_engine.owner = owner
this.variables = vars
const src = '' + sourceFile
const resolveFs = (f) => resolve_path(f, false, vars)
let options_: IOptions = {
fs: defaultFS(resolveFs),
...this.options,
resolve: resolveFs
}
this.options = options_
let source = read(sourceFile)
let tmpFile = (sourceFile + '.tmp').replace('.md', '._md')
tmpFile = (sourceFile + '.tmp').replace('.html', '._html')
if (this.options.template && exists(this.options.template)) {
const template = read(this.options.template, 'string')
if (!template) {
logger.error(`Invalid template file: ${this.options.template}`)
return false
}
source = substitute(false, template as string, { SOURCE: source, ...this.variables })
write(tmpFile, source)
sourceFile = tmpFile
}
//@todo
_engine.options['string'] = source
registerDSTag(_engine)
registerJS(_engine)
registerI18n(_engine)
registerOpenAI(_engine)
registerPrint(_engine)
registerMInclude(_engine)
registerGET(_engine)
let _fm: any = { fm: {} }
try {
if (fm.test(_engine.options['string'])) {
_fm = {
fm: fm(_engine.options['string']).attributes,
body: fm(_engine.options['string']).body
}
vars.fm ? _fm.fm = { ...vars.fm, ..._fm.fm } : null
}
} catch (e) {
logger.error(`Error parsing front matter: ${e.message} : ${sourceFile}`)
}
let parsed = await _engine.parseFile(sourceFile)
_engine.registerFilter('jseval', async x => await runJS(x, options_, owner, parsed))
_engine.registerFilter('jsexp', x => runJSExpressionEx(x, options_, _engine, parsed, _engine, {}, ""))
_engine.registerFilter('i18n', async x => await i18n(x, options_, this, parsed, _engine, {}, ""))
_engine.registerFilter('html', async x => await html(x, options_, this))
_engine.registerFilter('turndown', async x => await markdown(x, options_, this))
_engine.registerFilter('pretty', async x => await pretty(x, options_, this))
_engine.registerFilter('openAI', async x => await openAI(x, options_ as any, this, parsed, _engine, {}, ""))
let fmChanged = false
if (Object.keys(_fm.fm) && Object.keys(_fm.fm).length > 0) {
for (let k in _fm.fm) {
if (k.endsWith('_i18n')) {
const val = await i18n(_fm.fm[k], options_, this, parsed, _engine, {}, "")
if (val) {
_fm.fm[k] = val
delete _fm.fm[k]
_fm.fm[k.replace('_i18n', '')] = val
fmChanged = true
}
}
}
}
fmChanged = true
if (fmChanged && _fm.fm && Object.keys(_fm.fm).length > 0) {
source = '---\n' + stringify(_fm.fm) + '---\n' + _fm.body
write(tmpFile, source)
sourceFile = tmpFile
options_['source'] = tmpFile
}
const childCtx = new LContext(
{},
this.options as any,
{
sync: false,
globals: {
..._fm.fm,
...this.variables,
fm: _fm.fm
},
strictVariables: false,
ownPropertyOnly: false
})
let ret = null;
debugger
try {
ret = await _engine.renderFile(sourceFile, childCtx, {
globals: {
..._fm.fm,
...this.variables,
fm: _fm.fm
}
})
} catch (e) {
logger.error(`Error rendering file: ${sourceFile} \n\t : ${e.message}`)
rm(tmpFile)
return false
}
rm(tmpFile)
options_['source'] = src
if (options_.format === 'html') {
ret = html_beautify(md2html(ret))
}
if (options_.format === 'pretty') {
ret = html_beautify(ret)
}
if(this.options.trace){
const traceFile = resolveFs(this.options.trace)
write(traceFile, { ...this.stats, ...this.options.profile })
}
return ret
}
async parse(string: string, vars: any, iterations: number = 5) {
let _engine = this.engine;
_engine.options['string'] = string;
let _fm = { fm: {} };
if (fm.test(_engine.options['string'])) {
_fm = {
fm: fm(_engine.options['string']).attributes
}
}
this.variables = {
...vars,
..._fm.fm
};
let owner = this;
(_engine as any).owner = this;
let options_ = {
fs: {
exists: (f) => {
return exists(f)
},
existsSync: existsSync,
readFile: (f) => readFile(path.resolve(resolve_path(f))),
readFileSync: readFileSync,
resolve
},
...this.options
}
registerDSTag(_engine);
registerJS(_engine);
registerMInclude(_engine);
let parsed = _engine.parse(string);
_engine.registerFilter('jseval', x => runJS(x, options_, owner, parsed))
_engine.registerFilter('jsexp', x => runJSExpression(x, options_, owner, parsed))
parsed = _engine.parse(string)
let t = await _engine.render(parsed, vars, options_ as LiquidOptions)
for (let i = 0; i < iterations; i++) {
t = await this.engine.render(parsed, vars, options_ as LiquidOptions);
}
if (options_.format === 'html') {
t = html_beautify(md2html(t))
}
return t
}
}