@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.
Files changed (49) hide show
  1. package/dist/build.mjs +4 -1
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/serve.d.mts.map +1 -1
  4. package/dist/serve.mjs +22 -10
  5. package/dist/serve.mjs.map +1 -1
  6. package/dist/server/compatibility.d.mts +54 -2
  7. package/dist/server/compatibility.d.mts.map +1 -1
  8. package/dist/server/compatibility.mjs +890 -76
  9. package/dist/server/compatibility.mjs.map +1 -1
  10. package/dist/server/linter.d.mts +15 -2
  11. package/dist/server/linter.d.mts.map +1 -1
  12. package/dist/server/linter.mjs +194 -43
  13. package/dist/server/linter.mjs.map +1 -1
  14. package/dist/server/sfc-utils.d.mts +18 -0
  15. package/dist/server/sfc-utils.d.mts.map +1 -0
  16. package/dist/server/sfc-utils.mjs +184 -0
  17. package/dist/server/sfc-utils.mjs.map +1 -0
  18. package/dist/server/ui/App.vue +4 -41
  19. package/dist/server/ui/components/SidebarClose.vue +12 -0
  20. package/dist/server/ui/components/ui/command/Command.vue +1 -0
  21. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  22. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  23. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
  24. package/dist/server/ui/pages/Preview.vue +191 -151
  25. package/dist/transformers/addAttributes.mjs +10 -6
  26. package/dist/transformers/addAttributes.mjs.map +1 -1
  27. package/dist/transformers/inlineCSS.mjs +2 -2
  28. package/dist/transformers/inlineCSS.mjs.map +1 -1
  29. package/dist/transformers/purgeCSS.mjs +1 -1
  30. package/dist/transformers/purgeCSS.mjs.map +1 -1
  31. package/dist/transformers/tailwindcss.mjs +2 -4
  32. package/dist/transformers/tailwindcss.mjs.map +1 -1
  33. package/dist/types/config.d.mts +42 -2
  34. package/dist/types/config.d.mts.map +1 -1
  35. package/dist/types/index.d.mts +2 -2
  36. package/package.json +1 -3
  37. package/dist/_virtual/_rolldown/runtime.mjs +0 -32
  38. package/dist/node_modules/picomatch/index.mjs +0 -13
  39. package/dist/node_modules/picomatch/index.mjs.map +0 -1
  40. package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
  41. package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
  42. package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
  43. package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
  44. package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
  45. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
  46. package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
  47. package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
  48. package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
  49. 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, Info } from 'lucide-vue-next'
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 CompatibilityIssue {
92
- type: 'error' | 'warning'
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
- interface LintIssue {
101
- type: 'error' | 'warning'
102
- title: string
103
- message: string
104
- line?: number
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<CompatibilityIssue[]>([])
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('/__maizzle/compatibility', {
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
- compatibilityIssues.value = data
263
- // Default to first category that has issues
264
- const firstCat = compatibilityCategories.find(cat => data.some((i: CompatibilityIssue) => i.category === cat))
265
- compatibilityCategory.value = firstCat || ''
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
- async function fetchLint() {
275
- lintLoading.value = true
276
- try {
277
- const template = props.templates?.find(t => t.href === '/' + route.params.template)
278
- const filePath = template?.path ?? route.params.template
279
- const res = await fetch(`/__maizzle/lint/${filePath}`)
280
- const data = await res.json()
281
- lintIssues.value = Array.isArray(data) ? data.filter((i: LintIssue) => i.title) : []
282
- } catch {
283
- lintIssues.value = []
284
- } finally {
285
- lintLoading.value = false
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().then(fetchCompatibility)
300
- fetchLint()
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().then(fetchCompatibility)
323
- fetchLint()
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
- // Remove previous highlight
357
- el.querySelectorAll('.shiki-highlight-line').forEach(l => l.classList.remove('shiki-highlight-line'))
358
-
359
- // Find and highlight the line
360
- const lineEl = el.querySelector(`[data-line="${line}"]`)
361
- if (lineEl) {
362
- lineEl.classList.add('shiki-highlight-line')
363
- lineEl.scrollIntoView({ block: 'center', behavior: 'smooth' })
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 = 'compatibility'
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 startY = e.clientY
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 onMouseMove = (e: MouseEvent) => {
576
- const newHeight = Math.max(40, Math.min(maxHeight, startHeight + startY - e.clientY))
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 = 'compatibility'
626
+ activeTab.value = defaultTab()
584
627
  }
585
628
  }
586
629
 
587
- const onMouseUp = () => {
630
+ const onEnd = () => {
588
631
  tabsDragging.value = false
589
- document.removeEventListener('mousemove', onMouseMove)
590
- document.removeEventListener('mouseup', onMouseUp)
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', onMouseMove)
594
- document.addEventListener('mouseup', onMouseUp)
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
- Compatibility
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-pointer transition-colors"
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">Checking compatibility...</p>
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 compatibility issues found.</p>
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-1.5 hover:bg-gray-50 dark:hover:bg-white/5"
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">&middot;</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-start justify-between gap-4">
815
+ <div class="flex items-center justify-between gap-4">
798
816
  <div>
799
- <span class="font-medium" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
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">{{ issue.message }}</div>
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="goToLine(issue.line!)">L{{ issue.line }}</button>
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
- <div v-else class="pr-4 py-3 flex items-center gap-6 text-xs">
815
- <div class="flex items-center gap-1.5">
816
- <span class="text-gray-500 dark:text-gray-400">Size</span>
817
- <span
818
- class="font-medium tabular-nums"
819
- :class="stats.size.bytes > 102400 ? 'text-red-600' : stats.size.bytes > 51200 ? 'text-amber-600' : 'text-gray-900 dark:text-gray-300'"
820
- >{{ stats.size.formatted }}</span>
821
- <TooltipProvider :delay-duration="0">
822
- <Tooltip>
823
- <TooltipTrigger as-child>
824
- <button type="button">
825
- <Info class="size-3 text-gray-400 dark:text-gray-500" />
826
- </button>
827
- </TooltipTrigger>
828
- <TooltipContent class="max-w-60">
829
- Compiled HTML size, excludes image files. Gmail clips content at ~100KB.
830
- </TooltipContent>
831
- </Tooltip>
832
- </TooltipProvider>
833
- </div>
834
- <div class="flex items-center gap-1.5">
835
- <span class="text-gray-500 dark:text-gray-400">Images</span>
836
- <span class="font-medium tabular-nums">{{ stats.images }}</span>
837
- </div>
838
- <div class="flex items-center gap-1.5">
839
- <span class="text-gray-500 dark:text-gray-400">Links</span>
840
- <span class="font-medium tabular-nums">{{ stats.links }}</span>
841
- </div>
842
- </div>
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 &lt;img&gt; 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 &lt;a&gt; 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)) if (attrName === "class" && el.attribs.class) {
54
- const existingClasses = el.attribs.class.split(/\s+/).filter(Boolean);
55
- const newClasses = String(attrValue).split(/\s+/).filter(Boolean);
56
- const mergedClasses = [...new Set([...existingClasses, ...newClasses])];
57
- if (mergedClasses.join(" ") !== el.attribs.class) el.attribs.class = mergedClasses.join(" ");
58
- } else if (!(attrName in el.attribs)) el.attribs[attrName] = String(attrValue);
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;EAE3E,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,CAE5D,KAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;KAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;KACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;KACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,SAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;eAIxC,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"}
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.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
32
- if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
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
  });