ru.coon 3.0.85 → 3.0.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/common/component/editor/creators/NoteEditorCreator.js +23 -10
- package/src/report/component/reportpanel/ReportGrid.js +36 -0
- package/src/report/component/reportpanel/ReportTree.js +15 -0
- package/src/uielement/plugin/OpenPanelPlugin.js +2 -6
- package/src/version.js +1 -1
- package/AGENTS.md +0 -26
- package/src/IndentTemplate.js +0 -324
- package/src/Template.js +0 -420
- package/src/app/viewPort/Permalink.d2 +0 -27
- package/src/app/viewPort/Router.d2 +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
# Version 3.0.88, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/a72099d6e504067f855a039fa59064412650d2ed)
|
|
2
|
+
* ## Features
|
|
3
|
+
* <span style='color:green'>feat: restore initial sort order(HT-15548)</span> ([1e05ed], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/1e05ed4fa8c952cedb098ac7f22d8bff75ee76e1))
|
|
4
|
+
|
|
5
|
+
* upd ([f528fe], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/f528fef7cfbc8dbf082e87aa25178591ab070673))
|
|
6
|
+
* update: CHANGELOG.md ([266027], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/2660273ba4d148d6533d409f99b5f55038ec1d25))
|
|
7
|
+
|
|
8
|
+
# Version 3.0.87, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/2f65647d7dc56cc63610f4dcd70c765c1ec25a35)
|
|
9
|
+
* ## Fixes
|
|
10
|
+
* <span style='color:red'> HT-10298 Доработка внешнего вида NoteEditorCreator</span> ([4f14da], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/4f14da33f15fc83b19cabf83cc1ba51814f656bf))
|
|
11
|
+
|
|
12
|
+
* upd ([20e397], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/20e397d293e06e79c40860b952d1b067cc90369a))
|
|
13
|
+
* upd ([970ad4], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/970ad4a00fe880c845f0631f84b4470beba87229))
|
|
14
|
+
* update: CHANGELOG.md ([b6f217], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/b6f2171d4910b5b355a338c3aad366771e023629))
|
|
15
|
+
|
|
16
|
+
# Version 3.0.86, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/a568914d31b78421977a33a435390084439a1778)
|
|
17
|
+
* ## Fixes
|
|
18
|
+
* <span style='color:red'>fix</span> ([36dceb], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/36dcebbe4096ea1b91d82a7e9cdffaccb1d38cc8))
|
|
19
|
+
|
|
20
|
+
* update: CHANGELOG.md ([dbde81], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/dbde8119310c7536df00629a3be387bb1d88cd39))
|
|
21
|
+
|
|
1
22
|
# Version 3.0.85, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/d03e2cb1d340c80862f949a561196d650d1b720a)
|
|
2
23
|
* ## Fixes
|
|
3
24
|
* <span style='color:red'>fix</span> ([0c1ddd], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/0c1dddad599b412a7c363398f70bcecdb295ca32))
|
package/package.json
CHANGED
|
@@ -22,9 +22,19 @@ Ext.define('Coon.common.component.editor.creators.NoteEditorCreator', {
|
|
|
22
22
|
|
|
23
23
|
showPanel: function(record, parent) {
|
|
24
24
|
this.noteID = record.get('value');
|
|
25
|
-
this.
|
|
25
|
+
this.textTypeField = Ext.create({
|
|
26
|
+
xtype: 'textfield',
|
|
27
|
+
fieldLabel: 'Характеристика',
|
|
28
|
+
editable: false,
|
|
29
|
+
cls: 'no-input-border-bottom',
|
|
30
|
+
width: '100%',
|
|
31
|
+
value: record.get('typeDescription'),
|
|
32
|
+
}),
|
|
33
|
+
this.textValueField = Ext.create({
|
|
26
34
|
xtype: 'textarea',
|
|
35
|
+
fieldLabel: 'Значение',
|
|
27
36
|
width: '99%',
|
|
37
|
+
flex: 1,
|
|
28
38
|
maxLength: 4000,
|
|
29
39
|
maxLengthText: 'Максимальная допустимая длина поля - 4000 символов',
|
|
30
40
|
});
|
|
@@ -32,14 +42,17 @@ Ext.define('Coon.common.component.editor.creators.NoteEditorCreator', {
|
|
|
32
42
|
if (!Ext.isEmpty(this.noteID)) {
|
|
33
43
|
const command = Ext.create('Coon.common.command.GetNoteCommand');
|
|
34
44
|
command.on('complete', function(response) {
|
|
35
|
-
this.
|
|
45
|
+
this.textValueField.setValue(response.text);
|
|
36
46
|
}, this);
|
|
37
47
|
command.execute(this.noteID);
|
|
38
48
|
}
|
|
39
49
|
const textPanel = new Ext.Panel({
|
|
40
|
-
layout: '
|
|
50
|
+
layout: 'vbox',
|
|
41
51
|
padding: 10,
|
|
42
|
-
items:
|
|
52
|
+
items: [
|
|
53
|
+
this.textTypeField,
|
|
54
|
+
this.textValueField
|
|
55
|
+
],
|
|
43
56
|
});
|
|
44
57
|
|
|
45
58
|
const win = this.win = Ext.widget('WindowWrap', {
|
|
@@ -70,16 +83,16 @@ Ext.define('Coon.common.component.editor.creators.NoteEditorCreator', {
|
|
|
70
83
|
],
|
|
71
84
|
}],
|
|
72
85
|
|
|
73
|
-
title: 'Редактирование характеристики
|
|
86
|
+
title: 'Редактирование характеристики',
|
|
74
87
|
});
|
|
75
88
|
return this.win;
|
|
76
89
|
},
|
|
77
90
|
|
|
78
91
|
saveValue: function(record, parent) {
|
|
79
|
-
if (!this.
|
|
92
|
+
if (!this.textValueField.validate()) {
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
82
|
-
if (Ext.isEmpty(this.
|
|
95
|
+
if (Ext.isEmpty(this.textValueField.getValue().trim())) {
|
|
83
96
|
this.noteID = '';
|
|
84
97
|
this.setValueInField(record, parent);
|
|
85
98
|
} else {
|
|
@@ -88,13 +101,13 @@ Ext.define('Coon.common.component.editor.creators.NoteEditorCreator', {
|
|
|
88
101
|
this.noteID = response.noteId;
|
|
89
102
|
this.setValueInField(record, parent);
|
|
90
103
|
}, this);
|
|
91
|
-
saveCommand.execute(this.noteID, this.
|
|
104
|
+
saveCommand.execute(this.noteID, this.textValueField.getValue());
|
|
92
105
|
}
|
|
93
106
|
},
|
|
94
107
|
|
|
95
108
|
setValueInField(record, parent) {
|
|
96
|
-
const value = this.
|
|
97
|
-
const description = this.
|
|
109
|
+
const value = this.textValueField.getValue().trim() ? this.getValue() : '';
|
|
110
|
+
const description = this.textValueField.getValue();
|
|
98
111
|
record.set('value', value);
|
|
99
112
|
record.set('valueDescription', description);
|
|
100
113
|
this.field.setVisibleValue(description);
|
|
@@ -30,7 +30,17 @@ Ext.define('Coon.report.component.reportpanel.ReportGrid', {
|
|
|
30
30
|
*/
|
|
31
31
|
bodyCls: 'enable-striped-rows',
|
|
32
32
|
|
|
33
|
+
locals: {},
|
|
34
|
+
|
|
33
35
|
initComponent: function() {
|
|
36
|
+
const self = this;
|
|
37
|
+
this.fields.push({
|
|
38
|
+
name: '__initialSortOrder',
|
|
39
|
+
convert(_, record) {
|
|
40
|
+
record.data.__initialSortOrder = self.locals.initialOrderIndex;
|
|
41
|
+
self.locals.initialOrderIndex += 1;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
34
44
|
this.store = Ext.create('Coon.report.component.reportpanel.ReportStore', {
|
|
35
45
|
fields: this.fields,
|
|
36
46
|
groupField: this.groupField,
|
|
@@ -42,6 +52,10 @@ Ext.define('Coon.report.component.reportpanel.ReportGrid', {
|
|
|
42
52
|
pageSize: this.pageSize,
|
|
43
53
|
});
|
|
44
54
|
|
|
55
|
+
this.store.on('beforeload', function() {
|
|
56
|
+
this.locals.initialOrderIndex = 0;
|
|
57
|
+
}, this);
|
|
58
|
+
|
|
45
59
|
this.on('afterrender', () => {
|
|
46
60
|
this.doAutoSizeAllColumn();
|
|
47
61
|
this.store.on('load', () => {
|
|
@@ -79,6 +93,28 @@ Ext.define('Coon.report.component.reportpanel.ReportGrid', {
|
|
|
79
93
|
this.callParent();
|
|
80
94
|
this.relayEvents(this.getStore(), ['datachanged', 'load']);
|
|
81
95
|
this.increaseToolbarHeight();
|
|
96
|
+
this.addRestoreSortingMenuItem();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
addRestoreSortingMenuItem() {
|
|
100
|
+
const menu = this.getHeaderContainer?.().getMenu?.();
|
|
101
|
+
if (!menu) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const menuItem = menu.add({
|
|
105
|
+
text: 'отменить сортировку',
|
|
106
|
+
handler: this.restoreInitianSorting.bind(this),
|
|
107
|
+
});
|
|
108
|
+
menu.moveAfter(menuItem, menu.getComponent(1));
|
|
109
|
+
menu.on('beforeshow', function(menu) {
|
|
110
|
+
const column = menu.ownerCmp;
|
|
111
|
+
menuItem.setDisabled(!column.sortable);
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
restoreInitianSorting() {
|
|
116
|
+
this.store.getSorters().remove();
|
|
117
|
+
this.store.sort('__initialSortOrder', 'ASC');
|
|
82
118
|
},
|
|
83
119
|
|
|
84
120
|
/**
|
|
@@ -21,6 +21,7 @@ Ext.define('Coon.report.component.reportpanel.ReportTree', {
|
|
|
21
21
|
treeColumnIndex: 'TREE_ELEMENT_TEXT',
|
|
22
22
|
treeReducers: [],
|
|
23
23
|
checkedRecords: [],
|
|
24
|
+
enableAutoSize: false,
|
|
24
25
|
},
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -38,6 +39,9 @@ Ext.define('Coon.report.component.reportpanel.ReportTree', {
|
|
|
38
39
|
this.getColumnModel = Ext.bind(Ext.grid.Panel.prototype.getColumnModel, this);
|
|
39
40
|
const me = this;
|
|
40
41
|
this.plugins = [].concat(this.plugins || [], 'GridRecordViewerPlugin');
|
|
42
|
+
this.on('afterrender', () => {
|
|
43
|
+
this.doAutoSizeAllColumn();
|
|
44
|
+
}, this, {single: true});
|
|
41
45
|
this.store = Ext.create('Coon.report.component.reportpanel.ReportTreeStore', {
|
|
42
46
|
fields: this.fields,
|
|
43
47
|
groupField: this.groupField,
|
|
@@ -117,9 +121,20 @@ Ext.define('Coon.report.component.reportpanel.ReportTree', {
|
|
|
117
121
|
Ext.Component.generateTestId(store.getAt(index).get(this.idIndex), domElement);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
124
|
+
this.doAutoSizeAllColumn();
|
|
120
125
|
}, this);
|
|
121
126
|
},
|
|
122
127
|
|
|
128
|
+
doAutoSizeAllColumn() {
|
|
129
|
+
if (
|
|
130
|
+
this.getEnableAutoSize() &&
|
|
131
|
+
typeof this.getColumnModel === 'function' &&
|
|
132
|
+
typeof this.getColumnModel().autoSizeAllColumn === 'function'
|
|
133
|
+
) {
|
|
134
|
+
this.getColumnModel().autoSizeAllColumn();
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
123
138
|
clear: function() {
|
|
124
139
|
if (this.rendered) {
|
|
125
140
|
this.getSelectionModel().clearSelections();
|
|
@@ -29,12 +29,8 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
29
29
|
Coon.log.error('OpenPanelPlugin.init: viewModel is required');
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
if (!this.config.uiElementCd) {
|
|
33
|
-
Coon.log.error('OpenPanelPlugin.init: uiElementCd is required');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (!this.config.panelXType) {
|
|
37
|
-
Coon.log.error('OpenPanelPlugin.init: panelXType is required');
|
|
32
|
+
if (!this.config.uiElementCd && !this.config.panelXType) {
|
|
33
|
+
Coon.log.error('OpenPanelPlugin.init: uiElementCd||panelXType is required');
|
|
38
34
|
return;
|
|
39
35
|
}
|
|
40
36
|
this.initialized = true;
|
package/src/version.js
CHANGED
package/AGENTS.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Project Name
|
|
2
|
-
|
|
3
|
-
core of ExtJS 7 extensive expandable components
|
|
4
|
-
|
|
5
|
-
## Code Style
|
|
6
|
-
|
|
7
|
-
- Follow ESLint configuration
|
|
8
|
-
- Use 2 spaces for indentation
|
|
9
|
-
- one file for one class
|
|
10
|
-
|
|
11
|
-
## Architecture
|
|
12
|
-
|
|
13
|
-
- Follow DRY pattern
|
|
14
|
-
- Keep components under 200 lines
|
|
15
|
-
- Use dependency injection
|
|
16
|
-
|
|
17
|
-
## Testing
|
|
18
|
-
|
|
19
|
-
- Write unit tests for all business logic
|
|
20
|
-
- Maintain >80% code coverage
|
|
21
|
-
- Use Jest for testing
|
|
22
|
-
|
|
23
|
-
## Security
|
|
24
|
-
|
|
25
|
-
- Never commit API keys or secrets
|
|
26
|
-
- Validate all user inputs
|
package/src/IndentTemplate.js
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/* eslint-disable require-jsdoc */
|
|
2
|
-
/**
|
|
3
|
-
* A template engine that uses indentation-based syntax to define structure,
|
|
4
|
-
* supporting mixins, loops, and variable interpolation.
|
|
5
|
-
*/
|
|
6
|
-
class IndentTemplate {
|
|
7
|
-
/**
|
|
8
|
-
* Creates a new IndentTemplate instance.
|
|
9
|
-
* @param {string} [template=''] - The initial template string
|
|
10
|
-
*/
|
|
11
|
-
constructor(template = '') {
|
|
12
|
-
this._template = template;
|
|
13
|
-
this._ast = null;
|
|
14
|
-
this._mixins = {};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Sets or changes the template string.
|
|
19
|
-
* @param {string} template - The template string to set
|
|
20
|
-
* @returns {IndentTemplate} This instance for chaining
|
|
21
|
-
*/
|
|
22
|
-
setTemplate(template) {
|
|
23
|
-
this._template = template;
|
|
24
|
-
this._ast = null;
|
|
25
|
-
this._mixins = {};
|
|
26
|
-
return this; // chainable
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Compiles the template by parsing it into an AST and collecting mixins.
|
|
31
|
-
* @returns {IndentTemplate} This instance for chaining
|
|
32
|
-
* @throws {Error} If the template is empty or has invalid syntax
|
|
33
|
-
*/
|
|
34
|
-
compile() {
|
|
35
|
-
if (!this._template) {
|
|
36
|
-
throw new Error('Шаблон пуст. Используй setTemplate(template).');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const rawLines = this._template
|
|
40
|
-
.split('\n')
|
|
41
|
-
.map((l) => l.replace(/\r$/, ''))
|
|
42
|
-
.filter((l) => l.trim().length > 0);
|
|
43
|
-
|
|
44
|
-
const lines = rawLines.map((line) => {
|
|
45
|
-
const m = line.match(/^(\s*)/);
|
|
46
|
-
const indent = m ? m[1].length : 0;
|
|
47
|
-
const level = Math.floor(indent / 2); // 2 пробела = 1 уровень
|
|
48
|
-
return {
|
|
49
|
-
level,
|
|
50
|
-
content: line.trim(),
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
this._mixins = {};
|
|
55
|
-
let index = 0;
|
|
56
|
-
|
|
57
|
-
const parseBlock = (expectedLevel) => {
|
|
58
|
-
const nodes = [];
|
|
59
|
-
|
|
60
|
-
while (index < lines.length) {
|
|
61
|
-
const {level, content} = lines[index];
|
|
62
|
-
|
|
63
|
-
if (level < expectedLevel) {
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
if (level > expectedLevel) {
|
|
67
|
-
// некорректный отступ — считаем конец этого блока
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---- миксин определение ------------------------------------
|
|
72
|
-
if (content.startsWith('@mixin ')) {
|
|
73
|
-
if (expectedLevel !== 0) {
|
|
74
|
-
throw new Error('@mixin должен быть на верхнем уровне без отступов');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const def = content.slice(7).trim(); // после "@mixin "
|
|
78
|
-
const parts = def.split(/\s+/);
|
|
79
|
-
const name = parts[0];
|
|
80
|
-
const params = parts.slice(1); // список имён параметров
|
|
81
|
-
|
|
82
|
-
index++;
|
|
83
|
-
// тело миксина — блок на уровне 1
|
|
84
|
-
const body = parseBlock(expectedLevel + 1);
|
|
85
|
-
|
|
86
|
-
this._mixins[name] = {params, body};
|
|
87
|
-
continue; // миксин не попадает в основной AST
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ---- цикл @for ---------------------------------------------
|
|
91
|
-
if (content.startsWith('@for ')) {
|
|
92
|
-
const forLine = content.slice(5).trim(); // убираем "@for "
|
|
93
|
-
const match = forLine.match(/^(\w+)\s+in\s+(.+)$/);
|
|
94
|
-
if (!match) {
|
|
95
|
-
throw new Error('Неверный синтаксис @for: ' + content);
|
|
96
|
-
}
|
|
97
|
-
const varName = match[1];
|
|
98
|
-
const listExpr = match[2].trim();
|
|
99
|
-
|
|
100
|
-
index++;
|
|
101
|
-
const children = parseBlock(expectedLevel + 1);
|
|
102
|
-
|
|
103
|
-
nodes.push({
|
|
104
|
-
type: 'for',
|
|
105
|
-
varName,
|
|
106
|
-
listExpr,
|
|
107
|
-
children,
|
|
108
|
-
});
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---- использование миксина: @use name(arg1, arg2) ----------
|
|
113
|
-
if (content.startsWith('@use ')) {
|
|
114
|
-
const call = content.slice(5).trim();
|
|
115
|
-
const m = call.match(/^(\w+)\s*\((.*)\)$/);
|
|
116
|
-
if (!m) {
|
|
117
|
-
throw new Error('Неверный синтаксис @use: ' + content);
|
|
118
|
-
}
|
|
119
|
-
const mixinName = m[1];
|
|
120
|
-
const argsRaw = m[2].trim();
|
|
121
|
-
const argExprs = argsRaw ?
|
|
122
|
-
argsRaw.split(',').map((s) => s.trim()) :
|
|
123
|
-
[];
|
|
124
|
-
|
|
125
|
-
index++;
|
|
126
|
-
|
|
127
|
-
nodes.push({
|
|
128
|
-
type: 'use',
|
|
129
|
-
mixinName,
|
|
130
|
-
argExprs,
|
|
131
|
-
});
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---- обычный тег -------------------------------------------
|
|
136
|
-
const [tag, ...restParts] = content.split(/\s+/);
|
|
137
|
-
const text = restParts.join(' ');
|
|
138
|
-
const selfClosing = new Set(['br', 'hr', 'img', 'input', 'meta', 'link']);
|
|
139
|
-
const lowerTag = tag.toLowerCase();
|
|
140
|
-
const isSelfClosing = selfClosing.has(lowerTag);
|
|
141
|
-
|
|
142
|
-
index++;
|
|
143
|
-
|
|
144
|
-
let children = [];
|
|
145
|
-
if (!text && !isSelfClosing && index < lines.length) {
|
|
146
|
-
const next = lines[index];
|
|
147
|
-
if (next.level > expectedLevel) {
|
|
148
|
-
children = parseBlock(expectedLevel + 1);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
nodes.push({
|
|
153
|
-
type: 'tag',
|
|
154
|
-
tag,
|
|
155
|
-
text,
|
|
156
|
-
isSelfClosing,
|
|
157
|
-
children,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return nodes;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
this._ast = parseBlock(0);
|
|
165
|
-
return this; // chainable
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Builds the final HTML output by rendering the compiled template with provided data.
|
|
170
|
-
* @param {Object} [data={}] - The data object to render the template with
|
|
171
|
-
* @returns {string} The rendered HTML string
|
|
172
|
-
*/
|
|
173
|
-
build(data = {}) {
|
|
174
|
-
if (!this._ast) {
|
|
175
|
-
this.compile();
|
|
176
|
-
}
|
|
177
|
-
return this._renderNodes(this._ast, data);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ===================== RENDER =====================
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Renders an array of nodes with the given scope.
|
|
184
|
-
* @param {Array} nodes - Array of AST nodes to render
|
|
185
|
-
* @param {Object} scope - The current scope/data context
|
|
186
|
-
* @returns {string} Concatenated rendered nodes
|
|
187
|
-
* @private
|
|
188
|
-
*/
|
|
189
|
-
_renderNodes(nodes, scope) {
|
|
190
|
-
return nodes.map((node) => this._renderNode(node, scope)).join('');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Renders a single node based on its type.
|
|
195
|
-
* @param {Object} node - The AST node to render
|
|
196
|
-
* @param {Object} scope - The current scope/data context
|
|
197
|
-
* @returns {string} The rendered HTML for this node
|
|
198
|
-
* @private
|
|
199
|
-
*/
|
|
200
|
-
_renderNode(node, scope) {
|
|
201
|
-
// for
|
|
202
|
-
if (node.type === 'for') {
|
|
203
|
-
const list = this._resolvePath(scope, node.listExpr);
|
|
204
|
-
if (!Array.isArray(list) || list.length === 0) {
|
|
205
|
-
return '';
|
|
206
|
-
}
|
|
207
|
-
let out = '';
|
|
208
|
-
for (const item of list) {
|
|
209
|
-
const childScope = Object.create(scope);
|
|
210
|
-
childScope[node.varName] = item;
|
|
211
|
-
out += this._renderNodes(node.children, childScope);
|
|
212
|
-
}
|
|
213
|
-
return out;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// use mixin
|
|
217
|
-
if (node.type === 'use') {
|
|
218
|
-
const def = this._mixins[node.mixinName];
|
|
219
|
-
if (!def) {
|
|
220
|
-
throw new Error(`Миксин "${node.mixinName}" не найден`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const {params, body} = def;
|
|
224
|
-
const argValues = node.argExprs.map((expr) => this._resolvePath(scope, expr));
|
|
225
|
-
|
|
226
|
-
const childScope = Object.create(scope);
|
|
227
|
-
params.forEach((name, i) => {
|
|
228
|
-
childScope[name] = argValues[i];
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
return this._renderNodes(body, childScope);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// обычный тег
|
|
235
|
-
if (node.type === 'tag') {
|
|
236
|
-
const {tag, text, isSelfClosing, children} = node;
|
|
237
|
-
|
|
238
|
-
if (isSelfClosing) {
|
|
239
|
-
return `<${tag}>`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const innerText = text ?
|
|
243
|
-
this._renderTextWithTemplates(text, scope) :
|
|
244
|
-
'';
|
|
245
|
-
|
|
246
|
-
const childrenHtml = children && children.length ?
|
|
247
|
-
this._renderNodes(children, scope) :
|
|
248
|
-
'';
|
|
249
|
-
|
|
250
|
-
return `<${tag}>${innerText}${childrenHtml}</${tag}>`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return '';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Renders text with template expressions replaced by values from scope.
|
|
258
|
-
* @param {string} text - Text potentially containing template expressions
|
|
259
|
-
* @param {Object} scope - The current scope/data context
|
|
260
|
-
* @returns {string} Text with expressions replaced by values
|
|
261
|
-
* @private
|
|
262
|
-
*/
|
|
263
|
-
_renderTextWithTemplates(text, scope) {
|
|
264
|
-
let result = '';
|
|
265
|
-
let lastIndex = 0;
|
|
266
|
-
const regex = /\{\{([^}]+)\}\}/g;
|
|
267
|
-
|
|
268
|
-
text.replace(regex, (match, expr, offset) => {
|
|
269
|
-
// статичный кусок до {{ ... }}
|
|
270
|
-
const staticPart = text.slice(lastIndex, offset);
|
|
271
|
-
result += this._escape(staticPart);
|
|
272
|
-
|
|
273
|
-
const value = this._resolvePath(scope, expr.trim());
|
|
274
|
-
const strVal = value == null ? '' : String(value);
|
|
275
|
-
result += this._escape(strVal);
|
|
276
|
-
|
|
277
|
-
lastIndex = offset + match.length;
|
|
278
|
-
return '';
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// хвост после последнего {{ ... }}
|
|
282
|
-
const tail = text.slice(lastIndex);
|
|
283
|
-
result += this._escape(tail);
|
|
284
|
-
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Resolves a dot-notation path in the scope object.
|
|
290
|
-
* @param {Object} scope - The scope object to look in
|
|
291
|
-
* @param {string} path - Dot-notation path like "user.name"
|
|
292
|
-
* @returns {*} The resolved value or undefined
|
|
293
|
-
* @private
|
|
294
|
-
*/
|
|
295
|
-
_resolvePath(scope, path) {
|
|
296
|
-
if (!path) {
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
const parts = path.split('.');
|
|
300
|
-
let value = scope;
|
|
301
|
-
for (const part of parts) {
|
|
302
|
-
if (value == null) {
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
value = value[part];
|
|
306
|
-
}
|
|
307
|
-
return value;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Escapes HTML special characters in text.
|
|
312
|
-
* @param {string} text - Text to escape
|
|
313
|
-
* @returns {string} Escaped text
|
|
314
|
-
* @private
|
|
315
|
-
*/
|
|
316
|
-
_escape(text) {
|
|
317
|
-
return String(text)
|
|
318
|
-
.replace(/&/g, '&')
|
|
319
|
-
.replace(/</g, '<')
|
|
320
|
-
.replace(/>/g, '>');
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export default IndentTemplate;
|
package/src/Template.js
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A template engine that parses and compiles templates with custom syntax.
|
|
3
|
-
* Supports elements, mixins, loops, conditions, and variable interpolation.
|
|
4
|
-
*/
|
|
5
|
-
class Template {
|
|
6
|
-
/**
|
|
7
|
-
* Creates a new Template instance.
|
|
8
|
-
* @param {string} source - The template source string
|
|
9
|
-
*/
|
|
10
|
-
constructor(source) {
|
|
11
|
-
this.source = source;
|
|
12
|
-
this.ast = this.parse(source);
|
|
13
|
-
this.renderFn = this.compile(this.ast);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Builds the final output by rendering the compiled template with provided context.
|
|
18
|
-
* @param {Object} context - The context object to render the template with
|
|
19
|
-
* @returns {string} The rendered output string
|
|
20
|
-
*/
|
|
21
|
-
build(context = {}) {
|
|
22
|
-
return this.renderFn(context);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ========== ПАРСЕР ==========
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Parses the template source into an Abstract Syntax Tree (AST).
|
|
29
|
-
* @param {string} source - The template source string
|
|
30
|
-
* @returns {Object} The parsed AST representation
|
|
31
|
-
*/
|
|
32
|
-
parse(source) {
|
|
33
|
-
const lines = source.split('\n').filter((line) => line.trim() !== '');
|
|
34
|
-
const mixins = new Map();
|
|
35
|
-
const {nodes} = this.parseBlock(lines, 0, 0, mixins);
|
|
36
|
-
return {type: 'Root', children: nodes, mixins};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parses a block of lines at a specific indentation level.
|
|
41
|
-
* @param {string[]} lines - Array of template lines
|
|
42
|
-
* @param {number} start - Starting index in the lines array
|
|
43
|
-
* @param {number} indentLevel - Current indentation level
|
|
44
|
-
* @param {Map} mixins - Map of defined mixins
|
|
45
|
-
* @returns {Object} Object containing parsed nodes and the next index
|
|
46
|
-
*/
|
|
47
|
-
parseBlock(lines, start, indentLevel, mixins) {
|
|
48
|
-
const nodes = [];
|
|
49
|
-
let i = start;
|
|
50
|
-
|
|
51
|
-
while (i < lines.length) {
|
|
52
|
-
const line = lines[i];
|
|
53
|
-
const match = line.match(/^ */);
|
|
54
|
-
const currentIndent = match ? match[0].length : 0;
|
|
55
|
-
|
|
56
|
-
if (currentIndent < indentLevel) {
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
if (currentIndent !== indentLevel) {
|
|
60
|
-
throw new Error(`Invalid indentation at line ${i + 1}. Expected ${indentLevel}, got ${currentIndent}.`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const content = line.trim();
|
|
64
|
-
let node;
|
|
65
|
-
|
|
66
|
-
if (content.startsWith('@mixin ')) {
|
|
67
|
-
const mixin = this.parseMixinDecl(content);
|
|
68
|
-
const {nodes: body, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
69
|
-
mixin.body = body;
|
|
70
|
-
mixins.set(mixin.name, mixin);
|
|
71
|
-
node = mixin;
|
|
72
|
-
i = nextIndex;
|
|
73
|
-
} else if (content.startsWith('@use ')) {
|
|
74
|
-
const call = this.parseMixinCall(content);
|
|
75
|
-
const def = mixins.get(call.name);
|
|
76
|
-
if (!def) {
|
|
77
|
-
throw new Error(`Undefined mixin: ${call.name}`);
|
|
78
|
-
}
|
|
79
|
-
node = {type: 'MixinCall', definition: def, args: call.args};
|
|
80
|
-
i++;
|
|
81
|
-
} else if (content.startsWith('@each ')) {
|
|
82
|
-
const each = this.parseEach(content);
|
|
83
|
-
const {nodes: body, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
84
|
-
each.children = body;
|
|
85
|
-
node = each;
|
|
86
|
-
i = nextIndex;
|
|
87
|
-
} else if (content.startsWith('@if ')) {
|
|
88
|
-
const condition = content.slice(4).trim();
|
|
89
|
-
const ifNode = {type: 'If', condition, thenBranch: [], elseBranch: []};
|
|
90
|
-
const {nodes: thenBody, nextIndex: afterThen} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
91
|
-
ifNode.thenBranch = thenBody;
|
|
92
|
-
i = afterThen;
|
|
93
|
-
|
|
94
|
-
// Проверяем, есть ли @else сразу после
|
|
95
|
-
if (i < lines.length) {
|
|
96
|
-
const elseLine = lines[i].trim();
|
|
97
|
-
const elseIndent = lines[i].match(/^ */)[0].length;
|
|
98
|
-
if (elseIndent === indentLevel && elseLine === '@else') {
|
|
99
|
-
const {nodes: elseBody, nextIndex: afterElse} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
100
|
-
ifNode.elseBranch = elseBody;
|
|
101
|
-
i = afterElse;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
node = ifNode;
|
|
106
|
-
} else {
|
|
107
|
-
node = this.parseElement(content);
|
|
108
|
-
const {nodes: children, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
109
|
-
node.children = children;
|
|
110
|
-
i = nextIndex;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (node.type !== 'MixinDecl') {
|
|
114
|
-
nodes.push(node);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return {nodes, nextIndex: i};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Parses an element line into a structured object.
|
|
123
|
-
* @param {string} line - A line containing an element definition
|
|
124
|
-
* @returns {Object} Parsed element object
|
|
125
|
-
*/
|
|
126
|
-
parseElement(line) {
|
|
127
|
-
const tagMatch = line.match(/^([a-zA-Z][a-zA-Z0-9]*)\b/);
|
|
128
|
-
if (!tagMatch) {
|
|
129
|
-
throw new Error(`Invalid tag: "${line}"`);
|
|
130
|
-
}
|
|
131
|
-
const tagName = tagMatch[1];
|
|
132
|
-
const rest = line.slice(tagMatch[0].length).trim();
|
|
133
|
-
|
|
134
|
-
const selfClosingTags = new Set([
|
|
135
|
-
'img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
const attrs = {};
|
|
139
|
-
let text = '';
|
|
140
|
-
|
|
141
|
-
// Простой парсинг: всё, что содержит = → атрибут; остальное — текст
|
|
142
|
-
const tokens = rest.match(/(?:[^"\s=]+|"[^"]*")+/g) || [];
|
|
143
|
-
let inAttrs = true;
|
|
144
|
-
for (const t of tokens) {
|
|
145
|
-
if (t.includes('=')) {
|
|
146
|
-
const [key, val] = t.split('=', 2);
|
|
147
|
-
let finalVal = val;
|
|
148
|
-
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
149
|
-
(val.startsWith('\'') && val.endsWith('\''))) {
|
|
150
|
-
finalVal = val.slice(1, -1);
|
|
151
|
-
}
|
|
152
|
-
attrs[key] = finalVal;
|
|
153
|
-
} else if (inAttrs && !t.startsWith('"') && !t.includes('{{')) {
|
|
154
|
-
// Предполагаем, что это булев атрибут
|
|
155
|
-
attrs[t] = true;
|
|
156
|
-
} else {
|
|
157
|
-
inAttrs = false;
|
|
158
|
-
if (text) {
|
|
159
|
-
text += ' ';
|
|
160
|
-
}
|
|
161
|
-
text += t;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
type: 'Element',
|
|
167
|
-
tagName,
|
|
168
|
-
attrs,
|
|
169
|
-
text,
|
|
170
|
-
children: [],
|
|
171
|
-
selfClosing: selfClosingTags.has(tagName),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Parses a mixin declaration line.
|
|
177
|
-
* @param {string} line - A line containing a mixin declaration
|
|
178
|
-
* @returns {Object} Parsed mixin declaration object
|
|
179
|
-
*/
|
|
180
|
-
parseMixinDecl(line) {
|
|
181
|
-
const match = line.match(/^@mixin\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/);
|
|
182
|
-
if (!match) {
|
|
183
|
-
throw new Error(`Invalid mixin declaration: ${line}`);
|
|
184
|
-
}
|
|
185
|
-
const name = match[1];
|
|
186
|
-
const params = match[2].split(',').map((p) => p.trim()).filter((p) => p);
|
|
187
|
-
return {type: 'MixinDecl', name, params, body: []};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Parses a mixin call line.
|
|
192
|
-
* @param {string} line - A line containing a mixin call
|
|
193
|
-
* @returns {Object} Parsed mixin call object
|
|
194
|
-
*/
|
|
195
|
-
parseMixinCall(line) {
|
|
196
|
-
const match = line.match(/^@use\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/);
|
|
197
|
-
if (!match) {
|
|
198
|
-
throw new Error(`Invalid mixin call: ${line}`);
|
|
199
|
-
}
|
|
200
|
-
const name = match[1];
|
|
201
|
-
const argsStr = match[2];
|
|
202
|
-
|
|
203
|
-
const args = [];
|
|
204
|
-
let current = '';
|
|
205
|
-
let inQuote = null;
|
|
206
|
-
for (let k = 0; k < argsStr.length; k++) {
|
|
207
|
-
const c = argsStr[k];
|
|
208
|
-
if (c === inQuote) {
|
|
209
|
-
inQuote = null;
|
|
210
|
-
} else if (!inQuote && (c === '"' || c === '\'')) {
|
|
211
|
-
inQuote = c;
|
|
212
|
-
} else if (!inQuote && c === ',') {
|
|
213
|
-
args.push(current.trim());
|
|
214
|
-
current = '';
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
current += c;
|
|
218
|
-
}
|
|
219
|
-
if (current.trim()) {
|
|
220
|
-
args.push(current.trim());
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {type: 'MixinCall', name, args};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Parses an each loop declaration.
|
|
228
|
-
* @param {string} line - A line containing an each loop declaration
|
|
229
|
-
* @returns {Object} Parsed each loop object
|
|
230
|
-
*/
|
|
231
|
-
parseEach(line) {
|
|
232
|
-
const match = line.match(/^@each\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z_][a-zA-Z0-9_.]*)/);
|
|
233
|
-
if (!match) {
|
|
234
|
-
throw new Error(`Invalid @each: ${line}`);
|
|
235
|
-
}
|
|
236
|
-
return {type: 'Each', iterator: match[1], collection: match[2], children: []};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ========== КОМПИЛЯТОР ==========
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Compiles the AST into a render function.
|
|
243
|
-
* @param {Object} ast - The Abstract Syntax Tree to compile
|
|
244
|
-
* @returns {Function} A function that renders the template with a given context
|
|
245
|
-
*/
|
|
246
|
-
compile(ast) {
|
|
247
|
-
const code = this.generateCode(ast);
|
|
248
|
-
return new Function('ctx', `
|
|
249
|
-
const escape = (s) => {
|
|
250
|
-
if (s == null) return '';
|
|
251
|
-
return String(s)
|
|
252
|
-
.replace(/&/g, '&')
|
|
253
|
-
.replace(/</g, '<')
|
|
254
|
-
.replace(/>/g, '>')
|
|
255
|
-
.replace(/"/g, '"')
|
|
256
|
-
.replace(/'/g, ''');
|
|
257
|
-
};
|
|
258
|
-
with(ctx || {}) {
|
|
259
|
-
return ${code};
|
|
260
|
-
}
|
|
261
|
-
`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Generates JavaScript code from the AST nodes.
|
|
266
|
-
* @param {Object} node - The AST node to generate code for
|
|
267
|
-
* @returns {string} Generated JavaScript code as a string
|
|
268
|
-
*/
|
|
269
|
-
generateCode(node) {
|
|
270
|
-
switch (node.type) {
|
|
271
|
-
case 'Root':
|
|
272
|
-
return node.children.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
273
|
-
|
|
274
|
-
case 'Element':
|
|
275
|
-
const {tagName, attrs, text, children: nodeChildren, selfClosing} = node;
|
|
276
|
-
let attrStr = '';
|
|
277
|
-
for (const [key, val] of Object.entries(attrs)) {
|
|
278
|
-
if (val === true) {
|
|
279
|
-
attrStr += ` ${key}`;
|
|
280
|
-
} else {
|
|
281
|
-
const expr = this.interpolateToExpr(val, true);
|
|
282
|
-
attrStr += ` ${key}="\${${expr}}}`;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const textExpr = text ? this.interpolateToExpr(text, false) : '';
|
|
287
|
-
const childrenExpr = nodeChildren.map((child) => this.generateCode(child)).join(' + ');
|
|
288
|
-
const content = [textExpr, childrenExpr].filter((s) => s).join(' + ');
|
|
289
|
-
|
|
290
|
-
if (selfClosing) {
|
|
291
|
-
return '`<' + tagName + attrStr + '>`';
|
|
292
|
-
} else {
|
|
293
|
-
if (content) {
|
|
294
|
-
return '`<' + tagName + attrStr + '>\${' + content + '}</' + tagName + '>``';
|
|
295
|
-
} else {
|
|
296
|
-
return '`<' + tagName + attrStr + '></' + tagName + '>``';
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
case 'Each':
|
|
301
|
-
const {iterator, collection, children} = node;
|
|
302
|
-
const body = children.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
303
|
-
return `(Array.isArray(${collection}) ? ${collection}.map(${iterator} => ${body}).join('') : '')`;
|
|
304
|
-
|
|
305
|
-
case 'If':
|
|
306
|
-
const {condition, thenBranch, elseBranch} = node;
|
|
307
|
-
const thenCode = thenBranch.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
308
|
-
const elseCode = elseBranch.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
309
|
-
return `(${condition} ? ${thenCode} : ${elseCode})`;
|
|
310
|
-
|
|
311
|
-
case 'MixinCall':
|
|
312
|
-
const {definition, args} = node;
|
|
313
|
-
const paramMap = {};
|
|
314
|
-
definition.params.forEach((param, idx) => {
|
|
315
|
-
paramMap[param] = args[idx] || '""';
|
|
316
|
-
});
|
|
317
|
-
const substitutedBody = this.substituteParamsInAST(definition.body, paramMap);
|
|
318
|
-
const bodyCode = substitutedBody.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
319
|
-
return bodyCode;
|
|
320
|
-
|
|
321
|
-
default:
|
|
322
|
-
return '""';
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Substitutes parameters in AST nodes with actual values.
|
|
328
|
-
* @param {Array} nodes - Array of AST nodes
|
|
329
|
-
* @param {Object} paramMap - Parameter mapping object
|
|
330
|
-
* @returns {Array} New array of AST nodes with substituted parameters
|
|
331
|
-
*/
|
|
332
|
-
substituteParamsInAST(nodes, paramMap) {
|
|
333
|
-
return nodes.map((node) => {
|
|
334
|
-
const newNode = {...node};
|
|
335
|
-
if (node.type === 'Element') {
|
|
336
|
-
const newAttrs = {};
|
|
337
|
-
for (const [key, val] of Object.entries(node.attrs)) {
|
|
338
|
-
newAttrs[key] = this.substituteInString(val, paramMap);
|
|
339
|
-
}
|
|
340
|
-
newNode.attrs = newAttrs;
|
|
341
|
-
newNode.text = this.substituteInString(node.text, paramMap);
|
|
342
|
-
newNode.children = this.substituteParamsInAST(node.children, paramMap);
|
|
343
|
-
} else if (node.type === 'Each' || node.type === 'If') {
|
|
344
|
-
newNode.children = this.substituteParamsInAST(node.children || node.thenBranch || [], paramMap);
|
|
345
|
-
if (node.elseBranch) {
|
|
346
|
-
newNode.elseBranch = this.substituteParamsInAST(node.elseBranch, paramMap);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return newNode;
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Substitutes parameter placeholders in a string with actual values.
|
|
355
|
-
* @param {string} str - String with parameter placeholders
|
|
356
|
-
* @param {Object} paramMap - Parameter mapping object
|
|
357
|
-
* @returns {string} String with substituted parameters
|
|
358
|
-
*/
|
|
359
|
-
substituteInString(str, paramMap) {
|
|
360
|
-
if (!str) {
|
|
361
|
-
return str;
|
|
362
|
-
}
|
|
363
|
-
let result = str;
|
|
364
|
-
for (const [param, replacement] of Object.entries(paramMap)) {
|
|
365
|
-
const regex = new RegExp(`{{\\s*${param}\\s*}}`, 'g');
|
|
366
|
-
result = result.replace(regex, replacement);
|
|
367
|
-
}
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Converts a string with interpolation expressions to executable JavaScript code.
|
|
373
|
-
* @param {string} str - String with interpolation expressions
|
|
374
|
-
* @param {boolean} inAttr - Whether the string is inside an attribute
|
|
375
|
-
* @returns {string} JavaScript code expression
|
|
376
|
-
*/
|
|
377
|
-
interpolateToExpr(str, inAttr = false) {
|
|
378
|
-
if (!str || !/{{.*?}}/.test(str)) {
|
|
379
|
-
if (inAttr) {
|
|
380
|
-
return JSON.stringify(str);
|
|
381
|
-
} else {
|
|
382
|
-
return `escape(${JSON.stringify(str)})`;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
str = str.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
387
|
-
const parts = [];
|
|
388
|
-
let lastIndex = 0;
|
|
389
|
-
str.replace(/{{\s*([^}]+?)\s*}}/g, (match, expr, offset) => {
|
|
390
|
-
if (offset > lastIndex) {
|
|
391
|
-
const literal = str.slice(lastIndex, offset);
|
|
392
|
-
if (inAttr) {
|
|
393
|
-
parts.push(JSON.stringify(literal));
|
|
394
|
-
} else {
|
|
395
|
-
parts.push(`escape(${JSON.stringify(literal)})`;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
expr = expr.trim();
|
|
399
|
-
if (inAttr) {
|
|
400
|
-
parts.push(expr);
|
|
401
|
-
} else {
|
|
402
|
-
parts.push(`escape(${expr})`);
|
|
403
|
-
}
|
|
404
|
-
lastIndex = offset + match.length;
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
if (lastIndex < str.length) {
|
|
408
|
-
const literal = str.slice(lastIndex);
|
|
409
|
-
if (inAttr) {
|
|
410
|
-
parts.push(JSON.stringify(literal));
|
|
411
|
-
} else {
|
|
412
|
-
parts.push(`escape(${JSON.stringify(literal)})`;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return parts.join(' + ') || '""';
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export default Template;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Router{
|
|
2
|
-
assignUrl
|
|
3
|
-
urlAction
|
|
4
|
-
}
|
|
5
|
-
OCP{
|
|
6
|
-
label: openComponentPlugin
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
RegisterComponent{
|
|
10
|
-
RouteUnit{
|
|
11
|
-
shape: class
|
|
12
|
-
action: string
|
|
13
|
-
componentType: string
|
|
14
|
-
componentId: string
|
|
15
|
-
parameters: object
|
|
16
|
-
shortUrl: string
|
|
17
|
-
id: string
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
OCP -> RegisterComponent
|
|
22
|
-
Menu -> RegisterComponent
|
|
23
|
-
|
|
24
|
-
RegisterComponent -> OpenComponent
|
|
25
|
-
RegisterComponent -> Router.assignUrl
|
|
26
|
-
OpenComponent -> CenterView.setActiveComponent
|
|
27
|
-
Router.urlAction -> RegisterComponent
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
changeUrl->Router.routes./\#r/ReportId{label: загрузить репорт в dev-режиме}
|
|
2
|
-
changeUrl->Router.routes./\#p/CustomPanelId :загрузить кастомную панель в dev-режиме
|
|
3
|
-
changeUrl->Router.routes./\#UiElementCd :начитать uiElementCd для загрузки компонента пункта меню
|
|
4
|
-
|
|
5
|
-
Router: {
|
|
6
|
-
routes: {
|
|
7
|
-
/\#r/ReportId
|
|
8
|
-
/\#p/CustomPanelId
|
|
9
|
-
/\#UiElementCd
|
|
10
|
-
}
|
|
11
|
-
routes./\#r/ReportId -> ReportPanel_create
|
|
12
|
-
routes./\#p/CustomPanelId -> loadUlElement(xtype=UiCustomPanel): async load
|
|
13
|
-
loadUlElement(xtype=UiCustomPanel) -> UiCustomPanel_instancing
|
|
14
|
-
routes./\#UiElementCd -> loadUlElement
|
|
15
|
-
loadUlElement -> 'xtype=ReportPanel'
|
|
16
|
-
loadUlElement -> 'xtype=UiCustomPanel' -> loadUlElement(xtype=UiCustomPanel)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
Router.loadUlElement.'xtype=ReportPanel' -> ComponentFactory.createRootNavComponent
|
|
20
|
-
|
|
21
|
-
ComponentFactory: {
|
|
22
|
-
createRootNavComponent
|
|
23
|
-
}
|