vanduo-framework 1.1.8 → 1.2.1

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.
@@ -377,8 +377,10 @@
377
377
  // Apply syntax highlighting
378
378
  html = this.highlightHtml(html);
379
379
 
380
- // Set content
381
- pane.innerHTML = '<code>' + html + '</code>';
380
+ // Set content via DOM API to avoid string-based HTML insertion
381
+ const codeEl = document.createElement('code');
382
+ codeEl.innerHTML = html;
383
+ pane.replaceChildren(codeEl);
382
384
  pane.dataset.extracted = 'true';
383
385
  },
384
386
 
@@ -537,7 +539,7 @@
537
539
  // Wrap code content
538
540
  const codeWrapper = document.createElement('div');
539
541
  codeWrapper.className = 'vd-code-snippet-code';
540
- codeWrapper.innerHTML = code.outerHTML;
542
+ codeWrapper.appendChild(code.cloneNode(true));
541
543
 
542
544
  // Replace code with new structure
543
545
  code.parentNode.removeChild(code);
@@ -36,7 +36,7 @@
36
36
  /**
37
37
  * Default configuration
38
38
  */
39
- var DEFAULTS = {
39
+ const DEFAULTS = {
40
40
  // Behavior
41
41
  minQueryLength: 2,
42
42
  maxResults: 10,
@@ -92,10 +92,10 @@
92
92
  * @returns {Object} Search instance
93
93
  */
94
94
  function createSearch(options) {
95
- var config = Object.assign({}, DEFAULTS, options || {});
95
+ const config = Object.assign({}, DEFAULTS, options || {});
96
96
 
97
97
  // Instance state
98
- var state = {
98
+ const state = {
99
99
  initialized: false,
100
100
  index: [],
101
101
  results: [],
@@ -109,6 +109,23 @@
109
109
  boundHandlers: {}
110
110
  };
111
111
 
112
+ function safeInvokeCallback(name, fn, ...args) {
113
+ try {
114
+ fn(...args);
115
+ } catch (error) {
116
+ console.warn('[Vanduo Search] Callback error in "' + name + '":', error);
117
+ }
118
+ }
119
+
120
+ function setResultsHtml(html) {
121
+ if (!state.resultsContainer) return;
122
+ try {
123
+ state.resultsContainer.innerHTML = html;
124
+ } catch (error) {
125
+ console.warn('[Vanduo Search] Failed to render results:', error);
126
+ }
127
+ }
128
+
112
129
  /**
113
130
  * Initialize the search component
114
131
  * Idempotent — safe to call more than once on the same instance.
@@ -175,22 +192,22 @@
175
192
  }
176
193
 
177
194
  // Build from DOM
178
- var sections = document.querySelectorAll(config.contentSelector);
179
- var categoryMap = buildCategoryMap();
195
+ const sections = document.querySelectorAll(config.contentSelector);
196
+ const categoryMap = buildCategoryMap();
180
197
 
181
198
  sections.forEach(function(section) {
182
- var id = section.id;
199
+ const id = section.id;
183
200
  if (!id) return;
184
201
 
185
- var titleEl = section.querySelector(config.titleSelector);
186
- var title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, '').trim() : id;
187
- var category = categoryMap[id] || 'Documentation';
188
- var content = extractContent(section);
189
- var keywords = extractKeywords(section, title);
190
- var iconEl = titleEl ? titleEl.querySelector('i.ph') : null;
191
- var icon = '';
202
+ const titleEl = section.querySelector(config.titleSelector);
203
+ const title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, '').trim() : id;
204
+ const category = categoryMap[id] || 'Documentation';
205
+ const content = extractContent(section);
206
+ const keywords = extractKeywords(section, title);
207
+ const iconEl = titleEl ? titleEl.querySelector('i.ph') : null;
208
+ let icon = '';
192
209
  if (iconEl && iconEl.classList) {
193
- for (var ci = 0; ci < iconEl.classList.length; ci++) {
210
+ for (let ci = 0; ci < iconEl.classList.length; ci++) {
194
211
  if (iconEl.classList[ci].indexOf('ph-') === 0) {
195
212
  icon = iconEl.classList[ci];
196
213
  break;
@@ -215,17 +232,17 @@
215
232
  * Build a map of section IDs to their categories
216
233
  */
217
234
  function buildCategoryMap() {
218
- var map = {};
219
- var currentCategory = 'Documentation';
220
- var navItems = document.querySelectorAll(config.navSelector + ', ' + config.sectionSelector);
235
+ const map = {};
236
+ let currentCategory = 'Documentation';
237
+ const navItems = document.querySelectorAll(config.navSelector + ', ' + config.sectionSelector);
221
238
 
222
239
  navItems.forEach(function(item) {
223
240
  if (item.classList.contains('doc-nav-section')) {
224
241
  currentCategory = item.textContent.trim();
225
242
  } else {
226
- var href = item.getAttribute('href');
243
+ const href = item.getAttribute('href');
227
244
  if (href && href.startsWith('#')) {
228
- var id = href.substring(1);
245
+ const id = href.substring(1);
229
246
  map[id] = currentCategory;
230
247
  }
231
248
  }
@@ -238,14 +255,14 @@
238
255
  * Extract searchable content from a section
239
256
  */
240
257
  function extractContent(section) {
241
- var clone = section.cloneNode(true);
258
+ const clone = section.cloneNode(true);
242
259
 
243
- var toRemove = clone.querySelectorAll(config.excludeFromContent);
260
+ const toRemove = clone.querySelectorAll(config.excludeFromContent);
244
261
  toRemove.forEach(function(el) {
245
262
  el.remove();
246
263
  });
247
264
 
248
- var text = clone.textContent || '';
265
+ let text = clone.textContent || '';
249
266
  text = text.replace(/\s+/g, ' ').trim();
250
267
 
251
268
  return text.substring(0, config.maxContentLength);
@@ -255,7 +272,7 @@
255
272
  * Extract keywords from a section
256
273
  */
257
274
  function extractKeywords(section, title) {
258
- var keywords = [];
275
+ const keywords = [];
259
276
 
260
277
  // Add title words
261
278
  title.toLowerCase().split(/\s+/).forEach(function(word) {
@@ -265,10 +282,10 @@
265
282
  });
266
283
 
267
284
  // Add class names from code examples
268
- var codeBlocks = section.querySelectorAll('code');
285
+ const codeBlocks = section.querySelectorAll('code');
269
286
  codeBlocks.forEach(function(code) {
270
- var text = code.textContent || '';
271
- var classMatches = text.match(/\.([\w-]+)/g);
287
+ const text = code.textContent || '';
288
+ const classMatches = text.match(/\.([\w-]+)/g);
272
289
  if (classMatches) {
273
290
  classMatches.forEach(function(match) {
274
291
  keywords.push(match.substring(1).toLowerCase());
@@ -277,9 +294,9 @@
277
294
  });
278
295
 
279
296
  // Add data attributes
280
- var dataAttrs = section.querySelectorAll('[data-tooltip], [data-modal]');
297
+ const dataAttrs = section.querySelectorAll('[data-tooltip], [data-modal]');
281
298
  dataAttrs.forEach(function(el) {
282
- var attrs = el.getAttributeNames().filter(function(name) {
299
+ const attrs = el.getAttributeNames().filter(function(name) {
283
300
  return name.startsWith('data-');
284
301
  });
285
302
  attrs.forEach(function(attr) {
@@ -294,7 +311,7 @@
294
311
  * Extract keywords from text string
295
312
  */
296
313
  function extractKeywordsFromText(text) {
297
- var words = text.toLowerCase().split(/\s+/);
314
+ const words = text.toLowerCase().split(/\s+/);
298
315
  return words.filter(function(word) {
299
316
  return word.length > 2;
300
317
  });
@@ -336,9 +353,9 @@
336
353
  }
337
354
  };
338
355
  state.boundHandlers.handleResultClick = function(e) {
339
- var result = e.target.closest('.vd-doc-search-result');
356
+ const result = e.target.closest('.vd-doc-search-result');
340
357
  if (result) {
341
- var index = parseInt(result.dataset.index, 10);
358
+ const index = parseInt(result.dataset.index, 10);
342
359
  select(index);
343
360
  }
344
361
  };
@@ -378,7 +395,7 @@
378
395
  * Set up ARIA attributes
379
396
  */
380
397
  function setupAria() {
381
- var resultsId = state.resultsContainer.id || 'search-results-' + Math.random().toString(36).substr(2, 9);
398
+ const resultsId = state.resultsContainer.id || 'search-results-' + Math.random().toString(36).substr(2, 9);
382
399
  state.resultsContainer.id = resultsId;
383
400
 
384
401
  state.input.setAttribute('role', 'combobox');
@@ -394,7 +411,7 @@
394
411
  * Handle input changes
395
412
  */
396
413
  function handleInput(e) {
397
- var query = e.target.value.trim();
414
+ const query = e.target.value.trim();
398
415
 
399
416
  if (state.debounceTimer) {
400
417
  clearTimeout(state.debounceTimer);
@@ -415,7 +432,7 @@
415
432
 
416
433
  // Callback
417
434
  if (typeof config.onSearch === 'function') {
418
- config.onSearch(query, state.results);
435
+ safeInvokeCallback('onSearch', config.onSearch, query, state.results);
419
436
  }
420
437
  }, config.debounceMs);
421
438
  }
@@ -469,16 +486,16 @@
469
486
  * Perform search
470
487
  */
471
488
  function search(query) {
472
- var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
489
+ const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
473
490
  return t.length > 0;
474
491
  });
475
- var scored = [];
492
+ const scored = [];
476
493
 
477
494
  state.index.forEach(function(entry) {
478
- var score = 0;
479
- var titleLower = entry.title.toLowerCase();
480
- var categoryLower = entry.category.toLowerCase();
481
- var contentLower = entry.content.toLowerCase();
495
+ let score = 0;
496
+ const titleLower = entry.title.toLowerCase();
497
+ const categoryLower = entry.category.toLowerCase();
498
+ const contentLower = entry.content.toLowerCase();
482
499
 
483
500
  terms.forEach(function(term) {
484
501
  // Title match - highest priority
@@ -497,7 +514,7 @@
497
514
  }
498
515
 
499
516
  // Keyword match
500
- var keywordMatch = entry.keywords.some(function(k) {
517
+ const keywordMatch = entry.keywords.some(function(k) {
501
518
  return k.includes(term);
502
519
  });
503
520
  if (keywordMatch) {
@@ -536,16 +553,16 @@
536
553
  */
537
554
  function render() {
538
555
  if (state.results.length === 0) {
539
- state.resultsContainer.innerHTML = renderEmpty();
556
+ setResultsHtml(renderEmpty());
540
557
  return;
541
558
  }
542
559
 
543
- var html = '<ul class="vd-doc-search-results-list" role="listbox">';
560
+ let html = '<ul class="vd-doc-search-results-list" role="listbox">';
544
561
 
545
562
  state.results.forEach(function(result, index) {
546
- var isActive = index === state.activeIndex;
547
- var icon = result.icon || getCategoryIcon(result.categorySlug);
548
- var excerpt = getExcerpt(result.content, state.query);
563
+ const isActive = index === state.activeIndex;
564
+ const icon = result.icon || getCategoryIcon(result.categorySlug);
565
+ const excerpt = getExcerpt(result.content, state.query);
549
566
 
550
567
  html += '<li class="vd-doc-search-result' + (isActive ? ' is-active' : '') + '"' +
551
568
  ' role="option"' +
@@ -568,7 +585,7 @@
568
585
  html += '</ul>';
569
586
  html += renderFooter();
570
587
 
571
- state.resultsContainer.innerHTML = html;
588
+ setResultsHtml(html);
572
589
  }
573
590
 
574
591
  /**
@@ -604,13 +621,13 @@
604
621
  * Get excerpt from content
605
622
  */
606
623
  function getExcerpt(content, query) {
607
- var terms = query.toLowerCase().split(/\s+/);
608
- var contentLower = content.toLowerCase();
609
- var excerptLength = 100;
624
+ const terms = query.toLowerCase().split(/\s+/);
625
+ const contentLower = content.toLowerCase();
626
+ const excerptLength = 100;
610
627
 
611
- var matchPos = -1;
612
- for (var i = 0; i < terms.length; i++) {
613
- var pos = contentLower.indexOf(terms[i]);
628
+ let matchPos = -1;
629
+ for (let i = 0; i < terms.length; i++) {
630
+ const pos = contentLower.indexOf(terms[i]);
614
631
  if (pos !== -1 && (matchPos === -1 || pos < matchPos)) {
615
632
  matchPos = pos;
616
633
  }
@@ -620,9 +637,9 @@
620
637
  return content.substring(0, excerptLength) + '...';
621
638
  }
622
639
 
623
- var start = Math.max(0, matchPos - 30);
624
- var end = Math.min(content.length, matchPos + excerptLength);
625
- var excerpt = content.substring(start, end);
640
+ const start = Math.max(0, matchPos - 30);
641
+ const end = Math.min(content.length, matchPos + excerptLength);
642
+ let excerpt = content.substring(start, end);
626
643
 
627
644
  if (start > 0) {
628
645
  excerpt = '...' + excerpt;
@@ -640,15 +657,15 @@
640
657
  function highlight(text, query) {
641
658
  if (!query) return escapeHtml(text);
642
659
 
643
- var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
660
+ const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
644
661
  return t.length > 0;
645
662
  });
646
- var escaped = escapeHtml(text);
663
+ let escaped = escapeHtml(text);
647
664
 
648
665
  terms.forEach(function(term) {
649
666
  // Skip overly long terms to prevent ReDoS
650
667
  if (term.length > 50) return;
651
- var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
668
+ const regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
652
669
  escaped = escaped.replace(regex, '<' + config.highlightTag + '>$1</' + config.highlightTag + '>');
653
670
  });
654
671
 
@@ -659,7 +676,7 @@
659
676
  * Escape HTML entities
660
677
  */
661
678
  function escapeHtml(text) {
662
- var div = document.createElement('div');
679
+ const div = document.createElement('div');
663
680
  div.textContent = text;
664
681
  return div.innerHTML;
665
682
  }
@@ -668,7 +685,7 @@
668
685
  * Navigate results with keyboard
669
686
  */
670
687
  function navigate(direction) {
671
- var newIndex = state.activeIndex + direction;
688
+ let newIndex = state.activeIndex + direction;
672
689
 
673
690
  if (newIndex < 0) {
674
691
  newIndex = state.results.length - 1;
@@ -683,7 +700,7 @@
683
700
  * Set active result index
684
701
  */
685
702
  function setActiveIndex(index) {
686
- var prevActive = state.resultsContainer.querySelector('.vd-doc-search-result.is-active');
703
+ const prevActive = state.resultsContainer.querySelector('.vd-doc-search-result.is-active');
687
704
  if (prevActive) {
688
705
  prevActive.classList.remove('is-active');
689
706
  prevActive.setAttribute('aria-selected', 'false');
@@ -691,7 +708,7 @@
691
708
 
692
709
  state.activeIndex = index;
693
710
 
694
- var newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
711
+ const newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
695
712
  if (newActive) {
696
713
  newActive.classList.add('is-active');
697
714
  newActive.setAttribute('aria-selected', 'true');
@@ -704,7 +721,7 @@
704
721
  * Select a result
705
722
  */
706
723
  function select(index) {
707
- var result = state.results[index];
724
+ const result = state.results[index];
708
725
  if (!result) return;
709
726
 
710
727
  // Close search
@@ -714,12 +731,12 @@
714
731
 
715
732
  // Custom callback
716
733
  if (typeof config.onSelect === 'function') {
717
- config.onSelect(result);
734
+ safeInvokeCallback('onSelect', config.onSelect, result);
718
735
  return;
719
736
  }
720
737
 
721
738
  // Default behavior: navigate to section
722
- var section = document.querySelector(result.url);
739
+ const section = document.querySelector(result.url);
723
740
  if (section) {
724
741
  section.scrollIntoView({ behavior: 'smooth', block: 'start' });
725
742
  window.history.pushState(null, '', result.url);
@@ -731,7 +748,7 @@
731
748
  * Update sidebar navigation active state
732
749
  */
733
750
  function updateSidebarActive(sectionId) {
734
- var navLinks = document.querySelectorAll(config.navSelector);
751
+ const navLinks = document.querySelectorAll(config.navSelector);
735
752
  navLinks.forEach(function(link) {
736
753
  link.classList.remove('active');
737
754
  if (link.getAttribute('href') === '#' + sectionId) {
@@ -751,7 +768,7 @@
751
768
  state.input.setAttribute('aria-expanded', 'true');
752
769
 
753
770
  if (typeof config.onOpen === 'function') {
754
- config.onOpen();
771
+ safeInvokeCallback('onOpen', config.onOpen);
755
772
  }
756
773
  }
757
774
 
@@ -768,7 +785,7 @@
768
785
  state.input.removeAttribute('aria-activedescendant');
769
786
 
770
787
  if (typeof config.onClose === 'function') {
771
- config.onClose();
788
+ safeInvokeCallback('onClose', config.onClose);
772
789
  }
773
790
  }
774
791
 
@@ -789,7 +806,7 @@
789
806
  }
790
807
 
791
808
  if (state.resultsContainer) {
792
- state.resultsContainer.innerHTML = '';
809
+ setResultsHtml('');
793
810
  }
794
811
  }
795
812
 
@@ -822,7 +839,7 @@
822
839
  }
823
840
 
824
841
  // Public instance API
825
- var instance = {
842
+ const instance = {
826
843
  init: init,
827
844
  destroy: destroy,
828
845
  rebuild: rebuild,
@@ -840,12 +857,12 @@
840
857
  /**
841
858
  * Search Component (singleton for backward compatibility)
842
859
  */
843
- var Search = {
860
+ const Search = {
844
861
  // Factory method — creates and auto-initializes a new independent instance.
845
862
  // Always returns the instance so callers retain a reference even if the
846
863
  // DOM container is not yet available (they can retry init() later).
847
864
  create: function(options) {
848
- var instance = createSearch(options);
865
+ const instance = createSearch(options);
849
866
  if (instance) {
850
867
  instance.init();
851
868
  }
@@ -7,7 +7,7 @@
7
7
  (function () {
8
8
  'use strict';
9
9
 
10
- var supportsHas = (function () {
10
+ const supportsHas = (function () {
11
11
  try {
12
12
  return CSS.supports('selector(:has(*))');
13
13
  } catch (_e) {
@@ -18,14 +18,14 @@
18
18
  /**
19
19
  * Grid Layout Component
20
20
  */
21
- var GridLayout = {
21
+ const GridLayout = {
22
22
  instances: new Map(),
23
23
 
24
24
  /**
25
25
  * Initialize all grid layout containers
26
26
  */
27
27
  init: function () {
28
- var containers = document.querySelectorAll('[data-layout-mode]');
28
+ const containers = document.querySelectorAll('[data-layout-mode]');
29
29
 
30
30
  containers.forEach(function (container) {
31
31
  if (this.instances.has(container)) {
@@ -42,8 +42,8 @@
42
42
  * @param {HTMLElement} container - Element with data-layout-mode
43
43
  */
44
44
  initContainer: function (container) {
45
- var mode = container.getAttribute('data-layout-mode') || 'standard';
46
- var cleanupFunctions = [];
45
+ const mode = container.getAttribute('data-layout-mode') || 'standard';
46
+ const cleanupFunctions = [];
47
47
 
48
48
  this.applyMode(container, mode);
49
49
 
@@ -60,17 +60,17 @@
60
60
  * Initialize toggle buttons that target grid containers
61
61
  */
62
62
  initToggleButtons: function () {
63
- var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
63
+ const toggleButtons = document.querySelectorAll('[data-grid-toggle]');
64
64
 
65
65
  toggleButtons.forEach(function (button) {
66
66
  if (button.getAttribute('data-grid-initialized') === 'true') {
67
67
  return;
68
68
  }
69
69
 
70
- var clickHandler = function (e) {
70
+ const clickHandler = function (e) {
71
71
  e.preventDefault();
72
- var targetSelector = button.getAttribute('data-grid-toggle');
73
- var target;
72
+ const targetSelector = button.getAttribute('data-grid-toggle');
73
+ let target;
74
74
 
75
75
  if (targetSelector) {
76
76
  target = document.querySelector(targetSelector);
@@ -102,10 +102,10 @@
102
102
  applyFibFallback: function (container) {
103
103
  if (supportsHas) return;
104
104
 
105
- var rows = container.querySelectorAll('.vd-row, .row');
105
+ const rows = container.querySelectorAll('.vd-row, .row');
106
106
  rows.forEach(function (row) {
107
- var cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
108
- var count = cols.length;
107
+ const cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
108
+ const count = cols.length;
109
109
 
110
110
  if (count === 1) {
111
111
  row.style.gridTemplateColumns = '1fr';
@@ -126,7 +126,7 @@
126
126
  * @param {HTMLElement} container - Grid container
127
127
  */
128
128
  removeFibFallback: function (container) {
129
- var rows = container.querySelectorAll('.vd-row, .row');
129
+ const rows = container.querySelectorAll('.vd-row, .row');
130
130
  rows.forEach(function (row) {
131
131
  row.style.gridTemplateColumns = '';
132
132
  });
@@ -152,11 +152,11 @@
152
152
  container.setAttribute('aria-label', 'Grid layout: ' + mode + ' mode');
153
153
 
154
154
  // Update associated toggle button states
155
- var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
155
+ const toggleButtons = document.querySelectorAll('[data-grid-toggle]');
156
156
  toggleButtons.forEach(function (btn) {
157
- var targetSelector = btn.getAttribute('data-grid-toggle');
157
+ const targetSelector = btn.getAttribute('data-grid-toggle');
158
158
  if (targetSelector && container.matches(targetSelector)) {
159
- var isActive = (mode === 'fibonacci');
159
+ const isActive = (mode === 'fibonacci');
160
160
  if (isActive) {
161
161
  btn.classList.add('is-active');
162
162
  } else {
@@ -167,13 +167,13 @@
167
167
  });
168
168
 
169
169
  // Store mode in instance
170
- var instance = this.instances.get(container);
170
+ const instance = this.instances.get(container);
171
171
  if (instance) {
172
172
  instance.mode = mode;
173
173
  }
174
174
 
175
175
  // Dispatch custom event
176
- var event;
176
+ let event;
177
177
  try {
178
178
  event = new CustomEvent('grid:modechange', {
179
179
  bubbles: true,
@@ -202,8 +202,8 @@
202
202
  }
203
203
  if (!container) return;
204
204
 
205
- var currentMode = container.getAttribute('data-layout-mode') || 'standard';
206
- var newMode = (currentMode === 'fibonacci') ? 'standard' : 'fibonacci';
205
+ const currentMode = container.getAttribute('data-layout-mode') || 'standard';
206
+ const newMode = (currentMode === 'fibonacci') ? 'standard' : 'fibonacci';
207
207
  this.applyMode(container, newMode);
208
208
  },
209
209
 
@@ -240,7 +240,7 @@
240
240
  * @param {HTMLElement} container - Grid container
241
241
  */
242
242
  destroy: function (container) {
243
- var instance = this.instances.get(container);
243
+ const instance = this.instances.get(container);
244
244
  if (!instance) return;
245
245
 
246
246
  instance.cleanup.forEach(function (fn) { fn(); });
@@ -258,7 +258,7 @@
258
258
  this.destroy(container);
259
259
  }.bind(this));
260
260
 
261
- var toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
261
+ const toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
262
262
  toggleButtons.forEach(function (button) {
263
263
  if (button._gridCleanup) {
264
264
  button._gridCleanup();