|
|
|
@ -14,10 +14,14 @@ |
|
|
|
/// limitations under the License.
|
|
|
|
///
|
|
|
|
|
|
|
|
import { forkJoin, from, map, Observable, of, ReplaySubject, switchMap } from 'rxjs'; |
|
|
|
import { removeTbResourcePrefix } from '@shared/models/resource.models'; |
|
|
|
import { forkJoin, from, map, mergeMap, Observable, of, ReplaySubject, switchMap } from 'rxjs'; |
|
|
|
import { removeTbResourcePrefix, ResourceInfo } from '@shared/models/resource.models'; |
|
|
|
import { HttpClient } from '@angular/common/http'; |
|
|
|
import { defaultHttpOptionsFromConfig } from '@core/http/http-utils'; |
|
|
|
import { TbEditorCompleter, TbEditorCompletion } from '@shared/models/ace/completion.models'; |
|
|
|
import { blobToText } from '@core/utils'; |
|
|
|
import { catchError, finalize } from 'rxjs/operators'; |
|
|
|
import { parseError } from '@shared/models/error.models'; |
|
|
|
|
|
|
|
export interface TbFunctionWithModules { |
|
|
|
body: string; |
|
|
|
@ -59,6 +63,130 @@ export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...a |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
export const loadModulesCompleter = (http: HttpClient, modules: {[alias: string]: string }): Observable<TbEditorCompleter | null> => { |
|
|
|
if (!modules || !Object.keys(modules).length) { |
|
|
|
return of(null); |
|
|
|
} else { |
|
|
|
const modulesDescription: {[alias: string]: Observable<TbEditorCompletion>} = {}; |
|
|
|
for (const alias of Object.keys(modules)) { |
|
|
|
modulesDescription[alias] = loadModuleCompletion(http, modules[alias]); |
|
|
|
} |
|
|
|
return forkJoin(modulesDescription).pipe( |
|
|
|
map((completions) => { |
|
|
|
return new TbEditorCompleter(completions); |
|
|
|
}) |
|
|
|
); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
export const loadModuleMarkdownDescription = (http: HttpClient, resource: ResourceInfo): Observable<string> => { |
|
|
|
let description = `<div class="flex flex-col !pl-4 !pr-4"><h6>${resource.title}</h6><small>Module members</small></div>\n\n`; |
|
|
|
description += '<div class="divider !pt-2"></div>\n' + |
|
|
|
'<br/>\n\n'; |
|
|
|
return loadFunctionModuleWithSource(http, resource.link).pipe( |
|
|
|
map((moduleWithSource) => { |
|
|
|
const module = moduleWithSource.module; |
|
|
|
const propertiesData: { type: 'function' | 'const', propName: string, description: string }[] = []; |
|
|
|
for (const propName of Object.keys(module)) { |
|
|
|
let propDescription = ''; |
|
|
|
const prop = module[propName]; |
|
|
|
const type = typeof prop; |
|
|
|
if (type === 'function') { |
|
|
|
const funcArgs = getFunctionArguments(prop); |
|
|
|
propDescription += `<p class="!pl-4 !pr-4"><em>function</em> <strong>${propName}</strong> <em>(${funcArgs.join(', ')})</em>: <code>any</code></p>`; |
|
|
|
} else { |
|
|
|
propDescription += `<p class="!pl-4 !pr-4"><em>const</em> <strong>${propName}</strong>: <code>${type}</code>`; |
|
|
|
if (type !== 'object') { |
|
|
|
propDescription += ` = ${prop}`; |
|
|
|
} |
|
|
|
propDescription += '</p>'; |
|
|
|
} |
|
|
|
propDescription += '\n\n'; |
|
|
|
const propertyData: { type: 'function' | 'const', propName: string, description: string } = { |
|
|
|
type: type === 'function' ? 'function' : 'const', |
|
|
|
propName, |
|
|
|
description: propDescription |
|
|
|
} |
|
|
|
propertiesData.push(propertyData); |
|
|
|
} |
|
|
|
propertiesData.sort((a, b) => { |
|
|
|
if (a.type === b.type) { |
|
|
|
return a.propName.localeCompare(b.propName); |
|
|
|
} else if (a.type === 'const') return -1; |
|
|
|
else return 1; |
|
|
|
}); |
|
|
|
if (!propertiesData.length) { |
|
|
|
description += `<div class="!pl-4 !pr-4">Module has no exported members</div>\n\n`; |
|
|
|
} else { |
|
|
|
propertiesData.forEach((pData) => { |
|
|
|
description += pData.description; |
|
|
|
}); |
|
|
|
} |
|
|
|
return description; |
|
|
|
}), |
|
|
|
catchError(err => { |
|
|
|
const errorText = parseError(err); |
|
|
|
description += `<div class="!pl-4 !pr-4">Module load error:<br/><span style="color: red;">${errorText}</span></div>\n\n`; |
|
|
|
return of(description); |
|
|
|
}) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
export const loadModuleMarkdownSourceCode = (http: HttpClient, resource: ResourceInfo): Observable<string> => { |
|
|
|
let sourceCode = `<div class="flex flex-col !pl-4"><h6>${resource.title}</h6><small>Source code</small></div>\n\n`; |
|
|
|
return loadFunctionModuleSource(http, resource.link).pipe( |
|
|
|
map((source) => { |
|
|
|
sourceCode += '```javascript\n{:code-style="margin-left: -16px; margin-right: -16px;"}\n' + source + '\n```'; |
|
|
|
return sourceCode; |
|
|
|
}), |
|
|
|
catchError(err => { |
|
|
|
const errorText = parseError(err); |
|
|
|
sourceCode += `<div class="!pl-4 !pr-4">Source code load error:<br/><span style="color: red;">${errorText}</span></div>\n\n`; |
|
|
|
return of(sourceCode); |
|
|
|
}) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable<TbEditorCompletion> => { |
|
|
|
return loadFunctionModule(http, moduleLink).pipe( |
|
|
|
map((module) => { |
|
|
|
const completion: TbEditorCompletion = { |
|
|
|
meta: 'module', |
|
|
|
type: 'module', |
|
|
|
children: {} |
|
|
|
}; |
|
|
|
for (const propName of Object.keys(module)) { |
|
|
|
const prop = module[propName]; |
|
|
|
const type = typeof prop; |
|
|
|
const propertyCompletion: TbEditorCompletion = { |
|
|
|
meta: type === 'function' ? 'function' : 'constant', |
|
|
|
type |
|
|
|
}; |
|
|
|
if (type === 'function') { |
|
|
|
propertyCompletion.args = getFunctionArguments(prop).map(functionArg => { |
|
|
|
return {name: functionArg} |
|
|
|
}); |
|
|
|
propertyCompletion.return = { type: 'any'}; |
|
|
|
} else if (type !== 'object') { |
|
|
|
propertyCompletion.description = `<div class="tb-api-title">Constant value:</div><code class="title">${prop}</code>`; |
|
|
|
} |
|
|
|
completion.children[propName] = propertyCompletion; |
|
|
|
} |
|
|
|
return completion; |
|
|
|
}), |
|
|
|
catchError(err => { |
|
|
|
const completion: TbEditorCompletion = { |
|
|
|
meta: 'module', |
|
|
|
type: 'module', |
|
|
|
children: {} |
|
|
|
}; |
|
|
|
const errorText = parseError(err); |
|
|
|
completion.description = `<div>Module load error:<br/><span style="color: red;">${errorText}</span></div>`; |
|
|
|
return of(completion); |
|
|
|
}) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
export class CompiledTbFunction { |
|
|
|
|
|
|
|
constructor(private compiledFunction: Function, |
|
|
|
@ -101,11 +229,14 @@ const loadFunctionModule = (http: HttpClient, moduleLink: string): Observable<Sy |
|
|
|
modulesLoading[url] = request; |
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); |
|
|
|
http.get(url, {...options, ...{ observe: 'response', responseType: 'blob' } }).pipe( |
|
|
|
switchMap((response) => { |
|
|
|
mergeMap((response) => { |
|
|
|
const objectURL = URL.createObjectURL(response.body); |
|
|
|
const asyncModule = from(import(/* @vite-ignore */objectURL)); |
|
|
|
URL.revokeObjectURL(objectURL); |
|
|
|
return asyncModule; |
|
|
|
}), |
|
|
|
finalize(() => { |
|
|
|
delete modulesLoading[url]; |
|
|
|
}) |
|
|
|
).subscribe( |
|
|
|
{ |
|
|
|
@ -115,12 +246,44 @@ const loadFunctionModule = (http: HttpClient, moduleLink: string): Observable<Sy |
|
|
|
}, |
|
|
|
error: err => { |
|
|
|
request.error(err); |
|
|
|
}, |
|
|
|
complete: () => { |
|
|
|
delete modulesLoading[url]; |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
return request; |
|
|
|
} |
|
|
|
|
|
|
|
interface TbModuleWithSource { |
|
|
|
module: System.Module; |
|
|
|
source: string; |
|
|
|
} |
|
|
|
|
|
|
|
const loadFunctionModuleWithSource = (http: HttpClient, moduleLink: string): Observable<TbModuleWithSource> => { |
|
|
|
const url = removeTbResourcePrefix(moduleLink); |
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); |
|
|
|
return http.get(url, {...options, ...{ observe: 'response', responseType: 'blob' } }).pipe( |
|
|
|
switchMap((response) => { |
|
|
|
const objectURL = URL.createObjectURL(response.body); |
|
|
|
const asyncModule = from(import(/* @vite-ignore */objectURL)); |
|
|
|
URL.revokeObjectURL(objectURL); |
|
|
|
const asyncSource = blobToText(response.body); |
|
|
|
return forkJoin({ |
|
|
|
module: asyncModule, |
|
|
|
source: asyncSource |
|
|
|
}); |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
const loadFunctionModuleSource = (http: HttpClient, moduleLink: string): Observable<string> => { |
|
|
|
const url = removeTbResourcePrefix(moduleLink); |
|
|
|
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); |
|
|
|
return http.get(url, {...options, ...{ responseType: 'text' } }); |
|
|
|
} |
|
|
|
|
|
|
|
const getFunctionArguments = (func: Function): string[] => { |
|
|
|
const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); |
|
|
|
let result = new Array<string>(...fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)); |
|
|
|
if (result === null) |
|
|
|
result = []; |
|
|
|
return result; |
|
|
|
} |
|
|
|
|