@nuasite/cms 0.29.0 → 0.30.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/editor.js +11959 -12025
- package/package.json +1 -1
- package/src/editor/components/attribute-editor.tsx +2 -10
- package/src/editor/components/bg-image-overlay.tsx +2 -10
- package/src/editor/components/collections-browser.tsx +5 -13
- package/src/editor/components/color-toolbar.tsx +2 -9
- package/src/editor/components/confirm-dialog.tsx +4 -12
- package/src/editor/components/create-page-modal.tsx +1 -9
- package/src/editor/components/fields.tsx +134 -116
- package/src/editor/components/link-edit-popover.tsx +3 -6
- package/src/editor/components/markdown-editor-overlay.tsx +31 -37
- package/src/editor/components/markdown-inline-editor.tsx +2 -1
- package/src/editor/components/mdx-component-picker.tsx +3 -6
- package/src/editor/components/media-library.tsx +15 -37
- package/src/editor/components/modal-shell.tsx +34 -5
- package/src/editor/components/prop-editor.tsx +67 -68
- package/src/editor/components/reference-picker.tsx +6 -24
- package/src/editor/components/seo-editor.tsx +4 -10
- package/src/editor/components/spinner.tsx +17 -0
- package/src/editor/components/toolbar.tsx +2 -1
- package/src/editor/constants.ts +33 -0
- package/src/editor/hooks/index.ts +4 -0
- package/src/editor/hooks/useClickOutsideEscape.ts +43 -0
- package/src/editor/hooks/useSearchFilter.ts +21 -0
- package/src/html-processor.ts +75 -94
- package/src/index.ts +5 -0
- package/src/rehype-cms-marker.ts +15 -0
package/src/html-processor.ts
CHANGED
|
@@ -17,6 +17,35 @@ import { generateStableId } from './utils'
|
|
|
17
17
|
/** Type for parsed HTML element nodes from node-html-parser */
|
|
18
18
|
type HTMLNode = ParsedHTMLElement
|
|
19
19
|
|
|
20
|
+
/** Check whether any ancestor of `node` (inclusive) has `data-astro-source-file`. */
|
|
21
|
+
function hasAncestorSourceFile(node: HTMLNode): boolean {
|
|
22
|
+
let current: HTMLNode | null = node
|
|
23
|
+
while (current) {
|
|
24
|
+
if (current.getAttribute?.('data-astro-source-file')) return true
|
|
25
|
+
current = current.parentNode as HTMLNode | null
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Walk ancestors of `node` (inclusive) to find the nearest source file and line. */
|
|
31
|
+
function findAncestorSourceLocation(node: HTMLNode): { sourceFile?: string; sourceLine?: number } {
|
|
32
|
+
let current: HTMLNode | null = node
|
|
33
|
+
while (current) {
|
|
34
|
+
const file = current.getAttribute?.('data-astro-source-file')
|
|
35
|
+
if (file) {
|
|
36
|
+
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
37
|
+
let sourceLine: number | undefined
|
|
38
|
+
if (line) {
|
|
39
|
+
const parsed = parseInt(line.split(':')[0] ?? '1', 10)
|
|
40
|
+
if (!Number.isNaN(parsed)) sourceLine = parsed
|
|
41
|
+
}
|
|
42
|
+
return { sourceFile: file, sourceLine }
|
|
43
|
+
}
|
|
44
|
+
current = current.parentNode as HTMLNode | null
|
|
45
|
+
}
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Inline text styling elements that should NOT be marked with CMS IDs.
|
|
22
51
|
* These elements are text formatting and should be part of their parent's content.
|
|
@@ -434,52 +463,61 @@ export async function processHtml(
|
|
|
434
463
|
// This needs to run BEFORE image marking so we can skip images inside markdown
|
|
435
464
|
let markdownWrapperNode: HTMLNode | null = null
|
|
436
465
|
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
466
|
+
// Three strategies in priority order:
|
|
467
|
+
// 0. Rehype marker: the rehype-cms-marker plugin marks the first rendered element
|
|
468
|
+
// with data-cms-markdown-content — its parent is the wrapper
|
|
469
|
+
// 1. Dev mode heuristic: elements with data-astro-source-file whose children lack it
|
|
470
|
+
// 2. Build mode: find element whose content matches the markdown body text
|
|
440
471
|
if (collectionInfo) {
|
|
441
472
|
const allElements = root.querySelectorAll('*')
|
|
442
473
|
let foundWrapper = false
|
|
443
474
|
|
|
475
|
+
// Strategy 0: Rehype marker — most reliable
|
|
476
|
+
const markerEl = root.querySelector('[data-cms-markdown-content]')
|
|
477
|
+
if (markerEl) {
|
|
478
|
+
markerEl.removeAttribute('data-cms-markdown-content')
|
|
479
|
+
const parent = markerEl.parentNode as HTMLNode | null
|
|
480
|
+
if (parent && parent.tagName) {
|
|
481
|
+
const id = getNextId()
|
|
482
|
+
parent.setAttribute(attributeName, id)
|
|
483
|
+
parent.setAttribute('data-cms-markdown', 'true')
|
|
484
|
+
collectionWrapperId = id
|
|
485
|
+
markdownWrapperNode = parent
|
|
486
|
+
foundWrapper = true
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
444
490
|
// Strategy 1: Dev mode - look for source file attributes
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (parent.getAttribute?.(attributeName)?.startsWith('cms-collection-')) {
|
|
468
|
-
hasAncestorWrapper = true
|
|
469
|
-
break
|
|
491
|
+
if (!foundWrapper) {
|
|
492
|
+
const SKIP_WRAPPER_TAGS = new Set(['html', 'head', 'body', 'script', 'style', 'meta', 'link'])
|
|
493
|
+
for (const node of allElements) {
|
|
494
|
+
const tag = node.tagName?.toLowerCase?.() ?? ''
|
|
495
|
+
if (SKIP_WRAPPER_TAGS.has(tag)) continue
|
|
496
|
+
const sourceFile = node.getAttribute('data-astro-source-file')
|
|
497
|
+
if (!sourceFile) continue
|
|
498
|
+
|
|
499
|
+
// Check if this element has any direct child elements without source file attribute
|
|
500
|
+
// These would be markdown-rendered elements
|
|
501
|
+
const childElements = node.childNodes.filter(
|
|
502
|
+
(child): child is HTMLNode => child.nodeType === 1 && 'tagName' in child,
|
|
503
|
+
)
|
|
504
|
+
const hasMarkdownChildren = childElements.some(
|
|
505
|
+
(child) => !child.getAttribute?.('data-astro-source-file'),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if (hasMarkdownChildren) {
|
|
509
|
+
// Remove data-cms-markdown from previous (shallower) wrapper —
|
|
510
|
+
// we want only the deepest wrapper to have it
|
|
511
|
+
if (markdownWrapperNode) {
|
|
512
|
+
markdownWrapperNode.removeAttribute('data-cms-markdown')
|
|
470
513
|
}
|
|
471
|
-
parent = parent.parentNode as HTMLNode | null
|
|
472
|
-
}
|
|
473
514
|
|
|
474
|
-
if (!hasAncestorWrapper) {
|
|
475
|
-
// Mark this as the collection wrapper using the standard attribute
|
|
476
515
|
const id = getNextId()
|
|
477
516
|
node.setAttribute(attributeName, id)
|
|
478
517
|
node.setAttribute('data-cms-markdown', 'true')
|
|
479
518
|
collectionWrapperId = id
|
|
480
519
|
markdownWrapperNode = node
|
|
481
520
|
foundWrapper = true
|
|
482
|
-
// Don't break - we want the deepest wrapper, so we'll overwrite
|
|
483
521
|
}
|
|
484
522
|
}
|
|
485
523
|
}
|
|
@@ -636,42 +674,13 @@ export async function processHtml(
|
|
|
636
674
|
|
|
637
675
|
// When skipMarkdownContent is true (collection pages), only mark images
|
|
638
676
|
// that have source file attributes (from Astro templates, not markdown)
|
|
639
|
-
if (skipMarkdownContent)
|
|
640
|
-
// Check if the image or any ancestor has source file attribute
|
|
641
|
-
let hasSourceAttr = false
|
|
642
|
-
let current: HTMLNode | null = node
|
|
643
|
-
while (current) {
|
|
644
|
-
if (current.getAttribute?.('data-astro-source-file')) {
|
|
645
|
-
hasSourceAttr = true
|
|
646
|
-
break
|
|
647
|
-
}
|
|
648
|
-
current = current.parentNode as HTMLNode | null
|
|
649
|
-
}
|
|
650
|
-
if (!hasSourceAttr) return
|
|
651
|
-
}
|
|
677
|
+
if (skipMarkdownContent && !hasAncestorSourceFile(node)) return
|
|
652
678
|
|
|
653
679
|
const id = getNextId()
|
|
654
680
|
node.setAttribute(attributeName, id)
|
|
655
681
|
node.setAttribute('data-cms-img', 'true')
|
|
656
682
|
|
|
657
|
-
|
|
658
|
-
let sourceFile: string | undefined
|
|
659
|
-
let sourceLine: number | undefined
|
|
660
|
-
let current: HTMLNode | null = node
|
|
661
|
-
while (current && !sourceFile) {
|
|
662
|
-
const file = current.getAttribute?.('data-astro-source-file')
|
|
663
|
-
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
664
|
-
if (file) {
|
|
665
|
-
sourceFile = file
|
|
666
|
-
if (line) {
|
|
667
|
-
const lineNum = parseInt(line.split(':')[0] ?? '1', 10)
|
|
668
|
-
if (!Number.isNaN(lineNum)) {
|
|
669
|
-
sourceLine = lineNum
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
current = current.parentNode as HTMLNode | null
|
|
674
|
-
}
|
|
683
|
+
const { sourceFile, sourceLine } = findAncestorSourceLocation(node)
|
|
675
684
|
|
|
676
685
|
// Build image metadata
|
|
677
686
|
const metadata: ImageMetadata = {
|
|
@@ -708,41 +717,13 @@ export async function processHtml(
|
|
|
708
717
|
if (!bgMeta) return
|
|
709
718
|
|
|
710
719
|
// When skipMarkdownContent is true, only mark elements with source file attributes
|
|
711
|
-
if (skipMarkdownContent)
|
|
712
|
-
let hasSourceAttr = false
|
|
713
|
-
let current: HTMLNode | null = node
|
|
714
|
-
while (current) {
|
|
715
|
-
if (current.getAttribute?.('data-astro-source-file')) {
|
|
716
|
-
hasSourceAttr = true
|
|
717
|
-
break
|
|
718
|
-
}
|
|
719
|
-
current = current.parentNode as HTMLNode | null
|
|
720
|
-
}
|
|
721
|
-
if (!hasSourceAttr) return
|
|
722
|
-
}
|
|
720
|
+
if (skipMarkdownContent && !hasAncestorSourceFile(node)) return
|
|
723
721
|
|
|
724
722
|
const id = getNextId()
|
|
725
723
|
node.setAttribute(attributeName, id)
|
|
726
724
|
node.setAttribute('data-cms-bg-img', 'true')
|
|
727
725
|
|
|
728
|
-
|
|
729
|
-
let sourceFile: string | undefined
|
|
730
|
-
let sourceLine: number | undefined
|
|
731
|
-
let current: HTMLNode | null = node
|
|
732
|
-
while (current && !sourceFile) {
|
|
733
|
-
const file = current.getAttribute?.('data-astro-source-file')
|
|
734
|
-
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
735
|
-
if (file) {
|
|
736
|
-
sourceFile = file
|
|
737
|
-
if (line) {
|
|
738
|
-
const lineNum = parseInt(line.split(':')[0] ?? '1', 10)
|
|
739
|
-
if (!Number.isNaN(lineNum)) {
|
|
740
|
-
sourceLine = lineNum
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
current = current.parentNode as HTMLNode | null
|
|
745
|
-
}
|
|
726
|
+
const { sourceFile, sourceLine } = findAncestorSourceLocation(node)
|
|
746
727
|
|
|
747
728
|
bgImageEntries.set(id, {
|
|
748
729
|
metadata: bgMeta,
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { getErrorCollector, resetErrorCollector } from './error-collector'
|
|
|
13
13
|
import { ManifestWriter } from './manifest-writer'
|
|
14
14
|
import { createLocalStorageAdapter } from './media/local'
|
|
15
15
|
import type { MediaStorageAdapter } from './media/types'
|
|
16
|
+
import { rehypeCmsMarker } from './rehype-cms-marker'
|
|
16
17
|
import type { CmsFeatures, CmsMarkerOptions, ComponentDefinition } from './types'
|
|
17
18
|
import { createVitePlugin } from './vite-plugin'
|
|
18
19
|
|
|
@@ -283,6 +284,9 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
283
284
|
const needsAliases = !src && !hasPrebuiltBundle
|
|
284
285
|
|
|
285
286
|
updateConfig({
|
|
287
|
+
markdown: {
|
|
288
|
+
rehypePlugins: [rehypeCmsMarker],
|
|
289
|
+
},
|
|
286
290
|
vite: {
|
|
287
291
|
plugins: vitePlugins,
|
|
288
292
|
resolve: needsAliases
|
|
@@ -367,6 +371,7 @@ export type { Color, Date, DateTime, Email, Image, Reference, Textarea, Time, Ur
|
|
|
367
371
|
|
|
368
372
|
export { scanCollections } from './collection-scanner'
|
|
369
373
|
export { getProjectRoot, resetProjectRoot, setProjectRoot } from './config'
|
|
374
|
+
export { rehypeCmsMarker } from './rehype-cms-marker'
|
|
370
375
|
export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference } from './source-finder'
|
|
371
376
|
export { findCollectionSource, parseMarkdownContent } from './source-finder'
|
|
372
377
|
export type {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin that marks the first element of rendered markdown/MDX content
|
|
3
|
+
* with `data-cms-markdown-content`. The HTML processor uses this marker to
|
|
4
|
+
* reliably identify the wrapper element (the marker's parent) instead of
|
|
5
|
+
* relying on heuristics.
|
|
6
|
+
*/
|
|
7
|
+
export function rehypeCmsMarker() {
|
|
8
|
+
return (tree: any) => {
|
|
9
|
+
const firstElement = tree.children?.find((n: any) => n.type === 'element')
|
|
10
|
+
if (firstElement) {
|
|
11
|
+
firstElement.properties ??= {}
|
|
12
|
+
firstElement.properties['dataCmsMarkdownContent'] = ''
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|