From c4f6c2ca4a46d008dc78137fe2f564e551c68a2a Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <ben@bnf.dev> Date: Tue, 29 Aug 2023 20:07:46 +0200 Subject: [PATCH] [TASK] Migrate @typo3/t3editor/autocomplete/* to TypeScript Resolves: #101795 Related: #101783 Releases: main, 12.4 Change-Id: Id881225a77fcbb0fa6dbd9a7e75caba4fbe05ab1 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80821 Reviewed-by: Benjamin Franzke <ben@bnf.dev> Tested-by: Benjamin Franzke <ben@bnf.dev> Tested-by: core-ci <typo3@b13.com> --- .../autocomplete/completion-result.ts | 105 ++++ .../autocomplete/ts-code-completion.ts | 142 +++++ .../t3editor/autocomplete/ts-parser.ts | 485 +++++++++++++++++ .../t3editor/autocomplete/ts-ref.ts | 164 ++++++ .../t3editor/language/typoscript.ts | 18 +- Build/types/TYPO3/index.d.ts | 1 - .../autocomplete/completion-result.js | 110 +--- .../autocomplete/ts-code-completion.js | 149 +----- .../JavaScript/autocomplete/ts-parser.js | 506 +----------------- .../Public/JavaScript/autocomplete/ts-ref.js | 166 +----- .../Public/JavaScript/language/typoscript.js | 2 +- 11 files changed, 913 insertions(+), 935 deletions(-) create mode 100644 Build/Sources/TypeScript/t3editor/autocomplete/completion-result.ts create mode 100644 Build/Sources/TypeScript/t3editor/autocomplete/ts-code-completion.ts create mode 100644 Build/Sources/TypeScript/t3editor/autocomplete/ts-parser.ts create mode 100644 Build/Sources/TypeScript/t3editor/autocomplete/ts-ref.ts diff --git a/Build/Sources/TypeScript/t3editor/autocomplete/completion-result.ts b/Build/Sources/TypeScript/t3editor/autocomplete/completion-result.ts new file mode 100644 index 000000000000..2eb7118076ad --- /dev/null +++ b/Build/Sources/TypeScript/t3editor/autocomplete/completion-result.ts @@ -0,0 +1,105 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +import type { TsRef, TsRefType } from '@typo3/t3editor/autocomplete/ts-ref'; +import type { TreeNode } from '@typo3/t3editor/autocomplete/ts-parser'; + +export type Proposal = { + type: string, + word?: string, + cssClass?: string, +}; + +export class CompletionResult { + private readonly tsRef: TsRef; + private readonly tsTreeNode: TreeNode; + + constructor(tsRef: TsRef, tsTreeNode: TreeNode) { + this.tsRef = tsRef; + this.tsTreeNode = tsTreeNode; + } + + /** + * returns the type of the currentTsTreeNode + */ + public getType(): TsRefType | null { + const val = this.tsTreeNode.getValue(); + if (this.tsRef.isType(val)) { + return this.tsRef.getType(val); + } + return null; + } + + /** + * returns a list of possible path completions (proposals), which is: + * a list of the children of the current TsTreeNode (= userdefined properties) + * and a list of properties allowed for the current object in the TsRef + * remove all words from list that don't start with the string in filter + */ + public getFilteredProposals(filter: string): Array<Proposal> { + const defined: Record<string, boolean> = {}; + const propArr: Array<Proposal> = []; + const childNodes = this.tsTreeNode.getChildNodes(); + const value = this.tsTreeNode.getValue(); + + // first get the childNodes of the Node (=properties defined by the user) + for (const key in childNodes) { + if (typeof childNodes[key].value !== 'undefined' && childNodes[key].value !== null) { + const propObj: Proposal = {} as Proposal; + propObj.word = key; + if (this.tsRef.typeHasProperty(value, childNodes[key].name)) { + this.tsRef.cssClass = 'definedTSREFProperty'; + propObj.type = childNodes[key].value; + } else { + propObj.cssClass = 'userProperty'; + if (this.tsRef.isType(childNodes[key].value)) { + propObj.type = childNodes[key].value; + } else { + propObj.type = ''; + } + } + propArr.push(propObj); + defined[key] = true; + } + } + + // then get the tsref properties + const props = this.tsRef.getPropertiesFromTypeId(this.tsTreeNode.getValue()); + for (const key in props) { + // show just the TSREF properties - no properties of the array-prototype and no properties which have been defined by the user + if (typeof props[key].value !== 'undefined' && defined[key] !== true) { + const propObj = { + word: key, + cssClass: 'undefinedTSREFProperty', + type: props[key].value, + }; + propArr.push(propObj); + } + } + + const result: Array<Proposal> = []; + let wordBeginning = ''; + + for (let i = 0; i < propArr.length; i++) { + if (filter.length === 0) { + result.push(propArr[i]); + continue; + } + wordBeginning = propArr[i].word.substring(0, filter.length); + if (wordBeginning.toLowerCase() === filter.toLowerCase()) { + result.push(propArr[i]); + } + } + return result; + } +} diff --git a/Build/Sources/TypeScript/t3editor/autocomplete/ts-code-completion.ts b/Build/Sources/TypeScript/t3editor/autocomplete/ts-code-completion.ts new file mode 100644 index 000000000000..346731bfdb68 --- /dev/null +++ b/Build/Sources/TypeScript/t3editor/autocomplete/ts-code-completion.ts @@ -0,0 +1,142 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Module: @typo3/t3editor/autocomplete/ts-code-completion + * Contains the TsCodeCompletion class + */ +import AjaxRequest from '@typo3/core/ajax/ajax-request'; +import { TsRef } from '@typo3/t3editor/autocomplete/ts-ref'; +import { TsParser } from '@typo3/t3editor/autocomplete/ts-parser'; +import { CompletionResult, Proposal } from '@typo3/t3editor/autocomplete/completion-result'; + +import type { CodeMirror5CompatibleCompletionState } from '@typo3/t3editor/language/typoscript'; + +export type ContentObjectIdentifier = string; + +export type TsObjTree = { + c?: Record<string, TsObjTree>; + v?: ContentObjectIdentifier; +} + +export class TsCodeCompletion { + public extTsObjTree: TsObjTree = {}; + private readonly tsRef: TsRef; + private readonly parser: TsParser = null; + private proposals: Array<Proposal> = null; + private compResult: CompletionResult = null; + + constructor(id: number) { + this.tsRef = new TsRef(); + this.parser = new TsParser(this.tsRef, this.extTsObjTree); + this.tsRef.loadTsrefAsync(); + this.loadExtTemplatesAsync(id); + } + + /** + * Refreshes the code completion list based on the cursor's position + */ + public refreshCodeCompletion( + completionState: CodeMirror5CompatibleCompletionState + ): Array<Proposal['word']> { + // the cursornode has to be stored cause inserted breaks have to be deleted after pressing enter if the codecompletion is active + const filter = this.getFilter(completionState); + + // TODO: implement cases: operatorCompletion reference/copy path completion (formerly found in getCompletionResults()) + const currentTsTreeNode = this.parser.buildTsObjTree(completionState); + this.compResult = new CompletionResult( + this.tsRef, + currentTsTreeNode + ); + + this.proposals = this.compResult.getFilteredProposals(filter); + + const proposals: string[] = []; + for (let i = 0; i < this.proposals.length; i++) { + proposals[i] = this.proposals[i].word; + } + + return proposals; + } + + /** + * All external templates along the rootline have to be loaded, + * this function retrieves the JSON code by committing a AJAX request + */ + private loadExtTemplatesAsync(id: number): void { + if (Number.isNaN(id) || id === 0) { + return null; + } + new AjaxRequest(TYPO3.settings.ajaxUrls.t3editor_codecompletion_loadtemplates) + .withQueryArguments({ pageId: id }) + .get() + .then(async (response) => { + this.extTsObjTree.c = await response.resolve(); + this.resolveExtReferencesRec(this.extTsObjTree.c); + }); + } + + /** + * Since the references are not resolved server side we have to do it client-side + * Benefit: less loading time due to less data which has to be transmitted + * + * @param {Array} childNodes + */ + private resolveExtReferencesRec(childNodes: Record<string, TsObjTree>): void { + for (const key of Object.keys(childNodes)) { + let childNode; + // if the childnode has a value and there is a part of a reference operator ('<') + // and it does not look like a html tag ('>') + if (childNodes[key].v && childNodes[key].v[0] === '<' && childNodes[key].v.indexOf('>') === -1) { + const path = childNodes[key].v.replace(/</, '').trim(); + // if there are still whitespaces it's no path + if (path.indexOf(' ') === -1) { + childNode = this.getExtChildNode(path); + // if the node was found - reference it + if (childNode !== null) { + childNodes[key] = childNode; + } + } + } + // if there was no reference-resolving then we go deeper into the tree + if (!childNode && childNodes[key].c) { + this.resolveExtReferencesRec(childNodes[key].c); + } + } + } + + /** + * Get the child node of given path + */ + private getExtChildNode(path: string): object { + let extTree = this.extTsObjTree; + + const pathParts = path.split('.'); + for (let i = 0; i < pathParts.length; i++) { + const pathSeg = pathParts[i]; + if (typeof extTree.c === 'undefined' || typeof extTree.c[pathSeg] === 'undefined') { + return null; + } + extTree = extTree.c[pathSeg]; + } + return extTree; + } + + private getFilter(completionState: CodeMirror5CompatibleCompletionState): string { + if (completionState.completingAfterDot) { + return ''; + } + + return completionState.token.string.replace('.', '').replace(/\s/g, ''); + } +} diff --git a/Build/Sources/TypeScript/t3editor/autocomplete/ts-parser.ts b/Build/Sources/TypeScript/t3editor/autocomplete/ts-parser.ts new file mode 100644 index 000000000000..c893bad5db99 --- /dev/null +++ b/Build/Sources/TypeScript/t3editor/autocomplete/ts-parser.ts @@ -0,0 +1,485 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +import type { TsRef } from '@typo3/t3editor/autocomplete/ts-ref'; +import type { TsObjTree } from '@typo3/t3editor/autocomplete/ts-code-completion'; +import type { CodeMirror5CompatibleCompletionState } from '@typo3/t3editor/language/typoscript'; + +export class TreeNode { + public value: string; + public childNodes: Record<string, TreeNode> = {}; + public extPath = ''; + public name: string; + public isExternal: boolean; + + public global: boolean; + public parent: TreeNode = null; + + private readonly tsParser: TsParser; + + constructor(nodeName: string, tsParser: TsParser) { + this.name = nodeName; + this.childNodes = {}; + this.extPath = ''; + this.value = ''; + this.isExternal = false; + + this.tsParser = tsParser; + } + + /** + * Returns local properties and the properties of the external templates + */ + public getChildNodes(): Record<string, TreeNode> { + const node = this.getExtNode(); + if (node !== null && typeof node.c === 'object') { + for (const key of Object.keys(node.c)) { + const tn = new TreeNode(key, this.tsParser); + tn.global = true; + tn.value = (node.c[key].v) ? node.c[key].v : ''; + tn.isExternal = true; + this.childNodes[key] = tn; + } + } + return this.childNodes; + } + + /** + * Returns the value of a node + */ + public getValue(): string { + if (this.value) { + return this.value; + } + const node = this.getExtNode(); + if (node && node.v) { + return node.v; + } + + const type = this.getNodeTypeFromTsref(); + if (type) { + return type; + } + return ''; + } + + /** + * This method will try to resolve the properties recursively from right + * to left. If the node's value property is not set, it will look for the + * value of its parent node, and if there is a matching childProperty + * (according to the TSREF) it will return the childProperties value. + * If there is no value in the parent node it will go one step further + * and look into the parent node of the parent node,... + */ + private getNodeTypeFromTsref(): string { + const path = this.extPath.split('.'), + lastSeg = path.pop(); + + // attention: there will be recursive calls if necessary + const parentValue = this.parent.getValue(); + if (parentValue) { + if (this.tsParser.tsRef.typeHasProperty(parentValue, lastSeg)) { + const type = this.tsParser.tsRef.getType(parentValue); + return type.properties[lastSeg].value; + } + } + return ''; + } + + /** + * Will look in the external ts-tree (static templates, templates on other pages) + * if there is a value or childproperties assigned to the current node. + * The method uses the extPath of the current node to navigate to the corresponding + * node in the external tree + */ + private getExtNode(): TsObjTree { + let extTree = this.tsParser.extTsObjTree; + + if (this.extPath === '') { + return extTree; + } + const path = this.extPath.split('.'); + + for (let i = 0; i < path.length; i++) { + const pathSeg = path[i]; + if (typeof extTree.c === 'undefined' || typeof extTree.c[pathSeg] === 'undefined') { + return null; + } + extTree = extTree.c[pathSeg]; + } + return extTree; + } +} + +class Stack<T> extends Array<T> { + public lastElementEquals(el: T): boolean { + return this.length > 0 && this[this.length - 1] === el; + } + + public popIfLastElementEquals(el: T): boolean { + if (this.lastElementEquals(el)) { + this.pop(); + return true; + } + return false; + } +} + +export class TsParser { + public readonly tsRef: TsRef; + public readonly extTsObjTree: TsObjTree; + private tsTree: TreeNode; + + private clone: <T extends object | unknown>(myObj: T) => T; + + constructor(tsRef: TsRef, extTsObjTree: TsObjTree) { + this.tsRef = tsRef; + this.extTsObjTree = extTsObjTree; + this.tsTree = new TreeNode('_L_', this); + } + + /** + * Check if there is an operator in the line and return it + * if there is none, return -1 + */ + public getOperator(line: string): string | -1 { + const operators = [':=', '=<', '<', '>', '=']; + for (let i = 0; i < operators.length; i++) { + const op = operators[i]; + if (line.indexOf(op) !== -1) { + // check if there is some HTML in this line (simple check, however it's the only difference between a reference operator and HTML) + // we do this check only in case of the two operators "=<" and "<" since the delete operator would trigger our "HTML-finder" + if ((op === '=<' || op === '<') && line.indexOf('>') > -1) { + // if there is a ">" in the line suppose there's some HTML + return '='; + } + return op; + } + } + return -1; + } + + /** + * Build the TypoScript object tree + */ + public buildTsObjTree(completionState: CodeMirror5CompatibleCompletionState): TreeNode { + this.tsTree = new TreeNode('', this); + this.tsTree.value = 'TLO'; + + let currentLine = 1, + line = '', + ignoreLine = false, + insideCondition = false; + const stack = new Stack<string>(); + const prefixes = []; + let path; + + while (currentLine <= completionState.currentLineNumber) { + line = ''; + const tokens = completionState.lineTokens[currentLine - 1]; + for (let i = 0; i <= tokens.length; ++i) { + if (i < tokens.length && tokens[i].string.length > 0) { + const tokenValue = tokens[i].string; + + if (tokenValue[0] === '#') { + stack.push('#'); + } else if (tokenValue === '(') { + stack.push('('); + } else if (tokenValue[0] === '/' && tokenValue[1] === '*') { + stack.push('/*'); + } else if (tokenValue === '{') { + // TODO: ignore whole block if wrong whitespaces in this line + if (this.getOperator(line) === -1) { + stack.push('{'); + prefixes.push(line.trim()); + ignoreLine = true; + } + } + // TODO: conditions + // if condition starts -> ignore everything until end of condition + if (tokenValue.search(/^\s*\[.*\]/) !== -1 + && line.search(/\S/) === -1 + && tokenValue.search(/^\s*\[(global|end|GLOBAL|END)\]/) === -1 + && !stack.lastElementEquals('#') + && !stack.lastElementEquals('/*') + && !stack.lastElementEquals('{') + && !stack.lastElementEquals('(') + ) { + insideCondition = true; + ignoreLine = true; + } + + // if end of condition reached + if (line.search(/\S/) === -1 + && !stack.lastElementEquals('#') + && !stack.lastElementEquals('/*') + && !stack.lastElementEquals('(') + && ( + (tokenValue.search(/^\s*\[(global|end|GLOBAL|END)\]/) !== -1 + && !stack.lastElementEquals('{')) + || (tokenValue.search(/^\s*\[(global|GLOBAL)\]/) !== -1) + ) + ) { + insideCondition = false; + ignoreLine = true; + } + + if (tokenValue === ')') { + stack.popIfLastElementEquals('('); + } + if (tokenValue[0] === '*' && tokenValue[1] === '/') { + stack.popIfLastElementEquals('/*'); + ignoreLine = true; + } + if (tokenValue === '}') { + //no characters except whitespace allowed before closing bracket + const trimmedLine = line.replace(/\s/g, ''); + if (trimmedLine === '') { + stack.popIfLastElementEquals('{'); + if (prefixes.length > 0) { + prefixes.pop(); + } + ignoreLine = true; + } + } + if (!stack.lastElementEquals('#')) { + line += tokenValue; + } + } else { + // ignore comments, ... + if (!stack.lastElementEquals('/*') && !stack.lastElementEquals('(') && !ignoreLine && !insideCondition) { + line = line.trim(); + // check if there is any operator in this line + const op = this.getOperator(line); + if (op !== -1) { + // figure out the position of the operator + const pos = line.indexOf(op); + // the target objectpath should be left to the operator + path = line.substring(0, pos); + // if we are in between curly brackets: add prefixes to object path + if (prefixes.length > 0) { + path = prefixes.join('.') + '.' + path; + } + // the type or value should be right to the operator + let str = line.substring(pos + op.length, line.length).trim(); + path = path.trim(); + switch (op) { // set a value or create a new object + case '=': + //ignore if path is empty or contains whitespace + if (path.search(/\s/g) === -1 && path.length > 0) { + this.setTreeNodeValue(path, str); + } + break; + case '=<': // reference to another object in the tree + // resolve relative path + if (prefixes.length > 0 && str.substr(0, 1) === '.') { + str = prefixes.join('.') + str; + } + //ignore if either path or str is empty or contains whitespace + if (path.search(/\s/g) === -1 + && path.length > 0 + && str.search(/\s/g) === -1 + && str.length > 0 + ) { + this.setReference(path, str); + } + break; + case '<': // copy from another object in the tree + // resolve relative path + if (prefixes.length > 0 && str.substr(0, 1) === '.') { + str = prefixes.join('.') + str; + } + //ignore if either path or str is empty or contains whitespace + if (path.search(/\s/g) === -1 + && path.length > 0 + && str.search(/\s/g) === -1 + && str.length > 0 + ) { + this.setCopy(path, str); + } + break; + case '>': // delete object value and properties + this.deleteTreeNodeValue(path); + break; + case ':=': // function operator + // TODO: function-operator + break; + default: + break; + } + } + } + stack.popIfLastElementEquals('#'); + ignoreLine = false; + } + } + currentLine++; + } + // when node at cursorPos is reached: + // save currentLine, currentTsTreeNode and filter if necessary + // if there is a reference or copy operator ('<' or '=<') + // return the treeNode of the path right to the operator, + // else try to build a path from the whole line + if (!stack.lastElementEquals('/*') && !stack.lastElementEquals('(') && !ignoreLine) { + const i = line.indexOf('<'); + if (i !== -1) { + path = line.substring(i + 1, line.length).trim(); + if (prefixes.length > 0 && path.substr(0, 1) === '.') { + path = prefixes.join('.') + path; + } + } else { + path = line; + if (prefixes.length > 0) { + path = prefixes.join('.') + '.' + path; + path = path.replace(/\s/g, ''); + } + } + const lastDot = path.lastIndexOf('.'); + path = path.substring(0, lastDot); + } + return this.getTreeNode(path); + } + + /** + * Iterates through the object tree, and creates treenodes + * along the path, if necessary + */ + public getTreeNode(path: string): TreeNode | undefined { + path = path.trim(); + if (path.length === 0) { + return this.tsTree; + } + const aPath = path.split('.'); + + let subTree = this.tsTree.childNodes, + pathSeg, + parent = this.tsTree; + + // step through the path from left to right + for (let i = 0; i < aPath.length; i++) { + pathSeg = aPath[i]; + + // if there isn't already a treenode + if (typeof subTree[pathSeg] === 'undefined' || typeof subTree[pathSeg].childNodes === 'undefined') { // if this subpath is not defined in the code + // create a new treenode + subTree[pathSeg] = new TreeNode(pathSeg, this); + subTree[pathSeg].parent = parent; + // the extPath has to be set, so the TreeNode can retrieve the respecting node in the external templates + let extPath = parent.extPath; + if (extPath) { + extPath += '.'; + } + extPath += pathSeg; + subTree[pathSeg].extPath = extPath; + } + if (i === aPath.length - 1) { + return subTree[pathSeg]; + } + parent = subTree[pathSeg]; + subTree = subTree[pathSeg].childNodes; + } + return undefined; + } + + /** + * Navigates to the respecting treenode, + * create nodes in the path, if necessary, and sets the value + */ + public setTreeNodeValue(path: string, value: string): void { + const treeNode = this.getTreeNode(path); + // if we are inside a GIFBUILDER Object + if (treeNode.parent !== null && treeNode.parent.value === 'GIFBUILDER' && value === 'TEXT') { + value = 'GB_TEXT'; + } + if (treeNode.parent !== null && treeNode.parent.value === 'GIFBUILDER' && value === 'IMAGE') { + value = 'GB_IMAGE'; + } + + // just override if it is a real objecttype + if (this.tsRef.isType(value)) { + treeNode.value = value; + } + } + + /** + * Navigates to the respecting treenode, + * creates nodes if necessary, empties the value and childNodes-Array + */ + public deleteTreeNodeValue(path: string) { + const treeNode = this.getTreeNode(path); + // currently the node is not deleted really, it's just not displayed cause value == null + // deleting it would be a cleaner solution + treeNode.value = null; + treeNode.childNodes = {}; + } + + /** + * Copies a reference of the treeNode specified by path2 + * to the location specified by path1 + */ + public setReference(path1: string, path2: string): void { + const path1arr = path1.split('.'), + lastNodeName = path1arr[path1arr.length - 1], + treeNode1 = this.getTreeNode(path1), + treeNode2 = this.getTreeNode(path2); + + if (treeNode1.parent !== null) { + treeNode1.parent.childNodes[lastNodeName] = treeNode2; + } else { + this.tsTree.childNodes[lastNodeName] = treeNode2; + } + } + + /** + * copies a treeNode specified by path2 + * to the location specified by path1 + */ + public setCopy(path1: string, path2: string): void { + this.clone = <T extends object | unknown>(myObj: T): T => { + if (typeof myObj !== 'object') { + return myObj; + } + + const myNewObj: Record<string, unknown> = {}; + for (const i in myObj) { + if (i === 'tsParser') { + continue; + } + // disable recursive cloning for parent object -> copy by reference + if (i !== 'parent') { + if (typeof myObj[i] === 'object') { + myNewObj[i] = this.clone(myObj[i]); + } else { + myNewObj[i] = myObj[i]; + } + } else { + if ('parent' in myObj) { + myNewObj.parent = myObj.parent; + } + } + } + return myNewObj as T; + }; + + const path1arr = path1.split('.'), + lastNodeName = path1arr[path1arr.length - 1], + treeNode1 = this.getTreeNode(path1), + treeNode2 = this.getTreeNode(path2); + + if (treeNode1.parent !== null) { + treeNode1.parent.childNodes[lastNodeName] = this.clone(treeNode2); + } else { + this.tsTree.childNodes[lastNodeName] = this.clone(treeNode2); + } + } +} diff --git a/Build/Sources/TypeScript/t3editor/autocomplete/ts-ref.ts b/Build/Sources/TypeScript/t3editor/autocomplete/ts-ref.ts new file mode 100644 index 000000000000..f9a47cb9ac0c --- /dev/null +++ b/Build/Sources/TypeScript/t3editor/autocomplete/ts-ref.ts @@ -0,0 +1,164 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +import AjaxRequest from '@typo3/core/ajax/ajax-request'; + +type TypeId = string; + +type PropertyName = string; + +type PlainDocData = Record<TypeId, { + extends: TypeId, + name: TypeId, + properties: Record<PropertyName, { + name: PropertyName, + type: string + }>, +}>; + +export class TsRefType { + public readonly typeId: TypeId; + public extends: TypeId; + public properties: Record<PropertyName, TsRefProperty> & { clone?: () => void } = {}; + + constructor( + typeId: string, + extendsTypeId: string | null, + properties: Record<PropertyName, TsRefProperty> + ) { + this.typeId = typeId; + this.extends = extendsTypeId; + this.properties = properties; + } +} + +export class TsRefProperty { + public readonly parentType: string; + public readonly name: PropertyName; + public readonly value: string; + + constructor(parentType: string, name: string, value: string) { + this.parentType = parentType; + this.name = name; + this.value = value; + } +} + +export class TsRef { + public typeTree: Record<TypeId, TsRefType> = {}; + public doc: PlainDocData = null; + public cssClass: string; + + /** + * Load available TypoScript reference + */ + public async loadTsrefAsync(): Promise<void> { + const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.t3editor_tsref).get(); + this.doc = await response.resolve(); + this.buildTree(); + } + + /** + * Build the TypoScript reference tree + */ + public buildTree(): void { + for (const typeId of Object.keys(this.doc)) { + const arr = this.doc[typeId]; + this.typeTree[typeId] = new TsRefType( + typeId, + arr.extends || undefined, + Object.fromEntries( + Object.entries(arr.properties).map( + ([propName, property]) => [propName, new TsRefProperty(typeId, propName, property.type)] + ) + ) + ); + } + for (const typeId of Object.keys(this.typeTree)) { + if (typeof this.typeTree[typeId].extends !== 'undefined') { + this.addPropertiesToType(this.typeTree[typeId], this.typeTree[typeId].extends, 100); + } + } + } + + /** + * Adds properties to TypoScript types + */ + public addPropertiesToType( + addToType: TsRefType, + addFromTypeNames: string, + maxRecDepth: number + ): void { + if (maxRecDepth < 0) { + throw 'Maximum recursion depth exceeded while trying to resolve the extends in the TSREF!'; + } + const exts = addFromTypeNames.split(','); + for (let i = 0; i < exts.length; i++) { + // "Type 'array' which is used to extend 'undefined', was not found in the TSREF!" + if (typeof this.typeTree[exts[i]] !== 'undefined') { + if (typeof this.typeTree[exts[i]].extends !== 'undefined') { + this.addPropertiesToType(this.typeTree[exts[i]], this.typeTree[exts[i]].extends, maxRecDepth - 1); + } + const properties = this.typeTree[exts[i]].properties; + for (const propName in properties) { + // only add this property if it was not already added by a supertype (subtypes override supertypes) + if (typeof addToType.properties[propName] === 'undefined') { + addToType.properties[propName] = properties[propName]; + } + } + } + } + } + + /** + * Get properties from given TypoScript type id + */ + public getPropertiesFromTypeId(tId: TypeId): Record<PropertyName, TsRefProperty> { + if (typeof this.typeTree[tId] !== 'undefined') { + // clone is needed to assure that nothing of the tsref is overwritten by user setup + this.typeTree[tId].properties.clone = function() { + const result = {} as Record<PropertyName, TsRefProperty>; + for (const key of Object.keys(this)) { + result[key] = new TsRefProperty(this[key].parentType, this[key].name, this[key].value); + } + return result; + } + return this.typeTree[tId].properties; + } + return {}; + } + + /** + * Check if a property of a type exists + */ + public typeHasProperty(typeId: TypeId, propertyName: PropertyName): boolean { + return ( + typeof this.typeTree[typeId] !== 'undefined' && + typeof this.typeTree[typeId].properties[propertyName] !== 'undefined' + ); + } + + /** + * Get the type + */ + public getType(typeId: TypeId): TsRefType { + return this.typeTree[typeId]; + } + + /** + * Check if type exists in the type tree + */ + public isType(typeId: TypeId): boolean { + return typeof this.typeTree[typeId] !== 'undefined'; + } +} diff --git a/Build/Sources/TypeScript/t3editor/language/typoscript.ts b/Build/Sources/TypeScript/t3editor/language/typoscript.ts index 388b538f3a00..2522a8a6c336 100644 --- a/Build/Sources/TypeScript/t3editor/language/typoscript.ts +++ b/Build/Sources/TypeScript/t3editor/language/typoscript.ts @@ -1,7 +1,8 @@ +import DocumentService from '@typo3/core/document-service'; import { StreamLanguage, LanguageSupport } from '@codemirror/language'; import { CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { typoScriptStreamParser } from '@typo3/t3editor/stream-parser/typoscript'; -import TsCodeCompletion from '@typo3/t3editor/autocomplete/ts-code-completion'; +import { TsCodeCompletion } from '@typo3/t3editor/autocomplete/ts-code-completion'; import { syntaxTree } from '@codemirror/language'; import type { SyntaxNodeRef } from '@lezer/common'; @@ -12,7 +13,7 @@ interface Token { end: number; } -interface CodeMirror5CompatibleCompletionState { +export interface CodeMirror5CompatibleCompletionState { lineTokens: Token[][]; currentLineNumber: number; currentLine: string; @@ -41,7 +42,13 @@ export function typoscript() { return new LanguageSupport(language, [completion]); } -export function complete (context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null { +const tsCodeCompletionInitializer = (async (): Promise<TsCodeCompletion> => { + await DocumentService.ready(); + const effectivePid = parseInt((document.querySelector('input[name="effectivePid"]') as HTMLInputElement)?.value, 10); + return new TsCodeCompletion(effectivePid); +})(); + +export async function complete(context: CompletionContext): Promise<CompletionResult | null> { if (!context.explicit) { return null; } @@ -70,7 +77,8 @@ export function complete (context: CompletionContext): Promise<CompletionResult } cm5state.token = tokenMetadata; - const keywords = TsCodeCompletion.refreshCodeCompletion(cm5state); + const tsCodeCompletion = await tsCodeCompletionInitializer; + const keywords = tsCodeCompletion.refreshCodeCompletion(cm5state); if ((token.name === 'string' || token.name === 'comment') && tokenIsSubStringOfKeywords(tokenValue, keywords)) { return null; @@ -83,8 +91,6 @@ export function complete (context: CompletionContext): Promise<CompletionResult return { label: result, type: 'keyword' }; }) }; - - return null; } function parseCodeMirror5CompatibleCompletionState(context: CompletionContext): CodeMirror5CompatibleCompletionState { diff --git a/Build/types/TYPO3/index.d.ts b/Build/types/TYPO3/index.d.ts index 037badb0af5c..0b5ecbdf91c3 100644 --- a/Build/types/TYPO3/index.d.ts +++ b/Build/types/TYPO3/index.d.ts @@ -92,7 +92,6 @@ declare module '@typo3/dashboard/contrib/chartjs'; declare module '@typo3/backend/contrib/mark'; declare module '@typo3/t3editor/stream-parser/typoscript'; -declare module '@typo3/t3editor/autocomplete/ts-code-completion'; interface Taboverride { set(elems: HTMLElement|HTMLElement[], enable?: boolean): Taboverride diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/completion-result.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/completion-result.js index 74a1cfa42698..f382ed1430db 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/completion-result.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/completion-result.js @@ -10,112 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ - -/** - * Module: @typo3/t3editor/autocomplete/completion-result - * Contains the CompletionResult class - */ - -export default (function() { - /** - * - * @type {{tsRef: null, tsTreeNode: null}} - * @exports @typo3/t3editor/addon/hint/completion-result - */ - var CompletionResult = { - tsRef: null, - tsTreeNode: null - }; - - /** - * - * @param {Object} config - * @returns {{tsRef: null, tsTreeNode: null}} - */ - CompletionResult.init = function(config) { - CompletionResult.tsRef = config.tsRef; - CompletionResult.tsTreeNode = config.tsTreeNode; - - return CompletionResult; - }; - - /** - * returns the type of the currentTsTreeNode - * - * @returns {*} - */ - CompletionResult.getType = function() { - var val = CompletionResult.tsTreeNode.getValue(); - if (CompletionResult.tsRef.isType(val)) { - return CompletionResult.tsRef.getType(val); - } - return null; - }; - - /** - * returns a list of possible path completions (proposals), which is: - * a list of the children of the current TsTreeNode (= userdefined properties) - * and a list of properties allowed for the current object in the TsRef - * remove all words from list that don't start with the string in filter - * - * @param {String} filter beginning of the words contained in the proposal list - * @return {Array} an Array of Proposals - */ - CompletionResult.getFilteredProposals = function(filter) { - var defined = [], - propArr = [], - childNodes = CompletionResult.tsTreeNode.getChildNodes(), - value = CompletionResult.tsTreeNode.getValue(); - - // first get the childNodes of the Node (=properties defined by the user) - for (var key in childNodes) { - if (typeof childNodes[key].value !== 'undefined' && childNodes[key].value !== null) { - var propObj = {}; - propObj.word = key; - if (CompletionResult.tsRef.typeHasProperty(value, childNodes[key].name)) { - CompletionResult.tsRef.cssClass = 'definedTSREFProperty'; - propObj.type = childNodes[key].value; - } else { - propObj.cssClass = 'userProperty'; - if (CompletionResult.tsRef.isType(childNodes[key].value)) { - propObj.type = childNodes[key].value; - } else { - propObj.type = ''; - } - } - propArr.push(propObj); - defined[key] = true; - } - } - - // then get the tsref properties - var props = CompletionResult.tsRef.getPropertiesFromTypeId(CompletionResult.tsTreeNode.getValue()); - for (var key in props) { - // show just the TSREF properties - no properties of the array-prototype and no properties which have been defined by the user - if (typeof props[key].value !== 'undefined' && defined[key] !== true) { - var propObj = {}; - propObj.word = key; - propObj.cssClass = 'undefinedTSREFProperty'; - propObj.type = props[key].value; - propArr.push(propObj); - } - } - - var result = [], - wordBeginning = ''; - - for (var i = 0; i < propArr.length; i++) { - if (filter.length === 0) { - result.push(propArr[i]); - continue; - } - wordBeginning = propArr[i].word.substring(0, filter.length); - if (wordBeginning.toLowerCase() === filter.toLowerCase()) { - result.push(propArr[i]); - } - } - return result; - }; - - return CompletionResult; -})(); +export class CompletionResult{constructor(e,t){this.tsRef=e,this.tsTreeNode=t}getType(){const e=this.tsTreeNode.getValue();return this.tsRef.isType(e)?this.tsRef.getType(e):null}getFilteredProposals(e){const t={},s=[],o=this.tsTreeNode.getChildNodes(),r=this.tsTreeNode.getValue();for(const e in o)if(void 0!==o[e].value&&null!==o[e].value){const l={};l.word=e,this.tsRef.typeHasProperty(r,o[e].name)?(this.tsRef.cssClass="definedTSREFProperty",l.type=o[e].value):(l.cssClass="userProperty",this.tsRef.isType(o[e].value)?l.type=o[e].value:l.type=""),s.push(l),t[e]=!0}const l=this.tsRef.getPropertiesFromTypeId(this.tsTreeNode.getValue());for(const e in l)if(void 0!==l[e].value&&!0!==t[e]){const t={word:e,cssClass:"undefinedTSREFProperty",type:l[e].value};s.push(t)}const i=[];let n="";for(let t=0;t<s.length;t++)0!==e.length?(n=s[t].word.substring(0,e.length),n.toLowerCase()===e.toLowerCase()&&i.push(s[t])):i.push(s[t]);return i}} \ No newline at end of file diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-code-completion.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-code-completion.js index a53954182b73..d32b4018abaa 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-code-completion.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-code-completion.js @@ -10,151 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ - -/** - * Module: @typo3/t3editor/autocomplete/ts-code-completion - * Contains the TsCodeCompletion class - */ -import AjaxRequest from '@typo3/core/ajax/ajax-request.js'; -import DocumentService from '@typo3/core/document-service.js'; -import TsRef from '@typo3/t3editor/autocomplete/ts-ref.js'; -import TsParser from '@typo3/t3editor/autocomplete/ts-parser.js'; -import CompletionResult from '@typo3/t3editor/autocomplete/completion-result.js'; - -export default (function() { - /** - * - * @type {{tsRef: *, proposals: null, compResult: null, extTsObjTree: {}, parser: null, plugins: string[]}} - * @exports @typo3/t3editor/code-completion/ts-code-completion - */ - var TsCodeCompletion = { - tsRef: TsRef, - proposals: null, - compResult: null, - extTsObjTree: {}, - parser: null - }; - - /** - * All external templates along the rootline have to be loaded, - * this function retrieves the JSON code by committing a AJAX request - * - * @param {number} id - */ - TsCodeCompletion.loadExtTemplatesAsync = function(id) { - // Ensure id is an integer - id *= 1; - if (Number.isNaN(id) || id === 0) { - return null; - } - new AjaxRequest(TYPO3.settings.ajaxUrls['t3editor_codecompletion_loadtemplates']) - .withQueryArguments({pageId: id}) - .get() - .then(async function (response) { - TsCodeCompletion.extTsObjTree.c = await response.resolve(); - TsCodeCompletion.resolveExtReferencesRec(TsCodeCompletion.extTsObjTree.c); - }); - }; - - /** - * Since the references are not resolved server side we have to do it client-side - * Benefit: less loading time due to less data which has to be transmitted - * - * @param {Array} childNodes - */ - TsCodeCompletion.resolveExtReferencesRec = function(childNodes) { - for (var key in childNodes) { - var childNode; - // if the childnode has a value and there is a part of a reference operator ('<') - // and it does not look like a html tag ('>') - if (childNodes[key].v && childNodes[key].v[0] === '<' && childNodes[key].v.indexOf('>') === -1) { - var path = childNodes[key].v.replace(/</, '').trim(); - // if there are still whitespaces it's no path - if (path.indexOf(' ') === -1) { - childNode = TsCodeCompletion.getExtChildNode(path); - // if the node was found - reference it - if (childNode !== null) { - childNodes[key] = childNode; - } - } - } - // if there was no reference-resolving then we go deeper into the tree - if (!childNode && childNodes[key].c) { - TsCodeCompletion.resolveExtReferencesRec(childNodes[key].c); - } - } - }; - - /** - * Get the child node of given path - * - * @param {String} path - * @returns {Object} - */ - TsCodeCompletion.getExtChildNode = function(path) { - var extTree = TsCodeCompletion.extTsObjTree, - path = path.split('.'), - pathSeg; - - for (var i = 0; i < path.length; i++) { - pathSeg = path[i]; - if (typeof extTree.c === 'undefined' || typeof extTree.c[pathSeg] === 'undefined') { - return null; - } - extTree = extTree.c[pathSeg]; - } - return extTree; - }; - - /** - * - * @param {String} currentLine - * @returns {String} - */ - TsCodeCompletion.getFilter = function(completionState) { - if (completionState.completingAfterDot) { - return ''; - } - - return completionState.token.string.replace('.', '').replace(/\s/g, ''); - }; - - /** - * Refreshes the code completion list based on the cursor's position - */ - TsCodeCompletion.refreshCodeCompletion = function(completionState) { - // the cursornode has to be stored cause inserted breaks have to be deleted after pressing enter if the codecompletion is active - var filter = TsCodeCompletion.getFilter(completionState); - - // TODO: implement cases: operatorCompletion reference/copy path completion (formerly found in getCompletionResults()) - var currentTsTreeNode = TsCodeCompletion.parser.buildTsObjTree(completionState); - TsCodeCompletion.compResult = CompletionResult.init({ - tsRef: TsRef, - tsTreeNode: currentTsTreeNode - }); - - TsCodeCompletion.proposals = TsCodeCompletion.compResult.getFilteredProposals(filter); - - var proposals = []; - for (var i = 0; i < TsCodeCompletion.proposals.length; i++) { - proposals[i] = TsCodeCompletion.proposals[i].word; - } - - return proposals; - }; - - /** - * Resets the completion list - */ - TsCodeCompletion.resetCompList = function() { - TsCodeCompletion.compResult = null; - }; - - DocumentService.ready().then(function () { - TsCodeCompletion.parser = TsParser.init(TsCodeCompletion.tsRef, TsCodeCompletion.extTsObjTree); - TsCodeCompletion.tsRef.loadTsrefAsync(); - TsCodeCompletion.loadExtTemplatesAsync(document.querySelector('input[name="effectivePid"]')?.value); - }); - - return TsCodeCompletion; -})(); +import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{TsRef}from"@typo3/t3editor/autocomplete/ts-ref.js";import{TsParser}from"@typo3/t3editor/autocomplete/ts-parser.js";import{CompletionResult}from"@typo3/t3editor/autocomplete/completion-result.js";export class TsCodeCompletion{constructor(e){this.extTsObjTree={},this.parser=null,this.proposals=null,this.compResult=null,this.tsRef=new TsRef,this.parser=new TsParser(this.tsRef,this.extTsObjTree),this.tsRef.loadTsrefAsync(),this.loadExtTemplatesAsync(e)}refreshCodeCompletion(e){const t=this.getFilter(e),s=this.parser.buildTsObjTree(e);this.compResult=new CompletionResult(this.tsRef,s),this.proposals=this.compResult.getFilteredProposals(t);const o=[];for(let e=0;e<this.proposals.length;e++)o[e]=this.proposals[e].word;return o}loadExtTemplatesAsync(e){if(Number.isNaN(e)||0===e)return null;new AjaxRequest(TYPO3.settings.ajaxUrls.t3editor_codecompletion_loadtemplates).withQueryArguments({pageId:e}).get().then((async e=>{this.extTsObjTree.c=await e.resolve(),this.resolveExtReferencesRec(this.extTsObjTree.c)}))}resolveExtReferencesRec(e){for(const t of Object.keys(e)){let s;if(e[t].v&&"<"===e[t].v[0]&&-1===e[t].v.indexOf(">")){const o=e[t].v.replace(/</,"").trim();-1===o.indexOf(" ")&&(s=this.getExtChildNode(o),null!==s&&(e[t]=s))}!s&&e[t].c&&this.resolveExtReferencesRec(e[t].c)}}getExtChildNode(e){let t=this.extTsObjTree;const s=e.split(".");for(let e=0;e<s.length;e++){const o=s[e];if(void 0===t.c||void 0===t.c[o])return null;t=t.c[o]}return t}getFilter(e){return e.completingAfterDot?"":e.token.string.replace(".","").replace(/\s/g,"")}} \ No newline at end of file diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-parser.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-parser.js index df33284a4d76..35ffbc492a52 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-parser.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-parser.js @@ -10,508 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ - -/** - * Module: @typo3/t3editor/addon/hint/ts-parser - * Contains the TsCodeCompletion class - */ - -import '@typo3/t3editor/autocomplete/ts-ref.js'; - -export default (function() { - - /** - * - * @type {{typeId: null, properties: null, typeTree: Array, doc: null, tsRef: null, extTsObjTree: Array, tsTree: null}} - * @exports @typo3/t3editor/addon/hint/ts-parser - */ - var TsParser = { - typeId: null, - properties: null, - typeTree: [], - doc: null, - tsRef: null, - extTsObjTree: [], - tsTree: null - }; - - /** - * - * @param {Object} tsRef - * @param {Object} extTsObjTree - * @returns {{typeId: null, properties: null, typeTree: Array, doc: null, tsRef: null, extTsObjTree: Array, tsTree: null}} - */ - TsParser.init = function(tsRef, extTsObjTree) { - TsParser.tsRef = tsRef; - TsParser.extTsObjTree = extTsObjTree; - TsParser.tsTree = new TsParser.treeNode('_L_'); - - return TsParser; - }; - - /** - * - * @param {String} nodeName - */ - TsParser.treeNode = function(nodeName) { - this.name = nodeName; - this.childNodes = []; - this.extPath = ''; - this.value = ''; - this.isExternal = false; - - /** - * Returns local properties and the properties of the external templates - * - * @return {Array} - */ - this.getChildNodes = function() { - var node = this.getExtNode(); - if (node !== null && typeof node.c === 'object') { - for (let key in node.c) { - var tn = new TsParser.treeNode(key, this.tsObjTree); - tn.global = true; - tn.value = (node.c[key].v) ? node.c[key].v : ""; - tn.isExternal = true; - this.childNodes[key] = tn; - } - } - return this.childNodes; - }; - - /** - * Returns the value of a node - * - * @returns {String} - */ - this.getValue = function() { - if (this.value) { - return this.value; - } - var node = this.getExtNode(); - if (node && node.v) { - return node.v; - } - - var type = this.getNodeTypeFromTsref(); - if (type) { - return type; - } - return ''; - }; - - /** - * This method will try to resolve the properties recursively from right - * to left. If the node's value property is not set, it will look for the - * value of its parent node, and if there is a matching childProperty - * (according to the TSREF) it will return the childProperties value. - * If there is no value in the parent node it will go one step further - * and look into the parent node of the parent node,... - * - * @return {String} - */ - this.getNodeTypeFromTsref = function() { - var path = this.extPath.split('.'), - lastSeg = path.pop(); - - // attention: there will be recursive calls if necessary - var parentValue = this.parent.getValue(); - if (parentValue) { - if (TsParser.tsRef.typeHasProperty(parentValue, lastSeg)) { - var type = TsParser.tsRef.getType(parentValue); - return type.properties[lastSeg].value; - } - } - return ''; - }; - - /** - * Will look in the external ts-tree (static templates, templates on other pages) - * if there is a value or childproperties assigned to the current node. - * The method uses the extPath of the current node to navigate to the corresponding - * node in the external tree - * - * @return {Object} - */ - this.getExtNode = function() { - var extTree = TsParser.extTsObjTree, - path, - pathSeg; - - if (this.extPath === '') { - return extTree; - } - path = this.extPath.split('.'); - - for (var i = 0; i < path.length; i++) { - pathSeg = path[i]; - if (typeof extTree.c === 'undefined' || typeof extTree.c[pathSeg] === 'undefined') { - return null; - } - extTree = extTree.c[pathSeg]; - } - return extTree; - }; - }; - - /** - * Check if there is an operator in the line and return it - * if there is none, return -1 - * - * @return {(String|Number)} - */ - TsParser.getOperator = function(line) { - var operators = [':=', '=<', '<', '>', '=']; - for (var i = 0; i < operators.length; i++) { - var op = operators[i]; - if (line.indexOf(op) !== -1) { - // check if there is some HTML in this line (simple check, however it's the only difference between a reference operator and HTML) - // we do this check only in case of the two operators "=<" and "<" since the delete operator would trigger our "HTML-finder" - if ((op === '=<' || op === '<') && line.indexOf('>') > -1) { - // if there is a ">" in the line suppose there's some HTML - return '='; - } - return op; - } - } - return -1; - }; - - /** - * Build the TypoScript object tree - */ - TsParser.buildTsObjTree = function(completionState) { - TsParser.tsTree = new TsParser.treeNode(''); - TsParser.tsTree.value = 'TLO'; - - function Stack() { - } - - Stack.prototype = []; - Stack.prototype.lastElementEquals = function(str) { - return this.length > 0 && this[this.length - 1] === str; - }; - - Stack.prototype.popIfLastElementEquals = function(str) { - if (this.lastElementEquals(str)) { - this.pop(); - return true; - } - return false; - }; - - var currentLine = 1, - line = '', - stack = new Stack(), - prefixes = [], - ignoreLine = false, - insideCondition = false; - - while (currentLine <= completionState.currentLineNumber) { - line = ''; - var tokens = completionState.lineTokens[currentLine - 1]; - for (var i = 0; i <= tokens.length; ++i) { - if (i < tokens.length && tokens[i].string.length > 0) { - var tokenValue = tokens[i].string; - - if (tokenValue[0] === '#') { - stack.push('#'); - } else if (tokenValue === '(') { - stack.push('('); - } else if (tokenValue[0] === '/' && tokenValue[1] === '*') { - stack.push('/*'); - } else if (tokenValue === '{') { - // TODO: ignore whole block if wrong whitespaces in this line - if (TsParser.getOperator(line) === -1) { - stack.push('{'); - prefixes.push(line.trim()); - ignoreLine = true; - } - } - // TODO: conditions - // if condition starts -> ignore everything until end of condition - if (tokenValue.search(/^\s*\[.*\]/) !== -1 - && line.search(/\S/) === -1 - && tokenValue.search(/^\s*\[(global|end|GLOBAL|END)\]/) === -1 - && !stack.lastElementEquals('#') - && !stack.lastElementEquals('/*') - && !stack.lastElementEquals('{') - && !stack.lastElementEquals('(') - ) { - insideCondition = true; - ignoreLine = true; - } - - // if end of condition reached - if (line.search(/\S/) === -1 - && !stack.lastElementEquals('#') - && !stack.lastElementEquals('/*') - && !stack.lastElementEquals('(') - && ( - (tokenValue.search(/^\s*\[(global|end|GLOBAL|END)\]/) !== -1 - && !stack.lastElementEquals('{')) - || (tokenValue.search(/^\s*\[(global|GLOBAL)\]/) !== -1) - ) - ) { - insideCondition = false; - ignoreLine = true; - } - - if (tokenValue === ')') { - stack.popIfLastElementEquals('('); - } - if (tokenValue[0] === '*' && tokenValue[1] === '/') { - stack.popIfLastElementEquals('/*'); - ignoreLine = true; - } - if (tokenValue === '}') { - //no characters except whitespace allowed before closing bracket - var trimmedLine = line.replace(/\s/g, ''); - if (trimmedLine === '') { - stack.popIfLastElementEquals('{'); - if (prefixes.length > 0) { - prefixes.pop(); - } - ignoreLine = true; - } - } - if (!stack.lastElementEquals('#')) { - line += tokenValue; - } - } else { - // ignore comments, ... - if (!stack.lastElementEquals('/*') && !stack.lastElementEquals('(') && !ignoreLine && !insideCondition) { - line = line.trim(); - // check if there is any operator in this line - var op = TsParser.getOperator(line); - if (op !== -1) { - // figure out the position of the operator - var pos = line.indexOf(op); - // the target objectpath should be left to the operator - var path = line.substring(0, pos); - // if we are in between curly brackets: add prefixes to object path - if (prefixes.length > 0) { - path = prefixes.join('.') + '.' + path; - } - // the type or value should be right to the operator - var str = line.substring(pos + op.length, line.length).trim(); - path = path.trim(); - switch (op) { // set a value or create a new object - case '=': - //ignore if path is empty or contains whitespace - if (path.search(/\s/g) === -1 && path.length > 0) { - TsParser.setTreeNodeValue(path, str); - } - break; - case '=<': // reference to another object in the tree - // resolve relative path - if (prefixes.length > 0 && str.substr(0, 1) === '.') { - str = prefixes.join('.') + str; - } - //ignore if either path or str is empty or contains whitespace - if (path.search(/\s/g) === -1 - && path.length > 0 - && str.search(/\s/g) === -1 - && str.length > 0 - ) { - TsParser.setReference(path, str); - } - break; - case '<': // copy from another object in the tree - // resolve relative path - if (prefixes.length > 0 && str.substr(0, 1) === '.') { - str = prefixes.join('.') + str; - } - //ignore if either path or str is empty or contains whitespace - if (path.search(/\s/g) === -1 - && path.length > 0 - && str.search(/\s/g) === -1 - && str.length > 0 - ) { - TsParser.setCopy(path, str); - } - break; - case '>': // delete object value and properties - TsParser.deleteTreeNodeValue(path); - break; - case ':=': // function operator - // TODO: function-operator - break; - } - } - } - stack.popIfLastElementEquals('#'); - ignoreLine = false; - } - } - currentLine++; - } - // when node at cursorPos is reached: - // save currentLine, currentTsTreeNode and filter if necessary - // if there is a reference or copy operator ('<' or '=<') - // return the treeNode of the path right to the operator, - // else try to build a path from the whole line - if (!stack.lastElementEquals('/*') && !stack.lastElementEquals('(') && !ignoreLine) { - var i = line.indexOf('<'); - - if (i !== -1) { - var path = line.substring(i + 1, line.length).trim(); - if (prefixes.length > 0 && path.substr(0, 1) === '.') { - path = prefixes.join('.') + path; - } - } else { - var path = line; - if (prefixes.length > 0) { - path = prefixes.join('.') + '.' + path; - path = path.replace(/\s/g, ''); - } - } - var lastDot = path.lastIndexOf('.'); - path = path.substring(0, lastDot); - } - return TsParser.getTreeNode(path); - }; - - /** - * Iterates through the object tree, and creates treenodes - * along the path, if necessary - * - * @param {String} path - * @returns {Object} - */ - TsParser.getTreeNode = function(path) { - path = path.trim(); - if (path.length === 0) { - return TsParser.tsTree; - } - var aPath = path.split('.'); - - var subTree = TsParser.tsTree.childNodes, - pathSeg, - parent = TsParser.tsTree; - - // step through the path from left to right - for (var i = 0; i < aPath.length; i++) { - pathSeg = aPath[i]; - - // if there isn't already a treenode - if (typeof subTree[pathSeg] === 'undefined' || typeof subTree[pathSeg].childNodes === 'undefined') { // if this subpath is not defined in the code - // create a new treenode - subTree[pathSeg] = new TsParser.treeNode(pathSeg); - subTree[pathSeg].parent = parent; - // the extPath has to be set, so the TreeNode can retrieve the respecting node in the external templates - var extPath = parent.extPath; - if (extPath) { - extPath += '.'; - } - extPath += pathSeg; - subTree[pathSeg].extPath = extPath; - } - if (i === aPath.length - 1) { - return subTree[pathSeg]; - } - parent = subTree[pathSeg]; - subTree = subTree[pathSeg].childNodes; - } - }; - - /** - * Navigates to the respecting treenode, - * create nodes in the path, if necessary, and sets the value - * - * @param {String} path - * @param {String} value - */ - TsParser.setTreeNodeValue = function(path, value) { - var treeNode = TsParser.getTreeNode(path); - // if we are inside a GIFBUILDER Object - if (treeNode.parent !== null && treeNode.parent.value === "GIFBUILDER" && value === "TEXT") { - value = 'GB_TEXT'; - } - if (treeNode.parent !== null && treeNode.parent.value === "GIFBUILDER" && value === "IMAGE") { - value = 'GB_IMAGE'; - } - - // just override if it is a real objecttype - if (TsParser.tsRef.isType(value)) { - treeNode.value = value; - } - }; - - /** - * Navigates to the respecting treenode, - * creates nodes if necessary, empties the value and childNodes-Array - * - * @param {String} path - */ - TsParser.deleteTreeNodeValue = function(path) { - var treeNode = TsParser.getTreeNode(path); - // currently the node is not deleted really, it's just not displayed cause value == null - // deleting it would be a cleaner solution - treeNode.value = null; - treeNode.childNodes = {}; - }; - - /** - * Copies a reference of the treeNode specified by path2 - * to the location specified by path1 - * - * @param {String} path1 - * @param {String} path2 - */ - TsParser.setReference = function(path1, path2) { - var path1arr = path1.split('.'), - lastNodeName = path1arr[path1arr.length - 1], - treeNode1 = TsParser.getTreeNode(path1), - treeNode2 = TsParser.getTreeNode(path2); - - if (treeNode1.parent !== null) { - treeNode1.parent.childNodes[lastNodeName] = treeNode2; - } else { - TsParser.tsTree.childNodes[lastNodeName] = treeNode2; - } - }; - - /** - * copies a treeNode specified by path2 - * to the location specified by path1 - * - * @param {String} path1 - * @param {String} path2 - */ - TsParser.setCopy = function(path1, path2) { - this.clone = function(myObj) { - if (typeof myObj !== 'object') { - return myObj; - } - - var myNewObj = {}; - for (var i in myObj) { - // disable recursive cloning for parent object -> copy by reference - if (i !== 'parent') { - if (typeof myObj[i] === 'object') { - myNewObj[i] = this.clone(myObj[i]); - } else { - myNewObj[i] = myObj[i]; - } - } else { - myNewObj.parent = myObj.parent; - } - } - return myNewObj; - }; - - var path1arr = path1.split('.'), - lastNodeName = path1arr[path1arr.length - 1], - treeNode1 = TsParser.getTreeNode(path1), - treeNode2 = TsParser.getTreeNode(path2); - - if (treeNode1.parent !== null) { - treeNode1.parent.childNodes[lastNodeName] = this.clone(treeNode2); - } else { - TsParser.tsTree.childNodes[lastNodeName] = this.clone(treeNode2); - } - }; - - return TsParser; -})(); +export class TreeNode{constructor(e,t){this.childNodes={},this.extPath="",this.parent=null,this.name=e,this.childNodes={},this.extPath="",this.value="",this.isExternal=!1,this.tsParser=t}getChildNodes(){const e=this.getExtNode();if(null!==e&&"object"==typeof e.c)for(const t of Object.keys(e.c)){const s=new TreeNode(t,this.tsParser);s.global=!0,s.value=e.c[t].v?e.c[t].v:"",s.isExternal=!0,this.childNodes[t]=s}return this.childNodes}getValue(){if(this.value)return this.value;const e=this.getExtNode();if(e&&e.v)return e.v;const t=this.getNodeTypeFromTsref();return t||""}getNodeTypeFromTsref(){const e=this.extPath.split(".").pop(),t=this.parent.getValue();if(t&&this.tsParser.tsRef.typeHasProperty(t,e)){return this.tsParser.tsRef.getType(t).properties[e].value}return""}getExtNode(){let e=this.tsParser.extTsObjTree;if(""===this.extPath)return e;const t=this.extPath.split(".");for(let s=0;s<t.length;s++){const l=t[s];if(void 0===e.c||void 0===e.c[l])return null;e=e.c[l]}return e}}class Stack extends Array{lastElementEquals(e){return this.length>0&&this[this.length-1]===e}popIfLastElementEquals(e){return!!this.lastElementEquals(e)&&(this.pop(),!0)}}export class TsParser{constructor(e,t){this.tsRef=e,this.extTsObjTree=t,this.tsTree=new TreeNode("_L_",this)}getOperator(e){const t=[":=","=<","<",">","="];for(let s=0;s<t.length;s++){const l=t[s];if(-1!==e.indexOf(l))return("=<"===l||"<"===l)&&e.indexOf(">")>-1?"=":l}return-1}buildTsObjTree(e){this.tsTree=new TreeNode("",this),this.tsTree.value="TLO";let t=1,s="",l=!1,r=!1;const n=new Stack,i=[];let a;for(;t<=e.currentLineNumber;){s="";const h=e.lineTokens[t-1];for(let e=0;e<=h.length;++e)if(e<h.length&&h[e].string.length>0){const t=h[e].string;if("#"===t[0]?n.push("#"):"("===t?n.push("("):"/"===t[0]&&"*"===t[1]?n.push("/*"):"{"===t&&-1===this.getOperator(s)&&(n.push("{"),i.push(s.trim()),l=!0),-1===t.search(/^\s*\[.*\]/)||-1!==s.search(/\S/)||-1!==t.search(/^\s*\[(global|end|GLOBAL|END)\]/)||n.lastElementEquals("#")||n.lastElementEquals("/*")||n.lastElementEquals("{")||n.lastElementEquals("(")||(r=!0,l=!0),-1!==s.search(/\S/)||n.lastElementEquals("#")||n.lastElementEquals("/*")||n.lastElementEquals("(")||(-1===t.search(/^\s*\[(global|end|GLOBAL|END)\]/)||n.lastElementEquals("{"))&&-1===t.search(/^\s*\[(global|GLOBAL)\]/)||(r=!1,l=!0),")"===t&&n.popIfLastElementEquals("("),"*"===t[0]&&"/"===t[1]&&(n.popIfLastElementEquals("/*"),l=!0),"}"===t){""===s.replace(/\s/g,"")&&(n.popIfLastElementEquals("{"),i.length>0&&i.pop(),l=!0)}n.lastElementEquals("#")||(s+=t)}else{if(!(n.lastElementEquals("/*")||n.lastElementEquals("(")||l||r)){s=s.trim();const e=this.getOperator(s);if(-1!==e){const t=s.indexOf(e);a=s.substring(0,t),i.length>0&&(a=i.join(".")+"."+a);let l=s.substring(t+e.length,s.length).trim();switch(a=a.trim(),e){case"=":-1===a.search(/\s/g)&&a.length>0&&this.setTreeNodeValue(a,l);break;case"=<":i.length>0&&"."===l.substr(0,1)&&(l=i.join(".")+l),-1===a.search(/\s/g)&&a.length>0&&-1===l.search(/\s/g)&&l.length>0&&this.setReference(a,l);break;case"<":i.length>0&&"."===l.substr(0,1)&&(l=i.join(".")+l),-1===a.search(/\s/g)&&a.length>0&&-1===l.search(/\s/g)&&l.length>0&&this.setCopy(a,l);break;case">":this.deleteTreeNodeValue(a)}}}n.popIfLastElementEquals("#"),l=!1}t++}if(!n.lastElementEquals("/*")&&!n.lastElementEquals("(")&&!l){const e=s.indexOf("<");-1!==e?(a=s.substring(e+1,s.length).trim(),i.length>0&&"."===a.substr(0,1)&&(a=i.join(".")+a)):(a=s,i.length>0&&(a=i.join(".")+"."+a,a=a.replace(/\s/g,"")));const t=a.lastIndexOf(".");a=a.substring(0,t)}return this.getTreeNode(a)}getTreeNode(e){if(0===(e=e.trim()).length)return this.tsTree;const t=e.split(".");let s,l=this.tsTree.childNodes,r=this.tsTree;for(let e=0;e<t.length;e++){if(s=t[e],void 0===l[s]||void 0===l[s].childNodes){l[s]=new TreeNode(s,this),l[s].parent=r;let e=r.extPath;e&&(e+="."),e+=s,l[s].extPath=e}if(e===t.length-1)return l[s];r=l[s],l=l[s].childNodes}}setTreeNodeValue(e,t){const s=this.getTreeNode(e);null!==s.parent&&"GIFBUILDER"===s.parent.value&&"TEXT"===t&&(t="GB_TEXT"),null!==s.parent&&"GIFBUILDER"===s.parent.value&&"IMAGE"===t&&(t="GB_IMAGE"),this.tsRef.isType(t)&&(s.value=t)}deleteTreeNodeValue(e){const t=this.getTreeNode(e);t.value=null,t.childNodes={}}setReference(e,t){const s=e.split("."),l=s[s.length-1],r=this.getTreeNode(e),n=this.getTreeNode(t);null!==r.parent?r.parent.childNodes[l]=n:this.tsTree.childNodes[l]=n}setCopy(e,t){this.clone=e=>{if("object"!=typeof e)return e;const t={};for(const s in e)"tsParser"!==s&&("parent"!==s?"object"==typeof e[s]?t[s]=this.clone(e[s]):t[s]=e[s]:"parent"in e&&(t.parent=e.parent));return t};const s=e.split("."),l=s[s.length-1],r=this.getTreeNode(e),n=this.getTreeNode(t);null!==r.parent?r.parent.childNodes[l]=this.clone(n):this.tsTree.childNodes[l]=this.clone(n)}} \ No newline at end of file diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-ref.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-ref.js index db15773c64e6..ed706c121ea6 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-ref.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/autocomplete/ts-ref.js @@ -10,168 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ - -import AjaxRequest from '@typo3/core/ajax/ajax-request.js'; - -/** - * Module: @typo3/t3editor/code-completion/ts-ref - * Contains the TsCodeCompletion class - */ -export default (function() { - /** - * - * @type {{typeId: null, properties: null, typeTree: Array, doc: null}} - * @exports @typo3/t3editor/code-completion/ts-ref - */ - var TsRef = { - typeId: null, - properties: null, - typeTree: [], - doc: null - }; - - /** - * Prototypes a TS reference type object - * - * @param {String} typeId - */ - TsRef.TsRefType = function(typeId) { - this.typeId = typeId; - this.properties = []; - }; - - /** - * Prototypes a TS reference property object - * - * @param {String} parentType - * @param {String} name - * @param {String} value - * @constructor - */ - TsRef.TsRefProperty = function(parentType, name, value) { - this.parentType = parentType; - this.name = name; - this.value = value; - }; - - /** - * Load available TypoScript reference - */ - TsRef.loadTsrefAsync = function() { - new AjaxRequest(TYPO3.settings.ajaxUrls['t3editor_tsref']) - .get() - .then(async function (response) { - TsRef.doc = await response.resolve(); - TsRef.buildTree(); - }); - }; - - /** - * Build the TypoScript reference tree - */ - TsRef.buildTree = function() { - for (var typeId in TsRef.doc) { - var arr = TsRef.doc[typeId]; - TsRef.typeTree[typeId] = new TsRef.TsRefType(typeId); - - if (typeof arr['extends'] !== 'undefined') { - TsRef.typeTree[typeId]['extends'] = arr['extends']; - } - for (var propName in arr.properties) { - var propType = arr.properties[propName].type; - TsRef.typeTree[typeId].properties[propName] = new TsRef.TsRefProperty(typeId, propName, propType); - } - } - for (var typeId in TsRef.typeTree) { - if (typeof TsRef.typeTree[typeId]['extends'] !== 'undefined') { - TsRef.addPropertiesToType(TsRef.typeTree[typeId], TsRef.typeTree[typeId]['extends'], 100); - } - } - }; - - /** - * Adds properties to TypoScript types - * - * @param {String} addToType - * @param {String} addFromTypeNames - * @param {Number} maxRecDepth - */ - TsRef.addPropertiesToType = function(addToType, addFromTypeNames, maxRecDepth) { - if (maxRecDepth < 0) { - throw "Maximum recursion depth exceeded while trying to resolve the extends in the TSREF!"; - return; - } - var exts = addFromTypeNames.split(','), - i; - for (i = 0; i < exts.length; i++) { - // "Type 'array' which is used to extend 'undefined', was not found in the TSREF!" - if (typeof TsRef.typeTree[exts[i]] !== 'undefined') { - if (typeof TsRef.typeTree[exts[i]]['extends'] !== 'undefined') { - TsRef.addPropertiesToType(TsRef.typeTree[exts[i]], TsRef.typeTree[exts[i]]['extends'], maxRecDepth - 1); - } - var properties = TsRef.typeTree[exts[i]].properties; - for (var propName in properties) { - // only add this property if it was not already added by a supertype (subtypes override supertypes) - if (typeof addToType.properties[propName] === 'undefined') { - addToType.properties[propName] = properties[propName]; - } - } - } - } - }; - - /** - * Get properties from given TypoScript type id - * - * @param {String} tId - * @return {Array} - */ - TsRef.getPropertiesFromTypeId = function(tId) { - if (typeof TsRef.typeTree[tId] !== 'undefined') { - // clone is needed to assure that nothing of the tsref is overwritten by user setup - TsRef.typeTree[tId].properties.clone = function() { - var result = []; - for (key in this) { - result[key] = new TsRef.TsRefProperty(this[key].parentType, this[key].name, this[key].value); - } - return result; - } - return TsRef.typeTree[tId].properties; - } - return []; - }; - - /** - * Check if a property of a type exists - * - * @param {String} typeId - * @param {String} propertyName - * @return {Boolean} - */ - TsRef.typeHasProperty = function(typeId, propertyName) { - return typeof TsRef.typeTree[typeId] !== 'undefined' - && typeof TsRef.typeTree[typeId].properties[propertyName] !== 'undefined'; - }; - - /** - * Get the type - * - * @param {String} typeId - * @return {Object} - */ - TsRef.getType = function(typeId) { - return TsRef.typeTree[typeId]; - }; - - /** - * Check if type exists in the type tree - * - * @param {String} typeId - * @return {Boolean} - */ - TsRef.isType = function(typeId) { - return typeof TsRef.typeTree[typeId] !== 'undefined'; - }; - - return TsRef; -})(); +import AjaxRequest from"@typo3/core/ajax/ajax-request.js";export class TsRefType{constructor(e,t,s){this.properties={},this.typeId=e,this.extends=t,this.properties=s}}export class TsRefProperty{constructor(e,t,s){this.parentType=e,this.name=t,this.value=s}}export class TsRef{constructor(){this.typeTree={},this.doc=null}async loadTsrefAsync(){const e=await new AjaxRequest(TYPO3.settings.ajaxUrls.t3editor_tsref).get();this.doc=await e.resolve(),this.buildTree()}buildTree(){for(const e of Object.keys(this.doc)){const t=this.doc[e];this.typeTree[e]=new TsRefType(e,t.extends||void 0,Object.fromEntries(Object.entries(t.properties).map((([t,s])=>[t,new TsRefProperty(e,t,s.type)]))))}for(const e of Object.keys(this.typeTree))void 0!==this.typeTree[e].extends&&this.addPropertiesToType(this.typeTree[e],this.typeTree[e].extends,100)}addPropertiesToType(e,t,s){if(s<0)throw"Maximum recursion depth exceeded while trying to resolve the extends in the TSREF!";const r=t.split(",");for(let t=0;t<r.length;t++)if(void 0!==this.typeTree[r[t]]){void 0!==this.typeTree[r[t]].extends&&this.addPropertiesToType(this.typeTree[r[t]],this.typeTree[r[t]].extends,s-1);const i=this.typeTree[r[t]].properties;for(const t in i)void 0===e.properties[t]&&(e.properties[t]=i[t])}}getPropertiesFromTypeId(e){return void 0!==this.typeTree[e]?(this.typeTree[e].properties.clone=function(){const e={};for(const t of Object.keys(this))e[t]=new TsRefProperty(this[t].parentType,this[t].name,this[t].value);return e},this.typeTree[e].properties):{}}typeHasProperty(e,t){return void 0!==this.typeTree[e]&&void 0!==this.typeTree[e].properties[t]}getType(e){return this.typeTree[e]}isType(e){return void 0!==this.typeTree[e]}} \ No newline at end of file diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/language/typoscript.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/language/typoscript.js index a38b5e2f3511..5b50a8b2f80f 100644 --- a/typo3/sysext/t3editor/Resources/Public/JavaScript/language/typoscript.js +++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/language/typoscript.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import{StreamLanguage,LanguageSupport}from"@codemirror/language";import{typoScriptStreamParser}from"@typo3/t3editor/stream-parser/typoscript.js";import TsCodeCompletion from"@typo3/t3editor/autocomplete/ts-code-completion.js";import{syntaxTree}from"@codemirror/language";export function typoscript(){const t=StreamLanguage.define(typoScriptStreamParser),e=t.data.of({autocomplete:complete});return new LanguageSupport(t,[e])}export function complete(t){if(!t.explicit)return null;const e=parseCodeMirror5CompatibleCompletionState(t),o=t.pos-(e.completingAfterDot?1:0),r=syntaxTree(t.state).resolveInner(o,-1),n="Document"===r.name||e.completingAfterDot?"":t.state.sliceDoc(r.from,o),s="Document"===r.name||e.completingAfterDot?t.pos:r.from;let a={start:r.from,end:o,string:n,type:r.name};/^[\w$_]*$/.test(n)||(a={start:t.pos,end:t.pos,string:"",type:"."===n?"property":null}),e.token=a;const i=TsCodeCompletion.refreshCodeCompletion(e);if(("string"===r.name||"comment"===r.name)&&tokenIsSubStringOfKeywords(n,i))return null;return{from:s,options:getCompletions(n,i).map((t=>({label:t,type:"keyword"})))}}function parseCodeMirror5CompatibleCompletionState(t){const e=t.state.sliceDoc().split(t.state.lineBreak).length,o=t.state.sliceDoc(0,t.pos).split(t.state.lineBreak).length,r=t.state.sliceDoc().split(t.state.lineBreak)[o-1],n="."===t.state.sliceDoc(t.pos-1,t.pos);return{lineTokens:extractCodemirror5StyleLineTokens(e,t),currentLineNumber:o,currentLine:r,lineCount:e,completingAfterDot:n}}function extractCodemirror5StyleLineTokens(t,e){const o=Array(t).fill("").map((()=>[]));let r=0,n=1;return syntaxTree(e.state).cursor().iterate((s=>{const a=s.type.name||s.name;if("Document"===a)return;const i=s.from,l=s.to;r<i&&e.state.sliceDoc(r,i).split(e.state.lineBreak).forEach((e=>{e&&(o[Math.min(n-1,t-1)].push({type:null,string:e,start:r,end:r+e.length}),n++,r+=e.length)}));const p=e.state.sliceDoc(s.from,s.to);n=e.state.sliceDoc(0,s.from).split(e.state.lineBreak).length,o[n-1].push({type:a,string:p,start:i,end:l}),r=l})),r<e.state.doc.length&&o[n-1].push({type:null,string:e.state.sliceDoc(r),start:r,end:e.state.doc.length}),o}function tokenIsSubStringOfKeywords(t,e){const o=t.length;for(let r=0;r<e.length;++r)if(t===e[r].substr(o))return!0;return!1}function getCompletions(t,e){const o=new Set;for(let n=0,s=e.length;n<s;++n)0!==(r=e[n]).lastIndexOf(t,0)||o.has(r)||o.add(r);var r;const n=Array.from(o);return n.sort(),n} \ No newline at end of file +import DocumentService from"@typo3/core/document-service.js";import{StreamLanguage,LanguageSupport}from"@codemirror/language";import{typoScriptStreamParser}from"@typo3/t3editor/stream-parser/typoscript.js";import{TsCodeCompletion}from"@typo3/t3editor/autocomplete/ts-code-completion.js";import{syntaxTree}from"@codemirror/language";export function typoscript(){const t=StreamLanguage.define(typoScriptStreamParser),e=t.data.of({autocomplete:complete});return new LanguageSupport(t,[e])}const tsCodeCompletionInitializer=(async()=>{await DocumentService.ready();const t=parseInt(document.querySelector('input[name="effectivePid"]')?.value,10);return new TsCodeCompletion(t)})();export async function complete(t){if(!t.explicit)return null;const e=parseCodeMirror5CompatibleCompletionState(t),o=t.pos-(e.completingAfterDot?1:0),r=syntaxTree(t.state).resolveInner(o,-1),n="Document"===r.name||e.completingAfterDot?"":t.state.sliceDoc(r.from,o),s="Document"===r.name||e.completingAfterDot?t.pos:r.from;let i={start:r.from,end:o,string:n,type:r.name};/^[\w$_]*$/.test(n)||(i={start:t.pos,end:t.pos,string:"",type:"."===n?"property":null}),e.token=i;const a=(await tsCodeCompletionInitializer).refreshCodeCompletion(e);if(("string"===r.name||"comment"===r.name)&&tokenIsSubStringOfKeywords(n,a))return null;return{from:s,options:getCompletions(n,a).map((t=>({label:t,type:"keyword"})))}}function parseCodeMirror5CompatibleCompletionState(t){const e=t.state.sliceDoc().split(t.state.lineBreak).length,o=t.state.sliceDoc(0,t.pos).split(t.state.lineBreak).length,r=t.state.sliceDoc().split(t.state.lineBreak)[o-1],n="."===t.state.sliceDoc(t.pos-1,t.pos);return{lineTokens:extractCodemirror5StyleLineTokens(e,t),currentLineNumber:o,currentLine:r,lineCount:e,completingAfterDot:n}}function extractCodemirror5StyleLineTokens(t,e){const o=Array(t).fill("").map((()=>[]));let r=0,n=1;return syntaxTree(e.state).cursor().iterate((s=>{const i=s.type.name||s.name;if("Document"===i)return;const a=s.from,l=s.to;r<a&&e.state.sliceDoc(r,a).split(e.state.lineBreak).forEach((e=>{e&&(o[Math.min(n-1,t-1)].push({type:null,string:e,start:r,end:r+e.length}),n++,r+=e.length)}));const c=e.state.sliceDoc(s.from,s.to);n=e.state.sliceDoc(0,s.from).split(e.state.lineBreak).length,o[n-1].push({type:i,string:c,start:a,end:l}),r=l})),r<e.state.doc.length&&o[n-1].push({type:null,string:e.state.sliceDoc(r),start:r,end:e.state.doc.length}),o}function tokenIsSubStringOfKeywords(t,e){const o=t.length;for(let r=0;r<e.length;++r)if(t===e[r].substr(o))return!0;return!1}function getCompletions(t,e){const o=new Set;for(let n=0,s=e.length;n<s;++n)0!==(r=e[n]).lastIndexOf(t,0)||o.has(r)||o.add(r);var r;const n=Array.from(o);return n.sort(),n} \ No newline at end of file -- GitLab