UNPKG

@unocss/preset-typography

Version:

Typography preset for UnoCSS

375 lines (370 loc) 11.5 kB
import { mergeDeep, definePreset, toEscapedSelector } from '@unocss/core'; import { alphaPlaceholders } from '@unocss/rule-utils'; function DEFAULT(ctx) { const { theme, generator } = ctx; const hasWind4 = generator.config.presets.some((p) => p.name === "@unocss/preset-wind4"); const fontKey = hasWind4 ? "font" : "fontFamily"; return { "h1,h2,h3,h4,h5,h6": { "color": "var(--un-prose-headings)", "font-weight": "600", "line-height": 1.25 }, "a": { "color": "var(--un-prose-links)", "text-decoration": "underline", "font-weight": "500" }, "a code": { color: "var(--un-prose-links)" }, "p,ul,ol,pre": { "margin": "1em 0", "line-height": 1.75 }, "blockquote": { "margin": "1em 0", "padding-left": "1em", "font-style": "italic", "border-left": ".25em solid var(--un-prose-borders)" }, // taking 16px as a base, we scale h1, h2, h3, and h4 like // 16 (base) > 18 (h4) > 22 (h3) > 28 (h2) > 36 (h1) "h1": { "margin": "1rem 0", // h1 is always at the top of the page, so only margin 1 * root font size "font-size": "2.25em" }, "h2": { "margin": "1.75em 0 .5em", "font-size": "1.75em" }, "h3": { "margin": "1.5em 0 .5em", "font-size": "1.375em" }, "h4": { "margin": "1em 0", "font-size": "1.125em" }, "img,video": { "max-width": "100%" }, "figure,picture": { margin: "1em 0" }, "figcaption": { "color": "var(--un-prose-captions)", "font-size": ".875em" }, "code": { "color": "var(--un-prose-code)", "font-size": ".875em", "font-weight": 600, "font-family": theme[fontKey]?.mono }, ":not(pre) > code::before,:not(pre) > code::after": { content: '"`"' }, "pre": { "padding": "1.25rem 1.5rem", "overflow-x": "auto", "border-radius": ".375rem" }, "pre,code": { "white-space": "pre", "word-spacing": "normal", "word-break": "normal", "word-wrap": "normal", "-moz-tab-size": 4, "-o-tab-size": 4, "tab-size": 4, "-webkit-hyphens": "none", "-moz-hyphens": "none", "hyphens": "none", "background": "transparent" }, "pre code": { "font-weight": "inherit" }, "ol,ul": { "padding-left": "1.25em" }, "ol": { "list-style-type": "decimal" }, 'ol[type="A"]': { "list-style-type": "upper-alpha" }, 'ol[type="a"]': { "list-style-type": "lower-alpha" }, 'ol[type="A" s]': { "list-style-type": "upper-alpha" }, 'ol[type="a" s]': { "list-style-type": "lower-alpha" }, 'ol[type="I"]': { "list-style-type": "upper-roman" }, 'ol[type="i"]': { "list-style-type": "lower-roman" }, 'ol[type="I" s]': { "list-style-type": "upper-roman" }, 'ol[type="i" s]': { "list-style-type": "lower-roman" }, 'ol[type="1"]': { "list-style-type": "decimal" }, "ul": { "list-style-type": "disc" }, "ol > li::marker,ul > li::marker,summary::marker": { color: "var(--un-prose-lists)" }, "hr": { margin: "2em 0", border: "1px solid var(--un-prose-hr)" }, "table": { "display": "block", "margin": "1em 0", "border-collapse": "collapse", "overflow-x": "auto" }, "tr:nth-child(2n)": { background: "var(--un-prose-bg-soft)" }, "td,th": { border: "1px solid var(--un-prose-borders)", padding: ".625em 1em" }, "abbr": { cursor: "help" }, "kbd": { "color": "var(--un-prose-code)", "border": "1px solid", "padding": ".25rem .5rem", "font-size": ".875em", "border-radius": ".25rem" }, "details": { margin: "1em 0", padding: "1.25rem 1.5rem", background: "var(--un-prose-bg-soft)" }, "summary": { "cursor": "pointer", "font-weight": "600" } }; } const modifiers = [ ["headings", "h1", "h2", "h3", "h4", "h5", "h6", "th"], ["h1"], ["h2"], ["h3"], ["h4"], ["h5"], ["h6"], ["p"], ["a"], ["blockquote"], ["figure"], ["figcaption"], ["strong"], ["em"], ["kbd"], ["code"], ["pre"], ["ol"], ["ul"], ["li"], ["table"], ["thead"], ["tr"], ["th"], ["td"], ["img"], ["video"], ["hr"] ]; function getElements(modifier) { for (const [name, ...selectors] of modifiers) { if (name === modifier) return selectors.length > 0 ? selectors : [name]; } } function getCSS(options) { let css = ""; const { escapedSelector, selectorName, preflights, compatibility, important } = options; const disableNotUtility = compatibility?.noColonNot || compatibility?.noColonWhere; for (const selector in preflights) { const cssDeclarationBlock = preflights[selector]; const notProseSelector = `:not(:where(.not-${selectorName},.not-${selectorName} *))`; const pseudoCSSMatchArray = selector.split(",").map((s) => { const match = s.match(/:[():\-\w]+$/g); if (match) { const matchStr = match[0]; s = s.replace(matchStr, ""); return escapedSelector.map((e) => disableNotUtility ? `${e} ${s}${matchStr}` : `${e} :where(${s})${notProseSelector}${matchStr}`).join(","); } return null; }).filter((v) => v); if (pseudoCSSMatchArray.length) { css += pseudoCSSMatchArray.join(","); } else { css += escapedSelector.map((e) => disableNotUtility ? selector.split(",").map((s) => `${e} ${s}`).join(",") : `${e} :where(${selector})${notProseSelector}`).join(","); } css += "{"; for (const k in cssDeclarationBlock) { const v = cssDeclarationBlock[k]; css += `${k}:${v}${important ? " !important" : ""};`; } css += "}"; } return css; } function getPreflights(context, options) { const { compatibility, selectorName, important = false } = options; const cssExtend = typeof options?.cssExtend === "function" ? options.cssExtend(context.theme) : options?.cssExtend; let escapedSelector = Array.from(options.escapedSelectors); if (!escapedSelector[escapedSelector.length - 1].startsWith(".") && !compatibility?.noColonIs) escapedSelector = [`:is(${escapedSelector[escapedSelector.length - 1]},.${options.selectorName})`]; if (typeof important === "string") { escapedSelector = escapedSelector.map((e) => !compatibility?.noColonIs ? `:is(${important}) ${e}` : `${important} ${e}`); } if (cssExtend) return getCSS({ escapedSelector, selectorName, preflights: mergeDeep(DEFAULT(context), cssExtend), compatibility, important: important === true }); return getCSS({ escapedSelector, selectorName, preflights: DEFAULT(context), compatibility, important: important === true }); } const presetTypography = definePreset((options) => { if (options?.className) console.warn('[unocss:preset-typography] "className" is deprecated. Please use "selectorName" instead.'); const escapedSelectors = /* @__PURE__ */ new Set(); const selectorName = options?.selectorName || options?.className || "prose"; const selectorNameRE = new RegExp(`^${selectorName}$`); const colorsRE = new RegExp(`^${selectorName}-([-\\w]+)$`); const invertRE = new RegExp(`^${selectorName}-invert$`); const disableNotUtility = options?.compatibility?.noColonNot || options?.compatibility?.noColonWhere; return { name: "@unocss/preset-typography", enforce: "post", layers: { typography: -20 }, rules: [ [ selectorNameRE, (_, { rawSelector }) => { escapedSelectors.add(toEscapedSelector(rawSelector)); return { "color": "var(--un-prose-body)", "max-width": "65ch" }; }, { layer: "typography" } ], [ colorsRE, ([, color], { theme }) => { const baseColor = theme.colors?.[color]; if (baseColor == null) return; const colorObject = typeof baseColor === "object" ? baseColor : {}; const TagColorMap = { "body": 700, "headings": 900, "links": 900, "lists": 400, "hr": 200, "captions": 500, "code": 900, "borders": 200, "bg-soft": 100, // invert colors (dark mode) "invert-body": 200, "invert-headings": 100, "invert-links": 100, "invert-lists": 500, "invert-hr": 700, "invert-captions": 400, "invert-code": 100, "invert-borders": 700, "invert-bg-soft": 800 }; const result = {}; for (const key in TagColorMap) { const value = TagColorMap[key]; const color2 = colorObject[value] ?? baseColor; let hasAlpha = false; for (const placeholder of alphaPlaceholders) { if (color2.includes(placeholder)) { hasAlpha = true; result[`--un-prose-${key}-opacity`] = 1; result[`--un-prose-${key}`] = color2.replace(placeholder, `var(--un-prose-${key}-opacity)`); break; } } if (!hasAlpha) result[`--un-prose-${key}`] = color2; } return result; }, { layer: "typography" } ], [ invertRE, () => { return { "--un-prose-body": "var(--un-prose-invert-body)", "--un-prose-headings": "var(--un-prose-invert-headings)", "--un-prose-links": "var(--un-prose-invert-links)", "--un-prose-lists": "var(--un-prose-invert-lists)", "--un-prose-hr": "var(--un-prose-invert-hr)", "--un-prose-captions": "var(--un-prose-invert-captions)", "--un-prose-code": "var(--un-prose-invert-code)", "--un-prose-borders": "var(--un-prose-invert-borders)", "--un-prose-bg-soft": "var(--un-prose-invert-bg-soft)" }; }, { layer: "typography" } ] ], variants: [ { name: "typography element modifiers", match: (matcher) => { if (matcher.startsWith(`${selectorName}-`)) { const modifyRe = new RegExp(`^${selectorName}-(\\w+)[:-].+$`); const modifier = matcher.match(modifyRe)?.[1]; if (modifier) { const elements = getElements(modifier); if (elements?.length) { return { matcher: matcher.slice(selectorName.length + modifier.length + 2), selector: (s) => { const notProseSelector = `:not(:where(.not-${selectorName},.not-${selectorName} *))`; const escapedSelector = disableNotUtility ? elements.map((e) => `${s} ${e}`).join(",") : `${s} :is(:where(${elements})${notProseSelector})`; return escapedSelector; } }; } } } } } ], preflights: [ { layer: "typography", getCSS: (context) => { if (escapedSelectors.size > 0) { return getPreflights(context, { escapedSelectors, ...options, selectorName }); } } } ] }; }); export { presetTypography as default, presetTypography };