Skip to content

Commit e10bb5e

Browse files
committed
Docs: Document new indent utils
1 parent 73a985d commit e10bb5e

File tree

7 files changed

+244
-15
lines changed

7 files changed

+244
-15
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ xx.xx.xxxx
2121
* **Feature** - Added [`addAttr()`](https://next.semantic-ui.com/api/query/attributes#addattr) method for adding one or more attributes with empty string values. Useful shorthand for boolean attributes common in web components.
2222

2323
### Utils
24+
* **Feature** - Added [`indentHTML()`](https://next.semantic-ui.com/docs/api/utils/html#indenthtml) for intelligently indenting HTML markup with proper nesting awareness. Handles void elements, self-closing tags, and comments correctly. Perfect for cleaning up HTML extracted from template literals.
25+
* **Feature** - Added [`indentLines()`](https://next.semantic-ui.com/docs/api/utils/html#indentlines) for adding consistent indentation to every line of text. Useful for formatting code snippets and template processing.
2426
* **Feature** - Added `reverseString()` for reversing strings with Unicode grapheme cluster handling using Intl.Segmenter. Preserves emojis, flag sequences, skin tone modifiers, and combined diacritics.
2527
* **Bug** - Fixed `weightedObjectSearch()` regex pattern escaping where multi-word queries with `matchAllWords: false` returned no results. Template string was using `\W` (literal 'W') instead of `\\W` (non-word character class), breaking word boundary matching.
2628
* **Enhancement** - `remove()` now removes all matching instances from an array instead of just the first. Uses an optimized two-pointer approach for O(n) performance. Returns the count of removed elements for backward compatibility.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: 'indentHTML'
3+
id: 'utils-indenthtml'
4+
exampleType: 'log'
5+
category: 'Utils'
6+
subcategory: 'HTML'
7+
description: 'Intelligently indent HTML markup with proper nesting awareness'
8+
tags: ['utils', 'html', 'indenthtml', 'formatting', 'markup']
9+
selectedFile: 'index.js'
10+
tip: 'Cleans up HTML from template literals. For simple line indentation, use [indentLines](/examples/utils-indentlines)'
11+
---
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: 'indentLines'
3+
id: 'utils-indentlines'
4+
exampleType: 'log'
5+
category: 'Utils'
6+
subcategory: 'HTML'
7+
description: 'Add consistent indentation to every line of text'
8+
tags: ['utils', 'html', 'indentlines', 'formatting', 'text']
9+
selectedFile: 'index.js'
10+
tip: 'For HTML-specific indentation with nesting awareness, use [indentHTML](/examples/utils-indenthtml)'
11+
---
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { indentHTML } from '@semantic-ui/utils';
2+
3+
// basic usage
4+
const simple = '<div>\n<p>Content</p>\n</div>';
5+
console.log(indentHTML(simple));
6+
7+
// nested structures
8+
const button = `<ui-button animated>
9+
<span slot="visible">Hover Me</span>
10+
<span slot="hidden">Hidden</span>
11+
</ui-button>`;
12+
13+
console.log(indentHTML(button));
14+
15+
// custom indentation with tabs
16+
const withTabs = '<div>\n<p>Content</p>\n</div>';
17+
console.log(indentHTML(withTabs, { indent: '\t' }));
18+
19+
// starting at nesting level
20+
const atLevel = '<div>\n<p>Content</p>\n</div>';
21+
console.log(indentHTML(atLevel, { startLevel: 1 }));
22+
23+
// handles void elements
24+
const voidElements = '<div>\n<img src="test.jpg">\n<br>\n<input type="text">\n</div>';
25+
console.log(indentHTML(voidElements));
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { indentLines } from '@semantic-ui/utils';
2+
3+
// basic usage with default 2 spaces
4+
const code = 'function test() {\n return true;\n}';
5+
console.log(indentLines(code));
6+
7+
// custom indentation
8+
console.log(indentLines(code, 4));
9+
10+
// single line
11+
console.log(indentLines('single line', 2));
12+
13+
// useful for template processing
14+
const template = `const greeting = 'Hello';
15+
const name = 'World';
16+
console.log(greeting, name);`;
17+
18+
console.log(indentLines(template, 2));

docs/src/helpers/injections.js

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -321,17 +321,33 @@ function createTableHTML(data) {
321321
return formatValue(data);
322322
}
323323
324+
// Helper to escape HTML to prevent XSS
325+
function escapeHTML(string) {
326+
const htmlEscapes = {
327+
'&': '&amp;',
328+
'<': '&lt;',
329+
'>': '&gt;',
330+
'"': '&quot;',
331+
"'": '&#39;'
332+
};
333+
const htmlRegExp = /[&<>"']/g;
334+
const hasHTML = RegExp(htmlRegExp.source);
335+
return (string && hasHTML.test(string))
336+
? string.replace(htmlRegExp, (chr) => htmlEscapes[chr])
337+
: (string || '');
338+
}
339+
324340
function formatValue(value, skipFormat = false, inTable = false) {
325341
if (skipFormat) {
326-
return String(value);
342+
return escapeHTML(String(value));
327343
}
328-
344+
329345
if (value === null) {
330346
return \`<span class="json-null">null</span>\`;
331347
} else if (value === undefined) {
332348
return \`<span class="json-undefined">undefined</span>\`;
333349
} else if (typeof value === 'string') {
334-
return \`<span class="json-string">"\${value}"</span>\`;
350+
return \`<span class="json-string">"\${escapeHTML(value)}"</span>\`;
335351
} else if (typeof value === 'number') {
336352
if (Number.isNaN(value)) {
337353
return \`<span class="json-nan">NaN</span>\`;
@@ -346,17 +362,17 @@ function formatValue(value, skipFormat = false, inTable = false) {
346362
} else if (typeof value === 'function') {
347363
const funcStr = value.toString();
348364
const isArrow = funcStr.includes('=>');
349-
const funcName = value.name || 'anonymous';
365+
const funcName = escapeHTML(value.name || 'anonymous');
350366
if (inTable) {
351367
return \`<span class="json-function">ƒ \${funcName}()</span>\`;
352368
}
353-
return \`<span class="json-function">ƒ \${funcName}() { \${isArrow ? funcStr : '...'} }</span>\`;
369+
return \`<span class="json-function">ƒ \${funcName}() { \${isArrow ? escapeHTML(funcStr) : '...'} }</span>\`;
354370
} else if (value instanceof Date) {
355-
return \`<span class="json-date">\${value.toISOString()}</span>\`;
371+
return \`<span class="json-date">\${escapeHTML(value.toISOString())}</span>\`;
356372
} else if (value instanceof RegExp) {
357-
return \`<span class="json-regexp">\${value.toString()}</span>\`;
373+
return \`<span class="json-regexp">\${escapeHTML(value.toString())}</span>\`;
358374
} else if (value instanceof Error) {
359-
return \`<span class="json-error">\${value.name}: \${value.message}</span>\`;
375+
return \`<span class="json-error">\${escapeHTML(value.name)}: \${escapeHTML(value.message)}</span>\`;
360376
} else if (Array.isArray(value)) {
361377
if (inTable) {
362378
return \`<span class="json-array">Array(\${value.length})</span>\`;
@@ -369,9 +385,9 @@ function formatValue(value, skipFormat = false, inTable = false) {
369385
} else if (typeof value === 'object' && value !== null) {
370386
// Handle DOM elements
371387
if (value.nodeType) {
372-
const tagName = value.tagName?.toLowerCase() || 'node';
373-
const id = value.id ? \`#\${value.id}\` : '';
374-
const className = value.className ? \`.\${value.className.split(' ').join('.')}\` : '';
388+
const tagName = escapeHTML(value.tagName?.toLowerCase() || 'node');
389+
const id = value.id ? \`#\${escapeHTML(value.id)}\` : '';
390+
const className = value.className ? \`.\${escapeHTML(value.className.split(' ').join('.'))}\` : '';
375391
return \`<span class="json-dom">&lt;\${tagName}\${id}\${className}&gt;</span>\`;
376392
}
377393
@@ -380,7 +396,7 @@ function formatValue(value, skipFormat = false, inTable = false) {
380396
const isCustomClass = constructor && constructor !== Object && constructor.name !== 'Object';
381397
382398
if (isCustomClass) {
383-
const className = constructor.name;
399+
const className = escapeHTML(constructor.name);
384400
const keys = Object.keys(value);
385401
if (inTable) {
386402
return \`<span class="json-class">\${className} {...}</span>\`;
@@ -389,7 +405,7 @@ function formatValue(value, skipFormat = false, inTable = false) {
389405
return \`<span class="json-class">\${className} {}</span>\`;
390406
}
391407
const formattedObject = keys.slice(0, 3).map(key => {
392-
return \`<span class="json-key">"\${key}"</span>: \${formatValue(value[key])}\`;
408+
return \`<span class="json-key">"\${escapeHTML(key)}"</span>: \${formatValue(value[key])}\`;
393409
}).join(', ');
394410
const more = keys.length > 3 ? ', ...' : '';
395411
return \`<span class="json-class">\${className}</span> {\${formattedObject}\${more}}\`;
@@ -404,12 +420,12 @@ function formatValue(value, skipFormat = false, inTable = false) {
404420
return \`<span class="json-object">{}</span>\`;
405421
}
406422
const formattedObject = keys.slice(0, 3).map(key => {
407-
return \`<span class="json-key">"\${key}"</span>: \${formatValue(value[key])}\`;
423+
return \`<span class="json-key">"\${escapeHTML(key)}"</span>: \${formatValue(value[key])}\`;
408424
}).join(', ');
409425
const more = keys.length > 3 ? ', ...' : '';
410426
return \`<span class="json-object">{\${formattedObject}\${more}}</span>\`;
411427
} else {
412-
return \`<span class="json-unknown">\${String(value)}</span>\`;
428+
return \`<span class="json-unknown">\${escapeHTML(String(value))}</span>\`;
413429
}
414430
}
415431
${hideMarkerEnd}`;
@@ -509,6 +525,7 @@ export const logCSS = `
509525
border-top: var(--border);
510526
padding: 4px 0;
511527
word-wrap: break-word;
528+
white-space: pre;
512529
}
513530
.log-entry:first-child {
514531
border-top: none;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
layout: '@layouts/Guide.astro'
3+
pageType: 'API Reference'
4+
title: HTML Utilities
5+
icon: code
6+
description: API reference for HTML and text formatting utility functions
7+
---
8+
9+
The HTML utilities provide functions for indenting text and HTML markup. These functions are useful for formatting code snippets, cleaning up HTML extracted from template literals, and preparing markup for documentation.
10+
11+
## Functions
12+
13+
### indentLines
14+
15+
```javascript
16+
function indentLines(text, spaces = 2)
17+
```
18+
19+
Adds consistent indentation to every line of text.
20+
21+
#### Parameters
22+
23+
| Name | Type | Description |
24+
|--------|--------|-------------|
25+
| text | string | The text to indent |
26+
| spaces | number | Number of spaces to indent each line (default: 2) |
27+
28+
#### Returns
29+
30+
The indented text. Returns empty string for non-string input.
31+
32+
#### Example
33+
34+
```javascript
35+
import { indentLines } from '@semantic-ui/utils';
36+
37+
const code = 'line 1\nline 2\nline 3';
38+
console.log(indentLines(code)); // ' line 1\n line 2\n line 3'
39+
console.log(indentLines(code, 4)); // ' line 1\n line 2\n line 3'
40+
41+
// useful for template processing
42+
const template = `function example() {
43+
return true;
44+
}`;
45+
console.log(indentLines(template, 2));
46+
```
47+
48+
### indentHTML
49+
50+
```javascript
51+
function indentHTML(html, { indent = ' ', startLevel = 0, trimEmptyLines = true } = {})
52+
```
53+
54+
Intelligently indents HTML markup with proper nesting awareness. Handles void elements, self-closing tags, and comments correctly.
55+
56+
> **Use Case** Perfect for cleaning up HTML extracted from JavaScript template literals where indentation gets messy, or for formatting documentation examples.
57+
58+
#### Parameters
59+
60+
| Name | Type | Description |
61+
|---------|--------|-------------|
62+
| html | string | The HTML string to indent |
63+
| options | object | Optional configuration |
64+
65+
##### Options
66+
67+
| Name | Type | Default | Description |
68+
|----------------|---------|---------|-------------|
69+
| indent | string | ' ' | String to use for each level of indentation |
70+
| startLevel | number | 0 | Initial nesting level to start indentation from |
71+
| trimEmptyLines | boolean | true | Whether to remove empty lines from the output |
72+
73+
#### Returns
74+
75+
The properly indented HTML. Returns empty string for non-string input.
76+
77+
#### Features
78+
79+
- **Nesting-aware** - Tracks depth and indents accordingly
80+
- **Void elements** - Handles `img`, `br`, `hr`, `input`, etc. without increasing depth
81+
- **Self-closing tags** - Recognizes `<component />` syntax
82+
- **Comments** - Preserves `<!-- -->` comments without affecting depth
83+
- **Same-line tags** - Detects `<tag>content</tag>` patterns on single lines
84+
85+
#### Limitations
86+
87+
- Works best with one tag per line (typical documentation format)
88+
- Multiple tags on same line are not split
89+
- Designed for well-formed HTML snippets, not error correction
90+
91+
#### Example
92+
93+
```javascript
94+
import { indentHTML } from '@semantic-ui/utils';
95+
96+
// basic usage
97+
const html = '<div>\n<p>Content</p>\n</div>';
98+
console.log(indentHTML(html));
99+
// <div>
100+
// <p>Content</p>
101+
// </div>
102+
103+
// nested structures
104+
const nested = `<div class="ui segment">
105+
<div class="ui header">Title</div>
106+
<p>Content here</p>
107+
<div class="ui list">
108+
<div class="item">
109+
<img src="image.jpg" />
110+
<div class="content">Item 1</div>
111+
</div>
112+
</div>
113+
</div>`;
114+
115+
console.log(indentHTML(nested));
116+
// <div class="ui segment">
117+
// <div class="ui header">Title</div>
118+
// <p>Content here</p>
119+
// <div class="ui list">
120+
// <div class="item">
121+
// <img src="image.jpg" />
122+
// <div class="content">Item 1</div>
123+
// </div>
124+
// </div>
125+
// </div>
126+
127+
// custom indentation
128+
console.log(indentHTML(html, { indent: ' ' })); // 4 spaces
129+
console.log(indentHTML(html, { indent: '\t' })); // tabs
130+
131+
// start at nesting level
132+
console.log(indentHTML(html, { startLevel: 1 }));
133+
// <div>
134+
// <p>Content</p>
135+
// </div>
136+
137+
// void elements
138+
const voidElements = '<div>\n<img src="test.jpg">\n<br>\n<input type="text">\n</div>';
139+
console.log(indentHTML(voidElements));
140+
// <div>
141+
// <img src="test.jpg">
142+
// <br>
143+
// <input type="text">
144+
// </div>
145+
```

0 commit comments

Comments
 (0)