UNPKG

@mapcss/preset-typography

Version:

Typography preset for MapCSS

348 lines (347 loc) 13.7 kB
import { customProperty, varFn } from "../../core/utils/format.js"; import { chain, deepMerge, isEmptyObject, isLength0, isObject, isString, isUndefined, mapEntries, prop, Rule, toAST, } from "../../deps.js"; import { $resolveTheme } from "../../core/resolve.js"; import { re$All } from "../../core/utils/regexp.js"; import parse from "../../deps/esm.sh/postcss-selector-parser.js"; import { removeDuplicatedDecl } from "../../core/postcss/_utils.js"; import { minifySelector } from "../../core/postcss/minify.js"; function generateDefault(varPrefix, prefix) { const varFnProperty = (property) => chain([prefix, property]).map(join).map((v) => customProperty(v, varPrefix)) .map(varFn).unwrap(); const varHeadings = varFnProperty("headings"); const varLinks = varFnProperty("links"); const varBorders = varFnProperty("borders"); const varCaptions = varFnProperty("captions"); const varCode = varFnProperty("code"); const varLists = varFnProperty("lists"); const varHr = varFnProperty("hr"); const varBgSoft = varFnProperty("bg-soft"); const DEFAULT = { "h1, h2": { color: varHeadings, fontWeight: 600, lineHeight: 1.25, }, a: { color: varLinks, textDecoration: "underline", fontWeight: "500", }, "a code": { color: varLinks, }, "p, ul, ol, pre": { margin: "1em 0", lineHeight: 1.75, }, blockquote: { margin: "1em 0", paddingLeft: "1em", fontStyle: "italic", borderLeft: `.25em solid ${varBorders}`, }, h3: { margin: "1.5em 0 .5em", fontSize: "1.375em", }, h4: { margin: "1em 0", fontSize: "1.125em", }, "img, video": { maxWidth: "100%", }, "figure, picture": { margin: "1em 0", }, figcaption: { color: varCaptions, fontSize: ".875em", }, code: { color: varCode, fontSize: ".875em", fontWeight: 600, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation-Mono, Courier-New, monospace", }, ":not(pre) > code::before, :not(pre) > code::after": { content: '"`"', }, pre: { padding: "1.25rem 1.5rem", overflowX: "auto", borderRadius: ".375rem", }, "pre, code": { whiteSpace: "pre", wordSpacing: "normal", wordBreak: "normal", wordWrap: "normal", tabSize: 4, "-moz-tab-size": 4, "-o-tab-size": 4, "-webkit-hyphens": "none", "-moz-hyphens": "none", hyphens: "none", background: "transparent", }, "pre code": { fontWeight: "inherit", }, "ol, ul": { paddingLeft: "1.25em", }, ul: { listStyleType: "disc", }, "ol > li::marker, ul > li::marker, summary::marker": { color: varLists, }, hr: { margin: "2em 0", border: `1px solid ${varHr}`, }, table: { display: "block", margin: "1em 0", borderCollapse: "collapse", overflowX: "auto", }, "tr:nth-child(2n)": { background: varBgSoft, }, "td, th": { border: `1px solid ${varBorders}`, padding: ".625em 1em", }, abbr: { cursor: "help", }, kbd: { color: varCode, border: "1px solid", padding: ".25rem .5rem", fontSize: ".875em", borderRadius: ".25rem", }, details: { margin: "1em 0", padding: "1.25rem 1.5rem", background: varBgSoft, }, summary: { cursor: "pointer", fontWeight: "600", }, }; return DEFAULT; } const join = (value) => value.join("-"); export function depsProse({ css, className: prefix }) { const prose = [ ["DEFAULT", (_, { variablePrefix, className }) => { const bodyColor = { [className]: { color: `var(${customProperty(`${prefix}-body`, variablePrefix)})`, }, }; const DEFAULT = generateDefault(variablePrefix, prefix); const [_css, disabledMap] = isolateEntries(css); const root = mergeAst(deepMerge(_css, DEFAULT), disabledMap); const bodyNodes = toAST(bodyColor); root.walkRules((rule) => { rule.selector = transformSelector(rule.selector, prefix); }); root.append(bodyNodes); return root; }], ["invert", (_, { key, variablePrefix, className }) => { const varProperty = (property) => customProperty(property, variablePrefix); const makeVarFnSet = (property) => [ chain([prefix, property]).map(join).map(varProperty).unwrap(), chain([prefix, key, property]).map(join).map(varProperty).map(varFn) .unwrap(), ]; const [varBody, varFnBody] = makeVarFnSet("body"); const [varHeadings, varFnHeadings] = makeVarFnSet("headings"); const [varLinks, varFnLinks] = makeVarFnSet("links"); const [varLists, varFnLists] = makeVarFnSet("lists"); const [varHr, varFnHr] = makeVarFnSet("hr"); const [varCaptions, varFnCaptions] = makeVarFnSet("captions"); const [varCode, varFnCode] = makeVarFnSet("code"); const [varBorders, varFnBorders] = makeVarFnSet("borders"); const [varBgSoft, varFnBgSoft] = makeVarFnSet("bg-soft"); return { type: "css", value: { [className]: { [varBody]: varFnBody, [varHeadings]: varFnHeadings, [varLinks]: varFnLinks, [varLists]: varFnLists, [varHr]: varFnHr, [varCaptions]: varFnCaptions, [varCode]: varFnCode, [varBorders]: varFnBorders, [varBgSoft]: varFnBgSoft, }, }, }; }], [re$All, ([, body], context) => { const maybeColor = $resolveTheme(body, "color", context); const { parentKey, variablePrefix } = context; if (isUndefined(parentKey) || isUndefined(maybeColor)) return; const _isString = isString(maybeColor); const colorBy = (colorWeight) => _isString ? maybeColor : (() => { const _color = prop(colorWeight, maybeColor); return isString(_color) ? _color : " "; })(); const varProperty = (property) => customProperty(property, variablePrefix); const makeProperty = (...properties) => chain(properties).map(join).map(varProperty).unwrap(); const makePropertySet = (property) => [ makeProperty(parentKey, property), makeProperty(parentKey, "invert", property), ]; const [varBody, varInvertBody] = makePropertySet("body"); const [varHeadings, varInvertHeadings] = makePropertySet("headings"); const [varLinks, varInvertLinks] = makePropertySet("links"); const [varLists, varInvertLists] = makePropertySet("lists"); const [varHr, varInvertHr] = makePropertySet("hr"); const [varCaptions, varInvertCaptions] = makePropertySet("captions"); const [varCode, varInvertCode] = makePropertySet("code"); const [varBorders, varInvertBorders] = makePropertySet("borders"); const [varBgSoft, varInvertBgSoft] = makePropertySet("bg-soft"); return { type: "css", value: { [context.className]: { [varBody]: colorBy(700), [varInvertBody]: colorBy(200), [varHeadings]: colorBy(900), [varInvertHeadings]: colorBy(100), [varLinks]: colorBy(900), [varInvertLinks]: colorBy(100), [varLists]: colorBy(400), [varInvertLists]: colorBy(500), [varHr]: colorBy(200), [varInvertHr]: colorBy(700), [varCaptions]: colorBy(500), [varInvertCaptions]: colorBy(400), [varCode]: colorBy(900), [varInvertCode]: colorBy(100), [varBorders]: colorBy(200), [varInvertBorders]: colorBy(700), [varBgSoft]: colorBy(100), [varInvertBgSoft]: colorBy(800), }, }, }; }], ]; return prose; } export function removeRuleOrDecl(root, removeMap) { const newRoot = root.clone(); const childRoot = toAST(removeMap); // lift up for non parent declaration to rule with no nodes childRoot.walkDecls((node) => { if (node.parent?.type !== "rule") { node.replaceWith(new Rule({ selector: minifySelector(node.prop) })); } }); const targetNodes = splitSelectorList(childRoot); targetNodes.walkRules((rule) => { newRoot.walkRules((node) => { if (minifySelector(node.selector) === rule.selector) { if (isLength0(rule.nodes)) { node.remove(); } else { rule.walkDecls((decl) => { node.walkDecls(decl.prop, (child) => { // If the declaration block becomes empty, the parent rule is deleted. if (child.parent?.type === "rule" && child.parent.nodes.length === 1) { child.parent.remove(); } else { child.remove(); } }); }); } } }); }); return newRoot; } export function mergeAst(css, disableMap) { const root = toAST(css); const map = recTransform(disableMap, () => ""); return chain(splitSelectorList(root)).map((root) => removeRuleOrDecl(root, map)).map(removeDuplicatedDecl) .unwrap(); } export function isolateEntries(value) { return Object.entries(value).reduce((acc, [key, value]) => { if (value === false) { return [acc[0], { ...acc[1], [key]: value }]; } if (isObject(value)) { const [a, b] = isolateEntries(value); const _a = isEmptyObject(a) ? {} : { [key]: { ...a } }; const _b = isEmptyObject(b) ? {} : { [key]: { ...b } }; return [{ ...acc[0], ..._a }, { ...acc[1], ..._b }]; } return [{ ...acc[0], [key]: value }, acc[1]]; }, [{}, {}]); } function isWhereableNode(node) { return node.type !== "pseudo" || node.type === "pseudo" && node.value === ":not"; } export function transformSelector(selector, className) { const result = parse((root) => { root.nodes.forEach((selector) => { const pseudos = selector.filter((v) => !isWhereableNode(v)); const where = parse.pseudo({ "value": ":where", nodes: [ parse.selector({ value: "", nodes: selector.filter(isWhereableNode), }), ], }); const classNameNode = parse.className({ value: className }); const combinator = parse.combinator({ value: " " }); const NOT = "not"; const notClassName = parse.className({ value: `${NOT}-${className}` }); const not = parse.pseudo({ value: ":not", nodes: [notClassName] }); const newSelector = parse.selector({ value: "", nodes: [classNameNode, combinator, where, not, ...pseudos], }); selector.replaceWith(newSelector); }); }).processSync(selector, { lossless: false }); return result; } export function splitSelectorList(root) { const newRoot = root.clone(); newRoot.walkRules((rule) => { const rules = rule.selectors.map((selector) => { return new Rule({ selector, nodes: rule.nodes }); }); rule.replaceWith(rules); }); return newRoot; } export function recTransform(object, transformer) { return mapEntries(object, ([key, value]) => [ key, isObject(value) ? recTransform(value, transformer) : transformer(value), ]); }