onelaraveljs 1.0.0
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/README.md +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- package/src/utils/validation.js +32 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { __defineProp } from "../../helpers/utils.js";
|
|
2
|
+
|
|
3
|
+
class StoreDep{
|
|
4
|
+
constructor(servive) {
|
|
5
|
+
/**
|
|
6
|
+
* @type {StoreService}
|
|
7
|
+
*/
|
|
8
|
+
this.service = servive;
|
|
9
|
+
this.ownProps = ['__dep__', 'set', 'get', 'has', 'subscribe', 'unasubscribe'];
|
|
10
|
+
this.existsKeys = new Set();
|
|
11
|
+
this.data = {};
|
|
12
|
+
this.changedKeys = new Set();
|
|
13
|
+
this.listeners = new Map();
|
|
14
|
+
this.multiKeyListeners = []; // Array of {keys: Set, callback: Function, called: Boolean}
|
|
15
|
+
this.hasPendingFlush = false;
|
|
16
|
+
}
|
|
17
|
+
set(key, value) {
|
|
18
|
+
if(this.ownProps.includes(key)){
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.data[key] = value;
|
|
22
|
+
this.emitsChange(key, value);
|
|
23
|
+
if(!this.existsKeys.has(key)){
|
|
24
|
+
this.existsKeys.add(key);
|
|
25
|
+
__defineProp(this.service, key, {
|
|
26
|
+
get: () => {
|
|
27
|
+
return this.get(key);
|
|
28
|
+
},
|
|
29
|
+
set: (newValue) => {
|
|
30
|
+
this.set(key, newValue);
|
|
31
|
+
},
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true
|
|
34
|
+
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
get(key) {
|
|
39
|
+
return typeof this.data[key] !== 'undefined' ? this.data[key] : null;
|
|
40
|
+
}
|
|
41
|
+
has(key) {
|
|
42
|
+
return this.existsKeys.has(key);
|
|
43
|
+
}
|
|
44
|
+
emitsChange(key, value) {
|
|
45
|
+
if(!this.changedKeys.has(key)){
|
|
46
|
+
this.changedKeys.add(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Chỉ schedule 1 flush duy nhất cho batch changes
|
|
50
|
+
if(!this.hasPendingFlush){
|
|
51
|
+
this.hasPendingFlush = true;
|
|
52
|
+
Promise.resolve().then(() => {
|
|
53
|
+
this.flushChanges();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
flushChanges() {
|
|
59
|
+
// Reset multi-key listeners called flag
|
|
60
|
+
this.multiKeyListeners.forEach(listener => {
|
|
61
|
+
listener.called = false;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Gọi single-key listeners
|
|
65
|
+
this.changedKeys.forEach(changedKey => {
|
|
66
|
+
if(this.listeners.has(changedKey)){
|
|
67
|
+
const listeners = this.listeners.get(changedKey);
|
|
68
|
+
listeners.forEach(callback => {
|
|
69
|
+
callback(this.get(changedKey));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check multi-key listeners
|
|
74
|
+
this.multiKeyListeners.forEach(listener => {
|
|
75
|
+
if(!listener.called && listener.keys.has(changedKey)){
|
|
76
|
+
listener.called = true;
|
|
77
|
+
// Collect all changed values for subscribed keys
|
|
78
|
+
const values = {};
|
|
79
|
+
listener.keys.forEach(k => {
|
|
80
|
+
if(this.changedKeys.has(k)){
|
|
81
|
+
values[k] = this.get(k);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
listener.callback(values);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.changedKeys.clear();
|
|
90
|
+
this.hasPendingFlush = false;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Đăng ký lắng nghe thay đổi của khoá lưu trữ
|
|
94
|
+
*
|
|
95
|
+
* @param {string|array} key - Khoá lưu trữ hoặc array của các khoá
|
|
96
|
+
* @param {function} callback - Hàm callback khi khoá thay đổi
|
|
97
|
+
* @return {function} Hàm hủy đăng ký lắng nghe
|
|
98
|
+
*/
|
|
99
|
+
subscribe(key, callback) {
|
|
100
|
+
// Hỗ trợ subscribe với array keys: subscribe(['key1', 'key2'], callback)
|
|
101
|
+
// Callback sẽ được gọi 1 lần duy nhất khi bất kỳ key nào thay đổi
|
|
102
|
+
if(Array.isArray(key)){
|
|
103
|
+
const keys = new Set(key.filter(k => this.existsKeys.has(k)));
|
|
104
|
+
if(keys.size === 0){
|
|
105
|
+
return () => {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const listener = { keys, callback, called: false };
|
|
109
|
+
this.multiKeyListeners.push(listener);
|
|
110
|
+
|
|
111
|
+
// Return unsubscribe function
|
|
112
|
+
return () => {
|
|
113
|
+
const index = this.multiKeyListeners.indexOf(listener);
|
|
114
|
+
if(index !== -1){
|
|
115
|
+
this.multiKeyListeners.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if(typeof key === "object" && key !== null){
|
|
121
|
+
|
|
122
|
+
const unsubscribes = {};
|
|
123
|
+
for(const k of Object.keys(key)){
|
|
124
|
+
unsubscribes[k] = this.subscribe(k, key[k]);
|
|
125
|
+
}
|
|
126
|
+
return () => {
|
|
127
|
+
for(const k of Object.keys(unsubscribes)){
|
|
128
|
+
unsubscribes[k]();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if(typeof key !== 'string' || !this.existsKeys.has(key) || typeof callback !== 'function'){
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if(!this.listeners.has(key)){
|
|
136
|
+
this.listeners.set(key, []);
|
|
137
|
+
}
|
|
138
|
+
const listeners = this.listeners.get(key);
|
|
139
|
+
listeners.push(callback);
|
|
140
|
+
|
|
141
|
+
return () => this.unasubscribe(key, callback);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hủy đăng ký lắng nghe thay đổi của khoá lưu trữ
|
|
146
|
+
*
|
|
147
|
+
* @param {string|array} key - Khoá lưu trữ hoặc array của các khoá
|
|
148
|
+
* @param {function|null} callback - Hàm callback đã đăng ký (nếu null sẽ hủy tất cả callback của khoá)
|
|
149
|
+
*/
|
|
150
|
+
unasubscribe(key, callback = null) {
|
|
151
|
+
// Hỗ trợ unsubscribe với array keys (không phân biệt thứ tự)
|
|
152
|
+
if(Array.isArray(key)){
|
|
153
|
+
const keySet = new Set(key);
|
|
154
|
+
|
|
155
|
+
// Helper function để check 2 Sets có giống nhau không
|
|
156
|
+
const areSetsEqual = (set1, set2) => {
|
|
157
|
+
if(set1.size !== set2.size) return false;
|
|
158
|
+
for(const k of set1){
|
|
159
|
+
if(!set2.has(k)) return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if(!callback){
|
|
165
|
+
// Xóa TẤT CẢ multi-key listeners có cùng set keys
|
|
166
|
+
for(let i = this.multiKeyListeners.length - 1; i >= 0; i--){
|
|
167
|
+
if(areSetsEqual(this.multiKeyListeners[i].keys, keySet)){
|
|
168
|
+
this.multiKeyListeners.splice(i, 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Xóa listener cụ thể với callback
|
|
175
|
+
const index = this.multiKeyListeners.findIndex(listener => {
|
|
176
|
+
return listener.callback === callback && areSetsEqual(listener.keys, keySet);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if(index !== -1){
|
|
180
|
+
this.multiKeyListeners.splice(index, 1);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if(typeof key === "object" && key !== null){
|
|
186
|
+
for(const k of Object.keys(key)){
|
|
187
|
+
this.unasubscribe(k, key[k]);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if(typeof key !== 'string' || !this.existsKeys.has(key)){
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if(callback && typeof callback !== 'function'){
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if(callback){
|
|
198
|
+
const listeners = this.listeners.get(key);
|
|
199
|
+
const index = listeners.indexOf(callback);
|
|
200
|
+
if(index !== -1){
|
|
201
|
+
listeners.splice(index, 1);
|
|
202
|
+
}
|
|
203
|
+
if(listeners.length === 0){
|
|
204
|
+
this.listeners.delete(key);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
this.listeners.delete(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class StoreService {
|
|
213
|
+
static intances = new Map();
|
|
214
|
+
static getInstance(name = null) {
|
|
215
|
+
if (!name || name === '' || name === 'default' || name === undefined) {
|
|
216
|
+
name = 'default';
|
|
217
|
+
}
|
|
218
|
+
if (!StoreService.intances.has(name)) {
|
|
219
|
+
StoreService.intances.set(name, new StoreService());
|
|
220
|
+
}
|
|
221
|
+
return StoreService.intances.get(name);
|
|
222
|
+
}
|
|
223
|
+
constructor() {
|
|
224
|
+
/**
|
|
225
|
+
* @type {Map<string, StoreDep>}
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
const dep = new StoreDep(this);
|
|
229
|
+
Object.defineProperty(this, '__dep__', {
|
|
230
|
+
value: dep,
|
|
231
|
+
writable: false,
|
|
232
|
+
enumerable: false,
|
|
233
|
+
configurable: false
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Lấy hoặc tạo một kho lưu trữ theo tên
|
|
238
|
+
* @param {string} name - Tên kho lưu trữ
|
|
239
|
+
* @returns {StoreDep}
|
|
240
|
+
*/
|
|
241
|
+
set(name, value = null) {
|
|
242
|
+
return this.__dep__.set(name, value);
|
|
243
|
+
}
|
|
244
|
+
get(name) {
|
|
245
|
+
return this.__dep__.get(name);
|
|
246
|
+
}
|
|
247
|
+
has(name) {
|
|
248
|
+
return this.__dep__.has(name);
|
|
249
|
+
}
|
|
250
|
+
subscribe(name, callback) {
|
|
251
|
+
return this.__dep__.subscribe(name, callback);
|
|
252
|
+
}
|
|
253
|
+
unsubscribe(name, callback) {
|
|
254
|
+
return this.__dep__.unasubscribe(name, callback);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default StoreService.getInstance();
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class quản lý việc parse và tìm kiếm các cặp comment tags
|
|
3
|
+
* Hỗ trợ tìm kiếm theo pattern với wildcard (*)
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const parser = new TemplateDetectorService(document.body);
|
|
7
|
+
* const pairs = parser.find('one-template:*');
|
|
8
|
+
* parser.display(pairs);
|
|
9
|
+
*/
|
|
10
|
+
export class TemplateDetectorService {
|
|
11
|
+
/**
|
|
12
|
+
* Khởi tạo parser với element gốc
|
|
13
|
+
* @param {Element} rootElement - Element gốc để tìm kiếm (mặc định: document.body)
|
|
14
|
+
*/
|
|
15
|
+
constructor(rootElement = document.body) {
|
|
16
|
+
this.rootElement = rootElement || document.body;
|
|
17
|
+
this.cachedComments = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Đặt element gốc mới
|
|
22
|
+
* @param {Element} element - Element gốc mới
|
|
23
|
+
*/
|
|
24
|
+
setRootElement(element) {
|
|
25
|
+
this.rootElement = element;
|
|
26
|
+
this.cachedComments = null;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lấy tất cả comment nodes từ rootElement
|
|
32
|
+
* @param {boolean} useCache - Sử dụng cache hay không
|
|
33
|
+
* @returns {Array<Comment>}
|
|
34
|
+
*/
|
|
35
|
+
getAllComments(useCache = true) {
|
|
36
|
+
if (useCache && this.cachedComments) {
|
|
37
|
+
return this.cachedComments;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const comments = [];
|
|
41
|
+
const walker = document.createTreeWalker(
|
|
42
|
+
this.rootElement,
|
|
43
|
+
NodeFilter.SHOW_COMMENT,
|
|
44
|
+
null,
|
|
45
|
+
false
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
let node;
|
|
49
|
+
while (node = walker.nextNode()) {
|
|
50
|
+
comments.push(node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (useCache) {
|
|
54
|
+
this.cachedComments = comments;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return comments;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse một comment node để lấy thông tin
|
|
62
|
+
* @param {Comment} commentNode - Comment node cần parse
|
|
63
|
+
* @returns {Object|null} - Thông tin tag hoặc null nếu không phải tag hợp lệ
|
|
64
|
+
*/
|
|
65
|
+
parseComment(commentNode) {
|
|
66
|
+
const text = commentNode.nodeValue.trim();
|
|
67
|
+
|
|
68
|
+
// Pattern tổng quát: [prefix:name] hoặc [prefix]
|
|
69
|
+
// Kiểm tra tag mở: <!-- [prefix:name attribute="..."] -->
|
|
70
|
+
const openMatch = text.match(/^\[([^\/\]]+?)(?:\s+(.+))?\]$/);
|
|
71
|
+
if (openMatch) {
|
|
72
|
+
const fullName = openMatch[1];
|
|
73
|
+
const attributes = {};
|
|
74
|
+
|
|
75
|
+
if (openMatch[2]) {
|
|
76
|
+
// Parse các attributes như subscribe="userState,items"
|
|
77
|
+
const attrMatch = openMatch[2].match(/(\w+)="([^"]+)"/g);
|
|
78
|
+
if (attrMatch) {
|
|
79
|
+
attrMatch.forEach(attr => {
|
|
80
|
+
const [key, value] = attr.split('=');
|
|
81
|
+
attributes[key] = value.replace(/"/g, '');
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
type: 'open',
|
|
88
|
+
fullName: fullName,
|
|
89
|
+
attributes: attributes,
|
|
90
|
+
node: commentNode
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Kiểm tra tag đóng: <!-- [/prefix:name] -->
|
|
95
|
+
const closeMatch = text.match(/^\[\/([^\/\]]+?)\]$/);
|
|
96
|
+
if (closeMatch) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'close',
|
|
99
|
+
fullName: closeMatch[1],
|
|
100
|
+
node: commentNode
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lấy tất cả các siblings giữa hai node (cùng parent)
|
|
109
|
+
* @param {Node} startNode - Node bắt đầu
|
|
110
|
+
* @param {Node} endNode - Node kết thúc
|
|
111
|
+
* @returns {Array<Node>|null} - Mảng nodes hoặc null nếu không cùng parent
|
|
112
|
+
*/
|
|
113
|
+
getNodesBetween(startNode, endNode) {
|
|
114
|
+
// Kiểm tra xem có cùng parent không
|
|
115
|
+
if (startNode.parentNode !== endNode.parentNode) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const nodes = [];
|
|
120
|
+
let current = startNode.nextSibling;
|
|
121
|
+
|
|
122
|
+
while (current && current !== endNode) {
|
|
123
|
+
nodes.push(current);
|
|
124
|
+
current = current.nextSibling;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return nodes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Chuyển đổi pattern thành regex
|
|
132
|
+
* @param {string} pattern - Pattern với wildcard (*)
|
|
133
|
+
* @returns {RegExp}
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* patternToRegex('*:*') // Match tất cả
|
|
137
|
+
* patternToRegex('one-*') // Match "one-template", "one-component"
|
|
138
|
+
* patternToRegex('one-template:*') // Match "one-template:profile", etc.
|
|
139
|
+
*/
|
|
140
|
+
patternToRegex(pattern) {
|
|
141
|
+
if (!pattern || pattern === '*' || pattern === '*:*') {
|
|
142
|
+
return /.*/;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Escape các ký tự đặc biệt trong regex (trừ *)
|
|
146
|
+
let regexStr = pattern
|
|
147
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
148
|
+
.replace(/\*/g, '.*'); // Convert * thành .*
|
|
149
|
+
|
|
150
|
+
// Thêm ^ và $ để match chính xác toàn bộ string
|
|
151
|
+
regexStr = '^' + regexStr + '$';
|
|
152
|
+
|
|
153
|
+
return new RegExp(regexStr);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Kiểm tra xem một tên có khớp với pattern không
|
|
158
|
+
* @param {string} fullName - Tên đầy đủ của tag
|
|
159
|
+
* @param {string} pattern - Pattern để so khớp
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
matchPattern(fullName, pattern) {
|
|
163
|
+
const regex = this.patternToRegex(pattern);
|
|
164
|
+
return regex.test(fullName);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tìm tất cả các cặp tags theo pattern
|
|
169
|
+
* @param {string} pattern - Pattern để lọc (mặc định: '*:*')
|
|
170
|
+
* @param {Object} options - Tùy chọn bổ sung
|
|
171
|
+
* @param {boolean} options.useCache - Sử dụng cache comments
|
|
172
|
+
* @returns {Array<Object>} - Mảng các cặp tags tìm được
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* parser.find('*:*') // Tìm tất cả
|
|
176
|
+
* parser.find('one-template:*') // Chỉ tìm one-template
|
|
177
|
+
* parser.find('one-template:profile') // Tìm chính xác profile
|
|
178
|
+
*/
|
|
179
|
+
find(pattern = '*:*', options = {}, total = null) {
|
|
180
|
+
const { useCache = true } = options;
|
|
181
|
+
const isTotal = total && typeof total === 'number' && total > 0;
|
|
182
|
+
const isLast = total && total === -1;
|
|
183
|
+
const isFirst = total && total === 1;
|
|
184
|
+
const comments = this.getAllComments(useCache);
|
|
185
|
+
const parsed = comments.map(c => this.parseComment(c)).filter(p => p !== null);
|
|
186
|
+
// console.error({ parsed });
|
|
187
|
+
const pairs = [];
|
|
188
|
+
const stack = [];
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
191
|
+
const current = parsed[i];
|
|
192
|
+
|
|
193
|
+
// Kiểm tra xem có khớp với pattern không
|
|
194
|
+
if (!this.matchPattern(current.fullName, pattern)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (current.type === 'open') {
|
|
199
|
+
// Đẩy tag mở vào stack
|
|
200
|
+
stack.push({
|
|
201
|
+
...current,
|
|
202
|
+
index: i
|
|
203
|
+
});
|
|
204
|
+
} else if (current.type === 'close') {
|
|
205
|
+
// Tìm tag mở tương ứng từ stack
|
|
206
|
+
let foundIndex = -1;
|
|
207
|
+
for (let j = stack.length - 1; j >= 0; j--) {
|
|
208
|
+
if (stack[j].fullName === current.fullName) {
|
|
209
|
+
foundIndex = j;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (foundIndex !== -1) {
|
|
215
|
+
const openTag = stack[foundIndex];
|
|
216
|
+
|
|
217
|
+
// Lấy các nodes nằm giữa
|
|
218
|
+
const nodesBetween = this.getNodesBetween(openTag.node, current.node);
|
|
219
|
+
|
|
220
|
+
// Chỉ thêm vào nếu cùng parent
|
|
221
|
+
if (nodesBetween !== null) {
|
|
222
|
+
pairs.push({
|
|
223
|
+
fullName: current.fullName,
|
|
224
|
+
openTag: openTag.node,
|
|
225
|
+
closeTag: current.node,
|
|
226
|
+
attributes: openTag.attributes,
|
|
227
|
+
nodes: nodesBetween,
|
|
228
|
+
parent: openTag.node.parentNode
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Xóa khỏi stack
|
|
233
|
+
stack.splice(foundIndex, 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return pairs;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Tìm một cặp tag duy nhất theo pattern
|
|
243
|
+
* @param {string} pattern - Pattern để tìm
|
|
244
|
+
* @returns {Object|null} - Cặp tag tìm được hoặc null
|
|
245
|
+
*/
|
|
246
|
+
findOne(pattern) {
|
|
247
|
+
const pairs = this.find(pattern);
|
|
248
|
+
return pairs.length > 0 ? pairs[0] : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Lọc các cặp tags theo điều kiện tùy chỉnh
|
|
253
|
+
* @param {Function} filterFn - Hàm filter (pair) => boolean
|
|
254
|
+
* @param {string} pattern - Pattern ban đầu
|
|
255
|
+
* @returns {Array<Object>}
|
|
256
|
+
*/
|
|
257
|
+
filter(filterFn, pattern = '*:*') {
|
|
258
|
+
const pairs = this.find(pattern);
|
|
259
|
+
return pairs.filter(filterFn);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Đếm số lượng cặp tags theo pattern
|
|
264
|
+
* @param {string} pattern - Pattern để đếm
|
|
265
|
+
* @returns {number}
|
|
266
|
+
*/
|
|
267
|
+
count(pattern = '*:*') {
|
|
268
|
+
return this.find(pattern).length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Lấy danh sách tất cả các tên tags duy nhất
|
|
273
|
+
* @param {string} pattern - Pattern để lọc
|
|
274
|
+
* @returns {Array<string>}
|
|
275
|
+
*/
|
|
276
|
+
getUniqueNames(pattern = '*:*') {
|
|
277
|
+
const pairs = this.find(pattern);
|
|
278
|
+
return [...new Set(pairs.map(p => p.fullName))];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Hiển thị kết quả tìm kiếm trong console
|
|
283
|
+
* @param {Array<Object>|string} pairsOrPattern - Mảng pairs hoặc pattern string
|
|
284
|
+
* @param {Object} options - Tùy chọn hiển thị
|
|
285
|
+
*/
|
|
286
|
+
display(pairsOrPattern, options = {}) {
|
|
287
|
+
const {
|
|
288
|
+
showAttributes = true,
|
|
289
|
+
showParent = true,
|
|
290
|
+
showNodes = true,
|
|
291
|
+
maxNodePreview = 3
|
|
292
|
+
} = options;
|
|
293
|
+
|
|
294
|
+
let pairs, patternInfo = '';
|
|
295
|
+
|
|
296
|
+
// Nếu truyền vào là string, tìm kiếm trước
|
|
297
|
+
if (typeof pairsOrPattern === 'string') {
|
|
298
|
+
patternInfo = ` với pattern "${pairsOrPattern}"`;
|
|
299
|
+
pairs = this.find(pairsOrPattern);
|
|
300
|
+
} else {
|
|
301
|
+
pairs = pairsOrPattern;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
305
|
+
console.log(`Tìm thấy ${pairs.length} cặp tags${patternInfo}`);
|
|
306
|
+
console.log(`${'='.repeat(80)}\n`);
|
|
307
|
+
|
|
308
|
+
if (pairs.length === 0) {
|
|
309
|
+
console.log('❌ Không tìm thấy cặp tags nào!');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
pairs.forEach((pair, index) => {
|
|
314
|
+
console.log(`\n📦 Cặp ${index + 1}: [${pair.fullName}]`);
|
|
315
|
+
console.log('-'.repeat(60));
|
|
316
|
+
|
|
317
|
+
if (showAttributes) {
|
|
318
|
+
console.log('📋 Attributes:',
|
|
319
|
+
Object.keys(pair.attributes).length > 0
|
|
320
|
+
? pair.attributes
|
|
321
|
+
: '(không có)'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (showParent) {
|
|
326
|
+
console.log('🔼 Parent:', pair.parent.tagName.toLowerCase(),
|
|
327
|
+
pair.parent.className ? `class="${pair.parent.className}"` : ''
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (showNodes) {
|
|
332
|
+
console.log(`📄 Nội dung (${pair.nodes.length} nodes):`);
|
|
333
|
+
|
|
334
|
+
const previewNodes = pair.nodes.slice(0, maxNodePreview);
|
|
335
|
+
previewNodes.forEach(node => {
|
|
336
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
337
|
+
const className = node.className ? ` class="${node.className}"` : '';
|
|
338
|
+
console.log(` ├─ <${node.tagName.toLowerCase()}${className}>`);
|
|
339
|
+
} else if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim()) {
|
|
340
|
+
const text = node.nodeValue.trim().substring(0, 50);
|
|
341
|
+
console.log(` ├─ Text: "${text}${text.length >= 50 ? '...' : ''}"`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (pair.nodes.length > maxNodePreview) {
|
|
346
|
+
console.log(` └─ ... và ${pair.nodes.length - maxNodePreview} nodes khác`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
console.log(`\n${'='.repeat(80)}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Xóa cache để buộc parse lại
|
|
356
|
+
*/
|
|
357
|
+
clearCache() {
|
|
358
|
+
this.cachedComments = null;
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test service để demo alias @app
|
|
3
|
+
*/
|
|
4
|
+
export class TestService {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'TestService';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getMessage() {
|
|
10
|
+
return `Hello from ${this.name}!`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static create() {
|
|
14
|
+
return new TestService();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default TestService;
|