@nuasite/cms 0.12.0 → 0.12.4
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/editor.js +14185 -14161
- package/package.json +19 -19
- package/src/editor/components/create-page-modal.tsx +1 -1
- package/src/handlers/source-writer.ts +111 -11
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"directory": "packages/astro-cms"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
-
"version": "0.12.
|
|
17
|
+
"version": "0.12.4",
|
|
18
18
|
"module": "src/index.ts",
|
|
19
19
|
"types": "src/index.ts",
|
|
20
20
|
"type": "module",
|
|
@@ -26,25 +26,25 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@astrojs/compiler": "^2.13.
|
|
30
|
-
"@babel/parser": "^7.
|
|
31
|
-
"node-html-parser": "^
|
|
29
|
+
"@astrojs/compiler": "^2.13.1",
|
|
30
|
+
"@babel/parser": "^7.29.0",
|
|
31
|
+
"node-html-parser": "^7.1.0",
|
|
32
32
|
"yaml": "^2.8.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@milkdown/core": "^7.
|
|
36
|
-
"@milkdown/ctx": "^7.
|
|
37
|
-
"@milkdown/plugin-listener": "^7.
|
|
38
|
-
"@milkdown/preset-commonmark": "^7.
|
|
39
|
-
"@milkdown/preset-gfm": "^7.
|
|
40
|
-
"@milkdown/prose": "^7.
|
|
41
|
-
"@milkdown/utils": "^7.
|
|
42
|
-
"@preact/signals": "^2.
|
|
43
|
-
"@tailwindcss/vite": "^4.1
|
|
44
|
-
"@types/bun": "
|
|
35
|
+
"@milkdown/core": "^7.19.0",
|
|
36
|
+
"@milkdown/ctx": "^7.19.0",
|
|
37
|
+
"@milkdown/plugin-listener": "^7.19.0",
|
|
38
|
+
"@milkdown/preset-commonmark": "^7.19.0",
|
|
39
|
+
"@milkdown/preset-gfm": "^7.19.0",
|
|
40
|
+
"@milkdown/prose": "^7.19.0",
|
|
41
|
+
"@milkdown/utils": "^7.19.0",
|
|
42
|
+
"@preact/signals": "^2.8.1",
|
|
43
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
44
|
+
"@types/bun": "1.3.10",
|
|
45
45
|
"clsx": "^2.1.1",
|
|
46
|
-
"marked": "^17.0.
|
|
47
|
-
"preact": "^10.28.
|
|
46
|
+
"marked": "^17.0.3",
|
|
47
|
+
"preact": "^10.28.4",
|
|
48
48
|
"prosemirror-commands": "^1.7.1",
|
|
49
49
|
"prosemirror-inputrules": "^1.5.1",
|
|
50
50
|
"prosemirror-keymap": "^1.2.3",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"prosemirror-state": "^1.4.4",
|
|
55
55
|
"prosemirror-transform": "^1.10.5",
|
|
56
56
|
"prosemirror-view": "^1.41.5",
|
|
57
|
-
"tailwind-merge": "^3.
|
|
57
|
+
"tailwind-merge": "^3.5.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"astro": "^5.
|
|
60
|
+
"astro": "^5.17",
|
|
61
61
|
"typescript": "^5",
|
|
62
|
-
"vite": "^
|
|
62
|
+
"vite": "^7.3.1",
|
|
63
63
|
"@aws-sdk/client-s3": "^3.0.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { NodeType, parse as parseHtml } from 'node-html-parser'
|
|
3
4
|
import { getProjectRoot } from '../config'
|
|
4
5
|
import type { AttributeChangePayload, ChangePayload, SaveBatchRequest } from '../editor/types'
|
|
5
6
|
import type { ManifestWriter } from '../manifest-writer'
|
|
@@ -532,16 +533,11 @@ export function applyTextChange(
|
|
|
532
533
|
const updatedSnippet = sourceSnippet.replace(resolvedOriginal, resolvedNewText)
|
|
533
534
|
|
|
534
535
|
if (updatedSnippet === sourceSnippet) {
|
|
535
|
-
// Try
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const brRegex = new RegExp(regexStr)
|
|
541
|
-
const updatedWithBr = sourceSnippet.replace(brRegex, resolvedNewText)
|
|
542
|
-
if (updatedWithBr !== sourceSnippet) {
|
|
543
|
-
return { success: true, content: content.replace(sourceSnippet, updatedWithBr) }
|
|
544
|
-
}
|
|
536
|
+
// Try AST-based <br> normalization (browser normalizes <br class="..." /> to <br>
|
|
537
|
+
// and collapses surrounding whitespace/indentation)
|
|
538
|
+
const brResult = tryBrNormalizedChange(sourceSnippet, resolvedOriginal, resolvedNewText)
|
|
539
|
+
if (brResult !== null) {
|
|
540
|
+
return { success: true, content: content.replace(sourceSnippet, brResult) }
|
|
545
541
|
}
|
|
546
542
|
|
|
547
543
|
// resolvedOriginal wasn't found in snippet - try HTML entity handling
|
|
@@ -657,7 +653,7 @@ function findTextInSnippet(snippet: string, decodedText: string): string | null
|
|
|
657
653
|
|
|
658
654
|
// Try matching with <br> tags stripped from snippet
|
|
659
655
|
const chars = [...decodedText].map((ch) => escapeRegExp(ch))
|
|
660
|
-
const brAwarePattern = chars.join('(?:<br\\
|
|
656
|
+
const brAwarePattern = chars.join('(?:<br\\b[^>]*\\/?>)*')
|
|
661
657
|
const brRegex = new RegExp(brAwarePattern)
|
|
662
658
|
const brMatch = snippet.match(brRegex)
|
|
663
659
|
|
|
@@ -737,6 +733,110 @@ function findExpressionSrcAttribute(text: string): { index: number; length: numb
|
|
|
737
733
|
}
|
|
738
734
|
}
|
|
739
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Extract visible text from an HTML string the way a browser would render it.
|
|
738
|
+
* Text nodes contribute their content, <br> elements become '\n',
|
|
739
|
+
* and whitespace around '\n' is collapsed (matching browser behavior).
|
|
740
|
+
*/
|
|
741
|
+
function getVisibleText(html: string): string {
|
|
742
|
+
const root = parseHtml(html, { blockTextElements: {} })
|
|
743
|
+
let text = ''
|
|
744
|
+
const walk = (node: ReturnType<typeof parseHtml>) => {
|
|
745
|
+
for (const child of node.childNodes) {
|
|
746
|
+
if (child.nodeType === NodeType.TEXT_NODE) {
|
|
747
|
+
text += child.rawText
|
|
748
|
+
} else if (child.nodeType === NodeType.ELEMENT_NODE && (child as any).rawTagName === 'br') {
|
|
749
|
+
text += '\n'
|
|
750
|
+
} else {
|
|
751
|
+
walk(child as any)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
walk(root)
|
|
756
|
+
// Collapse whitespace around newlines (browser behavior around <br>)
|
|
757
|
+
text = text.replace(/[ \t]*\n[ \t]*/g, '\n')
|
|
758
|
+
return text.trim()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Try to apply a text change when the mismatch is due to <br> normalization.
|
|
763
|
+
* The browser normalizes <br class="..." /> to plain <br> and collapses surrounding whitespace.
|
|
764
|
+
* This function preserves the original <br> elements (with attributes) and surrounding indentation.
|
|
765
|
+
* Returns the updated snippet, or null if this approach doesn't apply.
|
|
766
|
+
*/
|
|
767
|
+
function tryBrNormalizedChange(
|
|
768
|
+
sourceSnippet: string,
|
|
769
|
+
resolvedOriginal: string,
|
|
770
|
+
resolvedNewText: string,
|
|
771
|
+
): string | null {
|
|
772
|
+
// Only applies when the browser text contains <br>
|
|
773
|
+
if (!resolvedOriginal.includes('<br>')) return null
|
|
774
|
+
|
|
775
|
+
// Verify that the visible text matches after normalization
|
|
776
|
+
const sourceVisible = getVisibleText(sourceSnippet)
|
|
777
|
+
const originalVisible = getVisibleText(resolvedOriginal)
|
|
778
|
+
if (sourceVisible !== originalVisible) return null
|
|
779
|
+
|
|
780
|
+
// Split browser text by <br> into segments
|
|
781
|
+
const originalSegments = resolvedOriginal.split('<br>')
|
|
782
|
+
const newSegments = resolvedNewText.split('<br>')
|
|
783
|
+
|
|
784
|
+
// If segment count changed, user added/removed line breaks — let other fallbacks handle it
|
|
785
|
+
if (originalSegments.length !== newSegments.length) return null
|
|
786
|
+
|
|
787
|
+
// Parse the source snippet and identify text nodes and br elements
|
|
788
|
+
const root = parseHtml(sourceSnippet, { blockTextElements: {} })
|
|
789
|
+
|
|
790
|
+
// Find the outer element (e.g., <h1>, <p>)
|
|
791
|
+
const outerElement = root.childNodes.find(
|
|
792
|
+
(n) => n.nodeType === NodeType.ELEMENT_NODE,
|
|
793
|
+
) as any
|
|
794
|
+
if (!outerElement) return null
|
|
795
|
+
|
|
796
|
+
// Collect text nodes between br boundaries
|
|
797
|
+
const groups: Array<Array<{ node: any; index: number }>> = [[]]
|
|
798
|
+
for (let i = 0; i < outerElement.childNodes.length; i++) {
|
|
799
|
+
const child = outerElement.childNodes[i]
|
|
800
|
+
if (child.nodeType === NodeType.ELEMENT_NODE && (child as any).rawTagName === 'br') {
|
|
801
|
+
groups.push([])
|
|
802
|
+
} else if (child.nodeType === NodeType.TEXT_NODE) {
|
|
803
|
+
groups[groups.length - 1]!.push({ node: child, index: i })
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Number of groups should match number of segments
|
|
808
|
+
if (groups.length !== originalSegments.length) return null
|
|
809
|
+
|
|
810
|
+
// Replace text content in each group
|
|
811
|
+
let result = sourceSnippet
|
|
812
|
+
for (let g = groups.length - 1; g >= 0; g--) {
|
|
813
|
+
const group = groups[g]!
|
|
814
|
+
const origSegment = originalSegments[g]!.trim()
|
|
815
|
+
const newSegment = newSegments[g]!.trim()
|
|
816
|
+
|
|
817
|
+
if (origSegment === newSegment) continue
|
|
818
|
+
|
|
819
|
+
// Find the text node in this group that contains the meaningful text
|
|
820
|
+
for (const { node } of group) {
|
|
821
|
+
const raw: string = node.rawText
|
|
822
|
+
const trimmed = raw.trim()
|
|
823
|
+
if (!trimmed) continue
|
|
824
|
+
|
|
825
|
+
// Check if this text node's trimmed content matches the original segment
|
|
826
|
+
if (trimmed === origSegment) {
|
|
827
|
+
// Replace the meaningful text, preserving surrounding whitespace
|
|
828
|
+
const leadingWs = raw.slice(0, raw.indexOf(trimmed))
|
|
829
|
+
const trailingWs = raw.slice(raw.indexOf(trimmed) + trimmed.length)
|
|
830
|
+
const newRaw = leadingWs + newSegment + trailingWs
|
|
831
|
+
result = result.replace(raw, newRaw)
|
|
832
|
+
break
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return result !== sourceSnippet ? result : null
|
|
838
|
+
}
|
|
839
|
+
|
|
740
840
|
function escapeRegExp(string: string): string {
|
|
741
841
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
742
842
|
}
|