@solislab/postcss-type
Version:
PostCSS plugin to support responsive typography shorthands.
188 lines (162 loc) • 4.89 kB
JavaScript
import * as postcss from 'postcss'
import curry from 'lodash.curry'
/**
* PostCSS AtRule
* @see {@link http://api.postcss.org/AtRule.html|postcss API > AtRule}
* @typedef {Object} postcss.AtRule
*/
/**
* PostCSS Declaration
* @see {@link http://api.postcss.org/Declaration.html|postcss API > Declaration}
* @typedef {Object} postcss.Declaration
*/
/**
* PostCSS Node
* @see {@link http://api.postcss.org/Node.html|postcss API > Node}
* @typedef {Object} postcss.Node
*/
/**
* Create an @media rule for a custom media query
* @param {string} media
* @return {postcss.AtRule}
*/
const createMediaRule = media => postcss.atRule({ name: `media`, params: `(${media})` })
/**
* Create a declaration for a pair of key and value. Doesn't do anything if
* the value is empty or '/'
*
* @param {string} key
* @param {string} value
* @param {Node} source
* @return {postcss.Declaration | undefined}
*/
const createDecl = (key, value, source) => {
if (typeof value === 'undefined' || value === '/') {
return undefined
} else {
value = String(value)
const decl = postcss.decl({ prop: key, value: value })
decl.source = source.source
return decl
}
}
/**
* Append declaration to an AtRule if declaration is defined.
* This function is curried.
*
* @param {postcss.AtRule} atRule
* @param {postcss.Declaration | undefined} decl
* @return {postcss.AtRule}
*/
const appendDecl = curry((atRule, decl) => decl ? atRule.append(decl) : atRule)
/**
* Insert declaration into a parent rule before a certain position.
* This function is curried.
*
* @param {postcss.Node} parent
* @param {postcss.Node} position
* @param {postcss.Declaration} decl
* @return {postcss.Node}
*/
const insertDeclBefore = curry((parent, position, decl) => decl ? parent.insertBefore(position, decl) : parent)
/**
* Return true if the value's unit is px
*
* @param {string} val
* @return {boolean}
*/
const isPx = val => /\dpx/.test(val)
/**
* Return true if the string is a custom media query
*
* @param {string} val
* @return {boolean}
*/
const isCustomMedia = val => /^--/.test(val)
/**
* Format and round up a float number to a specific number of decimal places
*
* @param {number} num
* @param {number} dec
* @return {number}
*/
const toDecimalPlaces = (num, dec) => {
const multiplier = Math.pow(10, dec)
return Math.round(num * multiplier) / multiplier
}
/**
* Convert a pixel value to a ratio
*
* @param {string} val
* @param {number} ref
* @return {number}
*/
const pxToRatio = (val, ref) => toDecimalPlaces(parseFloat(val) / parseFloat(ref), 5)
/**
* Convert a pixel value to an em value
*
* @param {string} val
* @param {string} ref
* @return {string}
*/
const pxToEm = (val, ref) => pxToRatio(val, ref) + 'em'
/**
* Convert a pixel value to a rem value
*
* @param {string} val
* @param {string} ref
* @return {string}
*/
const pxToRem = (val, ref) => pxToRatio(val, ref) + 'rem'
/**
* PostCSS plugin to allow using `@type <media> <font-size> <line-height> <letter-spacing>` syntax
*/
export default postcss.plugin('postcss-type', (options = {}) => {
return root => {
if (options.rootSize && !isPx(options.rootSize)) {
throw root.error('rootSize option for postcss-type must be in pixel unit.')
}
const createFromValue = (value, rule) => {
const frags = value.split(/\s+/)
const media = (frags.length && isCustomMedia(frags[0])) ? frags.shift() : undefined
let [fontSize, lineHeight, letterSpacing] = frags
if (!fontSize) {
throw rule.error('Missing typography declarations for @type.')
}
if (fontSize && fontSize !== '/') {
if (typeof lineHeight !== 'undefined' && isPx(lineHeight)) {
lineHeight = pxToRatio(lineHeight, fontSize)
}
if (typeof letterSpacing !== 'undefined' && isPx(letterSpacing)) {
letterSpacing = pxToEm(letterSpacing, fontSize)
}
}
if (options.rootSize && isPx(options.rootSize)) {
fontSize = pxToRem(fontSize, options.rootSize)
}
const mediaRule = media
? createMediaRule(media)
: undefined
const insertTypeDecls = media
? appendDecl(mediaRule)
: insertDeclBefore(rule.parent, rule)
insertTypeDecls(createDecl('font-size', fontSize, rule))
insertTypeDecls(createDecl('line-height', lineHeight, rule))
insertTypeDecls(createDecl('letter-spacing', letterSpacing, rule))
if (mediaRule) {
rule.replaceWith(mediaRule)
} else {
rule.remove()
}
}
root.walk(rule => {
if (rule.type === 'atrule' && rule.name === 'type') {
createFromValue(rule.params, rule)
}
if (rule.type === 'decl' && rule.prop === 'type') {
createFromValue(rule.value, rule)
}
})
}
})
module.exports = exports['default']