@maizzle/framework 6.0.0-rc.11 → 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 +23 -11
- 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 +194 -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) {
|
|
@@ -412,6 +451,8 @@ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
|
|
|
412
451
|
const isHorizontal = edge === 'left' || edge === 'right'
|
|
413
452
|
const sign = (edge === 'left' || edge === 'top') ? -1 : 1
|
|
414
453
|
|
|
454
|
+
document.documentElement.style.cursor = isHorizontal ? 'ew-resize' : 'ns-resize'
|
|
455
|
+
|
|
415
456
|
const onMove = (ev: MouseEvent | TouchEvent) => {
|
|
416
457
|
const point = ev.type === 'touchmove' ? (ev as TouchEvent).touches[0] : (ev as MouseEvent)
|
|
417
458
|
if (isHorizontal) {
|
|
@@ -426,6 +467,7 @@ function onEdgeDrag(e: MouseEvent | TouchEvent, edge: Edge) {
|
|
|
426
467
|
|
|
427
468
|
const onUp = () => {
|
|
428
469
|
isDragging.value = false
|
|
470
|
+
document.documentElement.style.cursor = ''
|
|
429
471
|
updateFullSize()
|
|
430
472
|
document.removeEventListener('mousemove', onMove)
|
|
431
473
|
document.removeEventListener('mouseup', onUp)
|
|
@@ -533,11 +575,13 @@ const bottomPanelOpen = ref(false)
|
|
|
533
575
|
const tabsPanelHeight = ref(40)
|
|
534
576
|
const activeTab = ref<string | undefined>(undefined)
|
|
535
577
|
|
|
578
|
+
const defaultTab = () => compatibilityDisabled.value ? 'stats' : 'compatibility'
|
|
579
|
+
|
|
536
580
|
function toggleBottomPanel() {
|
|
537
581
|
bottomPanelOpen.value = !bottomPanelOpen.value
|
|
538
582
|
if (bottomPanelOpen.value) {
|
|
539
583
|
tabsPanelHeight.value = 300
|
|
540
|
-
if (!activeTab.value) activeTab.value =
|
|
584
|
+
if (!activeTab.value) activeTab.value = defaultTab()
|
|
541
585
|
} else {
|
|
542
586
|
tabsPanelHeight.value = 40
|
|
543
587
|
activeTab.value = undefined
|
|
@@ -560,35 +604,39 @@ function onTabClick(tab: string) {
|
|
|
560
604
|
|
|
561
605
|
const tabsDragging = ref(false)
|
|
562
606
|
|
|
563
|
-
function onTabsDragStart(e: MouseEvent) {
|
|
607
|
+
function onTabsDragStart(e: MouseEvent | TouchEvent) {
|
|
564
608
|
e.preventDefault()
|
|
565
609
|
tabsDragging.value = true
|
|
566
|
-
const
|
|
610
|
+
const isTouch = e.type === 'touchstart'
|
|
611
|
+
const startY = isTouch ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY
|
|
567
612
|
const startHeight = tabsPanelHeight.value
|
|
568
613
|
|
|
569
614
|
const rootEl = containerEl.value?.closest('.relative.h-full') as HTMLElement | null
|
|
570
615
|
const maxHeight = rootEl ? rootEl.getBoundingClientRect().height : Infinity
|
|
571
616
|
|
|
572
|
-
const
|
|
573
|
-
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))
|
|
574
620
|
tabsPanelHeight.value = newHeight
|
|
575
621
|
bottomPanelOpen.value = newHeight > 40
|
|
576
622
|
|
|
577
623
|
if (!bottomPanelOpen.value) {
|
|
578
624
|
activeTab.value = undefined
|
|
579
625
|
} else if (!activeTab.value) {
|
|
580
|
-
activeTab.value =
|
|
626
|
+
activeTab.value = defaultTab()
|
|
581
627
|
}
|
|
582
628
|
}
|
|
583
629
|
|
|
584
|
-
const
|
|
630
|
+
const onEnd = () => {
|
|
585
631
|
tabsDragging.value = false
|
|
586
|
-
document.removeEventListener('mousemove',
|
|
587
|
-
document.removeEventListener('mouseup',
|
|
632
|
+
document.removeEventListener('mousemove', onMove)
|
|
633
|
+
document.removeEventListener('mouseup', onEnd)
|
|
634
|
+
document.removeEventListener('touchmove', onMove)
|
|
635
|
+
document.removeEventListener('touchend', onEnd)
|
|
588
636
|
}
|
|
589
637
|
|
|
590
|
-
document.addEventListener('mousemove',
|
|
591
|
-
document.addEventListener('mouseup',
|
|
638
|
+
document.addEventListener(isTouch ? 'touchmove' : 'mousemove', onMove)
|
|
639
|
+
document.addEventListener(isTouch ? 'touchend' : 'mouseup', onEnd)
|
|
592
640
|
}
|
|
593
641
|
|
|
594
642
|
const stripeBg = {
|
|
@@ -719,20 +767,18 @@ const stripeBg = {
|
|
|
719
767
|
<div
|
|
720
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-['']"
|
|
721
769
|
@mousedown="onTabsDragStart"
|
|
770
|
+
@touchstart.prevent="onTabsDragStart"
|
|
722
771
|
/>
|
|
723
772
|
<Tabs :model-value="activeTab" class="flex flex-col min-h-0 h-full">
|
|
724
773
|
<div class="flex items-center justify-between min-h-10 pl-2 pr-3 shrink-0" :class="bottomPanelOpen ? 'border-b' : ''">
|
|
725
774
|
<TabsList class="h-full bg-transparent! rounded-none! p-0 gap-1">
|
|
726
|
-
<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')">
|
|
727
|
-
|
|
728
|
-
</TabsTrigger>
|
|
729
|
-
<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')">
|
|
730
|
-
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
|
|
731
777
|
</TabsTrigger>
|
|
732
|
-
<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')">
|
|
733
779
|
Stats
|
|
734
780
|
</TabsTrigger>
|
|
735
|
-
<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')">
|
|
736
782
|
Test
|
|
737
783
|
</TabsTrigger>
|
|
738
784
|
</TabsList>
|
|
@@ -746,7 +792,7 @@ const stripeBg = {
|
|
|
746
792
|
<button
|
|
747
793
|
v-for="cat in activeCompatibilityCategories"
|
|
748
794
|
:key="cat"
|
|
749
|
-
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"
|
|
750
796
|
:class="compatibilityCategory === cat
|
|
751
797
|
? 'bg-gray-900 text-white dark:bg-gray-600 dark:text-gray-100'
|
|
752
798
|
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10'"
|
|
@@ -757,48 +803,45 @@ const stripeBg = {
|
|
|
757
803
|
</button>
|
|
758
804
|
</div>
|
|
759
805
|
<ScrollArea class="h-full flex-1 min-h-0 pl-5">
|
|
760
|
-
<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>
|
|
761
807
|
<p v-else-if="compatibilityError" class="pr-4 py-3 text-xs text-red-500 dark:text-red-400">{{ compatibilityError }}</p>
|
|
762
|
-
<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>
|
|
763
809
|
<ul v-else class="text-xs divide-y">
|
|
764
810
|
<li
|
|
765
811
|
v-for="(issue, i) in filteredCompatibilityIssues"
|
|
766
812
|
:key="i"
|
|
767
|
-
class="pr-4 py-
|
|
813
|
+
class="pr-4 py-2"
|
|
768
814
|
>
|
|
769
|
-
<div class="flex items-center gap-
|
|
770
|
-
<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'">
|
|
771
|
-
{{ issue.title }}
|
|
772
|
-
</a>
|
|
773
|
-
<span v-else class="font-medium shrink-0" :class="issue.type === 'error' ? 'text-red-600' : 'text-amber-600'">
|
|
774
|
-
{{ issue.title }}
|
|
775
|
-
</span>
|
|
776
|
-
<span class="text-gray-400 dark:text-gray-600 shrink-0">·</span>
|
|
777
|
-
<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>
|
|
778
|
-
<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>
|
|
779
|
-
</div>
|
|
780
|
-
</li>
|
|
781
|
-
</ul>
|
|
782
|
-
</ScrollArea>
|
|
783
|
-
</TabsContent>
|
|
784
|
-
<TabsContent value="lint" class="mt-0 h-full">
|
|
785
|
-
<ScrollArea class="h-full pl-5">
|
|
786
|
-
<p v-if="lintLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Linting...</p>
|
|
787
|
-
<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>
|
|
788
|
-
<ul v-else class="text-xs divide-y">
|
|
789
|
-
<li
|
|
790
|
-
v-for="(issue, i) in lintIssues"
|
|
791
|
-
:key="i"
|
|
792
|
-
class="pr-4 py-2 hover:bg-gray-50 dark:hover:bg-white/5"
|
|
793
|
-
>
|
|
794
|
-
<div class="flex items-start justify-between gap-4">
|
|
815
|
+
<div class="flex items-center justify-between gap-4">
|
|
795
816
|
<div>
|
|
796
|
-
<
|
|
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)">
|
|
797
821
|
{{ issue.title }}
|
|
798
822
|
</span>
|
|
799
|
-
<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>
|
|
800
843
|
</div>
|
|
801
|
-
<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>
|
|
802
845
|
</div>
|
|
803
846
|
</li>
|
|
804
847
|
</ul>
|
|
@@ -808,35 +851,35 @@ const stripeBg = {
|
|
|
808
851
|
<ScrollArea class="h-full pl-5">
|
|
809
852
|
<p v-if="statsLoading" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">Loading stats...</p>
|
|
810
853
|
<p v-else-if="!stats" class="pr-4 py-3 text-xs text-gray-500 dark:text-gray-400">No stats available.</p>
|
|
811
|
-
<
|
|
812
|
-
<
|
|
813
|
-
<
|
|
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
|
-
</
|
|
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>
|
|
840
883
|
</ScrollArea>
|
|
841
884
|
</TabsContent>
|
|
842
885
|
<TabsContent value="test" class="mt-0 h-full">
|
|
@@ -844,19 +887,19 @@ const stripeBg = {
|
|
|
844
887
|
<div class="pr-4 py-3 max-w-md">
|
|
845
888
|
<div class="space-y-2">
|
|
846
889
|
<div class="flex items-center gap-2">
|
|
847
|
-
<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>
|
|
848
891
|
<TagsInput v-model="emailTo" delimiter=" " add-on-paste add-on-blur class="flex-1 min-h-7 gap-1 px-2 py-1">
|
|
849
892
|
<TagsInputItem v-for="item in emailTo" :key="item" :value="item" class="h-5 text-xs rounded">
|
|
850
893
|
<TagsInputItemText class="px-1.5 py-0 text-xs" />
|
|
851
894
|
<TagsInputItemDelete class="size-3.5" />
|
|
852
895
|
</TagsInputItem>
|
|
853
|
-
<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..." />
|
|
854
897
|
</TagsInput>
|
|
855
898
|
</div>
|
|
856
899
|
<div class="flex items-center gap-2">
|
|
857
|
-
<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>
|
|
858
901
|
<div class="flex-1 flex items-center gap-3">
|
|
859
|
-
<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" />
|
|
860
903
|
<label class="flex items-center gap-1.5 cursor-pointer select-none shrink-0">
|
|
861
904
|
<Checkbox v-model="emailPreventThreading" :default-checked="true" class="size-3.5" />
|
|
862
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
|
});
|