@akinon/ui-editor 1.0.9 → 1.1.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/dist/cjs/tiptap/extensions/__tests__/comment-extension.test.js +8 -4
- package/dist/cjs/tiptap/extensions/__tests__/comment-extension.test.ts +10 -4
- package/dist/cjs/tiptap/utils/tiptap-extensions.js +1 -1
- package/dist/esm/tiptap/extensions/__tests__/comment-extension.test.js +8 -4
- package/dist/esm/tiptap/extensions/__tests__/comment-extension.test.ts +10 -4
- package/dist/esm/tiptap/utils/tiptap-extensions.js +1 -1
- package/package.json +9 -9
- package/dist/cjs/tiptap/utils/__tests__/format-html.test.d.ts +0 -2
- package/dist/cjs/tiptap/utils/__tests__/format-html.test.d.ts.map +0 -1
- package/dist/cjs/tiptap/utils/__tests__/format-html.test.js +0 -68
- package/dist/cjs/tiptap/utils/__tests__/format-html.test.ts +0 -91
- package/dist/cjs/tiptap/utils/__tests__/normalize-html.test.d.ts +0 -2
- package/dist/cjs/tiptap/utils/__tests__/normalize-html.test.d.ts.map +0 -1
- package/dist/cjs/tiptap/utils/__tests__/normalize-html.test.js +0 -53
- package/dist/cjs/tiptap/utils/__tests__/normalize-html.test.ts +0 -77
- package/dist/cjs/tiptap/utils/format-html.d.ts +0 -6
- package/dist/cjs/tiptap/utils/format-html.d.ts.map +0 -1
- package/dist/cjs/tiptap/utils/format-html.js +0 -108
- package/dist/cjs/tiptap/utils/normalize-html.d.ts +0 -7
- package/dist/cjs/tiptap/utils/normalize-html.d.ts.map +0 -1
- package/dist/cjs/tiptap/utils/normalize-html.js +0 -109
- package/dist/esm/tiptap/utils/__tests__/format-html.test.d.ts +0 -2
- package/dist/esm/tiptap/utils/__tests__/format-html.test.d.ts.map +0 -1
- package/dist/esm/tiptap/utils/__tests__/format-html.test.js +0 -68
- package/dist/esm/tiptap/utils/__tests__/format-html.test.ts +0 -91
- package/dist/esm/tiptap/utils/__tests__/normalize-html.test.d.ts +0 -2
- package/dist/esm/tiptap/utils/__tests__/normalize-html.test.d.ts.map +0 -1
- package/dist/esm/tiptap/utils/__tests__/normalize-html.test.js +0 -53
- package/dist/esm/tiptap/utils/__tests__/normalize-html.test.ts +0 -77
- package/dist/esm/tiptap/utils/format-html.d.ts +0 -6
- package/dist/esm/tiptap/utils/format-html.d.ts.map +0 -1
- package/dist/esm/tiptap/utils/format-html.js +0 -108
- package/dist/esm/tiptap/utils/normalize-html.d.ts +0 -7
- package/dist/esm/tiptap/utils/normalize-html.d.ts.map +0 -1
- package/dist/esm/tiptap/utils/normalize-html.js +0 -109
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { HTML_ELEMENTS } from '../constants';
|
|
2
|
-
/**
|
|
3
|
-
* Normalizes HTML by removing formatting whitespace (newlines and indentation)
|
|
4
|
-
* while preserving meaningful whitespace within text content.
|
|
5
|
-
* Used when converting from source mode back to visual editor.
|
|
6
|
-
*/
|
|
7
|
-
export const normalizeHtml = (html) => {
|
|
8
|
-
const div = document.createElement(HTML_ELEMENTS.DIV);
|
|
9
|
-
div.innerHTML = html;
|
|
10
|
-
cleanScriptsAndStyles(div);
|
|
11
|
-
convertCommentsToSpans(div);
|
|
12
|
-
unwrapHrFromParagraphs(div);
|
|
13
|
-
flattenEmptyParagraphs(div);
|
|
14
|
-
mergeConsecutiveBreaks(div);
|
|
15
|
-
mergeConsecutiveBreaks(div);
|
|
16
|
-
return div.innerHTML;
|
|
17
|
-
};
|
|
18
|
-
const convertCommentsToSpans = (root) => {
|
|
19
|
-
const iterator = document.createNodeIterator(root, NodeFilter.SHOW_COMMENT, null);
|
|
20
|
-
let currentNode;
|
|
21
|
-
const comments = [];
|
|
22
|
-
while ((currentNode = iterator.nextNode())) {
|
|
23
|
-
comments.push(currentNode);
|
|
24
|
-
}
|
|
25
|
-
comments.forEach(comment => {
|
|
26
|
-
var _a;
|
|
27
|
-
const span = document.createElement(HTML_ELEMENTS.SPAN);
|
|
28
|
-
span.setAttribute('data-type', 'comment');
|
|
29
|
-
span.setAttribute('data-content', comment.textContent || '');
|
|
30
|
-
(_a = comment.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(span, comment);
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
const cleanScriptsAndStyles = (root) => {
|
|
34
|
-
const scripts = root.querySelectorAll(`${HTML_ELEMENTS.SCRIPT}, ${HTML_ELEMENTS.STYLE}`);
|
|
35
|
-
scripts.forEach(el => {
|
|
36
|
-
const content = el.textContent;
|
|
37
|
-
if (content && content.trim()) {
|
|
38
|
-
el.textContent = dedent(content);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
const unwrapHrFromParagraphs = (root) => {
|
|
43
|
-
const hrs = root.querySelectorAll(HTML_ELEMENTS.HR);
|
|
44
|
-
hrs.forEach(hr => {
|
|
45
|
-
var _a;
|
|
46
|
-
const parent = hr.parentElement;
|
|
47
|
-
if (parent && parent.tagName.toLowerCase() === HTML_ELEMENTS.P) {
|
|
48
|
-
// If HR is the only child (or only meaningful child), replace the p with hr
|
|
49
|
-
const siblings = Array.from(parent.childNodes).filter(node => {
|
|
50
|
-
var _a;
|
|
51
|
-
return node !== hr &&
|
|
52
|
-
!(node.nodeType === Node.TEXT_NODE && !((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim()));
|
|
53
|
-
});
|
|
54
|
-
if (siblings.length === 0) {
|
|
55
|
-
(_a = parent.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(hr, parent);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
const flattenEmptyParagraphs = (root) => {
|
|
61
|
-
// Remove empty paragraphs that only contain <br>
|
|
62
|
-
const paragraphs = root.querySelectorAll(HTML_ELEMENTS.P);
|
|
63
|
-
paragraphs.forEach(p => {
|
|
64
|
-
var _a;
|
|
65
|
-
const hasOnlyBrOrWhitespace = Array.from(p.childNodes).every(child => {
|
|
66
|
-
var _a;
|
|
67
|
-
return (child.nodeType === Node.ELEMENT_NODE &&
|
|
68
|
-
child.tagName.toLowerCase() === HTML_ELEMENTS.BR) ||
|
|
69
|
-
(child.nodeType === Node.TEXT_NODE && !((_a = child.textContent) === null || _a === void 0 ? void 0 : _a.trim()));
|
|
70
|
-
});
|
|
71
|
-
if (hasOnlyBrOrWhitespace && p.childNodes.length > 0) {
|
|
72
|
-
// Replace <p><br></p> with just <br>
|
|
73
|
-
const br = document.createElement(HTML_ELEMENTS.BR);
|
|
74
|
-
(_a = p.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(br, p);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const mergeConsecutiveBreaks = (root) => {
|
|
79
|
-
const brs = root.querySelectorAll(HTML_ELEMENTS.BR);
|
|
80
|
-
brs.forEach(br => {
|
|
81
|
-
var _a, _b;
|
|
82
|
-
let next = br.nextSibling;
|
|
83
|
-
// Skip whitespace nodes
|
|
84
|
-
while (next &&
|
|
85
|
-
next.nodeType === Node.TEXT_NODE &&
|
|
86
|
-
!((_a = next.textContent) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
87
|
-
next = next.nextSibling;
|
|
88
|
-
}
|
|
89
|
-
// If next is also a br, remove this one
|
|
90
|
-
if (next &&
|
|
91
|
-
next.nodeType === Node.ELEMENT_NODE &&
|
|
92
|
-
next.tagName.toLowerCase() === HTML_ELEMENTS.BR) {
|
|
93
|
-
(_b = br.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(br);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
const dedent = (str) => {
|
|
98
|
-
const lines = str.split('\n');
|
|
99
|
-
const nonEmptyLines = lines.filter(line => line.trim());
|
|
100
|
-
const LEADING_WHITESPACE_REGEX = /^\s*/;
|
|
101
|
-
const minIndent = nonEmptyLines.reduce((min, line) => {
|
|
102
|
-
const indent = line.match(LEADING_WHITESPACE_REGEX)[0].length;
|
|
103
|
-
return Math.min(min, indent);
|
|
104
|
-
}, Infinity);
|
|
105
|
-
return lines
|
|
106
|
-
.map(line => (line.length >= minIndent ? line.slice(minIndent) : line))
|
|
107
|
-
.join('\n')
|
|
108
|
-
.trim();
|
|
109
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"format-html.test.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/utils/__tests__/format-html.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { formatHtml } from '../format-html';
|
|
3
|
-
describe('formatHtml', () => {
|
|
4
|
-
it('should format simple nested divs with indentation', () => {
|
|
5
|
-
const input = '<div><div>content</div></div>';
|
|
6
|
-
const expected = `
|
|
7
|
-
<div>
|
|
8
|
-
<div>content</div>
|
|
9
|
-
</div>
|
|
10
|
-
`.trim();
|
|
11
|
-
expect(formatHtml(input)).toBe(expected);
|
|
12
|
-
});
|
|
13
|
-
it('should handle attributes correctly', () => {
|
|
14
|
-
const input = '<div class="foo" id="bar">content</div>';
|
|
15
|
-
expect(formatHtml(input)).toBe(input.trim());
|
|
16
|
-
});
|
|
17
|
-
it('should handle void elements like br and hr', () => {
|
|
18
|
-
const input = '<div>text<br />more text<hr /></div>';
|
|
19
|
-
const output = formatHtml(input);
|
|
20
|
-
expect(output).toContain('<br />');
|
|
21
|
-
expect(output).toContain('<hr />');
|
|
22
|
-
});
|
|
23
|
-
it('should preserve whitespace in pre tags', () => {
|
|
24
|
-
const input = '<pre> keep this space </pre>';
|
|
25
|
-
expect(formatHtml(input)).toBe(input.trim());
|
|
26
|
-
});
|
|
27
|
-
it('should ensure newline before block elements', () => {
|
|
28
|
-
const input = '<span>text</span><div>block</div>';
|
|
29
|
-
const expected = `
|
|
30
|
-
<span>text</span>
|
|
31
|
-
<div>block</div>
|
|
32
|
-
`.trim();
|
|
33
|
-
expect(formatHtml(input)).toBe(expected);
|
|
34
|
-
});
|
|
35
|
-
it('should dedent script content', () => {
|
|
36
|
-
const input = `<script>
|
|
37
|
-
function foo() {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
</script>`;
|
|
41
|
-
const output = formatHtml(input);
|
|
42
|
-
expect(output).toContain('function foo() {');
|
|
43
|
-
expect(output).toContain(' return true;');
|
|
44
|
-
});
|
|
45
|
-
it('should handle empty script tags', () => {
|
|
46
|
-
const input = '<script></script>';
|
|
47
|
-
expect(formatHtml(input)).toBe(input);
|
|
48
|
-
});
|
|
49
|
-
it('should convert comment spans back to HTML comments', () => {
|
|
50
|
-
const input = '<span data-type="comment" data-content=" my comment "></span>';
|
|
51
|
-
const expected = '<!-- my comment -->';
|
|
52
|
-
expect(formatHtml(input)).toBe(expected);
|
|
53
|
-
const inputMissingContent = '<span data-type="comment"></span>';
|
|
54
|
-
const expectedMissingContent = '<!---->';
|
|
55
|
-
expect(formatHtml(inputMissingContent)).toBe(expectedMissingContent);
|
|
56
|
-
});
|
|
57
|
-
it('should handle scripts with empty lines', () => {
|
|
58
|
-
const input = `<script>
|
|
59
|
-
var a = 1;
|
|
60
|
-
|
|
61
|
-
var b = 2;
|
|
62
|
-
</script>`;
|
|
63
|
-
const output = formatHtml(input);
|
|
64
|
-
expect(output).toContain('var a = 1;');
|
|
65
|
-
expect(output).toContain('var b = 2;');
|
|
66
|
-
expect(output).toMatch(/;\n\s*var b/);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { formatHtml } from '../format-html';
|
|
4
|
-
|
|
5
|
-
describe('formatHtml', () => {
|
|
6
|
-
it('should format simple nested divs with indentation', () => {
|
|
7
|
-
const input = '<div><div>content</div></div>';
|
|
8
|
-
const expected = `
|
|
9
|
-
<div>
|
|
10
|
-
<div>content</div>
|
|
11
|
-
</div>
|
|
12
|
-
`.trim();
|
|
13
|
-
|
|
14
|
-
expect(formatHtml(input)).toBe(expected);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should handle attributes correctly', () => {
|
|
18
|
-
const input = '<div class="foo" id="bar">content</div>';
|
|
19
|
-
|
|
20
|
-
expect(formatHtml(input)).toBe(input.trim());
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should handle void elements like br and hr', () => {
|
|
24
|
-
const input = '<div>text<br />more text<hr /></div>';
|
|
25
|
-
const output = formatHtml(input);
|
|
26
|
-
|
|
27
|
-
expect(output).toContain('<br />');
|
|
28
|
-
expect(output).toContain('<hr />');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should preserve whitespace in pre tags', () => {
|
|
32
|
-
const input = '<pre> keep this space </pre>';
|
|
33
|
-
|
|
34
|
-
expect(formatHtml(input)).toBe(input.trim());
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should ensure newline before block elements', () => {
|
|
38
|
-
const input = '<span>text</span><div>block</div>';
|
|
39
|
-
const expected = `
|
|
40
|
-
<span>text</span>
|
|
41
|
-
<div>block</div>
|
|
42
|
-
`.trim();
|
|
43
|
-
expect(formatHtml(input)).toBe(expected);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should dedent script content', () => {
|
|
47
|
-
const input = `<script>
|
|
48
|
-
function foo() {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
</script>`;
|
|
52
|
-
|
|
53
|
-
const output = formatHtml(input);
|
|
54
|
-
|
|
55
|
-
expect(output).toContain('function foo() {');
|
|
56
|
-
expect(output).toContain(' return true;');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should handle empty script tags', () => {
|
|
60
|
-
const input = '<script></script>';
|
|
61
|
-
|
|
62
|
-
expect(formatHtml(input)).toBe(input);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should convert comment spans back to HTML comments', () => {
|
|
66
|
-
const input =
|
|
67
|
-
'<span data-type="comment" data-content=" my comment "></span>';
|
|
68
|
-
const expected = '<!-- my comment -->';
|
|
69
|
-
|
|
70
|
-
expect(formatHtml(input)).toBe(expected);
|
|
71
|
-
|
|
72
|
-
const inputMissingContent = '<span data-type="comment"></span>';
|
|
73
|
-
const expectedMissingContent = '<!---->';
|
|
74
|
-
|
|
75
|
-
expect(formatHtml(inputMissingContent)).toBe(expectedMissingContent);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should handle scripts with empty lines', () => {
|
|
79
|
-
const input = `<script>
|
|
80
|
-
var a = 1;
|
|
81
|
-
|
|
82
|
-
var b = 2;
|
|
83
|
-
</script>`;
|
|
84
|
-
|
|
85
|
-
const output = formatHtml(input);
|
|
86
|
-
|
|
87
|
-
expect(output).toContain('var a = 1;');
|
|
88
|
-
expect(output).toContain('var b = 2;');
|
|
89
|
-
expect(output).toMatch(/;\n\s*var b/);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-html.test.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/utils/__tests__/normalize-html.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { normalizeHtml } from '../normalize-html';
|
|
3
|
-
describe('normalizeHtml', () => {
|
|
4
|
-
it('should convert HTML comments to span placeholders', () => {
|
|
5
|
-
const input = '<div><!-- my comment --></div>';
|
|
6
|
-
const output = normalizeHtml(input);
|
|
7
|
-
expect(output).toContain('<span data-type="comment" data-content=" my comment "></span>');
|
|
8
|
-
const inputEmpty = '<!---->';
|
|
9
|
-
expect(normalizeHtml(inputEmpty)).toContain('<span data-type="comment" data-content=""></span>');
|
|
10
|
-
});
|
|
11
|
-
it('should dedent script tags to avoid extra indentation', () => {
|
|
12
|
-
const input = `
|
|
13
|
-
<script>
|
|
14
|
-
console.log('test');
|
|
15
|
-
</script>
|
|
16
|
-
`;
|
|
17
|
-
const output = normalizeHtml(input);
|
|
18
|
-
expect(output).toContain("console.log('test');");
|
|
19
|
-
});
|
|
20
|
-
it('should flatten empty paragraphs containing only breaks', () => {
|
|
21
|
-
const input = '<p><br></p>';
|
|
22
|
-
const output = normalizeHtml(input);
|
|
23
|
-
expect(output).toBe('<br>');
|
|
24
|
-
});
|
|
25
|
-
it('should merge consecutive break tags if they are excessive', () => {
|
|
26
|
-
const input = 'Line<br><br>Line';
|
|
27
|
-
const output = normalizeHtml(input);
|
|
28
|
-
expect(output).toBe('Line<br>Line');
|
|
29
|
-
});
|
|
30
|
-
it('should merge consecutive break tags separated by whitespace', () => {
|
|
31
|
-
const input = 'Line<br> <br>Line';
|
|
32
|
-
const output = normalizeHtml(input);
|
|
33
|
-
expect(output).toBe('Line <br>Line');
|
|
34
|
-
});
|
|
35
|
-
it('should unwrap HR elements from paragraphs', () => {
|
|
36
|
-
const input = '<p><hr></p>';
|
|
37
|
-
const output = normalizeHtml(input);
|
|
38
|
-
expect(output).toBe('<hr>');
|
|
39
|
-
const inputWithSpace = '<p> <hr> </p>';
|
|
40
|
-
const outputWithSpace = normalizeHtml(inputWithSpace);
|
|
41
|
-
expect(outputWithSpace).toBe('<hr>');
|
|
42
|
-
});
|
|
43
|
-
it('should NOT unwrap HR from paragraphs if there is other content', () => {
|
|
44
|
-
const input = '<p>Content<hr></p>';
|
|
45
|
-
const output = normalizeHtml(input);
|
|
46
|
-
expect(output).toBe('<p>Content<hr></p>');
|
|
47
|
-
});
|
|
48
|
-
it('should flatten empty paragraphs containing whitespace and breaks', () => {
|
|
49
|
-
const input = '<p> <br> </p>';
|
|
50
|
-
const output = normalizeHtml(input);
|
|
51
|
-
expect(output).toBe('<br>');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { normalizeHtml } from '../normalize-html';
|
|
4
|
-
|
|
5
|
-
describe('normalizeHtml', () => {
|
|
6
|
-
it('should convert HTML comments to span placeholders', () => {
|
|
7
|
-
const input = '<div><!-- my comment --></div>';
|
|
8
|
-
const output = normalizeHtml(input);
|
|
9
|
-
|
|
10
|
-
expect(output).toContain(
|
|
11
|
-
'<span data-type="comment" data-content=" my comment "></span>'
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
const inputEmpty = '<!---->';
|
|
15
|
-
expect(normalizeHtml(inputEmpty)).toContain(
|
|
16
|
-
'<span data-type="comment" data-content=""></span>'
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should dedent script tags to avoid extra indentation', () => {
|
|
21
|
-
const input = `
|
|
22
|
-
<script>
|
|
23
|
-
console.log('test');
|
|
24
|
-
</script>
|
|
25
|
-
`;
|
|
26
|
-
const output = normalizeHtml(input);
|
|
27
|
-
|
|
28
|
-
expect(output).toContain("console.log('test');");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should flatten empty paragraphs containing only breaks', () => {
|
|
32
|
-
const input = '<p><br></p>';
|
|
33
|
-
const output = normalizeHtml(input);
|
|
34
|
-
|
|
35
|
-
expect(output).toBe('<br>');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should merge consecutive break tags if they are excessive', () => {
|
|
39
|
-
const input = 'Line<br><br>Line';
|
|
40
|
-
const output = normalizeHtml(input);
|
|
41
|
-
|
|
42
|
-
expect(output).toBe('Line<br>Line');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should merge consecutive break tags separated by whitespace', () => {
|
|
46
|
-
const input = 'Line<br> <br>Line';
|
|
47
|
-
const output = normalizeHtml(input);
|
|
48
|
-
|
|
49
|
-
expect(output).toBe('Line <br>Line');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should unwrap HR elements from paragraphs', () => {
|
|
53
|
-
const input = '<p><hr></p>';
|
|
54
|
-
const output = normalizeHtml(input);
|
|
55
|
-
|
|
56
|
-
expect(output).toBe('<hr>');
|
|
57
|
-
|
|
58
|
-
const inputWithSpace = '<p> <hr> </p>';
|
|
59
|
-
const outputWithSpace = normalizeHtml(inputWithSpace);
|
|
60
|
-
|
|
61
|
-
expect(outputWithSpace).toBe('<hr>');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should NOT unwrap HR from paragraphs if there is other content', () => {
|
|
65
|
-
const input = '<p>Content<hr></p>';
|
|
66
|
-
const output = normalizeHtml(input);
|
|
67
|
-
|
|
68
|
-
expect(output).toBe('<p>Content<hr></p>');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should flatten empty paragraphs containing whitespace and breaks', () => {
|
|
72
|
-
const input = '<p> <br> </p>';
|
|
73
|
-
const output = normalizeHtml(input);
|
|
74
|
-
|
|
75
|
-
expect(output).toBe('<br>');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"format-html.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/utils/format-html.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAQzC,CAAC"}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { BLOCK_ELEMENTS, HTML_ELEMENTS, VOID_ELEMENTS } from '../constants';
|
|
2
|
-
const TAB = ' ';
|
|
3
|
-
const NEWLINE = '\n';
|
|
4
|
-
/**
|
|
5
|
-
* Formats HTML string with proper indentation and cleaner structure.
|
|
6
|
-
* Uses browser DOM parsing to ensure valid HTML structure before formatting.
|
|
7
|
-
*/
|
|
8
|
-
export const formatHtml = (html) => {
|
|
9
|
-
const div = document.createElement(HTML_ELEMENTS.DIV);
|
|
10
|
-
div.innerHTML = html.trim();
|
|
11
|
-
const formatted = formatNode(div, 0).trim();
|
|
12
|
-
// Normalize multiple newlines to single newline
|
|
13
|
-
return formatted.replace(/\n\n+/g, NEWLINE);
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Removes common leading whitespace from a multi-line string.
|
|
17
|
-
*/
|
|
18
|
-
const dedent = (str) => {
|
|
19
|
-
const lines = str.split(NEWLINE);
|
|
20
|
-
const nonEmptyLines = lines.filter(line => line.trim());
|
|
21
|
-
const minIndent = nonEmptyLines.reduce((min, line) => {
|
|
22
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
23
|
-
return Math.min(min, indent);
|
|
24
|
-
}, Infinity);
|
|
25
|
-
return lines
|
|
26
|
-
.map(line => (line.length >= minIndent ? line.slice(minIndent) : line))
|
|
27
|
-
.join(NEWLINE)
|
|
28
|
-
.trim();
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Recursive function to traverse and format DOM nodes.
|
|
32
|
-
*/
|
|
33
|
-
const formatNode = (node, level) => {
|
|
34
|
-
const indentStr = TAB.repeat(level);
|
|
35
|
-
let result = '';
|
|
36
|
-
Array.from(node.childNodes).forEach(child => {
|
|
37
|
-
if (child.nodeType === Node.TEXT_NODE) {
|
|
38
|
-
result += formatTextNode(child);
|
|
39
|
-
}
|
|
40
|
-
else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
41
|
-
result += formatElementNode(child, level, indentStr);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return result;
|
|
45
|
-
};
|
|
46
|
-
const formatTextNode = (node) => {
|
|
47
|
-
return node.textContent;
|
|
48
|
-
};
|
|
49
|
-
const formatElementNode = (element, level, indentStr) => {
|
|
50
|
-
const tagName = element.tagName.toLowerCase();
|
|
51
|
-
const isSpan = tagName === HTML_ELEMENTS.SPAN;
|
|
52
|
-
const isComment = element.getAttribute('data-type') === 'comment';
|
|
53
|
-
if (isSpan && isComment) {
|
|
54
|
-
const content = element.getAttribute('data-content') || '';
|
|
55
|
-
return `<!--${content}-->`;
|
|
56
|
-
}
|
|
57
|
-
const isBlock = BLOCK_ELEMENTS.includes(tagName);
|
|
58
|
-
const isVoid = VOID_ELEMENTS.includes(tagName);
|
|
59
|
-
let output = '';
|
|
60
|
-
// Handle block element spacing
|
|
61
|
-
if (isBlock) {
|
|
62
|
-
output += NEWLINE;
|
|
63
|
-
output += indentStr;
|
|
64
|
-
}
|
|
65
|
-
// Construct opening tag with attributes
|
|
66
|
-
const attrs = Array.from(element.attributes)
|
|
67
|
-
.map(attr => ` ${attr.name}="${attr.value}"`)
|
|
68
|
-
.join('');
|
|
69
|
-
const openingTag = `<${tagName}${attrs}${isVoid ? ' />' : '>'}`;
|
|
70
|
-
output += openingTag;
|
|
71
|
-
// Handle content
|
|
72
|
-
if (tagName === HTML_ELEMENTS.PRE) {
|
|
73
|
-
output += element.innerHTML;
|
|
74
|
-
output += `</${tagName}>`;
|
|
75
|
-
}
|
|
76
|
-
else if (!isVoid) {
|
|
77
|
-
output += getElementChildren(element, tagName, level, isBlock, indentStr);
|
|
78
|
-
output += `</${tagName}>`;
|
|
79
|
-
}
|
|
80
|
-
// Close block element with newline
|
|
81
|
-
if (isBlock) {
|
|
82
|
-
output += NEWLINE;
|
|
83
|
-
}
|
|
84
|
-
return output;
|
|
85
|
-
};
|
|
86
|
-
const getElementChildren = (element, tagName, level, isBlock, indentStr) => {
|
|
87
|
-
const isScriptOrStyle = tagName === HTML_ELEMENTS.SCRIPT || tagName === HTML_ELEMENTS.STYLE;
|
|
88
|
-
const content = isScriptOrStyle
|
|
89
|
-
? formatScriptOrStyleContent(element.textContent || '', indentStr)
|
|
90
|
-
: formatNode(element, isBlock ? level + 1 : level);
|
|
91
|
-
if (!content)
|
|
92
|
-
return '';
|
|
93
|
-
const hasBlockChildren = content.includes(NEWLINE) || isScriptOrStyle;
|
|
94
|
-
if (isBlock && hasBlockChildren) {
|
|
95
|
-
return `${NEWLINE}${content}${NEWLINE}${indentStr}`;
|
|
96
|
-
}
|
|
97
|
-
return content;
|
|
98
|
-
};
|
|
99
|
-
const formatScriptOrStyleContent = (content, baseIndent) => {
|
|
100
|
-
if (!content.trim())
|
|
101
|
-
return '';
|
|
102
|
-
const dedented = dedent(content);
|
|
103
|
-
// Indent each line relative to the parent script/style tag
|
|
104
|
-
return dedented
|
|
105
|
-
.split(NEWLINE)
|
|
106
|
-
.map(line => (line ? `${baseIndent}${TAB}${line}` : ''))
|
|
107
|
-
.join(NEWLINE);
|
|
108
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalizes HTML by removing formatting whitespace (newlines and indentation)
|
|
3
|
-
* while preserving meaningful whitespace within text content.
|
|
4
|
-
* Used when converting from source mode back to visual editor.
|
|
5
|
-
*/
|
|
6
|
-
export declare const normalizeHtml: (html: string) => string;
|
|
7
|
-
//# sourceMappingURL=normalize-html.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-html.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/utils/normalize-html.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAY5C,CAAC"}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { HTML_ELEMENTS } from '../constants';
|
|
2
|
-
/**
|
|
3
|
-
* Normalizes HTML by removing formatting whitespace (newlines and indentation)
|
|
4
|
-
* while preserving meaningful whitespace within text content.
|
|
5
|
-
* Used when converting from source mode back to visual editor.
|
|
6
|
-
*/
|
|
7
|
-
export const normalizeHtml = (html) => {
|
|
8
|
-
const div = document.createElement(HTML_ELEMENTS.DIV);
|
|
9
|
-
div.innerHTML = html;
|
|
10
|
-
cleanScriptsAndStyles(div);
|
|
11
|
-
convertCommentsToSpans(div);
|
|
12
|
-
unwrapHrFromParagraphs(div);
|
|
13
|
-
flattenEmptyParagraphs(div);
|
|
14
|
-
mergeConsecutiveBreaks(div);
|
|
15
|
-
mergeConsecutiveBreaks(div);
|
|
16
|
-
return div.innerHTML;
|
|
17
|
-
};
|
|
18
|
-
const convertCommentsToSpans = (root) => {
|
|
19
|
-
const iterator = document.createNodeIterator(root, NodeFilter.SHOW_COMMENT, null);
|
|
20
|
-
let currentNode;
|
|
21
|
-
const comments = [];
|
|
22
|
-
while ((currentNode = iterator.nextNode())) {
|
|
23
|
-
comments.push(currentNode);
|
|
24
|
-
}
|
|
25
|
-
comments.forEach(comment => {
|
|
26
|
-
var _a;
|
|
27
|
-
const span = document.createElement(HTML_ELEMENTS.SPAN);
|
|
28
|
-
span.setAttribute('data-type', 'comment');
|
|
29
|
-
span.setAttribute('data-content', comment.textContent || '');
|
|
30
|
-
(_a = comment.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(span, comment);
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
const cleanScriptsAndStyles = (root) => {
|
|
34
|
-
const scripts = root.querySelectorAll(`${HTML_ELEMENTS.SCRIPT}, ${HTML_ELEMENTS.STYLE}`);
|
|
35
|
-
scripts.forEach(el => {
|
|
36
|
-
const content = el.textContent;
|
|
37
|
-
if (content && content.trim()) {
|
|
38
|
-
el.textContent = dedent(content);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
const unwrapHrFromParagraphs = (root) => {
|
|
43
|
-
const hrs = root.querySelectorAll(HTML_ELEMENTS.HR);
|
|
44
|
-
hrs.forEach(hr => {
|
|
45
|
-
var _a;
|
|
46
|
-
const parent = hr.parentElement;
|
|
47
|
-
if (parent && parent.tagName.toLowerCase() === HTML_ELEMENTS.P) {
|
|
48
|
-
// If HR is the only child (or only meaningful child), replace the p with hr
|
|
49
|
-
const siblings = Array.from(parent.childNodes).filter(node => {
|
|
50
|
-
var _a;
|
|
51
|
-
return node !== hr &&
|
|
52
|
-
!(node.nodeType === Node.TEXT_NODE && !((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim()));
|
|
53
|
-
});
|
|
54
|
-
if (siblings.length === 0) {
|
|
55
|
-
(_a = parent.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(hr, parent);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
const flattenEmptyParagraphs = (root) => {
|
|
61
|
-
// Remove empty paragraphs that only contain <br>
|
|
62
|
-
const paragraphs = root.querySelectorAll(HTML_ELEMENTS.P);
|
|
63
|
-
paragraphs.forEach(p => {
|
|
64
|
-
var _a;
|
|
65
|
-
const hasOnlyBrOrWhitespace = Array.from(p.childNodes).every(child => {
|
|
66
|
-
var _a;
|
|
67
|
-
return (child.nodeType === Node.ELEMENT_NODE &&
|
|
68
|
-
child.tagName.toLowerCase() === HTML_ELEMENTS.BR) ||
|
|
69
|
-
(child.nodeType === Node.TEXT_NODE && !((_a = child.textContent) === null || _a === void 0 ? void 0 : _a.trim()));
|
|
70
|
-
});
|
|
71
|
-
if (hasOnlyBrOrWhitespace && p.childNodes.length > 0) {
|
|
72
|
-
// Replace <p><br></p> with just <br>
|
|
73
|
-
const br = document.createElement(HTML_ELEMENTS.BR);
|
|
74
|
-
(_a = p.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(br, p);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const mergeConsecutiveBreaks = (root) => {
|
|
79
|
-
const brs = root.querySelectorAll(HTML_ELEMENTS.BR);
|
|
80
|
-
brs.forEach(br => {
|
|
81
|
-
var _a, _b;
|
|
82
|
-
let next = br.nextSibling;
|
|
83
|
-
// Skip whitespace nodes
|
|
84
|
-
while (next &&
|
|
85
|
-
next.nodeType === Node.TEXT_NODE &&
|
|
86
|
-
!((_a = next.textContent) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
87
|
-
next = next.nextSibling;
|
|
88
|
-
}
|
|
89
|
-
// If next is also a br, remove this one
|
|
90
|
-
if (next &&
|
|
91
|
-
next.nodeType === Node.ELEMENT_NODE &&
|
|
92
|
-
next.tagName.toLowerCase() === HTML_ELEMENTS.BR) {
|
|
93
|
-
(_b = br.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(br);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
const dedent = (str) => {
|
|
98
|
-
const lines = str.split('\n');
|
|
99
|
-
const nonEmptyLines = lines.filter(line => line.trim());
|
|
100
|
-
const LEADING_WHITESPACE_REGEX = /^\s*/;
|
|
101
|
-
const minIndent = nonEmptyLines.reduce((min, line) => {
|
|
102
|
-
const indent = line.match(LEADING_WHITESPACE_REGEX)[0].length;
|
|
103
|
-
return Math.min(min, indent);
|
|
104
|
-
}, Infinity);
|
|
105
|
-
return lines
|
|
106
|
-
.map(line => (line.length >= minIndent ? line.slice(minIndent) : line))
|
|
107
|
-
.join('\n')
|
|
108
|
-
.trim();
|
|
109
|
-
};
|