').appendTo(thead);
+ tr.addClass('layout-grid-row');
+
+ fields.forEach(field => {
+ const th = $('| ').html(field.label).appendTo(tr);
+ th.css({'width': field.width + 'px', 'border': '1px gray solid', 'text-align': 'center'});
+ });
+ }
+
+ toggleChildrenVisibility(nodeId, isExpanded) {
+ const selector = `.treegrid-parent-${nodeId}`;
+ $(selector).each((_, el) => {
+ $(el).toggle(!isExpanded);
+
+ if (isExpanded) { // If we are hiding the children
+ const childNodeId = $(el).data('nodeId'); // Assume data-nodeId is set when rows are created
+ this.toggleChildrenVisibility(childNodeId, true); // Recursively hide all descendants
+ const expander = $(`.treegrid-${nodeId} .treegrid-expander`);
+ expander.html(isExpanded ? '+' : '-');
+ }
+ });
+ }
+
+ setupExpander(cell, callback) {
+ const expander = $('').addClass('treegrid-expander').html('-').appendTo(cell);
+ expander.click(callback);
+ }
+}
+
+class LayoutTreeGridController {
+ constructor(model, view, options, totalMessageLengthSelector) {
+ this.model = model;
+ this.view = view;
+ this.options = options;
+ this.apiClient = new APIClient();
+ this.totalMessageLengthSelector = totalMessageLengthSelector;
+ }
+
+ init(callback) {
+ this.view.addHeaders(this.options);
+ this.processNode(this.model.dataRoot, 0);
+ this.setupEditableFields();
+ this.callback = callback;
+ }
+
+ async rebuild(dataRoot) {
+ this.model.dataRoot = dataRoot;
+ this.view.table.children().remove();
+ this.view.addHeaders(this.options);
+ this.processNode(this.model.dataRoot, 0);
+ this.setupEditableFields();
+ this.callback(await this.concatenateFieldValues(this.model.dataRoot));
+ }
+
+ getIndent(depth) {
+ return new Array(depth + 1).join('');
+ }
+
+ async fillPadding(value, dataLength) {
+ let paddedValue = await this.apiClient.processPreScriptAndVariables(this.model.apiRequest, value.toString());
+
+ while (paddedValue.length < dataLength) {
+ paddedValue = ' ' + paddedValue; // Prepend a space character to the string
+ }
+
+ return paddedValue;
+ }
+
+ async concatenateFieldValues(node) {
+ let concatenatedValues = '';
+
+ // Process the current node if it is of type 'Field'
+ if (node.loutItemType === 'Field') {
+ concatenatedValues += await this.fillPadding(node.value, node.loutItemLength) || '';
+ }
+
+ // Recursively process each child
+ if (node.children && node.children.length > 0) {
+ for (const child of node.children) {
+ concatenatedValues += await this.concatenateFieldValues(child);
+ }
+ }
+
+ return concatenatedValues;
+ }
+
+ sumFieldLoutItemLengths() {
+ const queue = [this.model.dataRoot];
+ let sum = 0;
+
+ while (queue.length > 0) {
+ const node = queue.shift();
+
+ if (node.loutItemType === 'Field') {
+ sum += parseInt(node.loutItemLength) || 0; // Add the loutItemLength of the node, or 0 if undefined
+ }
+
+ if (node.children && node.children.length > 0) {
+ for (const child of node.children) {
+ queue.push(child);
+ }
+ }
+ }
+ return sum;
+ }
+
+ setupEditableFields() {
+ this.view.table.find('.editable').each((index, editableElement) => {
+ const editableInput = new EditableInput({
+ name: editableElement.dataset.name,
+ value: editableElement.dataset.value,
+ path: editableElement.dataset.path
+ });
+
+ editableInput.init(editableElement, async (value) => {
+ console.log(editableElement.dataset.path);
+ console.log(value);
+ _.set(this.model.dataRoot, editableElement.dataset.path, value);
+ console.log(this.model.dataRoot);
+ this.callback(await this.concatenateFieldValues(this.model.dataRoot));
+ });
+ });
+
+ this.totalMessageLengthSelector.text(this.sumFieldLoutItemLengths());
+ }
+
+ addCell(row, node, fieldInfo, depth) {
+ const td = $(' | ').appendTo(row);
+ const padding = fieldInfo.isFirst ? this.getIndent(depth) : '';
+ const nodePath = this.model.getNodePath(this.model.dataRoot, node.id) + '.value';
+
+ if (node.loutItemType === 'Field' && fieldInfo.type === 'textfield') {
+ const div = $('');
+ div.addClass('editable');
+ div.appendTo(td);
+ div.attr('data-name', fieldInfo.name);
+ div.attr('data-value', node[fieldInfo.name]);
+ div.attr('data-path', nodePath);
+ } else {
+ td.html(padding); // Default to just displaying the content
+ }
+
+ if (fieldInfo.width) {
+ td.css({'width': fieldInfo.width + 'px', 'border': '1px gray solid'});
+ }
+
+ if (fieldInfo.align) {
+ td.css({'text-align': fieldInfo.align});
+ }
+
+ return td;
+ }
+
+ addRow(node, depth) {
+ const tr = $(' ').addClass('treegrid-' + node.id).data('nodeId', node.id); // Storing node ID for easy access later
+
+ if (depth === 0) {
+ tr.css({'display': 'none'});
+ }
+
+ if (node.parent) {
+ const parentSelector = `.treegrid-${node.parent}`;
+ const lastChildSelector = `.treegrid-parent-${node.parent}`;
+
+ if ($(this.view.table).find(lastChildSelector).length > 0) {
+ const lastChild = $(this.view.table).find(lastChildSelector).last();
+ tr.insertAfter(lastChild);
+ } else {
+ tr.insertAfter($(this.view.table).find(parentSelector));
+ }
+ tr.addClass('treegrid-parent-' + node.parent);
+ } else {
+ tr.appendTo(this.view.table);
+ }
+ return tr;
+ }
+
+ //processNode in bfs
+ processNode(node, depth) {
+ const queue = [{node, depth}];
+ while (queue.length > 0) {
+ const {node, depth} = queue.shift();
+ const tr = this.addRow(node, depth);
+
+ this.options.forEach((fieldInfo) => {
+ const content = node[fieldInfo.name];
+ const td = this.addCell(tr, node, fieldInfo, depth);
+
+ if (fieldInfo.isFirst) {
+ if (node.children && node.children.length > 0) {
+ this.view.setupExpander($(tr.children()[0]), () => this.handleExpanderClick(node.id));
+ }
+ }
+
+ if (fieldInfo.type !== 'textfield') {
+ td.append(content);
+ }
+ });
+
+ if (node.children && node.children.length > 0) {
+ for (const child of node.children) {
+ queue.push({node: child, depth: depth + 1});
+ }
+ }
+ }
+ }
+
+ handleExpanderClick(nodeId) {
+ const expander = $(`.treegrid-${nodeId} .treegrid-expander`);
+ const isExpanded = expander.html() === '-';
+ expander.html(isExpanded ? '+' : '-');
+ this.view.toggleChildrenVisibility(nodeId, isExpanded);
+ }
+}
+
+class LayoutTreeDataGrid {
+ constructor(selector, root, apiRequest, options, totalMessageLengthSelector) {
+ this.model = new LayoutTreeDataGridModel(root, apiRequest);
+ this.view = new LayoutTreeDataGridView(selector);
+ this.controller = new LayoutTreeGridController(this.model, this.view, options, totalMessageLengthSelector);
+ }
+
+ init(callback) {
+ this.controller.init(callback);
+ }
+
+ rebuild(dataRoot) {
+ this.controller.rebuild(dataRoot);
+ }
+
+}
+
+export default LayoutTreeDataGrid;
diff --git a/ApiTestManager/src/components/LayoutTreeGrid.js b/ApiTestManager/src/components/LayoutTreeGrid.js
new file mode 100644
index 0000000..82184b7
--- /dev/null
+++ b/ApiTestManager/src/components/LayoutTreeGrid.js
@@ -0,0 +1,231 @@
+import EditableInput from './EditableInput';
+
+class LayoutTreeGridModel {
+ constructor(root) {
+ this.root = root;
+ this.rootId = root.id;
+ }
+
+ setRoot(newRoot) {
+ this.root = newRoot;
+ }
+
+ getNodePath(node, targetId, path = "") {
+ // Check if the current node is the target node
+ if (node.id === targetId) {
+ return path;
+ }
+
+ // If the current node has children, iterate over them
+ if (node.children) {
+ for (let i = 0; i < node.children.length; i++) {
+ // Construct the path to the current child
+ const currentPath = path ? `${path}.children[${i}]` : `children[${i}]`;
+
+ // Recursively call findPath on the child
+ const resultPath = this.getNodePath(node.children[i], targetId, currentPath);
+
+ // If resultPath is not null, the target has been found in the subtree
+ if (resultPath) {
+ return resultPath;
+ }
+ }
+ }
+
+ // Return null if the target is not found in this subtree
+ return null;
+ }
+}
+
+class LayoutTreeGridView {
+ constructor(selector) {
+ this.selector = selector;
+ this.table = $('').appendTo($(this.selector));
+ this.table.addClass('layout_table');
+ this.table.css({'width': 'inherit', 'border-collapse': 'collapse', 'border': '1px gray solid'});
+ this.init();
+ }
+
+ init() {
+ this.table.children().remove();
+ }
+
+ clear() {
+ this.init();
+ }
+
+ addHeaders(fields) {
+ const thead = $('').appendTo(this.table);
+ const tr = $('').appendTo(thead);
+ tr.addClass('layout-grid-row');
+
+ fields.forEach(field => {
+ const th = $('').html(field.label).appendTo(tr);
+ th.css({'width': field.width + 'px', 'border': '1px gray solid', 'text-align': 'center'});
+ });
+ }
+
+
+
+ toggleChildrenVisibility(nodeId, isExpanded) {
+ const selector = `.treegrid-parent-${nodeId}`;
+ $(selector).each((_, el) => {
+ $(el).toggle(!isExpanded);
+
+ if (isExpanded) { // If we are hiding the children
+ const childNodeId = $(el).data('nodeId'); // Assume data-nodeId is set when rows are created
+ this.toggleChildrenVisibility(childNodeId, true); // Recursively hide all descendants
+ const expander = $(`.treegrid-${nodeId} .treegrid-expander`);
+ expander.html(isExpanded ? '+' : '-');
+ }
+ });
+ }
+
+ setupExpander(cell, callback) {
+ const expander = $('').addClass('treegrid-expander').html('-').appendTo(cell);
+ expander.click(callback);
+ }
+}
+
+class LayoutTreeGridController {
+ constructor(model, view, options, callback) {
+ this.model = model;
+ this.view = view;
+ this.options = options;
+ this.callback = callback;
+ this.init();
+ }
+
+ init() {
+ this.view.addHeaders(this.options);
+ this.processNode(this.model.root, 0);
+ this.view.table.find('.editable').each((index, editableElement) => {
+ const editableInput = new EditableInput({
+ name: editableElement.dataset.name,
+ value: editableElement.dataset.value,
+ path: editableElement.dataset.path
+ });
+
+ editableInput.init(editableElement, async(value) => {
+ _.set(this.model.root, editableElement.dataset.path, value);
+ this.callback(this.model.root);
+ });
+ });
+ }
+
+ getIndent(depth) {
+ return new Array(depth + 1).join('');
+ }
+
+ addRow(node, depth) {
+ const tr = $('').addClass('treegrid-' + node.id).data('nodeId', node.id); // Storing node ID for easy access later
+
+ if (depth === 0) {
+ tr.css({'display': 'none'});
+ }
+
+ if (node.parent || (node.parent !== null && parseInt(node.parent) !== this.model.rootId)) {
+ const parentSelector = `.treegrid-${node.parent}`;
+ const lastChildSelector = `.treegrid-parent-${node.parent}`;
+
+ if ($(this.view.table).find(lastChildSelector).length > 0) {
+ const lastChild = $(this.view.table).find(lastChildSelector).last();
+ console.log('lastChild: ', lastChild);
+ tr.insertAfter(lastChild);
+ } else {
+ console.log('parentSelector: ', parentSelector);
+ tr.insertAfter($(this.view.table).find(parentSelector));
+ }
+ tr.addClass('treegrid-parent-' + node.parent);
+ } else {
+ console.log('no parent');
+ tr.appendTo(this.view.table);
+ }
+ console.log('addRow, node: ', node.id, ' parent: ', node.parent);
+ console.log('addRow tr: ', tr);
+ return tr;
+ }
+
+ addCell(row, node, fieldInfo, depth) {
+ const td = $('').appendTo(row);
+ const padding = fieldInfo.isFirst ? this.getIndent(depth) : '';
+ const nodePath = this.model.getNodePath(this.model.root, node.id) + ".value";
+
+ if (node.loutItemType === 'Field' && fieldInfo.type === 'textfield') {
+ const div = $('');
+ div.addClass('editable');
+ div.appendTo(td);
+ div.attr('data-name', fieldInfo.name);
+ div.attr('data-value', node[fieldInfo.name]);
+ div.attr('data-path', nodePath);
+
+ } else {
+ td.html(padding); // Default to just displaying the content
+ }
+
+ if (fieldInfo.width) {
+ td.css({'width': fieldInfo.width + 'px', 'border': '1px gray solid'});
+ }
+
+ if (fieldInfo.align) {
+ td.css({'text-align': fieldInfo.align});
+ }
+
+ return td;
+ }
+
+ processNode(node, depth) {
+ console.log('node: ', node.id);
+ const tr = this.addRow(node, depth);
+
+ this.options.forEach((fieldInfo) => {
+ const content = node[fieldInfo.name];
+ const td = this.addCell(tr, node, fieldInfo, depth);
+
+ if (fieldInfo.isFirst) {
+ if (node.children && node.children.length > 0) {
+ this.view.setupExpander($(tr.children()[0]), () => this.handleExpanderClick(node.id));
+ }
+ }
+
+ if (fieldInfo.type !== 'textfield') {
+ td.append(content);
+ }
+ });
+
+ // Recursively process each child node if any
+ if (node.children && node.children.length > 0) {
+ node.children.forEach(child => {
+ this.processNode(child, depth + 1); // Increment depth for each child
+ });
+ }
+ }
+
+ handleExpanderClick(nodeId) {
+ const expander = $(`.treegrid-${nodeId} .treegrid-expander`);
+ const isExpanded = expander.html() === '-';
+ expander.html(isExpanded ? '+' : '-');
+ this.view.toggleChildrenVisibility(nodeId, isExpanded);
+ }
+
+ rebuild(newRoot) {
+ this.model.setRoot(newRoot);
+ this.view.clear();
+ this.init();
+ }
+}
+
+class LayoutTreeGrid {
+ constructor(selector, root, options, callback) {
+ this.model = new LayoutTreeGridModel(root);
+ this.view = new LayoutTreeGridView(selector);
+ this.controller = new LayoutTreeGridController(this.model, this.view, options, callback);
+ }
+
+ rebuild(root, callback) {
+ this.controller.rebuild(root);
+ callback(root);
+ }
+}
+
+export default LayoutTreeGrid;
diff --git a/ApiTestManager/src/components/LoadTestResultDetail.js b/ApiTestManager/src/components/LoadTestResultDetail.js
index 88a9333..43053f4 100644
--- a/ApiTestManager/src/components/LoadTestResultDetail.js
+++ b/ApiTestManager/src/components/LoadTestResultDetail.js
@@ -33,8 +33,16 @@ class LoadTestResultDetailController {
showDetail(result) {
this.view.outputEditor.setValue('');
- this.logHttpRequest(result.request);
- this.logHttpResponse(result.response);
+ console.log(result);
+ if (result.request.type === 'TCP') {
+ this.logTcpRequest(result.request);
+ this.logTcpResponse(result.response);
+
+ } else {
+ this.logHttpRequest(result.request);
+ this.logHttpResponse(result.response);
+ }
+
}
reset(){
@@ -48,18 +56,32 @@ class LoadTestResultDetailController {
this.view.outputEditor.executeEdits('my-source', [op]);
}
+
logHttpRequest(request) {
console.log({request});
let log = `[API Request]\n${request.method} ${request.path} HTTP/1.1\n[Request Headers]\n${this.parseRequestHeaders(request.headers)}\n[Request Body]\n${request.requestBody}`;
this.appendLog(log);
}
+ logTcpRequest(model) {
+ console.log(model);
+ let log = `[TCP API Request]\n[Request Body]\n${model.requestBody}`;
+ this.appendLog(log);
+ }
+
+
logHttpResponse(response) {
console.log({response});
let log = `[Response]\nHTTP/1.1 ${response.status}\n[Response Headers]\n${this.parseHeaders(response.headers)}\n[Response Body]\n${response.body}\n`;
this.appendLog(log);
}
+ logTcpResponse(response) {
+ console.log({response});
+ let log = `[Response]\n${response.body}\n`;
+ this.appendLog(log);
+ }
+
parseRequestHeaders(headers) {
let parsedHeaders = '';
headers.forEach(header => {
diff --git a/ApiTestManager/src/components/ServerManager.js b/ApiTestManager/src/components/ServerManager.js
index 6836d1a..bfd250c 100644
--- a/ApiTestManager/src/components/ServerManager.js
+++ b/ApiTestManager/src/components/ServerManager.js
@@ -1,6 +1,8 @@
+import EditableInput from './EditableInput';
+
class ServerManager {
constructor(servers) {
- this.servers = servers ||[];
+ this.servers = servers || [];
}
basePath(serverId) {
@@ -23,17 +25,93 @@ class ServerManager {
}
renderTcpServers(selectedServer) {
- const optionsHtml = this.servers.filter( server => server.scheme === 'tcp')
- .map(server => ` `).join('');
+ const optionsHtml = this.servers.filter(server => server.scheme === 'tcp').map(server => ` `).join('');
return ` `;
}
+
+ getHeader(headers, key) {
+ if (headers === undefined || headers === null) {
+ return '';
+ }
+
+ // Filter the headers to find the matching key
+ const filteredHeaders = headers.filter(header => header.key === key);
+
+ // Check if any header was found that matches the key
+ if (filteredHeaders.length === 0) {
+ console.log('new header');
+ let newHeader = { key: key, value: '', enabled : true };
+ headers.push(newHeader);
+ return newHeader;
+ }
+ // Return the value of the first matching header
+ return filteredHeaders[0];
+ }
+
+ renderTcpServerOptions(target, serverOptions, headers, parent) {
+ console.log(headers);
+
+ let tableHTML = `
+
+
+
+ | Option Key |
+ Option Name |
+ Offset |
+ Length |
+ Value |
+
+
+
+ ${serverOptions.map(option => `
+
+ | ${option.optionKey} |
+ ${option.optionName} |
+ ${option.offset} |
+ ${option.length} |
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+
+ const container = document.querySelector(target);
+ container.innerHTML += tableHTML;
+
+ container.querySelectorAll('.editable').forEach(editableElement => {
+ const editableInput = new EditableInput({
+ name: editableElement.dataset.name,
+ value: editableElement.dataset.value
+ });
+
+ editableInput.init(editableElement, (value) => {
+ let header = this.getHeader(headers, editableElement.dataset.name);
+ header.value = value;
+ console.log({value});
+ console.log(header);
+
+ $('span.variable').off('mouseover');
+ $('span.variable').off('mouseout');
+ document.querySelectorAll('span.variable').forEach(element => {
+ let text = element.innerText;
+ element.addEventListener('mouseover', () => parent.showPopper(element, text));
+ element.addEventListener('mouseout', () => parent.hidePopper(element));
+ });
+ });
+ });
+ }
+
renderHttpServers(selectedServer) {
- const optionsHtml = this.servers.filter( server => server.scheme !== 'tcp')
- .map(server => ` `).join('');
+ const optionsHtml = this.servers.filter(server => server.scheme !== 'tcp').
+ map(server => ` `).
+ join('');
return `
+
+
-
+
+ -
+
+
+ -
+
+
+ -
+
-
@@ -314,13 +155,30 @@ class TCPClientView {
-
+
+
-
+
+
+
@@ -345,6 +203,7 @@ class TCPClientView {
+
전체 메시지 길이:
diff --git a/ApiTestManager/webpack.config.js b/ApiTestManager/webpack.config.js
index 13ce448..cb5057a 100644
--- a/ApiTestManager/webpack.config.js
+++ b/ApiTestManager/webpack.config.js
@@ -31,21 +31,29 @@ module.exports = {
]
},
{
- test: /\.css$/,
- use: ['style-loader', 'css-loader']
+ test: /\.(woff|woff2|eot|ttf|otf)$/i,
+ type: 'asset/resource',
+ generator: {
+ filename: 'fonts/[name][ext]',
+ },
},
{
- test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
- use: [
- {
- loader: 'file-loader',
- options: {
- name: '[name].[ext]',
- outputPath: 'fonts/',
- },
- },
- ],
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
}
+ //,
+ // {
+ // test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
+ // use: [
+ // {
+ // loader: 'file-loader',
+ // options: {
+ // name: '[name].[ext]',
+ // outputPath: 'fonts/',
+ // },
+ // },
+ // ],
+ // }
]
},
plugins: [new MonacoWebpackPlugin()],
diff --git a/build.gradle b/build.gradle
index 5a1636d..f81bdc0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,8 +10,8 @@ jar.enabled = false
group = 'com.eactive'
version = '2.0.0'
-sourceCompatibility = '11'
-targetCompatibility = 11
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
configurations {
annotationProcessor
diff --git a/src/main/resources/static/plugins/apitestmanager/vendors-node_modules_monaco-editor_esm_vs_basic-languages_twig_twig_js.bundle.js b/src/main/resources/static/plugins/apitestmanager/vendors-node_modules_monaco-editor_esm_vs_basic-languages_twig_twig_js.bundle.js
new file mode 100644
index 0000000..cedead4
--- /dev/null
+++ b/src/main/resources/static/plugins/apitestmanager/vendors-node_modules_monaco-editor_esm_vs_basic-languages_twig_twig_js.bundle.js
@@ -0,0 +1,22 @@
+"use strict";
+/*
+ * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
+ * This devtool is neither made for production nor for readable output files.
+ * It uses "eval()" calls to create a separate source file in the browser devtools.
+ * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
+ * or disable the default devtool with "devtool: false".
+ * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
+ */
+((typeof self !== 'undefined' ? self : this)["webpackChunkAPITestManager"] = (typeof self !== 'undefined' ? self : this)["webpackChunkAPITestManager"] || []).push([["vendors-node_modules_monaco-editor_esm_vs_basic-languages_twig_twig_js"],{
+
+/***/ "./node_modules/monaco-editor/esm/vs/basic-languages/twig/twig.js":
+/*!************************************************************************!*\
+ !*** ./node_modules/monaco-editor/esm/vs/basic-languages/twig/twig.js ***!
+ \************************************************************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ conf: () => (/* binding */ conf),\n/* harmony export */ language: () => (/* binding */ language)\n/* harmony export */ });\n/*!-----------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.47.0(69991d66135e4a1fc1cf0b1ac4ad25d429866a0d)\n * Released under the MIT license\n * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt\n *-----------------------------------------------------------------------------*/\n\n\n// src/basic-languages/twig/twig.ts\nvar conf = {\n wordPattern: /(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\s]+)/g,\n comments: {\n blockComment: [\"{#\", \"#}\"]\n },\n brackets: [\n [\"{#\", \"#}\"],\n [\"{%\", \"%}\"],\n [\"{{\", \"}}\"],\n [\"(\", \")\"],\n [\"[\", \"]\"],\n // HTML\n [\"\"],\n [\"<\", \">\"]\n ],\n autoClosingPairs: [\n { open: \"{# \", close: \" #}\" },\n { open: \"{% \", close: \" %}\" },\n { open: \"{{ \", close: \" }}\" },\n { open: \"[\", close: \"]\" },\n { open: \"(\", close: \")\" },\n { open: '\"', close: '\"' },\n { open: \"'\", close: \"'\" }\n ],\n surroundingPairs: [\n { open: '\"', close: '\"' },\n { open: \"'\", close: \"'\" },\n // HTML\n { open: \"<\", close: \">\" }\n ]\n};\nvar language = {\n defaultToken: \"\",\n tokenPostfix: \"\",\n ignoreCase: true,\n keywords: [\n // (opening) tags\n \"apply\",\n \"autoescape\",\n \"block\",\n \"deprecated\",\n \"do\",\n \"embed\",\n \"extends\",\n \"flush\",\n \"for\",\n \"from\",\n \"if\",\n \"import\",\n \"include\",\n \"macro\",\n \"sandbox\",\n \"set\",\n \"use\",\n \"verbatim\",\n \"with\",\n // closing tags\n \"endapply\",\n \"endautoescape\",\n \"endblock\",\n \"endembed\",\n \"endfor\",\n \"endif\",\n \"endmacro\",\n \"endsandbox\",\n \"endset\",\n \"endwith\",\n // literals\n \"true\",\n \"false\"\n ],\n tokenizer: {\n root: [\n // whitespace\n [/\\s+/],\n // Twig Tag Delimiters\n [/{#/, \"comment.twig\", \"@commentState\"],\n [/{%[-~]?/, \"delimiter.twig\", \"@blockState\"],\n [/{{[-~]?/, \"delimiter.twig\", \"@variableState\"],\n // HTML\n [/)/, [\"delimiter.html\", \"tag.html\", \"\", \"delimiter.html\"]],\n [/(<)(script)/, [\"delimiter.html\", { token: \"tag.html\", next: \"@script\" }]],\n [/(<)(style)/, [\"delimiter.html\", { token: \"tag.html\", next: \"@style\" }]],\n [/(<)((?:[\\w\\-]+:)?[\\w\\-]+)/, [\"delimiter.html\", { token: \"tag.html\", next: \"@otherTag\" }]],\n [/(<\\/)((?:[\\w\\-]+:)?[\\w\\-]+)/, [\"delimiter.html\", { token: \"tag.html\", next: \"@otherTag\" }]],\n [/, \"delimiter.html\"],\n [/[^<{]+/]\n // text\n ],\n /**\n * Comment Tag Handling\n */\n commentState: [\n [/#}/, \"comment.twig\", \"@pop\"],\n [/./, \"comment.twig\"]\n ],\n /**\n * Block Tag Handling\n */\n blockState: [\n [/[-~]?%}/, \"delimiter.twig\", \"@pop\"],\n // whitespace\n [/\\s+/],\n // verbatim\n // Unlike other blocks, verbatim ehas its own state\n // transition to ensure we mark its contents as strings.\n [\n /(verbatim)(\\s*)([-~]?%})/,\n [\"keyword.twig\", \"\", { token: \"delimiter.twig\", next: \"@rawDataState\" }]\n ],\n { include: \"expression\" }\n ],\n rawDataState: [\n // endverbatim\n [\n /({%[-~]?)(\\s*)(endverbatim)(\\s*)([-~]?%})/,\n [\"delimiter.twig\", \"\", \"keyword.twig\", \"\", { token: \"delimiter.twig\", next: \"@popall\" }]\n ],\n [/./, \"string.twig\"]\n ],\n /**\n * Variable Tag Handling\n */\n variableState: [[/[-~]?}}/, \"delimiter.twig\", \"@pop\"], { include: \"expression\" }],\n stringState: [\n // closing double quoted string\n [/\"/, \"string.twig\", \"@pop\"],\n // interpolation start\n [/#{\\s*/, \"string.twig\", \"@interpolationState\"],\n // string part\n [/[^#\"\\\\]*(?:(?:\\\\.|#(?!\\{))[^#\"\\\\]*)*/, \"string.twig\"]\n ],\n interpolationState: [\n // interpolation end\n [/}/, \"string.twig\", \"@pop\"],\n { include: \"expression\" }\n ],\n /**\n * Expression Handling\n */\n expression: [\n // whitespace\n [/\\s+/],\n // operators - math\n [/\\+|-|\\/{1,2}|%|\\*{1,2}/, \"operators.twig\"],\n // operators - logic\n [/(and|or|not|b-and|b-xor|b-or)(\\s+)/, [\"operators.twig\", \"\"]],\n // operators - comparison (symbols)\n [/==|!=|<|>|>=|<=/, \"operators.twig\"],\n // operators - comparison (words)\n [/(starts with|ends with|matches)(\\s+)/, [\"operators.twig\", \"\"]],\n // operators - containment\n [/(in)(\\s+)/, [\"operators.twig\", \"\"]],\n // operators - test\n [/(is)(\\s+)/, [\"operators.twig\", \"\"]],\n // operators - misc\n [/\\||~|:|\\.{1,2}|\\?{1,2}/, \"operators.twig\"],\n // names\n [\n /[^\\W\\d][\\w]*/,\n {\n cases: {\n \"@keywords\": \"keyword.twig\",\n \"@default\": \"variable.twig\"\n }\n }\n ],\n // numbers\n [/\\d+(\\.\\d+)?/, \"number.twig\"],\n // punctuation\n [/\\(|\\)|\\[|\\]|{|}|,/, \"delimiter.twig\"],\n // strings\n [/\"([^#\"\\\\]*(?:\\\\.[^#\"\\\\]*)*)\"|\\'([^\\'\\\\]*(?:\\\\.[^\\'\\\\]*)*)\\'/, \"string.twig\"],\n // opening double quoted string\n [/\"/, \"string.twig\", \"@stringState\"],\n // misc syntactic constructs\n // These are not operators per se, but for the purposes of lexical analysis we\n // can treat them as such.\n // arrow functions\n [/=>/, \"operators.twig\"],\n // assignment\n [/=/, \"operators.twig\"]\n ],\n /**\n * HTML\n */\n doctype: [\n [/[^>]+/, \"metatag.content.html\"],\n [/>/, \"metatag.html\", \"@pop\"]\n ],\n comment: [\n [/-->/, \"comment.html\", \"@pop\"],\n [/[^-]+/, \"comment.content.html\"],\n [/./, \"comment.content.html\"]\n ],\n otherTag: [\n [/\\/?>/, \"delimiter.html\", \"@pop\"],\n [/\"([^\"]*)\"/, \"attribute.value.html\"],\n [/'([^']*)'/, \"attribute.value.html\"],\n [/[\\w\\-]+/, \"attribute.name.html\"],\n [/=/, \"delimiter.html\"],\n [/[ \\t\\r\\n]+/]\n // whitespace\n ],\n // -- BEGIN | | |