@nuasite/cms 0.5.0 → 0.5.1
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 +2126 -2110
- package/package.json +1 -1
- package/src/editor/dom.ts +16 -5
- package/src/editor/editor.ts +18 -0
- package/src/source-finder/snippet-utils.ts +62 -1
package/package.json
CHANGED
package/src/editor/dom.ts
CHANGED
|
@@ -161,16 +161,19 @@ export function isStyledSpan(element: HTMLElement): boolean {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* Note: This returns plain text only - for styled content, use innerHTML directly.
|
|
164
|
+
* Block-level elements that browsers may create inside contentEditable on Enter.
|
|
165
|
+
* These are treated as line breaks when extracting text.
|
|
167
166
|
*/
|
|
167
|
+
const BLOCK_ELEMENTS = new Set(['div', 'p', 'section', 'article', 'header', 'footer', 'blockquote'])
|
|
168
|
+
|
|
168
169
|
function extractTextFromChildNodes(parentNode: HTMLElement): string {
|
|
169
170
|
let text = ''
|
|
170
171
|
|
|
171
172
|
parentNode.childNodes.forEach(node => {
|
|
172
173
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
173
|
-
|
|
174
|
+
// Normalize non-breaking spaces (\u00a0) that browsers insert in
|
|
175
|
+
// contentEditable to regular spaces
|
|
176
|
+
text += (node.nodeValue || '').replace(/\u00a0/g, ' ')
|
|
174
177
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
175
178
|
const element = node as HTMLElement
|
|
176
179
|
const tagName = element.tagName.toLowerCase()
|
|
@@ -186,9 +189,17 @@ function extractTextFromChildNodes(parentNode: HTMLElement): string {
|
|
|
186
189
|
if (directCmsId) {
|
|
187
190
|
// Element has CMS ID - replace with placeholder
|
|
188
191
|
text += `{{cms:${directCmsId}}}`
|
|
192
|
+
} else if (BLOCK_ELEMENTS.has(tagName)) {
|
|
193
|
+
// Block-level elements created by browser on Enter should be
|
|
194
|
+
// treated as line breaks, not collapsed into the text
|
|
195
|
+
const blockText = extractTextFromChildNodes(element)
|
|
196
|
+
if (blockText) {
|
|
197
|
+
// Only add <br> separator if there's already text before this block
|
|
198
|
+
text += (text ? '<br>' : '') + blockText
|
|
199
|
+
}
|
|
189
200
|
} else {
|
|
190
201
|
// For all other elements (including styled spans), just get their text content
|
|
191
|
-
text += element.textContent || ''
|
|
202
|
+
text += (element.textContent || '').replace(/\u00a0/g, ' ')
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
})
|
package/src/editor/editor.ts
CHANGED
|
@@ -154,10 +154,28 @@ export async function startEditMode(
|
|
|
154
154
|
makeElementEditable(el)
|
|
155
155
|
|
|
156
156
|
// Suppress browser native contentEditable undo/redo (we handle it ourselves)
|
|
157
|
+
// Also convert Enter (insertParagraph) to <br> instead of the browser's
|
|
158
|
+
// default behavior which creates <div> elements with characters
|
|
157
159
|
el.addEventListener('beforeinput', (e) => {
|
|
158
160
|
if (e.inputType === 'historyUndo' || e.inputType === 'historyRedo') {
|
|
159
161
|
e.preventDefault()
|
|
160
162
|
}
|
|
163
|
+
if (e.inputType === 'insertParagraph') {
|
|
164
|
+
e.preventDefault()
|
|
165
|
+
const selection = window.getSelection()
|
|
166
|
+
if (selection && selection.rangeCount > 0) {
|
|
167
|
+
const range = selection.getRangeAt(0)
|
|
168
|
+
range.deleteContents()
|
|
169
|
+
const br = document.createElement('br')
|
|
170
|
+
range.insertNode(br)
|
|
171
|
+
range.setStartAfter(br)
|
|
172
|
+
range.collapse(true)
|
|
173
|
+
selection.removeAllRanges()
|
|
174
|
+
selection.addRange(range)
|
|
175
|
+
// Trigger input event for change tracking
|
|
176
|
+
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
161
179
|
})
|
|
162
180
|
|
|
163
181
|
// Setup color tracking for elements with colorClasses in manifest
|
|
@@ -6,7 +6,7 @@ import type { Attribute, ManifestEntry } from '../types'
|
|
|
6
6
|
import { escapeRegex, generateSourceHash } from '../utils'
|
|
7
7
|
import { buildDefinitionPath } from './ast-extractors'
|
|
8
8
|
import { getCachedParsedFile } from './ast-parser'
|
|
9
|
-
import { findAttributeSourceLocation } from './cross-file-tracker'
|
|
9
|
+
import { findAttributeSourceLocation, searchForExpressionProp, searchForPropInParents } from './cross-file-tracker'
|
|
10
10
|
import { findImageElementNearLine, findImageSourceLocation } from './image-finder'
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
@@ -640,6 +640,67 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
640
640
|
sourceHash,
|
|
641
641
|
}] as const
|
|
642
642
|
}
|
|
643
|
+
|
|
644
|
+
// Cross-file search for prop-driven dynamic text
|
|
645
|
+
// When text comes from a prop (e.g., {title} where title = Astro.props.title),
|
|
646
|
+
// trace it to where the prop value is actually defined in a parent component
|
|
647
|
+
if (cached) {
|
|
648
|
+
// Extract expression variables from the snippet to find props
|
|
649
|
+
const exprPattern = /\{(\w+(?:\.\w+|\[\d+\])*)\}/g
|
|
650
|
+
let exprMatch: RegExpExecArray | null
|
|
651
|
+
while ((exprMatch = exprPattern.exec(sourceSnippet)) !== null) {
|
|
652
|
+
const exprPath = exprMatch[1]!
|
|
653
|
+
const baseVar = exprPath.match(/^(\w+)/)?.[1]
|
|
654
|
+
if (baseVar && cached.propAliases.has(baseVar)) {
|
|
655
|
+
const propName = cached.propAliases.get(baseVar)!
|
|
656
|
+
const componentFileName = path.basename(filePath)
|
|
657
|
+
const result = await searchForExpressionProp(
|
|
658
|
+
componentFileName, propName, exprPath, entry.text!,
|
|
659
|
+
)
|
|
660
|
+
if (result) {
|
|
661
|
+
const propSnippet = result.snippet ?? trimmedText
|
|
662
|
+
const propSourceHash = generateSourceHash(propSnippet)
|
|
663
|
+
return [id, {
|
|
664
|
+
...entry,
|
|
665
|
+
sourcePath: result.file,
|
|
666
|
+
sourceLine: result.line,
|
|
667
|
+
sourceSnippet: propSnippet,
|
|
668
|
+
variableName: result.variableName,
|
|
669
|
+
attributes,
|
|
670
|
+
colorClasses,
|
|
671
|
+
sourceHash: propSourceHash,
|
|
672
|
+
}] as const
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Search for quoted prop values in parent components
|
|
678
|
+
// (handles <Component title="literal text" />)
|
|
679
|
+
const srcDir = path.join(getProjectRoot(), 'src')
|
|
680
|
+
for (const searchDir of ['pages', 'components', 'layouts']) {
|
|
681
|
+
try {
|
|
682
|
+
const result = await searchForPropInParents(
|
|
683
|
+
path.join(srcDir, searchDir), trimmedText,
|
|
684
|
+
)
|
|
685
|
+
if (result) {
|
|
686
|
+
const parentSnippet = result.snippet ?? trimmedText
|
|
687
|
+
const propSourceHash = generateSourceHash(parentSnippet)
|
|
688
|
+
return [id, {
|
|
689
|
+
...entry,
|
|
690
|
+
sourcePath: result.file,
|
|
691
|
+
sourceLine: result.line,
|
|
692
|
+
sourceSnippet: parentSnippet,
|
|
693
|
+
variableName: result.variableName,
|
|
694
|
+
attributes,
|
|
695
|
+
colorClasses,
|
|
696
|
+
sourceHash: propSourceHash,
|
|
697
|
+
}] as const
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
// Directory doesn't exist
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
643
704
|
}
|
|
644
705
|
|
|
645
706
|
// Original static content path
|