@maizzle/framework 6.0.0-rc.12 → 6.0.0-rc.13
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/build.mjs +4 -1
- package/dist/build.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +22 -10
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +54 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +890 -76
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/linter.d.mts +15 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +194 -43
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/sfc-utils.d.mts +18 -0
- package/dist/server/sfc-utils.d.mts.map +1 -0
- package/dist/server/sfc-utils.mjs +184 -0
- package/dist/server/sfc-utils.mjs.map +1 -0
- package/dist/server/ui/App.vue +4 -41
- package/dist/server/ui/components/SidebarClose.vue +12 -0
- package/dist/server/ui/components/ui/command/Command.vue +1 -0
- package/dist/server/ui/components/ui/input/Input.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
- package/dist/server/ui/pages/Preview.vue +191 -151
- package/dist/transformers/addAttributes.mjs +10 -6
- package/dist/transformers/addAttributes.mjs.map +1 -1
- package/dist/transformers/inlineCSS.mjs +2 -2
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.mjs +1 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +2 -4
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +42 -2
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +1 -3
- package/dist/_virtual/_rolldown/runtime.mjs +0 -32
- package/dist/node_modules/picomatch/index.mjs +0 -13
- package/dist/node_modules/picomatch/index.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
- package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
- package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
- package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
- package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
- package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useRoute } from 'vue-router'
|
|
4
|
-
import { ChevronUp, ChevronDown, Check
|
|
4
|
+
import { ChevronUp, ChevronDown, Check } from 'lucide-vue-next'
|
|
5
5
|
import {
|
|
6
6
|
DropdownMenu,
|
|
7
7
|
DropdownMenuContent,
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
TagsInputItemDelete,
|
|
21
21
|
TagsInputItemText,
|
|
22
22
|
} from '@/components/ui/tags-input'
|
|
23
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
24
23
|
|
|
25
24
|
import stripesUrl from '../stripes.svg'
|
|
26
25
|
|
|
@@ -88,20 +87,51 @@ function copySource() {
|
|
|
88
87
|
})
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
interface
|
|
92
|
-
|
|
90
|
+
interface CheckIssue {
|
|
91
|
+
kind: 'compat' | 'lint'
|
|
92
|
+
slug?: string
|
|
93
93
|
title: string
|
|
94
|
-
category: string
|
|
95
|
-
clients: Array<{ name: string, notes: string[] }>
|
|
96
94
|
url?: string
|
|
95
|
+
category: string
|
|
97
96
|
line?: number
|
|
97
|
+
file: string
|
|
98
|
+
// compat-only
|
|
99
|
+
supportLevel?: 'unsupported' | 'mitigated' | 'unknown'
|
|
100
|
+
supportLabel?: string
|
|
101
|
+
affectedClients?: string[]
|
|
102
|
+
// lint-only
|
|
103
|
+
severity?: 'error' | 'warning'
|
|
104
|
+
message?: string
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
function supportPrefix(issue: CheckIssue): string {
|
|
108
|
+
if (issue.supportLevel === 'unsupported') return 'Not supported in'
|
|
109
|
+
if (issue.supportLevel === 'mitigated') return 'Partial support in'
|
|
110
|
+
return 'Support unknown in'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Split a message on backtick-delimited code spans. Returns alternating
|
|
115
|
+
* { text } and { code } segments so the template can render <code> inline
|
|
116
|
+
* without needing v-html.
|
|
117
|
+
*/
|
|
118
|
+
function messageSegments(raw: string | undefined): Array<{ code: boolean, text: string }> {
|
|
119
|
+
if (!raw) return []
|
|
120
|
+
const out: Array<{ code: boolean, text: string }> = []
|
|
121
|
+
const parts = raw.split('`')
|
|
122
|
+
for (let i = 0; i < parts.length; i++) {
|
|
123
|
+
if (parts[i]) out.push({ code: i % 2 === 1, text: parts[i] })
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function issueColorClass(issue: CheckIssue): string {
|
|
129
|
+
if (issue.kind === 'lint') {
|
|
130
|
+
return issue.severity === 'error' ? 'text-rose-600' : 'text-amber-600'
|
|
131
|
+
}
|
|
132
|
+
if (issue.supportLevel === 'unsupported') return 'text-rose-600'
|
|
133
|
+
if (issue.supportLevel === 'mitigated') return 'text-amber-600'
|
|
134
|
+
return 'text-gray-500 dark:text-gray-400'
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
interface TemplateStats {
|
|
@@ -110,10 +140,16 @@ interface TemplateStats {
|
|
|
110
140
|
links: number
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
const compatibilityIssues = ref<
|
|
143
|
+
const compatibilityIssues = ref<CheckIssue[]>([])
|
|
114
144
|
const compatibilityLoading = ref(false)
|
|
115
145
|
const compatibilityError = ref('')
|
|
116
146
|
const compatibilityCategory = ref('')
|
|
147
|
+
// Injected by serveDevUI into index.html — synchronous, available before
|
|
148
|
+
// any HTTP calls, so the Checks tab never flashes in when disabled.
|
|
149
|
+
const checksConfig = (window as any).__MAIZZLE_CONFIG__?.checks
|
|
150
|
+
const compatibilityDisabled = ref(checksConfig === false)
|
|
151
|
+
const expandedIssueKeys = ref(new Set<string>())
|
|
152
|
+
const issueKey = (issue: CheckIssue, i: number): string => `${issue.file}|${issue.line ?? 0}|${issue.slug ?? issue.title}|${i}`
|
|
117
153
|
const compatibilityCategories = ['css', 'html', 'image', 'others'] as const
|
|
118
154
|
const activeCompatibilityCategories = computed(() =>
|
|
119
155
|
compatibilityCategories.filter(cat => compatibilityIssues.value.some(i => i.category === cat))
|
|
@@ -122,8 +158,6 @@ const filteredCompatibilityIssues = computed(() => {
|
|
|
122
158
|
if (!compatibilityCategory.value) return compatibilityIssues.value
|
|
123
159
|
return compatibilityIssues.value.filter(i => i.category === compatibilityCategory.value)
|
|
124
160
|
})
|
|
125
|
-
const lintIssues = ref<LintIssue[]>([])
|
|
126
|
-
const lintLoading = ref(false)
|
|
127
161
|
const stats = ref<TemplateStats | null>(null)
|
|
128
162
|
const statsLoading = ref(false)
|
|
129
163
|
|
|
@@ -247,22 +281,30 @@ async function fetchStats() {
|
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
async function fetchCompatibility() {
|
|
284
|
+
if (compatibilityDisabled.value) return
|
|
285
|
+
const template = props.templates?.find(t => t.href === '/' + route.params.template)
|
|
286
|
+
if (!template) return
|
|
287
|
+
|
|
250
288
|
compatibilityLoading.value = true
|
|
251
289
|
compatibilityError.value = ''
|
|
252
290
|
try {
|
|
253
|
-
const res = await fetch(
|
|
254
|
-
method: 'POST',
|
|
255
|
-
body: renderedHtml,
|
|
256
|
-
})
|
|
291
|
+
const res = await fetch(`/__maizzle/compatibility/${template.path}`)
|
|
257
292
|
const data = await res.json()
|
|
258
|
-
if (data?.error) {
|
|
293
|
+
if (!Array.isArray(data) && data?.error) {
|
|
259
294
|
compatibilityError.value = data.error
|
|
260
295
|
compatibilityIssues.value = []
|
|
261
296
|
} else {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
297
|
+
const issues: CheckIssue[] = Array.isArray(data) ? data : []
|
|
298
|
+
compatibilityIssues.value = issues
|
|
299
|
+
// Keep the current category if it still has issues; otherwise fall
|
|
300
|
+
// back to the first category that does. Prevents a "refresh" during
|
|
301
|
+
// edits from snapping back to CSS when the user is on HTML/Image.
|
|
302
|
+
const current = compatibilityCategory.value
|
|
303
|
+
const currentStillActive = current && issues.some((i) => i.category === current)
|
|
304
|
+
if (!currentStillActive) {
|
|
305
|
+
const firstCat = compatibilityCategories.find(cat => issues.some((i) => i.category === cat))
|
|
306
|
+
compatibilityCategory.value = firstCat || ''
|
|
307
|
+
}
|
|
266
308
|
}
|
|
267
309
|
} catch {
|
|
268
310
|
compatibilityIssues.value = []
|
|
@@ -271,19 +313,21 @@ async function fetchCompatibility() {
|
|
|
271
313
|
}
|
|
272
314
|
}
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
316
|
+
/** Check if an issue is from the currently viewed template file */
|
|
317
|
+
function isCurrentFile(issue: { file: string }): boolean {
|
|
318
|
+
const template = props.templates?.find(t => t.href === '/' + route.params.template)
|
|
319
|
+
if (!template) return true
|
|
320
|
+
return issue.file.endsWith(template.path)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Get a short display name for a component file path */
|
|
324
|
+
function componentName(filePath: string): string {
|
|
325
|
+
const parts = filePath.replace(/\\/g, '/').split('/')
|
|
326
|
+
return parts[parts.length - 1]?.replace(/\.vue$/, '') ?? filePath
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function openInEditor(file: string, line: number) {
|
|
330
|
+
fetch(`/__open-in-editor?file=${encodeURIComponent(file + ':' + line)}`)
|
|
287
331
|
}
|
|
288
332
|
|
|
289
333
|
watch(() => route.params.template, () => {
|
|
@@ -292,17 +336,23 @@ watch(() => route.params.template, () => {
|
|
|
292
336
|
plaintextContent.value = ''
|
|
293
337
|
compatibilityIssues.value = []
|
|
294
338
|
compatibilityError.value = ''
|
|
295
|
-
lintIssues.value = []
|
|
296
339
|
stats.value = null
|
|
297
340
|
emailResult.value = null
|
|
298
341
|
sourceView.value = 'compiled'
|
|
299
|
-
fetchTemplate()
|
|
300
|
-
|
|
342
|
+
fetchTemplate()
|
|
343
|
+
fetchCompatibility()
|
|
301
344
|
fetchStats()
|
|
302
345
|
fetchEmailConfig()
|
|
303
346
|
if (viewMode.value === 'source') fetchSource()
|
|
304
347
|
}, { immediate: true })
|
|
305
348
|
|
|
349
|
+
// Templates list loads async from App.vue — re-trigger once available
|
|
350
|
+
watch(() => props.templates, (templates) => {
|
|
351
|
+
if (templates?.length && !compatibilityIssues.value.length && !compatibilityLoading.value) {
|
|
352
|
+
fetchCompatibility()
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
306
356
|
watch(viewMode, (mode) => {
|
|
307
357
|
if (mode === 'source') {
|
|
308
358
|
if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
|
|
@@ -319,8 +369,8 @@ watch(sourceView, (view) => {
|
|
|
319
369
|
|
|
320
370
|
if ((import.meta as any).hot) {
|
|
321
371
|
;(import.meta as any).hot.on('maizzle:template-updated', () => {
|
|
322
|
-
fetchTemplate()
|
|
323
|
-
|
|
372
|
+
fetchTemplate()
|
|
373
|
+
fetchCompatibility()
|
|
324
374
|
fetchStats()
|
|
325
375
|
|
|
326
376
|
// Always clear all source views so they re-fetch when switched to
|
|
@@ -335,33 +385,22 @@ if ((import.meta as any).hot) {
|
|
|
335
385
|
if (sourceView.value === 'plaintext') fetchPlaintext()
|
|
336
386
|
}
|
|
337
387
|
})
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
async function goToLine(line: number) {
|
|
342
|
-
// Switch to source view showing Vue source
|
|
343
|
-
viewMode.value = 'source'
|
|
344
|
-
sourceView.value = 'vue'
|
|
345
|
-
|
|
346
|
-
// Ensure vue source is loaded
|
|
347
|
-
if (!vueSourceHtml.value) {
|
|
348
|
-
await fetchVueSource()
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
await nextTick()
|
|
352
|
-
|
|
353
|
-
const el = vueSourceEl.value
|
|
354
|
-
if (!el) return
|
|
355
388
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
// Keep the UI in sync with live config edits. Payload is the same shape
|
|
390
|
+
// as the initial `window.__MAIZZLE_CONFIG__` inject — we replace it and
|
|
391
|
+
// derive per-feature flags from there.
|
|
392
|
+
;(import.meta as any).hot.on('maizzle:config-updated', (data: Record<string, unknown>) => {
|
|
393
|
+
;(window as any).__MAIZZLE_CONFIG__ = data
|
|
394
|
+
const wasDisabled = compatibilityDisabled.value
|
|
395
|
+
const nowDisabled = data?.checks === false
|
|
396
|
+
compatibilityDisabled.value = nowDisabled
|
|
397
|
+
if (nowDisabled) {
|
|
398
|
+
compatibilityIssues.value = []
|
|
399
|
+
if (activeTab.value === 'compatibility') activeTab.value = 'stats'
|
|
400
|
+
} else if (wasDisabled) {
|
|
401
|
+
fetchCompatibility()
|
|
402
|
+
}
|
|
403
|
+
})
|
|
365
404
|
}
|
|
366
405
|
|
|
367
406
|
async function goToCompiledLine(line: number) {
|
|
@@ -536,11 +575,13 @@ const bottomPanelOpen = ref(false)
|
|
|
536
575
|
const tabsPanelHeight = ref(40)
|
|
537
576
|
const activeTab = ref<string | undefined>(undefined)
|
|
538
577
|
|
|
578
|
+
const defaultTab = () => compatibilityDisabled.value ? 'stats' : 'compatibility'
|
|
579
|
+
|
|
539
580
|
function toggleBottomPanel() {
|
|
540
581
|
bottomPanelOpen.value = !bottomPanelOpen.value
|
|
541
582
|
if (bottomPanelOpen.value) {
|
|
542
583
|
tabsPanelHeight.value = 300
|
|
543
|
-
if (!activeTab.value) activeTab.value =
|
|
584
|
+
if (!activeTab.value) activeTab.value = defaultTab()
|
|
544
585
|
} else {
|
|
545
586
|
tabsPanelHeight.value = 40
|
|
546
587
|
activeTab.value = undefined
|
|
@@ -563,35 +604,39 @@ function onTabClick(tab: string) {
|
|
|
563
604
|
|
|
564
605
|
const tabsDragging = ref(false)
|
|
565
606
|
|
|
566
|
-
function onTabsDragStart(e: MouseEvent) {
|
|
607
|
+
function onTabsDragStart(e: MouseEvent | TouchEvent) {
|
|
567
608
|
e.preventDefault()
|
|
568
609
|
tabsDragging.value = true
|
|
569
|
-
const
|
|
610
|
+
const isTouch = e.type === 'touchstart'
|
|
611
|
+
const startY = isTouch ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
|
570
612
|
const startHeight = tabsPanelHeight.value
|
|
571
613
|
|
|
572
614
|
const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
|
|
573
615
|
const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
|
|
574
616
|
|
|
575
|
-
const
|
|
576
|
-
const
|
|
617
|
+
const onMove = (e: MouseEvent | TouchEvent) => {
|
|
618
|
+
const clientY = e.type === 'touchmove' ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
|
619
|
+
const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - clientY))
|
|
577
620
|
tabsPanelHeight.value = newHeight
|
|
578
621
|
bottomPanelOpen.value = newHeight > 40
|
|
579
622
|
|
|
580
623
|
if (!bottomPanelOpen.value) {
|
|
581
624
|
activeTab.value = undefined
|
|
582
625
|
} else if (!activeTab.value) {
|
|
583
|
-
activeTab.value =
|
|
626
|
+
activeTab.value = defaultTab()
|
|
584
627
|
}
|
|
585
628
|
}
|
|
586
629
|
|
|
587
|
-
const
|
|
630
|
+
const onEnd = () => {
|
|
588
631
|
tabsDragging.value = false
|
|
589
|
-
document.removeEventListener('mousemove',
|
|
590
|
-
document.removeEventListener('mouseup',
|
|
632
|
+
document.removeEventListener('mousemove', onMove)
|
|
633
|
+
document.removeEventListener('mouseup', onEnd)
|
|
634
|
+
document.removeEventListener('touchmove', onMove)
|
|
635
|
+
document.removeEventListener('touchend', onEnd)
|
|
591
636
|
}
|
|
592
637
|
|
|
593
|
-
document.addEventListener('mousemove',
|
|
594
|
-
document.addEventListener('mouseup',
|
|
638
|
+
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onMove)
|
|
639
|
+
document.addEventListener(isTouch ? 'touchend' : 'mouseup', onEnd)
|
|
595
640
|
}
|
|
596
641
|
|
|
597
642
|
const stripeBg = {
|
|
@@ -722,20 +767,18 @@ const stripeBg = {
|
|
|
722
767
|
<div
|
|
723
768
|
class="relative h-0 cursor-row-resize before:absolute before:top-0 before:left-0 before:right-0 before:h-3.25 before:content-['']"
|
|
724
769
|
@mousedown="onTabsDragStart"
|
|
770
|
+
@touchstart.prevent="onTabsDragStart"
|
|
725
771
|
/>
|
|
726
772
|
<Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
|
|
727
773
|
<div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
|
|
728
774
|
<TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
|
|
729
|
-
<TabsTrigger value="compatibility" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('compatibility')">
|
|
730
|
-
|
|
731
|
-
</TabsTrigger>
|
|
732
|
-
<TabsTrigger value="lint" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('lint')">
|
|
733
|
-
Linter
|
|
775
|
+
<TabsTrigger v-if="!compatibilityDisabled" value="compatibility" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('compatibility')">
|
|
776
|
+
Checks
|
|
734
777
|
</TabsTrigger>
|
|
735
|
-
<TabsTrigger value="stats" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('stats')">
|
|
778
|
+
<TabsTrigger value="stats" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('stats')">
|
|
736
779
|
Stats
|
|
737
780
|
</TabsTrigger>
|
|
738
|
-
<TabsTrigger value="test" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent" @click="onTabClick('test')">
|
|
781
|
+
<TabsTrigger value="test" class="text-xs font-normal px-3 h-full rounded-none! border-0! shadow-none! border-b! border-transparent select-none data-[state=active]:border-gray-400 data-[state=active]:dark:border-gray-600 data-[state=active]:bg-transparent data-[state=inactive]:bg-transparent dark:bg-transparent! dark:hover:bg-transparent!" @click="onTabClick('test')">
|
|
739
782
|
Test
|
|
740
783
|
</TabsTrigger>
|
|
741
784
|
</TabsList>
|
|
@@ -749,7 +792,7 @@ const stripeBg = {
|
|
|
749
792
|
<button
|
|
750
793
|
v-for="cat in activeCompatibilityCategories"
|
|
751
794
|
:key="cat"
|
|
752
|
-
class="px-2 py-0.5 text-[11px] rounded-full cursor-
|
|
795
|
+
class="px-2 py-0.5 text-[11px] rounded-full cursor-default transition-colors"
|
|
753
796
|
:class="compatibilityCategory === cat
|
|
754
797
|
? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
|
|
755
798
|
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
|
|
@@ -760,48 +803,45 @@ const stripeBg = {
|
|
|
760
803
|
</button>
|
|
761
804
|
</div>
|
|
762
805
|
<ScrollArea class="h-full flex-1 min-h-0 pl-5">
|
|
763
|
-
<p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">
|
|
806
|
+
<p v-if="compatibilityLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Running checks...</p>
|
|
764
807
|
<p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
|
|
765
|
-
<p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No
|
|
808
|
+
<p v-else-if="compatibilityIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
|
|
766
809
|
<ul v-else class="text-xs divide-y">
|
|
767
810
|
<li
|
|
768
811
|
v-for="(issue, i) in filteredCompatibilityIssues"
|
|
769
812
|
:key="i"
|
|
770
|
-
class="pr-4 py-
|
|
771
|
-
>
|
|
772
|
-
<div class="flex items-center gap-2">
|
|
773
|
-
<a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
|
|
774
|
-
{{ issue.title }}
|
|
775
|
-
</a>
|
|
776
|
-
<span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
|
|
777
|
-
{{ issue.title }}
|
|
778
|
-
</span>
|
|
779
|
-
<span class="text-gray-400 dark:text-gray-600 shrink-0">·</span>
|
|
780
|
-
<span class="text-gray-500 dark:text-gray-400 truncate">{{ issue.type === 'error' ? 'Not supported' : 'Partial support' }} in {{ issue.clients.map((c: any) => c.name).join(', ') }}</span>
|
|
781
|
-
<button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0 ml-auto" @click="goToCompiledLine(issue.line!)">L{{ issue.line }}</button>
|
|
782
|
-
</div>
|
|
783
|
-
</li>
|
|
784
|
-
</ul>
|
|
785
|
-
</ScrollArea>
|
|
786
|
-
</TabsContent>
|
|
787
|
-
<TabsContent value="lint" class="mt-0 h-full">
|
|
788
|
-
<ScrollArea class="h-full pl-5">
|
|
789
|
-
<p v-if="lintLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
|
|
790
|
-
<p v-else-if="lintIssues.length === 0" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No issues found.</p>
|
|
791
|
-
<ul v-else class="text-xs divide-y">
|
|
792
|
-
<li
|
|
793
|
-
v-for="(issue, i) in lintIssues"
|
|
794
|
-
:key="i"
|
|
795
|
-
class="pr-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
|
|
813
|
+
class="pr-4 py-2"
|
|
796
814
|
>
|
|
797
|
-
<div class="flex items-
|
|
815
|
+
<div class="flex items-center justify-between gap-4">
|
|
798
816
|
<div>
|
|
799
|
-
<
|
|
817
|
+
<a v-if="issue.url" :href="issue.url" target="_blank" rel="noopener" class="font-medium hover:underline" :class="issueColorClass(issue)">
|
|
818
|
+
{{ issue.title }}
|
|
819
|
+
</a>
|
|
820
|
+
<span v-else class="font-medium" :class="issueColorClass(issue)">
|
|
800
821
|
{{ issue.title }}
|
|
801
822
|
</span>
|
|
802
|
-
<div class="text-gray-500 dark:text-gray-400 mt-0.5">
|
|
823
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">
|
|
824
|
+
<template v-if="issue.kind === 'lint'">
|
|
825
|
+
<template v-for="(seg, j) in messageSegments(issue.message)" :key="j">
|
|
826
|
+
<code v-if="seg.code" class="px-1 py-0.5 rounded bg-gray-100 dark:bg-white/10 font-mono text-[11px]">{{ seg.text }}</code>
|
|
827
|
+
<template v-else>{{ seg.text }}</template>
|
|
828
|
+
</template>
|
|
829
|
+
</template>
|
|
830
|
+
<template v-else>
|
|
831
|
+
{{ supportPrefix(issue) }}
|
|
832
|
+
<template v-if="(issue.affectedClients?.length ?? 0) <= 4 || expandedIssueKeys.has(issueKey(issue, i))">
|
|
833
|
+
{{ (issue.affectedClients ?? []).join(', ') }}
|
|
834
|
+
</template>
|
|
835
|
+
<template v-else>
|
|
836
|
+
{{ issue.affectedClients!.slice(0, 4).join(', ') }}
|
|
837
|
+
<button class="underline cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" @click="expandedIssueKeys.add(issueKey(issue, i)); expandedIssueKeys = new Set(expandedIssueKeys)">
|
|
838
|
+
+ {{ issue.affectedClients!.length - 4 }} others
|
|
839
|
+
</button>
|
|
840
|
+
</template>
|
|
841
|
+
</template>
|
|
842
|
+
</div>
|
|
803
843
|
</div>
|
|
804
|
-
<button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="
|
|
844
|
+
<button v-if="issue.line" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer tabular-nums shrink-0" @click="openInEditor(issue.file, issue.line!)">{{ isCurrentFile(issue) ? `L${issue.line}` : `${componentName(issue.file)}:${issue.line}` }}</button>
|
|
805
845
|
</div>
|
|
806
846
|
</li>
|
|
807
847
|
</ul>
|
|
@@ -811,35 +851,35 @@ const stripeBg = {
|
|
|
811
851
|
<ScrollArea class="h-full pl-5">
|
|
812
852
|
<p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
|
|
813
853
|
<p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
|
|
814
|
-
<
|
|
815
|
-
<
|
|
816
|
-
<
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
</
|
|
828
|
-
<
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
</
|
|
842
|
-
</
|
|
854
|
+
<ul v-else class="text-xs divide-y divide-gray-200 dark:divide-white/10">
|
|
855
|
+
<li class="pr-4 py-2">
|
|
856
|
+
<div class="flex items-center justify-between gap-4">
|
|
857
|
+
<div>
|
|
858
|
+
<span class="font-medium" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">Size</span>
|
|
859
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Compiled HTML size. Gmail clips emails larger than ~100KB.</div>
|
|
860
|
+
</div>
|
|
861
|
+
<span class="font-medium tabular-nums shrink-0" :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'">{{ stats.size.formatted }}</span>
|
|
862
|
+
</div>
|
|
863
|
+
</li>
|
|
864
|
+
<li class="pr-4 py-2">
|
|
865
|
+
<div class="flex items-center justify-between gap-4">
|
|
866
|
+
<div>
|
|
867
|
+
<span class="font-medium text-gray-900 dark:text-gray-300">Images</span>
|
|
868
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Total from <img> tags and CSS background images.</div>
|
|
869
|
+
</div>
|
|
870
|
+
<span class="font-medium tabular-nums shrink-0">{{ stats.images }}</span>
|
|
871
|
+
</div>
|
|
872
|
+
</li>
|
|
873
|
+
<li class="pr-4 py-2">
|
|
874
|
+
<div class="flex items-center justify-between gap-4">
|
|
875
|
+
<div>
|
|
876
|
+
<span class="font-medium text-gray-900 dark:text-gray-300">Links</span>
|
|
877
|
+
<div class="text-gray-500 dark:text-gray-400 mt-0.5">Total <a> tags with an href attribute.</div>
|
|
878
|
+
</div>
|
|
879
|
+
<span class="font-medium tabular-nums shrink-0">{{ stats.links }}</span>
|
|
880
|
+
</div>
|
|
881
|
+
</li>
|
|
882
|
+
</ul>
|
|
843
883
|
</ScrollArea>
|
|
844
884
|
</TabsContent>
|
|
845
885
|
<TabsContent value="test" class="mt-0 h-full">
|
|
@@ -847,19 +887,19 @@ const stripeBg = {
|
|
|
847
887
|
<div class="pr-4 py-3 max-w-md">
|
|
848
888
|
<div class="space-y-2">
|
|
849
889
|
<div class="flex items-center gap-2">
|
|
850
|
-
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">To</label>
|
|
890
|
+
<label for="email-to" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">To</label>
|
|
851
891
|
<TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
|
|
852
892
|
<TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
|
|
853
893
|
<TagsInputItemText class="px-1.5 py-0 text-xs" />
|
|
854
894
|
<TagsInputItemDelete class="size-3.5" />
|
|
855
895
|
</TagsInputItem>
|
|
856
|
-
<TagsInputInput class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
|
|
896
|
+
<TagsInputInput id="email-to" class="text-xs min-h-5 px-0.5" placeholder="Add emails..." />
|
|
857
897
|
</TagsInput>
|
|
858
898
|
</div>
|
|
859
899
|
<div class="flex items-center gap-2">
|
|
860
|
-
<label class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0">Subject</label>
|
|
900
|
+
<label for="email-subject" class="text-xs text-gray-500 dark:text-gray-400 w-12 shrink-0 cursor-pointer">Subject</label>
|
|
861
901
|
<div class="flex-1 flex items-center gap-3">
|
|
862
|
-
<Input v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
|
|
902
|
+
<Input id="email-subject" v-model="emailSubject" :placeholder="String(route.params.template)" class="flex-1 h-7 text-xs! px-2" />
|
|
863
903
|
<label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
|
|
864
904
|
<Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
|
|
865
905
|
<span class="text-xs text-gray-500 dark:text-gray-400">Prevent threading</span>
|
|
@@ -44,18 +44,22 @@ function addAttributes(dom, config = {}) {
|
|
|
44
44
|
const attributesToAdd = defu(typeof addConfig === "object" ? addConfig : {}, DEFAULT_ATTRIBUTES);
|
|
45
45
|
if (Object.keys(attributesToAdd).length === 0) return dom;
|
|
46
46
|
for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {
|
|
47
|
+
if (attributes === false) continue;
|
|
47
48
|
const selectors = selectorPattern.split(",").map((s) => s.trim());
|
|
48
49
|
walk(dom, (node) => {
|
|
49
50
|
const el = node;
|
|
50
51
|
if (!el.name) return;
|
|
51
52
|
if (selectors.some((selector) => elementMatches(el, selector))) {
|
|
52
53
|
if (!el.attribs) el.attribs = {};
|
|
53
|
-
for (const [attrName, attrValue] of Object.entries(attributes))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
for (const [attrName, attrValue] of Object.entries(attributes)) {
|
|
55
|
+
if (attrValue === false) continue;
|
|
56
|
+
if (attrName === "class" && el.attribs.class) {
|
|
57
|
+
const existingClasses = el.attribs.class.split(/\s+/).filter(Boolean);
|
|
58
|
+
const newClasses = String(attrValue).split(/\s+/).filter(Boolean);
|
|
59
|
+
const mergedClasses = [...new Set([...existingClasses, ...newClasses])];
|
|
60
|
+
if (mergedClasses.join(" ") !== el.attribs.class) el.attribs.class = mergedClasses.join(" ");
|
|
61
|
+
} else if (!(attrName in el.attribs)) el.attribs[attrName] = String(attrValue);
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
});
|
|
61
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, Record<string, string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;
|
|
1
|
+
{"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, false | Record<string, false | string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // User opted out of this selector entirely (e.g. `table: false`)\n if (attributes === false) continue\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // User opted out of this specific attribute (e.g. `role: false`)\n if (attrValue === false) continue\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;AAE3E,MAAI,eAAe,MAAO;EAE1B,MAAM,YAAY,gBAAgB,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS;GAClB,MAAM,KAAK;AACX,OAAI,CAAC,GAAG,KAAM;AAKd,OAFgB,UAAU,MAAK,aAAY,eAAe,IAAI,SAAS,CAAC,EAE3D;AAEX,QAAI,CAAC,GAAG,QACN,IAAG,UAAU,EAAE;AAGjB,SAAK,MAAM,CAAC,UAAU,cAAc,OAAO,QAAQ,WAAW,EAAE;AAE9D,SAAI,cAAc,MAAO;AAEzB,SAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;MAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;MACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;MACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,UAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;gBAIxC,EAAE,YAAY,GAAG,SACnB,IAAG,QAAQ,YAAY,OAAO,UAAU;;;IAKhD;;AAGJ,QAAO;;;;;;AAOT,SAAS,eAAe,IAAa,UAA2B;AAE9D,YAAW,SAAS,MAAM;CAG1B,MAAM,YAAY,SAAS,MAAM,gCAAgC;AACjE,KAAI,WAAW;EACb,MAAM,GAAG,UAAU,aAAa;AAChC,MAAI,cAAc,OAEhB,QAAO,aAAa,GAAG,WAAW,EAAE;MAGpC,QAAO,GAAG,UAAU,cAAc;;AAKtC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,YAAY,SAAS,MAAM,EAAE;AAEnC,UADgB,GAAG,SAAS,OAAO,MAAM,MAAM,IAAI,EAAE,EACtC,SAAS,UAAU;;AAIpC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,KAAK,SAAS,MAAM,EAAE;AAC5B,SAAO,GAAG,SAAS,OAAO;;CAK5B,MAAM,eAAe,SAAS,MAAM,kCAAkC;AACtE,KAAI,cAAc;EAChB,MAAM,GAAG,SAAS,YAAY;AAC9B,MAAI,GAAG,SAAS,QAAS,QAAO;EAGhC,MAAM,cAAc,SAAS,MAAM,sBAAsB;AACzD,MAAI,aAAa;GACf,MAAM,GAAG,UAAU,aAAa;AAChC,OAAI,cAAc,OAChB,QAAO,aAAa,GAAG,WAAW,EAAE;OAEpC,QAAO,GAAG,UAAU,cAAc;;AAGtC,SAAO;;AAIT,QAAO,GAAG,SAAS"}
|
|
@@ -28,8 +28,8 @@ function inlineCSS(dom, config = {}) {
|
|
|
28
28
|
walk(dom, (node) => {
|
|
29
29
|
const el = node;
|
|
30
30
|
if (el.name === "style" && el.attribs) {
|
|
31
|
-
if (el.attribs
|
|
32
|
-
if (
|
|
31
|
+
if ("embed" in el.attribs && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
|
|
32
|
+
if ("data-embed" in el.attribs && !("embed" in el.attribs)) el.attribs.embed = "";
|
|
33
33
|
if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
|
|
34
34
|
}
|
|
35
35
|
});
|