@setzkasten-cms/astro-admin 0.6.0 → 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/package.json +23 -6
- package/src/admin-page.astro +9 -8
- package/src/api-routes/__tests__/auth-guard.test.ts +134 -0
- package/src/api-routes/__tests__/commit-trailers.test.ts +69 -0
- package/src/api-routes/__tests__/github-cache.test.ts +100 -0
- package/src/api-routes/__tests__/github-token-for-request.test.ts +112 -0
- package/src/api-routes/__tests__/github-token.test.ts +78 -0
- package/src/api-routes/__tests__/global-config-theme.test.ts +71 -0
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +61 -0
- package/src/api-routes/__tests__/license-tier.test.ts +45 -0
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +189 -0
- package/src/api-routes/__tests__/pages-meta-store.test.ts +179 -0
- package/src/api-routes/__tests__/pages.test.ts +72 -0
- package/src/api-routes/__tests__/route-registry.test.ts +120 -0
- package/src/api-routes/__tests__/session-cookie.test.ts +67 -0
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +145 -0
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +192 -0
- package/src/api-routes/__tests__/setup-github-app.test.ts +107 -0
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +78 -0
- package/src/api-routes/__tests__/website-resolver-bootstrap-standalone.test.ts +85 -0
- package/src/api-routes/__tests__/website-resolver-bootstrap.test.ts +108 -0
- package/src/api-routes/__tests__/website-resolver.test.ts +123 -0
- package/src/api-routes/__tests__/websites-add.test.ts +305 -0
- package/src/api-routes/__tests__/websites-list.test.ts +112 -0
- package/src/api-routes/__tests__/websites-remove.test.ts +155 -0
- package/src/api-routes/_auth-guard.ts +153 -0
- package/src/api-routes/_commit-trailers.ts +16 -0
- package/src/api-routes/_github-cache.ts +32 -0
- package/src/api-routes/_github-token.ts +64 -0
- package/src/api-routes/_license-tier.ts +25 -0
- package/src/api-routes/_pages-meta-store.ts +134 -0
- package/src/api-routes/_session-cookie.ts +42 -0
- package/src/api-routes/_storage-config.ts +64 -4
- package/src/api-routes/_vercel-origin.ts +22 -0
- package/src/api-routes/_website-resolver.ts +243 -0
- package/src/api-routes/_websites-store.ts +120 -0
- package/src/api-routes/asset-proxy.ts +6 -4
- package/src/api-routes/auth-callback.ts +21 -53
- package/src/api-routes/auth-login.ts +18 -65
- package/src/api-routes/auth-logout.ts +5 -1
- package/src/api-routes/auth-setzkasten-login.ts +71 -0
- package/src/api-routes/catalog-add.ts +18 -5
- package/src/api-routes/catalog-export.ts +8 -4
- package/src/api-routes/config.ts +17 -5
- package/src/api-routes/editors.ts +205 -0
- package/src/api-routes/github-proxy.ts +5 -5
- package/src/api-routes/global-config.ts +149 -0
- package/src/api-routes/init-add-section.ts +21 -10
- package/src/api-routes/init-apply.ts +7 -4
- package/src/api-routes/init-migrate.ts +9 -6
- package/src/api-routes/init-scan-page.ts +26 -6
- package/src/api-routes/init-scan.ts +5 -3
- package/src/api-routes/migrate-to-multi.ts +255 -0
- package/src/api-routes/pages.ts +138 -6
- package/src/api-routes/section-add.ts +23 -5
- package/src/api-routes/section-commit-pending.ts +28 -5
- package/src/api-routes/section-delete.ts +24 -5
- package/src/api-routes/section-duplicate.ts +25 -5
- package/src/api-routes/section-prepare-copy.ts +15 -4
- package/src/api-routes/section-prepare.ts +12 -4
- package/src/api-routes/setup-github-app-bounce.ts +52 -0
- package/src/api-routes/setup-github-app-branches.ts +63 -0
- package/src/api-routes/setup-github-app-callback.ts +53 -0
- package/src/api-routes/setup-github-app-installed.ts +44 -0
- package/src/api-routes/setup-github-app-repos.ts +46 -0
- package/src/api-routes/setup-github-app.ts +58 -0
- package/src/api-routes/updater-check.ts +49 -0
- package/src/api-routes/updater-register.ts +90 -0
- package/src/api-routes/updater-transfer.ts +51 -0
- package/src/api-routes/updater-unbind.ts +59 -0
- package/src/api-routes/websites-add.ts +113 -0
- package/src/api-routes/websites-list.ts +40 -0
- package/src/api-routes/websites-remove.ts +74 -0
- package/src/init/__tests__/page-level.test.ts +47 -0
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +90 -0
- package/src/init/__tests__/section-pipeline.test.ts +3 -1
- package/src/init/astro-section-analyzer-v2.ts +29 -2
- package/src/init/template-patcher-v2.ts +100 -0
- package/LICENSE +0 -37
|
@@ -1031,3 +1031,50 @@ describe('Nested map live-edit: inner items have data-sk-field', () => {
|
|
|
1031
1031
|
expect(patched).toMatch(/sections.*map.*section.*_i/)
|
|
1032
1032
|
})
|
|
1033
1033
|
})
|
|
1034
|
+
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
// Template literal variables in frontmatter must NOT become content fields
|
|
1037
|
+
// Reproduces: docs/catalog page with code examples in template literals
|
|
1038
|
+
// ---------------------------------------------------------------------------
|
|
1039
|
+
|
|
1040
|
+
describe('Page-level: template literal variables not extracted as fields', () => {
|
|
1041
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
1042
|
+
|
|
1043
|
+
beforeAll(async () => {
|
|
1044
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithTemplateLiterals.astro'), 'utf-8')
|
|
1045
|
+
section = await analyzeAstroSection(source, '_page_docs_example', 'docsExamplePage', 'src/pages/docs/example.astro', { mode: 'page' })
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('should detect the real content fields (heading, description)', () => {
|
|
1049
|
+
const keys = section.fields.map(f => f.key)
|
|
1050
|
+
expect(keys).toContain('heading')
|
|
1051
|
+
expect(keys).toContain('description')
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
it('should NOT extract variables from inside template literal strings (templates, hero, json, template)', () => {
|
|
1055
|
+
const keys = section.fields.map(f => f.key)
|
|
1056
|
+
expect(keys).not.toContain('templates')
|
|
1057
|
+
expect(keys).not.toContain('hero')
|
|
1058
|
+
expect(keys).not.toContain('json')
|
|
1059
|
+
expect(keys).not.toContain('template')
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
it('should NOT extract the code variables themselves (exampleCode, cliCode, apiCode)', () => {
|
|
1063
|
+
const keys = section.fields.map(f => f.key)
|
|
1064
|
+
expect(keys).not.toContain('exampleCode')
|
|
1065
|
+
expect(keys).not.toContain('cliCode')
|
|
1066
|
+
expect(keys).not.toContain('apiCode')
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
it('should find only legitimate fields (no spurious template-literal variables)', () => {
|
|
1070
|
+
const keys = section.fields.map(f => f.key)
|
|
1071
|
+
// Only real content fields should be present — no code variable names
|
|
1072
|
+
const spurious = ['templates', 'hero', 'json', 'template', 'exampleCode', 'cliCode', 'apiCode']
|
|
1073
|
+
for (const key of spurious) {
|
|
1074
|
+
expect(keys).not.toContain(key)
|
|
1075
|
+
}
|
|
1076
|
+
// heading and description must be present
|
|
1077
|
+
expect(keys).toContain('heading')
|
|
1078
|
+
expect(keys).toContain('description')
|
|
1079
|
+
})
|
|
1080
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patcher: mixed-content RTE wrapper conversion.
|
|
3
|
+
*
|
|
4
|
+
* Bug: Adopting a page like Impressum/Datenschutz where a `<p>` contains
|
|
5
|
+
* text + <br /> tags (e.g. address blocks) leaves the `<p>` as the wrapper
|
|
6
|
+
* with `set:html={skData?.description ?? `…`}`. When the field is later
|
|
7
|
+
* edited via the inline RTE, TipTap's getHTML() returns `<p>…</p>`-wrapped
|
|
8
|
+
* HTML — which then gets injected into the existing `<p data-sk-field>`,
|
|
9
|
+
* producing `<p><p>…</p></p>` at render time. HTML5 forbids `<p>` inside
|
|
10
|
+
* `<p>`, so the browser auto-closes the outer `<p>` immediately:
|
|
11
|
+
*
|
|
12
|
+
* <p data-sk-field="…"></p> ← empty! visible as a tiny outline box
|
|
13
|
+
* <p>LILAPIXEL<br>…</p> ← the actual content, unbound
|
|
14
|
+
*
|
|
15
|
+
* Fix: when patchMixedContentField wraps a phrasing-only element (`<p>`,
|
|
16
|
+
* `<span>`), the wrapper is converted to `<div>`. `<div>` is flow content
|
|
17
|
+
* and accepts arbitrary HTML children (including `<p>`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest'
|
|
21
|
+
import { patchTemplateForFields } from '../../init/template-patcher-v2'
|
|
22
|
+
import type { PatchField } from '../../init/template-patcher-v2'
|
|
23
|
+
|
|
24
|
+
const IMPRESSUM_SECTION = `---
|
|
25
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<BaseLayout>
|
|
29
|
+
<div class="mx-auto w-full max-w-3xl">
|
|
30
|
+
<h1 class="mb-10 text-3xl font-bold">Impressum</h1>
|
|
31
|
+
<section class="mb-8">
|
|
32
|
+
<h2 class="mb-3 text-xl font-semibold">Angaben gemäß § 5 DDG</h2>
|
|
33
|
+
<p class="text-sk-slate leading-relaxed">
|
|
34
|
+
LILAPIXEL<br />
|
|
35
|
+
Birgit Soring<br />
|
|
36
|
+
Le-Corbusier-Str. 31b<br />
|
|
37
|
+
26127 Oldenburg
|
|
38
|
+
</p>
|
|
39
|
+
</section>
|
|
40
|
+
</div>
|
|
41
|
+
</BaseLayout>
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
describe('patchMixedContentField — phrasing wrapper conversion', () => {
|
|
45
|
+
const fields: PatchField[] = [
|
|
46
|
+
{ key: 'heading', type: 'text', defaultValue: 'Impressum' },
|
|
47
|
+
{ key: 'heading2', type: 'text', defaultValue: 'Angaben gemäß § 5 DDG' },
|
|
48
|
+
{
|
|
49
|
+
key: 'description',
|
|
50
|
+
type: 'text',
|
|
51
|
+
defaultValue: 'LILAPIXEL Birgit Soring Le-Corbusier-Str. 31b 26127 Oldenburg',
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
it('converts a <p> mixed-content wrapper to <div> so RTE-injected <p> children remain valid', async () => {
|
|
56
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], { mode: 'page' })
|
|
57
|
+
|
|
58
|
+
// The patched description binding must NOT live on a <p> element —
|
|
59
|
+
// otherwise an RTE-edited value of "<p>…</p>" produces nested <p>'s.
|
|
60
|
+
expect(patched).not.toMatch(/<p[^>]*data-sk-field="_page_impressum\.description"/)
|
|
61
|
+
|
|
62
|
+
// Instead, the wrapper should be a <div> that retains the original classes.
|
|
63
|
+
expect(patched).toMatch(/<div[^>]*class="text-sk-slate leading-relaxed"[^>]*data-sk-field="_page_impressum\.description"/)
|
|
64
|
+
expect(patched).toMatch(/data-sk-field="_page_impressum\.description"[^>]*set:html=\{skData\?\.description/)
|
|
65
|
+
|
|
66
|
+
// The closing tag must match (no </p> for this binding's wrapper).
|
|
67
|
+
// A simple sanity check: the <div ... data-sk-field="…description"> must be followed by </div>.
|
|
68
|
+
const openMatch = patched.match(/<div[^>]*data-sk-field="_page_impressum\.description"[^>]*>/)
|
|
69
|
+
expect(openMatch).toBeTruthy()
|
|
70
|
+
const openIdx = patched.indexOf(openMatch![0])
|
|
71
|
+
const afterOpen = patched.slice(openIdx + openMatch![0].length)
|
|
72
|
+
// The next closing tag for this wrapper must be </div>, not </p>.
|
|
73
|
+
expect(afterOpen).toMatch(/^[\s\S]*?<\/div>/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('does NOT convert headings — h2 stays h2 even if it had inline content', async () => {
|
|
77
|
+
// Headings rarely host RTE content but must remain semantically intact.
|
|
78
|
+
// patchTextField (not patchMixedContentField) handles plain text headings,
|
|
79
|
+
// so the <h2> should keep its tag name.
|
|
80
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], { mode: 'page' })
|
|
81
|
+
expect(patched).toMatch(/<h2[^>]*data-sk-field="_page_impressum\.heading2"/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps the original <p> classes on the converted <div> wrapper', async () => {
|
|
85
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], { mode: 'page' })
|
|
86
|
+
// The Tailwind classes from the original <p class="text-sk-slate leading-relaxed">
|
|
87
|
+
// must be preserved on the new <div>.
|
|
88
|
+
expect(patched).toContain('text-sk-slate leading-relaxed')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -68,7 +68,9 @@ function checkCssIntegrity(
|
|
|
68
68
|
return { ok: lost.length === 0, lost }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// icon and image fields now get data-sk-field via patchIconOrImageField
|
|
72
|
+
// (only when the patcher can find a suitable container in the AST)
|
|
73
|
+
const SK_FIELD_NOT_NEEDED = new Set<string>([])
|
|
72
74
|
|
|
73
75
|
function fieldNeedsSkBinding(field: { key: string; type: string }): boolean {
|
|
74
76
|
if (SK_FIELD_NOT_NEEDED.has(field.type)) return false
|
|
@@ -201,16 +201,43 @@ function splitAstroFile(source: string): { frontmatter: string; template: string
|
|
|
201
201
|
return { frontmatter, template: source.slice(templateStart), templateOffset: templateStart }
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function stripTemplateLiterals(source: string): string {
|
|
205
|
+
let result = ''
|
|
206
|
+
let i = 0
|
|
207
|
+
while (i < source.length) {
|
|
208
|
+
if (source[i] === '`') {
|
|
209
|
+
// Skip entire template literal (including nested ${...} expressions)
|
|
210
|
+
i++
|
|
211
|
+
let depth = 0
|
|
212
|
+
while (i < source.length) {
|
|
213
|
+
if (source[i] === '\\') { i += 2; continue }
|
|
214
|
+
if (source[i] === '$' && source[i + 1] === '{') { depth++; i += 2; continue }
|
|
215
|
+
if (source[i] === '{') { depth++; i++; continue }
|
|
216
|
+
if (source[i] === '}' && depth > 0) { depth--; i++; continue }
|
|
217
|
+
if (source[i] === '`' && depth === 0) { i++; break }
|
|
218
|
+
i++
|
|
219
|
+
}
|
|
220
|
+
result += '``' // placeholder so surrounding code stays parseable
|
|
221
|
+
} else {
|
|
222
|
+
result += source[i]
|
|
223
|
+
i++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result
|
|
227
|
+
}
|
|
228
|
+
|
|
204
229
|
function extractFrontmatterVariables(frontmatter: string): string[] {
|
|
230
|
+
// Strip template literals so code inside backtick strings isn't parsed as variables
|
|
231
|
+
const stripped = stripTemplateLiterals(frontmatter)
|
|
205
232
|
const variables: string[] = []
|
|
206
233
|
const constRegex = /(?:const|let)\s+(\w+)\s*=\s*(.*)/g
|
|
207
234
|
let match: RegExpExecArray | null
|
|
208
|
-
while ((match = constRegex.exec(
|
|
235
|
+
while ((match = constRegex.exec(stripped)) !== null) {
|
|
209
236
|
const name = match[1]!
|
|
210
237
|
const rhs = match[2]?.trim() ?? ''
|
|
211
238
|
if (isInternalVariable(name)) continue
|
|
212
239
|
// Skip exported declarations (e.g. "export const prerender = true")
|
|
213
|
-
const charBefore = match.index > 0 ?
|
|
240
|
+
const charBefore = match.index > 0 ? stripped.slice(Math.max(0, match.index - 10), match.index) : ''
|
|
214
241
|
if (/export\s*$/.test(charBefore)) continue
|
|
215
242
|
if (/\.\s*map\s*\(/.test(rhs) || /\w+\?\.\w+/.test(rhs)) continue
|
|
216
243
|
if (/^\[/.test(rhs) && /^default/i.test(name)) continue
|
|
@@ -296,6 +296,15 @@ export async function patchTemplateForFields(
|
|
|
296
296
|
continue
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// --- Icon / Image fields: add data-sk-field to their container element ---
|
|
300
|
+
// These fields can't be matched by text content. Instead we look for:
|
|
301
|
+
// 1. A CMS expression referencing this field (e.g. {skData?.icon} as an attr value)
|
|
302
|
+
// 2. A component/element with an `icon` or `src`/image prop that matches the default
|
|
303
|
+
if (field.type === 'icon' || field.type === 'image') {
|
|
304
|
+
patchIconOrImageField(source, sectionKey, field, varName, ast, edits)
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
299
308
|
// Fields without string defaultValue can't be matched by content
|
|
300
309
|
if (!field.defaultValue || typeof field.defaultValue !== 'string') continue
|
|
301
310
|
if (field.defaultValue.length < 2) continue
|
|
@@ -547,10 +556,28 @@ function getElementTextContent(node: AstNode): string {
|
|
|
547
556
|
return (node.children ?? []).map(getElementTextContent).join('')
|
|
548
557
|
}
|
|
549
558
|
|
|
559
|
+
/**
|
|
560
|
+
* HTML elements whose content model only allows phrasing content (no block-level
|
|
561
|
+
* children, especially no nested <p>). When such an element is selected as the
|
|
562
|
+
* RTE wrapper, a TipTap-edited value of `<p>…</p>` would be injected via set:html,
|
|
563
|
+
* producing invalid markup like `<p><p>…</p></p>`. The browser auto-closes the
|
|
564
|
+
* outer wrapper immediately, leaving the data-sk-field element empty.
|
|
565
|
+
*
|
|
566
|
+
* Patcher converts these wrappers to <div> (flow content, accepts everything).
|
|
567
|
+
* Headings (<h1>-<h6>) are intentionally not in this list — they are handled
|
|
568
|
+
* by patchTextField, not patchMixedContentField, and converting them would
|
|
569
|
+
* silently drop semantic meaning.
|
|
570
|
+
*/
|
|
571
|
+
const PHRASING_ONLY_RTE_WRAPPERS = new Set(['p', 'span'])
|
|
572
|
+
|
|
550
573
|
/**
|
|
551
574
|
* Patch a mixed-content element as a rich text field.
|
|
552
575
|
* Adds data-sk-field + set:html to the element, keeping original HTML as fallback.
|
|
553
576
|
* The live editor uses the RTE (contentEditable) for this field.
|
|
577
|
+
*
|
|
578
|
+
* If the wrapper is a phrasing-content-only tag (p, span), it is converted to
|
|
579
|
+
* <div> so that future RTE-injected block content (e.g. TipTap's <p>…</p>)
|
|
580
|
+
* remains valid HTML.
|
|
554
581
|
*/
|
|
555
582
|
function patchMixedContentField(
|
|
556
583
|
source: string,
|
|
@@ -605,6 +632,79 @@ function patchMixedContentField(
|
|
|
605
632
|
deleteCount: closeIdx - innerStart,
|
|
606
633
|
insert: '',
|
|
607
634
|
})
|
|
635
|
+
|
|
636
|
+
// Phrasing-only wrappers cannot host RTE-injected block content (e.g. <p>).
|
|
637
|
+
// Rewrite the opening + closing tag to <div> while keeping all attributes.
|
|
638
|
+
if (PHRASING_ONLY_RTE_WRAPPERS.has(tagName)) {
|
|
639
|
+
edits.push({
|
|
640
|
+
offset: tagIdx + 1, // position of the tag-name char after '<'
|
|
641
|
+
deleteCount: tagName.length,
|
|
642
|
+
insert: 'div',
|
|
643
|
+
})
|
|
644
|
+
edits.push({
|
|
645
|
+
offset: closeIdx + 2, // position of the tag-name char after '</'
|
|
646
|
+
deleteCount: tagName.length,
|
|
647
|
+
insert: 'div',
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Patches an icon or image field by finding the nearest suitable container
|
|
654
|
+
* in the AST and adding data-sk-field to it.
|
|
655
|
+
*
|
|
656
|
+
* Strategy:
|
|
657
|
+
* - For icon: look for an element whose `icon`, `name`, or `data-icon` attribute
|
|
658
|
+
* matches the default value, or a component named *Icon* — then add data-sk-field
|
|
659
|
+
* to its parent container element.
|
|
660
|
+
* - For image: look for <img> / <Image> element whose `src` matches the default,
|
|
661
|
+
* or an element whose expression refs the field key — add data-sk-field to parent.
|
|
662
|
+
* - Fallback: skip (no edit, no crash).
|
|
663
|
+
*/
|
|
664
|
+
function patchIconOrImageField(
|
|
665
|
+
source: string,
|
|
666
|
+
sectionKey: string,
|
|
667
|
+
field: { key: string; type: string; defaultValue?: unknown },
|
|
668
|
+
_varName: string,
|
|
669
|
+
ast: AstNode,
|
|
670
|
+
edits: Edit[],
|
|
671
|
+
): void {
|
|
672
|
+
const bindingKey = `${sectionKey}.${field.key}`
|
|
673
|
+
const defaultStr = typeof field.defaultValue === 'string' ? field.defaultValue : ''
|
|
674
|
+
|
|
675
|
+
let targetParent: AstNode | null = null
|
|
676
|
+
|
|
677
|
+
walkAst(ast, (node, parent) => {
|
|
678
|
+
if (targetParent) return // already found
|
|
679
|
+
if (!parent || !isElement(parent)) return
|
|
680
|
+
// Skip if parent already has data-sk-field
|
|
681
|
+
if (parent.attributes?.some(a => a.name === 'data-sk-field')) return
|
|
682
|
+
|
|
683
|
+
if (field.type === 'icon') {
|
|
684
|
+
// Match component named *Icon* or element with icon/name attr matching default
|
|
685
|
+
const isIconComp = isElement(node) && /icon/i.test(node.name ?? '')
|
|
686
|
+
const hasIconAttr = node.attributes?.some(
|
|
687
|
+
a => (a.name === 'icon' || a.name === 'name' || a.name === 'data-icon') &&
|
|
688
|
+
(a.value === defaultStr || a.value.includes(field.key))
|
|
689
|
+
)
|
|
690
|
+
if (isIconComp || hasIconAttr) {
|
|
691
|
+
targetParent = parent
|
|
692
|
+
}
|
|
693
|
+
} else if (field.type === 'image') {
|
|
694
|
+
// Match <img> / <Image> / <picture> element
|
|
695
|
+
const tag = node.name ?? ''
|
|
696
|
+
const isImgEl = /^(img|Image|picture)$/.test(tag)
|
|
697
|
+
const hasSrcAttr = node.attributes?.some(
|
|
698
|
+
a => a.name === 'src' && (a.value === defaultStr || a.value.includes(field.key))
|
|
699
|
+
)
|
|
700
|
+
if (isImgEl || hasSrcAttr) {
|
|
701
|
+
targetParent = parent
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
if (!targetParent) return
|
|
707
|
+
addAttributeToElement(source, targetParent, `data-sk-field="${bindingKey}"`, edits)
|
|
608
708
|
}
|
|
609
709
|
|
|
610
710
|
function patchCmsBoundField(
|
package/LICENSE
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
Setzkasten Community License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Lilapixel
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to use,
|
|
7
|
-
copy, modify, merge, publish, and distribute the Software, subject to the
|
|
8
|
-
following conditions:
|
|
9
|
-
|
|
10
|
-
1. The above copyright notice and this permission notice shall be included in
|
|
11
|
-
all copies or substantial portions of the Software.
|
|
12
|
-
|
|
13
|
-
2. The Software may not be used for commercial purposes without a separate
|
|
14
|
-
commercial license from the copyright holder. "Commercial purposes" means
|
|
15
|
-
any use of the Software that is primarily intended for or directed toward
|
|
16
|
-
commercial advantage or monetary compensation. This includes, but is not
|
|
17
|
-
limited to:
|
|
18
|
-
- Using the Software to manage content for a commercial website or product
|
|
19
|
-
- Offering the Software as part of a paid service
|
|
20
|
-
- Using the Software within a for-profit organization
|
|
21
|
-
|
|
22
|
-
3. Non-commercial use is permitted without restriction. This includes:
|
|
23
|
-
- Personal projects
|
|
24
|
-
- Open source projects
|
|
25
|
-
- Educational and academic use
|
|
26
|
-
- Non-profit organizations
|
|
27
|
-
|
|
28
|
-
4. A commercial license ("Enterprise License") may be obtained by contacting
|
|
29
|
-
Lilapixel at hello@lilapixel.de.
|
|
30
|
-
|
|
31
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
32
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
34
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
35
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
36
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
37
|
-
SOFTWARE.
|