|
| 1 | +import plugin from 'tailwindcss/plugin' |
| 2 | + |
| 3 | +export interface ResetOptions { |
| 4 | + /** |
| 5 | + * 控制 `button` reset 的注入与选择器,传入 `false` 可跳过该规则。 |
| 6 | + */ |
| 7 | + buttonReset?: false | ResetConfig |
| 8 | + /** |
| 9 | + * 控制 `image` reset(同时覆盖 `<image>` 与 `<img>`)。 |
| 10 | + */ |
| 11 | + imageReset?: false | ResetConfig |
| 12 | + /** |
| 13 | + * 额外的 reset 规则,可根据业务自定义。 |
| 14 | + */ |
| 15 | + extraResets?: ResetConfig[] |
| 16 | +} |
| 17 | + |
| 18 | +export interface ResetConfig { |
| 19 | + selectors?: string[] |
| 20 | + declarations?: Record<string, string | number | false | null | undefined> |
| 21 | + pseudo?: Record<string, string | number | false | null | undefined> |
| 22 | +} |
| 23 | + |
| 24 | +const DEFAULT_BUTTON_RESET_SELECTORS = ['button'] as const |
| 25 | +const DEFAULT_BUTTON_DECLARATIONS = { |
| 26 | + padding: '0', |
| 27 | + backgroundColor: 'transparent', |
| 28 | + fontSize: 'inherit', |
| 29 | + lineHeight: 'inherit', |
| 30 | + color: 'inherit', |
| 31 | + borderWidth: '0', |
| 32 | +} as const |
| 33 | +const BUTTON_RESET_PSEUDO_DECLARATIONS = { |
| 34 | + border: 'none', |
| 35 | +} as const |
| 36 | + |
| 37 | +const DEFAULT_IMAGE_RESET_SELECTORS = ['image', 'img'] as const |
| 38 | +const DEFAULT_IMAGE_DECLARATIONS = { |
| 39 | + display: 'block', |
| 40 | + borderWidth: '0', |
| 41 | + backgroundColor: 'transparent', |
| 42 | + maxWidth: '100%', |
| 43 | + height: 'auto', |
| 44 | +} as const |
| 45 | + |
| 46 | +type DeclValue = string | number | false | null | undefined |
| 47 | + |
| 48 | +function normalizeResetSelectors(option: ResetConfig | undefined, defaults: readonly string[]) { |
| 49 | + const resolved = option?.selectors?.length ? option.selectors : defaults |
| 50 | + const normalized: string[] = [] |
| 51 | + for (const selector of resolved) { |
| 52 | + const trimmed = selector.trim() |
| 53 | + if (!trimmed || normalized.includes(trimmed)) { |
| 54 | + continue |
| 55 | + } |
| 56 | + normalized.push(trimmed) |
| 57 | + } |
| 58 | + return normalized.length ? normalized : undefined |
| 59 | +} |
| 60 | + |
| 61 | +function convertSelectorForBase(selector: string) { |
| 62 | + if (selector.startsWith('.')) { |
| 63 | + const className = selector.slice(1) |
| 64 | + if (className.length > 0) { |
| 65 | + return `[class~="${className}"]` |
| 66 | + } |
| 67 | + } |
| 68 | + if (selector.startsWith('#')) { |
| 69 | + const id = selector.slice(1) |
| 70 | + if (id.length > 0) { |
| 71 | + return `[id="${id}"]` |
| 72 | + } |
| 73 | + } |
| 74 | + return selector |
| 75 | +} |
| 76 | + |
| 77 | +function normalizeDeclarations( |
| 78 | + option: ResetConfig | undefined, |
| 79 | + defaults: Record<string, string>, |
| 80 | +) { |
| 81 | + const normalized: Record<string, string> = { ...defaults } |
| 82 | + const overrides = option?.declarations |
| 83 | + if (!overrides) { |
| 84 | + return normalized |
| 85 | + } |
| 86 | + |
| 87 | + const entries = Object.entries(overrides) |
| 88 | + for (const [prop, value] of entries) { |
| 89 | + const resolved = normalizeDeclarationValue(value) |
| 90 | + if (resolved === undefined) { |
| 91 | + delete normalized[prop] |
| 92 | + } |
| 93 | + else { |
| 94 | + normalized[prop] = resolved |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + return normalized |
| 99 | +} |
| 100 | + |
| 101 | +function normalizePseudo(option: ResetConfig | undefined, defaults?: Record<string, string>) { |
| 102 | + const normalized: Record<string, string> = defaults ? { ...defaults } : {} |
| 103 | + const overrides = option?.pseudo |
| 104 | + if (!overrides) { |
| 105 | + return Object.keys(normalized).length ? normalized : undefined |
| 106 | + } |
| 107 | + |
| 108 | + const entries = Object.entries(overrides) |
| 109 | + for (const [prop, value] of entries) { |
| 110 | + const resolved = normalizeDeclarationValue(value) |
| 111 | + if (resolved === undefined) { |
| 112 | + delete normalized[prop] |
| 113 | + } |
| 114 | + else { |
| 115 | + normalized[prop] = resolved |
| 116 | + } |
| 117 | + } |
| 118 | + return Object.keys(normalized).length ? normalized : undefined |
| 119 | +} |
| 120 | + |
| 121 | +function normalizeDeclarationValue(value: DeclValue) { |
| 122 | + if (value === false || value === null || value === undefined) { |
| 123 | + return undefined |
| 124 | + } |
| 125 | + return typeof value === 'number' ? value.toString() : value |
| 126 | +} |
| 127 | + |
| 128 | +interface NormalizedResetRule { |
| 129 | + selectors: string[] |
| 130 | + declarations: Record<string, string> |
| 131 | + pseudo?: Record<string, string> |
| 132 | +} |
| 133 | + |
| 134 | +function createResetRule( |
| 135 | + option: ResetConfig | undefined | false, |
| 136 | + defaults: { |
| 137 | + selectors: readonly string[] |
| 138 | + declarations: Record<string, string> |
| 139 | + pseudo?: Record<string, string> |
| 140 | + }, |
| 141 | +): NormalizedResetRule | undefined { |
| 142 | + if (option === false) { |
| 143 | + return undefined |
| 144 | + } |
| 145 | + const selectors = normalizeResetSelectors(option, defaults.selectors) |
| 146 | + if (!selectors) { |
| 147 | + return undefined |
| 148 | + } |
| 149 | + const declarations = normalizeDeclarations(option ?? {}, defaults.declarations) |
| 150 | + if (Object.keys(declarations).length === 0) { |
| 151 | + return undefined |
| 152 | + } |
| 153 | + const pseudo = normalizePseudo(option ?? {}, defaults.pseudo) |
| 154 | + return { |
| 155 | + selectors: selectors.map(convertSelectorForBase), |
| 156 | + declarations, |
| 157 | + pseudo, |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +export const reset = plugin.withOptions<ResetOptions>( |
| 162 | + (options?: ResetOptions) => { |
| 163 | + const rules: NormalizedResetRule[] = [] |
| 164 | + const buttonRule = createResetRule(options?.buttonReset, { |
| 165 | + selectors: DEFAULT_BUTTON_RESET_SELECTORS, |
| 166 | + declarations: DEFAULT_BUTTON_DECLARATIONS, |
| 167 | + pseudo: BUTTON_RESET_PSEUDO_DECLARATIONS, |
| 168 | + }) |
| 169 | + if (buttonRule) { |
| 170 | + rules.push(buttonRule) |
| 171 | + } |
| 172 | + const imageRule = createResetRule(options?.imageReset, { |
| 173 | + selectors: DEFAULT_IMAGE_RESET_SELECTORS, |
| 174 | + declarations: DEFAULT_IMAGE_DECLARATIONS, |
| 175 | + }) |
| 176 | + if (imageRule) { |
| 177 | + rules.push(imageRule) |
| 178 | + } |
| 179 | + for (const extra of options?.extraResets ?? []) { |
| 180 | + const normalized = createResetRule(extra, { |
| 181 | + selectors: extra.selectors ?? [], |
| 182 | + declarations: {}, |
| 183 | + }) |
| 184 | + if (normalized) { |
| 185 | + rules.push(normalized) |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + return ({ addBase }) => { |
| 190 | + if (!rules.length) { |
| 191 | + return |
| 192 | + } |
| 193 | + const baseRules: Record<string, Record<string, string>> = {} |
| 194 | + for (const rule of rules) { |
| 195 | + baseRules[rule.selectors.join(',')] = rule.declarations |
| 196 | + if (rule.pseudo) { |
| 197 | + const pseudoSelectors = rule.selectors.map(selector => `${selector}::after`).join(',') |
| 198 | + baseRules[pseudoSelectors] = rule.pseudo |
| 199 | + } |
| 200 | + } |
| 201 | + addBase(baseRules) |
| 202 | + } |
| 203 | + }, |
| 204 | + () => { |
| 205 | + return {} |
| 206 | + }, |
| 207 | +) |
| 208 | + |
| 209 | +export default reset |
0 commit comments