* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { h, provide, reactive } from 'vue'
import { isHTMLTag, hyphenate } from '@vue/shared'
import { useBroadcastChannel } from '@vueuse/core'
import { constants, utils } from '@opentiny/tiny-engine-utils'
import babelPluginJSX from '@vue/babel-plugin-jsx'
import { transformSync } from '@babel/core'
import i18nHost from '@opentiny/tiny-engine-i18n-host'
import { CanvasRow, CanvasCol, CanvasRowColContainer } from '@opentiny/tiny-engine-builtin-component'
import { NODE_UID as DESIGN_UIDKEY, NODE_TAG as DESIGN_TAGKEY, NODE_LOOP as DESIGN_LOOPID } from '../../common'
import { context, conditions, setNode, getDesignMode, DESIGN_MODE } from './context'
import {
CanvasBox,
CanvasCollection,
CanvasIcon,
CanvasText,
CanvasSlot,
CanvasImg,
CanvasPlaceholder
} from './builtin'
const { BROADCAST_CHANNEL } = constants
const { hyphenateRE } = utils
const customElements = {}
const transformJSX = (code) => {
const res = transformSync(code, {
plugins: [
[
babelPluginJSX,
{
pragma: 'h',
isCustomElement: (name) => customElements[name]
}
]
]
})
return (res.code || '')
.replace(/import \{.+\} from "vue";/, '')
.replace(/h\(_?resolveComponent\((.*?)\)/g, `h(this.getComponent($1)`)
.replace(/_?resolveComponent/g, 'h')
.replace(/_?createTextVNode\((.*?)\)/g, '$1')
.trim()
}
export const blockSlotDataMap = reactive({})
const Mapper = {
Icon: CanvasIcon,
Text: CanvasText,
Collection: CanvasCollection,
div: CanvasBox,
Slot: CanvasSlot,
slot: CanvasSlot,
Template: CanvasBox,
Img: CanvasImg,
CanvasRow,
CanvasCol,
CanvasRowColContainer,
CanvasPlaceholder
}
const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify })
export const globalNotify = (options) => post(options)
export const collectionMethodsMap = {}
const getNative = (name) => {
return window.TinyLowcodeComponent?.[name]
}
const getBlock = (name) => {
return window.blocks?.[name]
}
const configure = {}
const controller = {}
export const setConfigure = (configureData) => {
Object.assign(configure, configureData)
}
export const setController = (controllerData) => {
Object.assign(controller, controllerData)
}
export const getController = () => controller
const isI18nData = (data) => {
return data && data.type === 'i18n'
}
const isJSSlot = (data) => {
return data && data.type === 'JSSlot'
}
const isJSExpression = (data) => {
return data && data.type === 'JSExpression'
}
const isJSFunction = (data) => {
return data && data.type === 'JSFunction'
}
const isJSResource = (data) => {
return data && data.type === 'JSResource'
}
const isString = (data) => {
return typeof data === 'string'
}
const isArray = (data) => {
return Array.isArray(data)
}
const isFunction = (data) => {
return typeof data === 'function'
}
const isObject = (data) => {
return typeof data === 'object'
}
export const isStateAccessor = (stateData) =>
stateData?.accessor?.getter?.type === 'JSFunction' || stateData?.accessor?.setter?.type === 'JSFunction'
export const newFn = (...argv) => {
const Fn = Function
return new Fn(...argv)
}
const parseExpression = (data, scope, ctx, isJsx = false) => {
try {
if (data.value.indexOf('this.i18n') > -1) {
ctx.i18n = i18nHost.global.t
} else if (data.value.indexOf('t(') > -1) {
ctx.t = i18nHost.global.t
}
const expression = isJsx ? transformJSX(data.value) : data.value
return newFn('$scope', `with($scope || {}) { return ${expression} }`).call(ctx, {
...ctx,
...scope,
slotScope: scope
})
} catch (err) {
if (!isJsx) {
return parseExpression(data, scope, ctx, true)
}
return undefined
}
}
const parseI18n = (i18n, scope, ctx) => {
return parseExpression(
{
type: 'JSExpression',
value: `this.i18n('${i18n.key}', ${JSON.stringify(i18n.params)})`
},
scope,
{ i18n: i18nHost.global.t, ...ctx }
)
}
const renderDefault = (children, scope, parent) =>
children.map?.((child) =>
h(renderer, {
schema: child,
scope,
parent
})
)
const parseJSSlot = (data, scope) => {
return ($scope) => renderDefault(data.value, { ...scope, ...$scope }, data)
}
export const generateFn = (innerFn, context) => {
return (...args) => {
const sourceId = collectionMethodsMap[innerFn.realName || innerFn.name]
if (sourceId) {
return innerFn.call(context, ...args)
} else {
let result = null
try {
result = innerFn.call(context, ...args)
} catch (error) {
globalNotify({
type: 'warning',
title: `函数:${innerFn.name}执行报错`,
message: error?.message || `函数:${innerFn.name}执行报错,请检查语法`
})
}
if (result.then) {
result = new Promise((resolve) => {
result.then(resolve).catch((error) => {
globalNotify({
type: 'warning',
title: '异步函数执行报错',
message: error?.message || '异步函数执行报错,请检查语法'
})
resolve({
result: [{}],
page: { total: 1 }
})
})
})
}
return result
}
}
}
const parseFunctionString = (fnStr) => {
const fnRegexp = /(async)?.*?(\w+) *\(([\s\S]*?)\) *\{([\s\S]*)\}/
const result = fnRegexp.exec(fnStr)
if (result) {
return {
type: result[1] || '',
name: result[2],
params: result[3]
.split(',')
.map((item) => item.trim())
.filter((item) => Boolean(item)),
body: result[4]
}
}
return null
}
const getPlainProps = (object = {}) => {
const { slot, ...rest } = object
const props = {}
if (slot) {
rest.slot = slot.name || slot
}
Object.entries(rest).forEach(([key, value]) => {
let renderKey = key
if (!/on[A-Z]/.test(renderKey) && hyphenateRE.test(renderKey)) {
renderKey = hyphenate(renderKey)
}
if (['boolean', 'string', 'number'].includes(typeof value)) {
props[renderKey] = value
} else {
props[`.${renderKey}`] = value
}
})
return props
}
const generateCollection = (schema) => {
if (schema.componentName === 'Collection' && schema.props?.dataSource && schema.children) {
schema.children.forEach((item) => {
const fetchData = item.props?.fetchData
const methodMatch = fetchData?.value?.match(/this\.(.+?)}/)
if (fetchData && methodMatch?.[1]) {
const methodName = methodMatch[1].trim()
collectionMethodsMap[methodName] = schema.props.dataSource
}
})
}
}
const generateBlockContent = (schema) => {
if (schema?.componentName === 'Collection') {
generateCollection(schema)
}
if (Array.isArray(schema?.children)) {
schema.children.forEach((item) => {
generateBlockContent(item)
})
}
}
const registerBlock = (componentName) => {
getController()
.registerBlock?.(componentName)
.then((res) => {
const blockSchema = res.content
generateBlockContent(blockSchema)
if (/height:\s*?[\d|.]+?%/.test(blockSchema?.props?.style)) {
const blockDoms = document.querySelectorAll(hyphenate(componentName))
blockDoms.forEach((item) => {
item.style.height = '100%'
})
}
})
}
export const wrapCustomElement = (componentName) => {
const material = getController().getMaterial(componentName)
if (!Object.keys(material).length) {
registerBlock(componentName)
}
customElements[componentName] = {
name: componentName + '.ce',
render() {
return h(
hyphenate(componentName),
window.parent.TinyGlobalConfig.dslMode === 'Vue' ? getPlainProps(this.$attrs) : this.$attrs,
this.$slots.default?.()
)
}
}
return customElements[componentName]
}
export const getComponent = (name) => {
return (
Mapper[name] ||
getNative(name) ||
getBlock(name) ||
customElements[name] ||
(isHTMLTag(name) ? name : wrapCustomElement(name))
)
}
const parseJSXFunction = (data, ctx) => {
try {
const newValue = transformJSX(data.value)
const fnInfo = parseFunctionString(newValue)
if (!fnInfo) throw Error('函数解析失败,请检查格式。示例:function fnName() { }')
return newFn(...fnInfo.params, fnInfo.body).bind({
...ctx,
getComponent
})
} catch (error) {
globalNotify({
type: 'warning',
title: '函数声明解析报错',
message: error?.message || '函数声明解析报错,请检查语法'
})
return newFn()
}
}
const parseJSFunction = (data, scope, ctx = context) => {
try {
const innerFn = newFn(`return ${data.value}`).bind(ctx)()
return generateFn(innerFn, ctx)
} catch (error) {
return parseJSXFunction(data, ctx)
}
}
const parseList = []
export function parseData(data, scope, ctx = context) {
let res = data
parseList.some((item) => {
if (item.type(data)) {
res = item.parseFunc(data, scope, ctx)
return true
}
return false
})
return res
}
const parseCondition = (condition, scope, ctx = context) => {
return condition == null ? true : parseData(condition, scope, ctx)
}
const parseLoopArgs = (_loop) => {
if (_loop) {
const { item, index, loopArgs = '' } = _loop
const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }`
return newFn('item,index', body)(item, index)
}
return undefined
}
export const getIcon = (name) => window.TinyVueIcon?.[name]?.() || ''
const parseObjectData = (data, scope, ctx) => {
if (!data) {
return data
}
if (isStateAccessor(data)) {
return parseData(data.defaultValue)
}
if (data.componentName === 'Icon') {
return getIcon(data.props.name)
}
const res = {}
Object.entries(data).forEach(([key, value]) => {
if (key === 'slot' && value?.name) {
res[key] = value.name
} else {
res[key] = parseData(value, scope, ctx)
}
})
return res
}
const parseString = (data) => {
return data.trim()
}
const parseArray = (data, scope, ctx) => {
return data.map((item) => parseData(item, scope, ctx))
}
const parseFunction = (data, scope, ctx) => {
return data.bind(ctx)
}
parseList.push(
...[
{
type: isJSExpression,
parseFunc: parseExpression
},
{
type: isI18nData,
parseFunc: parseI18n
},
{
type: isJSFunction,
parseFunc: parseJSFunction
},
{
type: isJSResource,
parseFunc: parseExpression
},
{
type: isJSSlot,
parseFunc: parseJSSlot
},
{
type: isString,
parseFunc: parseString
},
{
type: isArray,
parseFunc: parseArray
},
{
type: isFunction,
parseFunc: parseFunction
},
{
type: isObject,
parseFunc: parseObjectData
}
]
)
const stopEvent = (event) => {
event.preventDefault?.()
event.stopPropagation?.()
return false
}
const generateSlotGroup = (children, isCustomElm, schema) => {
const slotGroup = {}
children.forEach((child) => {
const { componentName, children, params = [], props } = child
const slot = child.slot || props?.slot?.name || props?.slot || 'default'
const isNotEmptyTemplate = componentName === 'Template' && children.length
isCustomElm && (child.props.slot = 'slot')
slotGroup[slot] = slotGroup[slot] || {
value: [],
params,
parent: isNotEmptyTemplate ? child : schema
}
slotGroup[slot].value.push(...(isNotEmptyTemplate ? children : [child]))
})
return slotGroup
}
const renderSlot = (children, scope, schema, isCustomElm) => {
if (children.some((a) => a.componentName === 'Template')) {
const slotGroup = generateSlotGroup(children, isCustomElm, schema)
const slots = {}
Object.keys(slotGroup).forEach((slotName) => {
const currentSlot = slotGroup[slotName]
slots[slotName] = ($scope) => renderDefault(currentSlot.value, { ...scope, ...$scope }, currentSlot.parent)
})
return slots
}
return { default: () => renderDefault(children, scope, schema) }
}
const checkGroup = (componentName) => configure[componentName]?.nestingRule?.childWhitelist?.length
const clickCapture = (componentName) => configure[componentName]?.clickCapture !== false
const getBindProps = (schema, scope) => {
const { id, componentName } = schema
const invalidity = configure[componentName]?.invalidity || []
if (componentName === 'CanvasPlaceholder') {
return {}
}
const bindProps = {
...parseData(schema.props, scope),
[DESIGN_UIDKEY]: id,
[DESIGN_TAGKEY]: componentName
}
if (getDesignMode() === DESIGN_MODE.DESIGN) {
bindProps.onMouseover = stopEvent
bindProps.onFocus = stopEvent
}
if (scope) {
bindProps[DESIGN_LOOPID] = scope.index === undefined ? scope.idx : scope.index
}
if (clickCapture(componentName) && getDesignMode() === DESIGN_MODE.DESIGN) {
bindProps.onClickCapture = stopEvent
}
if (Mapper[componentName]) {
bindProps.schema = schema
}
bindProps.class = bindProps.className
delete bindProps.className
bindProps.draggable = true
invalidity.forEach((prop) => delete bindProps[prop])
return bindProps
}
const getLoopScope = ({ scope, index, item, loopArgs }) => {
return {
...scope,
...(parseLoopArgs({
item,
index,
loopArgs
}) || {})
}
}
const injectPlaceHolder = (componentName, children) => {
const isEmptyArr = Array.isArray(children) && !children.length
if (configure[componentName]?.isContainer && (!children || isEmptyArr)) {
return [
{
componentName: 'CanvasPlaceholder'
}
]
}
return children
}
const renderGroup = (children, scope, parent) => {
return children.map?.((schema) => {
const { componentName, children, loop, loopArgs, condition, id } = schema
const loopList = parseData(loop, scope)
const renderElement = (item, index) => {
const mergeScope = getLoopScope({
scope,
index,
item,
loopArgs
})
setNode(schema, parent)
if (conditions[id] === false || !parseCondition(condition, mergeScope)) {
return null
}
const renderChildren = injectPlaceHolder(componentName, children)
return h(
getComponent(componentName),
getBindProps(schema, mergeScope),
Array.isArray(renderChildren)
? renderSlot(renderChildren, mergeScope, schema)
: parseData(renderChildren, mergeScope)
)
}
return loopList?.length ? loopList.map(renderElement) : renderElement()
})
}
const getChildren = (schema, mergeScope) => {
const { componentName, children } = schema
const renderChildren = injectPlaceHolder(componentName, children)
const component = getComponent(componentName)
const isNative = typeof component === 'string'
const isCustomElm = customElements[componentName]
const isGroup = checkGroup(componentName)
if (Array.isArray(renderChildren)) {
if (isNative || isCustomElm) {
return renderDefault(renderChildren, mergeScope, schema)
} else {
return isGroup
? renderGroup(renderChildren, mergeScope, schema)
: renderSlot(renderChildren, mergeScope, schema, isCustomElm)
}
} else {
return parseData(renderChildren, mergeScope)
}
}
export const renderer = {
name: 'renderer',
props: {
schema: Object,
scope: Object,
parent: Object
},
setup(props) {
provide('schema', props.schema)
},
render() {
const { scope, schema, parent } = this
const { componentName, loop, loopArgs, condition } = schema
generateCollection(schema)
if (!componentName) {
return parseData(schema, scope)
}
const component = getComponent(componentName)
const loopList = parseData(loop, scope)
const renderElement = (item, index) => {
let mergeScope = item
? getLoopScope({
item,
index,
loopArgs,
scope
})
: scope
if (parent?.componentType === 'Block' && componentName === 'Template' && schema.props?.slot?.params?.length) {
const slotName = schema.props.slot?.name || schema.props.slot
const blockName = parent.componentName
const slotData = blockSlotDataMap[blockName]?.[slotName] || {}
mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData
}
setNode(schema, parent)
if (conditions[schema.id] === false || !parseCondition(condition, mergeScope)) {
return null
}
return h(component, getBindProps(schema, mergeScope), getChildren(schema, mergeScope))
}
return loopList?.length ? loopList.map(renderElement) : renderElement()
}
}
export default renderer