@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/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.0",
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.0",
30
- "@babel/parser": "^7.24.0",
31
- "node-html-parser": "^6.1.13",
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.8.0",
36
- "@milkdown/ctx": "^7.8.0",
37
- "@milkdown/plugin-listener": "^7.8.0",
38
- "@milkdown/preset-commonmark": "^7.8.0",
39
- "@milkdown/preset-gfm": "^7.8.0",
40
- "@milkdown/prose": "^7.8.0",
41
- "@milkdown/utils": "^7.8.0",
42
- "@preact/signals": "^2.6.1",
43
- "@tailwindcss/vite": "^4.1.11",
44
- "@types/bun": "latest",
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.1",
47
- "preact": "^10.28.0",
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.4.0"
57
+ "tailwind-merge": "^3.5.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "astro": "^5.16.6",
60
+ "astro": "^5.17",
61
61
  "typescript": "^5",
62
- "vite": "^6",
62
+ "vite": "^7.3.1",
63
63
  "@aws-sdk/client-s3": "^3.0.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
@@ -20,7 +20,7 @@ export function CreatePageModal() {
20
20
  openMarkdownEditorForNewPage(col?.name, col)
21
21
  }
22
22
  }
23
- }, [visible, collections])
23
+ }, [collections])
24
24
 
25
25
  const handleClose = () => {
26
26
  resetCreatePageState()
@@ -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 with <br> tag normalization (browser normalizes <br /> to <br>)
536
- const brNorm = (s: string) => s.replace(/<br\s*\/?>/gi, '<br>')
537
- if (brNorm(sourceSnippet).includes(brNorm(resolvedOriginal))) {
538
- const parts = resolvedOriginal.split(/<br\s*\/?>/gi)
539
- const regexStr = parts.map((p) => escapeRegExp(p)).join('<br\\s*\\/?>')
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\\s*\\/?>)*')
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
  }