miki-template 2.2.2 → 2.3.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.
Files changed (66) hide show
  1. package/.github/workflows/docs.yml +3 -1
  2. package/.github/workflows/release.yml +1 -0
  3. package/benchmarks/ejs-results.json +6 -6
  4. package/benchmarks/ejs.js +5 -3
  5. package/benchmarks/handlebars-results.json +6 -6
  6. package/benchmarks/handlebars.js +5 -8
  7. package/benchmarks/miki-results.json +6 -6
  8. package/benchmarks/miki.js +6 -3
  9. package/benchmarks/pug-results.json +6 -6
  10. package/benchmarks/pug.js +5 -3
  11. package/docs/api/async-render.md +88 -3
  12. package/docs/api/cache.md +90 -3
  13. package/docs/api/compile.md +131 -3
  14. package/docs/api/context-processors.md +80 -3
  15. package/docs/api/filters.md +223 -3
  16. package/docs/api/finder.md +97 -3
  17. package/docs/api/helpers.md +56 -3
  18. package/docs/api/i18n.md +160 -3
  19. package/docs/api/index.md +82 -28
  20. package/docs/api/libraries.md +210 -3
  21. package/docs/api/render-partial.md +84 -3
  22. package/docs/api/render.md +95 -3
  23. package/docs/api/security.md +148 -3
  24. package/docs/api/setup-express.md +78 -2
  25. package/docs/api/tags.md +138 -4
  26. package/docs/filter.md +0 -0
  27. package/docs/guide/advanced-usage.md +403 -6
  28. package/docs/guide/async-rendering.md +312 -4
  29. package/docs/guide/context-processors.md +261 -4
  30. package/docs/guide/custom-filters.md +315 -4
  31. package/docs/guide/custom-tags.md +275 -4
  32. package/docs/guide/filters.md +675 -3
  33. package/docs/guide/getting-started.md +109 -7
  34. package/docs/guide/installation.md +99 -4
  35. package/docs/guide/partial-templates.md +371 -4
  36. package/docs/guide/quick-start.md +228 -6
  37. package/docs/guide/security.md +348 -3
  38. package/docs/guide/tags.md +789 -6
  39. package/docs/guide/template-discovery.md +174 -4
  40. package/docs/guide/template-inheritance.md +277 -4
  41. package/docs/index.md +24 -42
  42. package/docs/integrations/elysia.md +4 -2
  43. package/docs/integrations/express.md +219 -219
  44. package/docs/integrations/fastify.md +4 -2
  45. package/docs/integrations/hono.md +4 -2
  46. package/docs/integrations/index.md +68 -68
  47. package/docs/integrations/koa.md +4 -2
  48. package/docs/integrations/nestjs.md +4 -2
  49. package/docs/integrations/tsed.md +4 -2
  50. package/docs/performance.md +45 -8
  51. package/ex.mjs +1 -1
  52. package/mkdocs.yml +0 -22
  53. package/overrides/main.html +1 -1
  54. package/package.json +1 -1
  55. package/requirements-docs.txt +2 -1
  56. package/src/codegen.js +905 -0
  57. package/src/context.js +42 -30
  58. package/src/filters.js +16 -0
  59. package/src/index.js +66 -61
  60. package/src/tags/control.js +15 -12
  61. package/src/utils.js +60 -0
  62. package/tests/filters.test.js +9 -0
  63. package/.github/workflows/npm-publish-github-packages.yml +0 -36
  64. package/docs/javascripts/extra.js +0 -174
  65. package/docs/stylesheets/extra.css +0 -819
  66. package/overrides/partials/footer.html +0 -9
package/src/context.js CHANGED
@@ -12,6 +12,18 @@ class Context {
12
12
  this.partialDefs = new Map(); // Store partial definitions
13
13
  }
14
14
 
15
+ get _local() {
16
+ return this.scopes[0] || {};
17
+ }
18
+
19
+ set _local(value) {
20
+ if (this.scopes.length === 0) {
21
+ this.scopes.unshift(value);
22
+ } else {
23
+ this.scopes[0] = value;
24
+ }
25
+ }
26
+
15
27
  /**
16
28
  * Reset cycle state and blocks for a fresh render.
17
29
  * Called at the start of each render pass.
@@ -49,14 +61,20 @@ class Context {
49
61
  return '';
50
62
  }
51
63
 
52
- const parts = path.split('.');
53
- const baseName = parts[0];
64
+ // Fast path for non-dotted single variable lookups
65
+ const isString = typeof path === 'string';
66
+ const hasDot = isString && path.includes('.');
67
+
68
+ const baseName = hasDot ? path.split('.')[0] : path;
54
69
 
55
70
  let current = undefined;
56
71
  let found = false;
57
72
 
58
73
  // Search scopes from top (most local) to bottom (most global)
59
- for (const scope of this.scopes) {
74
+ const scopes = this.scopes;
75
+ const len = scopes.length;
76
+ for (let i = 0; i < len; i++) {
77
+ const scope = scopes[i];
60
78
  if (scope && typeof scope === 'object' && baseName in scope) {
61
79
  current = scope[baseName];
62
80
  found = true;
@@ -65,43 +83,37 @@ class Context {
65
83
  }
66
84
 
67
85
  if (!found) {
68
- // Variable truly missing — return undefined so templates can
69
- // distinguish "missing" from "explicitly null". The variable node
70
- // and filters handle undefined gracefully.
71
86
  return undefined;
72
87
  }
73
88
 
74
- // Traverse the rest of the dotted segments
75
- for (let i = 1; i < parts.length; i++) {
76
- if (current === undefined || current === null) {
77
- return current === null ? null : undefined;
78
- }
79
-
80
- const parent = current;
81
- const part = parts[i];
82
-
83
- // Resolve segment on the current value
84
- if (typeof current === 'object' && part in current) {
85
- current = current[part];
86
- } else if (Array.isArray(current) && !isNaN(part)) {
87
- // Handle array index resolution, e.g. items.0
88
- current = current[parseInt(part, 10)];
89
- } else {
90
- return undefined;
91
- }
92
-
93
- // If the property value is a function, evaluate it (Django style)
94
- if (typeof current === 'function') {
95
- current = current.call(parent);
89
+ if (hasDot) {
90
+ const parts = path.split('.');
91
+ for (let i = 1; i < parts.length; i++) {
92
+ if (current === undefined || current === null) {
93
+ return current === null ? null : undefined;
94
+ }
95
+
96
+ const parent = current;
97
+ const part = parts[i];
98
+
99
+ if (typeof current === 'object' && part in current) {
100
+ current = current[part];
101
+ } else if (Array.isArray(current) && !isNaN(part)) {
102
+ current = current[parseInt(part, 10)];
103
+ } else {
104
+ return undefined;
105
+ }
106
+
107
+ if (typeof current === 'function') {
108
+ current = current.call(parent);
109
+ }
96
110
  }
97
111
  }
98
112
 
99
- // If the final resolved value is a function, call it with no arguments
100
113
  if (typeof current === 'function') {
101
114
  current = current.call(null);
102
115
  }
103
116
 
104
- // Preserve null/undefined so filters like default_if_none can detect them.
105
117
  return current;
106
118
  }
107
119
 
package/src/filters.js CHANGED
@@ -971,5 +971,21 @@ registerFilter('uuid', () => {
971
971
  });
972
972
  });
973
973
 
974
+ registerFilter('repeat', (val, arg) => {
975
+ const str = String(val === null || val === undefined ? '' : val);
976
+ const count = parseInt(arg, 10);
977
+ if (Number.isNaN(count) || count <= 0) return '';
978
+ return str.repeat(count);
979
+ });
980
+
981
+ registerFilter('range', (val, arg) => {
982
+ const end = parseInt(val, 10);
983
+ const start = arg !== undefined && arg !== null ? parseInt(arg, 10) : 0;
984
+ const step = 1;
985
+ const out = [];
986
+ for (let i = start; i < end; i += step) out.push(i);
987
+ return out;
988
+ });
989
+
974
990
  module.exports = { registerFilter, getFilter };
975
991
 
package/src/index.js CHANGED
@@ -219,12 +219,15 @@ function readParentSource(parentName, viewsDirs) {
219
219
 
220
220
  async function renderASTAsync(nodes, context) {
221
221
  context.parentTemplate = null;
222
- const parts = [];
223
- for (const node of nodes) {
224
- const result = node.render(context);
225
- parts.push(result instanceof Promise ? await result : result);
222
+ let output = '';
223
+ for (let i = 0, len = nodes.length; i < len; i++) {
224
+ const result = nodes[i].render(context);
225
+ if (result instanceof Promise) {
226
+ output += await result;
227
+ } else {
228
+ output += result;
229
+ }
226
230
  }
227
- let output = parts.join('');
228
231
 
229
232
  if (context.parentTemplate) {
230
233
  const parentName = context.parentTemplate;
@@ -279,13 +282,14 @@ async function renderASTAsync(nodes, context) {
279
282
  */
280
283
  function renderAST(nodes, context) {
281
284
  context.parentTemplate = null;
282
- const output = nodes.map(node => {
283
- const result = node.render(context);
285
+ let output = '';
286
+ for (let i = 0, len = nodes.length; i < len; i++) {
287
+ const result = nodes[i].render(context);
284
288
  if (result instanceof Promise) {
285
289
  throw new Error('Async node encountered during sync render. Use asyncRender() instead.');
286
290
  }
287
- return result;
288
- }).join('');
291
+ output += result;
292
+ }
289
293
 
290
294
  if (context.parentTemplate) {
291
295
  const parentName = context.parentTemplate;
@@ -354,78 +358,79 @@ function compile(templateStr, options = {}) {
354
358
  }
355
359
  collectPartials(nodes);
356
360
 
361
+ const { renderBody } = require('./utils');
362
+ const { generateCode, canCodegen } = require('./codegen');
363
+
364
+ const useCodegen = true;
365
+ let compiledRender = null;
366
+ if (useCodegen && canCodegen(nodes)) {
367
+ compiledRender = generateCode(nodes);
368
+ }
369
+
370
+ function makeContext(contextObj, callOpts) {
371
+ const mergedOpts = callOpts ? { ...opts, ...callOpts } : opts;
372
+ const processedContextObj = applyContextProcessors({ ...contextObj });
373
+ const context = new Context(processedContextObj, mergedOpts);
374
+ context.reset();
375
+ if (parser.blocks) {
376
+ for (const [name, blockList] of Object.entries(parser.blocks)) {
377
+ context.blocks[name] = [...blockList];
378
+ }
379
+ }
380
+ for (const [name, partial] of Object.entries(partialDefs)) {
381
+ context.registerPartial(name, partial);
382
+ }
383
+ return context;
384
+ }
385
+
357
386
  return {
387
+ _usesCodegen: !!compiledRender,
358
388
  render: (contextObj = {}) => {
359
- const processedContextObj = applyContextProcessors({ ...contextObj });
360
- const context = new Context(processedContextObj, opts);
361
- context.reset();
362
- if (parser.blocks) {
363
- for (const [name, blockList] of Object.entries(parser.blocks)) {
364
- context.blocks[name] = [...blockList];
389
+ const context = makeContext(contextObj);
390
+ if (compiledRender) {
391
+ const res = compiledRender(context);
392
+ if (context.parentTemplate) {
393
+ return renderAST(nodes, context);
365
394
  }
366
- }
367
- // Register partial definitions from compile-time
368
- for (const [name, partial] of Object.entries(partialDefs)) {
369
- context.registerPartial(name, partial);
395
+ return res;
370
396
  }
371
397
  return renderAST(nodes, context);
372
398
  },
373
399
  renderWith: (contextObj = {}, callOptions = {}) => {
374
- const processedContextObj = applyContextProcessors({ ...contextObj });
375
- const mergedOpts = { ...opts, ...callOptions };
376
- const context = new Context(processedContextObj, mergedOpts);
377
- context.reset();
378
- if (parser.blocks) {
379
- for (const [name, blockList] of Object.entries(parser.blocks)) {
380
- context.blocks[name] = [...blockList];
400
+ const context = makeContext(contextObj, callOptions);
401
+ if (compiledRender) {
402
+ const res = compiledRender(context);
403
+ if (context.parentTemplate) {
404
+ return renderAST(nodes, context);
381
405
  }
382
- }
383
- for (const [name, partial] of Object.entries(partialDefs)) {
384
- context.registerPartial(name, partial);
406
+ return res;
385
407
  }
386
408
  return renderAST(nodes, context);
387
409
  },
388
410
  asyncRender: async (contextObj = {}) => {
389
- const processedContextObj = applyContextProcessors({ ...contextObj });
390
- const context = new Context(processedContextObj, opts);
391
- context.blocks = {};
392
- if (parser.blocks) {
393
- for (const [name, blockList] of Object.entries(parser.blocks)) {
394
- context.blocks[name] = [...blockList];
411
+ const context = makeContext(contextObj);
412
+ if (compiledRender) {
413
+ const res = compiledRender(context);
414
+ if (context.parentTemplate || res instanceof Promise || (typeof res === 'string' && res.includes('[object Promise]'))) {
415
+ return await renderASTAsync(nodes, context);
395
416
  }
396
- }
397
- for (const [name, partial] of Object.entries(partialDefs)) {
398
- context.registerPartial(name, partial);
417
+ return res;
399
418
  }
400
419
  return await renderASTAsync(nodes, context);
401
420
  },
402
421
  asyncRenderWith: async (contextObj = {}, callOptions = {}) => {
403
- const processedContextObj = applyContextProcessors({ ...contextObj });
404
- const mergedOpts = { ...opts, ...callOptions };
405
- const context = new Context(processedContextObj, mergedOpts);
406
- context.blocks = {};
407
- if (parser.blocks) {
408
- for (const [name, blockList] of Object.entries(parser.blocks)) {
409
- context.blocks[name] = [...blockList];
422
+ const context = makeContext(contextObj, callOptions);
423
+ if (compiledRender) {
424
+ const res = compiledRender(context);
425
+ if (context.parentTemplate || res instanceof Promise || (typeof res === 'string' && res.includes('[object Promise]'))) {
426
+ return await renderASTAsync(nodes, context);
410
427
  }
411
- }
412
- for (const [name, partial] of Object.entries(partialDefs)) {
413
- context.registerPartial(name, partial);
428
+ return res;
414
429
  }
415
430
  return await renderASTAsync(nodes, context);
416
431
  },
417
432
  renderBlock: (blockName, contextObj = {}) => {
418
- const processedContextObj = applyContextProcessors({ ...contextObj });
419
- const context = new Context(processedContextObj, opts);
420
- context.blocks = {};
421
- if (parser.blocks) {
422
- for (const [name, blockList] of Object.entries(parser.blocks)) {
423
- context.blocks[name] = [...blockList];
424
- }
425
- }
426
- for (const [name, partial] of Object.entries(partialDefs)) {
427
- context.registerPartial(name, partial);
428
- }
433
+ const context = makeContext(contextObj);
429
434
  renderAST(nodes, context);
430
435
  const blockStack = context.blocks[blockName];
431
436
  if (!blockStack || blockStack.length === 0) {
@@ -442,7 +447,7 @@ function compile(templateStr, options = {}) {
442
447
  }
443
448
  context.push({ block: { super: superVal } });
444
449
  context.blockRenderIndices[blockName] = 0;
445
- const result = blockStack[0].body.map(n => n.render(context)).join('');
450
+ const result = renderBody(blockStack[0].body, context);
446
451
  context.pop();
447
452
  context.blockRenderIndices[blockName] = -1;
448
453
  return result;
@@ -457,7 +462,7 @@ function compile(templateStr, options = {}) {
457
462
  if (!partial) {
458
463
  throw new Error(`Partial '${partialName}' not found`);
459
464
  }
460
- return partial.body.map(n => n.render(context)).join('');
465
+ return renderBody(partial.body, context);
461
466
  }
462
467
  };
463
468
  });
@@ -2,6 +2,7 @@
2
2
  * Control flow template tags: if, for, with, cycle, comment, with, firstof.
3
3
  */
4
4
  const { parseVariableExpression } = require('../parser');
5
+ const { getFilter } = require('../filters');
5
6
 
6
7
  /**
7
8
  * Helper to parse a string into an array of tokens.
@@ -206,27 +207,28 @@ class ForNode {
206
207
  this.filters = filters;
207
208
  this.body = body;
208
209
  this.emptyBody = emptyBody;
210
+ this._filterFns = filters.map(f => {
211
+ const fn = getFilter(f.name);
212
+ return { fn, arg: f.arg };
213
+ });
209
214
  }
210
215
 
211
216
  render(context) {
212
217
  let rawItems = context.get(this.iterablePath);
213
218
 
214
- // Apply any filters on the iterable path (e.g. items|regroup:"category")
215
- const { getFilter } = require('../filters');
216
- for (const filterInfo of this.filters) {
217
- const filterFn = getFilter(filterInfo.name);
218
- if (!filterFn) {
219
- throw new Error(`Unknown filter: '${filterInfo.name}'`);
219
+ for (const { fn, arg } of this._filterFns) {
220
+ if (!fn) {
221
+ throw new Error(`Unknown filter: '${arg.name}'`);
220
222
  }
221
223
  let argVal = undefined;
222
- if (filterInfo.arg) {
223
- if (filterInfo.arg.type === 'literal') {
224
- argVal = filterInfo.arg.value;
225
- } else if (filterInfo.arg.type === 'variable') {
226
- argVal = context.get(filterInfo.arg.value);
224
+ if (arg) {
225
+ if (arg.type === 'literal') {
226
+ argVal = arg.value;
227
+ } else if (arg.type === 'variable') {
228
+ argVal = context.get(arg.value);
227
229
  }
228
230
  }
229
- rawItems = filterFn(rawItems, argVal);
231
+ rawItems = fn(rawItems, argVal);
230
232
  }
231
233
 
232
234
  let items = [];
@@ -706,6 +708,7 @@ module.exports = {
706
708
  PartialDefNode,
707
709
  PartialNode,
708
710
  evaluateCondition,
711
+ tokenizeExpr,
709
712
  parsers: {
710
713
  if: parseIf,
711
714
  for: parseFor,
package/src/utils.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Fast string concatenation for an array of AST nodes.
3
+ *
4
+ * Replaces the common pattern
5
+ * body.map(n => n.render(context)).join('')
6
+ * with a tight loop that calls .render() and concatenates directly.
7
+ * V8 optimizes `let s = ''; s += x` well (cons strings), whereas
8
+ * `.map().join('')` allocates an intermediate array.
9
+ */
10
+ function renderBody(body, context) {
11
+ let out = '';
12
+ if (!body) return out;
13
+ for (let i = 0, len = body.length; i < len; i++) {
14
+ const r = body[i].render(context);
15
+ if (r instanceof Promise) {
16
+ out += '';
17
+ } else {
18
+ out += r;
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ /**
25
+ * Async version of renderBody — awaits Promises from any node.
26
+ */
27
+ async function renderBodyAsync(body, context) {
28
+ let out = '';
29
+ if (!body) return out;
30
+ for (let i = 0, len = body.length; i < len; i++) {
31
+ const r = body[i].render(context);
32
+ if (r instanceof Promise) out += await r;
33
+ else out += r;
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Render body, returning a Promise or string based on what nodes
40
+ * produce. Used by HelperNode and the render* entry points.
41
+ */
42
+ function renderBodyMaybeAsync(body, context) {
43
+ let out = '';
44
+ let pending = null;
45
+ for (let i = 0, len = body.length; i < len; i++) {
46
+ const r = body[i].render(context);
47
+ if (r instanceof Promise) {
48
+ if (!pending) pending = [];
49
+ pending.push(r.then(v => { out += v; }));
50
+ } else {
51
+ out += r;
52
+ }
53
+ }
54
+ if (pending) {
55
+ return Promise.all(pending).then(() => out);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ module.exports = { renderBody, renderBodyAsync, renderBodyMaybeAsync };
@@ -251,4 +251,13 @@ test('Filters - credit_card, ssn, ip_address, uuid', () => {
251
251
 
252
252
  const uuid = getFilter('uuid');
253
253
  assert.ok(uuid().match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/));
254
+
255
+ test('Filters - repeat and range', () => {
256
+ const repeat = getFilter('repeat');
257
+ assert.strictEqual(repeat('a', 3), 'aaa');
258
+ assert.strictEqual(repeat('', 5), '');
259
+ const range = getFilter('range');
260
+ assert.deepEqual(range('5'), [0,1,2,3,4]);
261
+ assert.deepEqual(range('5', '2'), [2,3,4]);
262
+ });
254
263
  });
@@ -1,36 +0,0 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
-
4
- name: Node.js Package
5
-
6
- on:
7
- release:
8
- types: [created]
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: 20
18
- - run: npm ci
19
- - run: npm test
20
-
21
- publish-gpr:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- permissions:
25
- contents: read
26
- packages: write
27
- steps:
28
- - uses: actions/checkout@v4
29
- - uses: actions/setup-node@v4
30
- with:
31
- node-version: 20
32
- registry-url: https://npm.pkg.github.com/
33
- - run: npm ci
34
- - run: npm publish
35
- env:
36
- NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -1,174 +0,0 @@
1
- document$.subscribe(function () {
2
- console.log("miki-template docs initialized");
3
-
4
- const navbar = document.querySelector(".md-header");
5
- if (navbar) {
6
- let lastScroll = 0;
7
- const scrollThreshold = 50;
8
-
9
- window.addEventListener("scroll", function () {
10
- const currentScroll = window.pageYOffset;
11
-
12
- if (currentScroll <= 0) {
13
- navbar.style.boxShadow = "0 2px 8px var(--md-shadow-color)";
14
- return;
15
- }
16
-
17
- if (currentScroll > lastScroll && currentScroll > scrollThreshold) {
18
- navbar.style.transform = "translateY(-100%)";
19
- navbar.style.transition = "transform 0.3s ease";
20
- } else {
21
- navbar.style.transform = "translateY(0)";
22
- navbar.style.transition = "transform 0.3s ease";
23
- }
24
-
25
- lastScroll = currentScroll;
26
- });
27
- }
28
-
29
- document.querySelectorAll(".md-clipboard").forEach(function (button) {
30
- button.addEventListener("click", async function () {
31
- const code = this.closest(".highlight").querySelector("code").innerText;
32
-
33
- try {
34
- await navigator.clipboard.writeText(code);
35
- const originalHTML = this.innerHTML;
36
- this.innerHTML = '<span class="md-icon">&#10003;</span> Copied!';
37
- this.style.color = "var(--md-accent-fg-color)";
38
- this.style.opacity = "1";
39
-
40
- setTimeout(function () {
41
- button.innerHTML = originalHTML;
42
- button.style.color = "";
43
- button.style.opacity = "";
44
- }, 2000);
45
- } catch (err) {
46
- console.error("Failed to copy: ", err);
47
- }
48
- });
49
- });
50
-
51
- document.querySelectorAll(".md-typeset a[href^='#']").forEach(function (anchor) {
52
- anchor.addEventListener("click", function (e) {
53
- const targetId = this.getAttribute("href");
54
- if (targetId === "#") return;
55
-
56
- const target = document.querySelector(targetId);
57
- if (target) {
58
- e.preventDefault();
59
- const headerOffset = 80;
60
- const elementPosition = target.getBoundingClientRect().top;
61
- const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
62
-
63
- window.scrollTo({
64
- top: offsetPosition,
65
- behavior: "smooth",
66
- });
67
-
68
- history.replaceState(null, null, targetId);
69
- }
70
- });
71
- });
72
-
73
- document.querySelectorAll(".md-typeset .task-list-item input[type='checkbox']").forEach(function (checkbox) {
74
- checkbox.addEventListener("change", function () {
75
- const label = this.closest(".task-list-item");
76
- if (this.checked) {
77
- label.style.opacity = "0.6";
78
- label.style.textDecoration = "line-through";
79
- } else {
80
- label.style.opacity = "1";
81
- label.style.textDecoration = "none";
82
- }
83
- });
84
- });
85
-
86
- document.querySelectorAll(".md-tabs__link").forEach(function (tab) {
87
- tab.addEventListener("click", function () {
88
- document.querySelectorAll(".md-tabs__link").forEach(function (t) {
89
- t.style.transform = "scale(1)";
90
- });
91
- this.style.transform = "scale(1.05)";
92
- setTimeout(function () {
93
- tab.style.transform = "scale(1)";
94
- }, 200);
95
- });
96
- });
97
-
98
- document.querySelectorAll(".md-typeset .tabbed-set > label").forEach(function (tabLabel) {
99
- tabLabel.addEventListener("click", function () {
100
- this.style.transform = "scale(0.98)";
101
- setTimeout(function () {
102
- tabLabel.style.transform = "scale(1)";
103
- }, 100);
104
- });
105
- });
106
-
107
- const searchInput = document.querySelector(".md-search__input");
108
- if (searchInput) {
109
- searchInput.addEventListener("focus", function () {
110
- this.parentElement.style.boxShadow = "0 0 0 3px var(--md-accent-fg-color), 0 4px 12px var(--md-shadow-color)";
111
- this.parentElement.style.transition = "box-shadow 0.2s ease";
112
- });
113
-
114
- searchInput.addEventListener("blur", function () {
115
- this.parentElement.style.boxShadow = "";
116
- });
117
- }
118
-
119
- document.querySelectorAll(".md-typeset .admonition").forEach(function (admonition) {
120
- admonition.addEventListener("mouseenter", function () {
121
- this.style.transform = "translateY(-2px)";
122
- this.style.transition = "transform 0.2s ease";
123
- });
124
-
125
- admonition.addEventListener("mouseleave", function () {
126
- this.style.transform = "translateY(0)";
127
- });
128
- });
129
-
130
- document.querySelectorAll(".md-typeset .md-button").forEach(function (button) {
131
- button.addEventListener("mouseenter", function () {
132
- this.style.transform = "translateY(-2px)";
133
- });
134
-
135
- button.addEventListener("mouseleave", function () {
136
- this.style.transform = "translateY(0)";
137
- });
138
- });
139
-
140
- document.querySelectorAll(".md-content__inner table:not([class]) tbody tr").forEach(function (row) {
141
- row.addEventListener("mouseenter", function () {
142
- this.style.transition = "background-color 0.2s ease";
143
- });
144
- });
145
-
146
- const observerOptions = {
147
- root: null,
148
- rootMargin: "0px",
149
- threshold: 0.1,
150
- };
151
-
152
- const observer = new IntersectionObserver(function (entries) {
153
- entries.forEach(function (entry) {
154
- if (entry.isIntersecting) {
155
- entry.target.style.opacity = "1";
156
- entry.target.style.transform = "translateY(0)";
157
- }
158
- });
159
- }, observerOptions);
160
-
161
- document.querySelectorAll(".md-typeset h2, .md-typeset h3, .md-typeset .admonition").forEach(function (el) {
162
- el.style.opacity = "0";
163
- el.style.transform = "translateY(10px)";
164
- el.style.transition = "opacity 0.5s ease, transform 0.5s ease";
165
- observer.observe(el);
166
- });
167
-
168
- setTimeout(function () {
169
- document.querySelectorAll(".md-typeset h2, .md-typeset h3, .md-typeset .admonition").forEach(function (el) {
170
- el.style.opacity = "1";
171
- el.style.transform = "translateY(0)";
172
- });
173
- }, 100);
174
- });