@chassis/core
Version:
CSS4 pre-processor and responsive framework for modern UI development
317 lines (240 loc) • 9.31 kB
JavaScript
import parser from 'postcss-scss'
import SetRule from './atrules/set/SetRule.js'
import Setting from './atrules/set/Setting.js'
import CSSUtils from './utilities/CSSUtils.js'
import LayoutUtils from './utilities/LayoutUtils.js'
import TypographyUtils from './utilities/TypographyUtils.js'
import UnitUtils from './utilities/UnitUtils.js'
import { CONFIG } from '../index.js'
export default class TypographyEngine {
#settings = []
#initialSettings = []
#orphanSettings = []
#theme
#viewports
#headingSelectors = [...[...Array(7).keys()].slice(1).map(n => `h${n}`), 'legend']
constructor (theme, viewports) {
this.#theme = theme
this.#viewports = viewports
}
get settings () {
return {
unbounded: this.#settings,
orphaned: this.#orphanSettings,
initial: this.#initialSettings
}
}
get viewports () {
return this.#viewports
}
addSetting (setRule) {
const setting = new Setting(setRule)
setRule.remove()
if (!setting.bounds) {
return this.#settings.push(setting)
}
const viewports = {
min: setting.bounds.min ? CONFIG.viewports.find(viewport => viewport.bounds.min >= setting.bounds.min) : null,
max: setting.bounds.max ? CONFIG.viewports.find(viewport => viewport.bounds.max >= setting.bounds.max) : null
}
const buffer = [0, 0]
if (viewports.min && viewports.min.bounds.max < setting.bounds.min) {
buffer[0] = setting.bounds.min - viewports.min.bounds.min
}
if (viewports.max && viewports.max.bounds.max > setting.bounds.max) {
buffer[1] = setting.bounds.max - viewports.max.bounds.max
}
if (!viewports.min || !viewports.max) {
const viewport = viewports.min ?? viewports.max
switch (viewport) {
case viewports.min: return this.#registerSetting(setting, viewports.min)
case viewports.max: return this.#registerSetting(setting, null, viewports.max)
}
}
if (viewports.min.name === viewports.max.name) {
const viewport = this.#viewports.find(viewport => viewport.name === viewports.min.name)
return viewport.settings.push(setting)
}
console.log('IN BETWEEN - render at viewports falling completely within the bounds of the setting, and add additional queries for buffer')
this.#orphanSettings.push(setting)
}
processInlineComponentSettings (atrule) {
let settings = []
atrule.nodes.forEach(node => {
if (node.type !== 'atrule') {
return
}
switch (node.name) {
case 'set':
settings.push(new Setting(new SetRule(node)))
return node.remove()
case 'state': return this.processInlineComponentSettings(node)
default: return
}
})
if (settings.length > 0) {
atrule.parent.insertAfter(atrule, this.renderViewports(CSSUtils.createRoot(), false, false, settings).nodes)
}
atrule.replaceWith(atrule.nodes)
}
renderSetting (setting, width = CONFIG.layout.width.min, fontSize = CONFIG.typography.baseFontSize, columns = 1, includeSelector = true) {
if (setting.typeset) {
fontSize = TypographyUtils.getFontSize(CONFIG.typography.baseFontSize, setting.typeset.size)
}
const lineHeight = TypographyUtils.getOptimalLineHeight(fontSize, width, columns)
const cfg = {
selector: setting.selector,
decls: []
}
if (setting.typeset) {
if (!setting.typeset.sizeSet) {
cfg.fontSize = fontSize
setting.typeset.sizeSet = true
}
cfg.lineHeight = lineHeight
}
if (setting.margin) {
cfg.decls.push(...this.#getLayoutDecls('margin', setting.margin, fontSize, lineHeight, width, columns))
}
if (setting.padding) {
cfg.decls.push(...this.#getLayoutDecls('padding', setting.padding, fontSize, lineHeight, width, columns))
}
const typeset = parser.parse(this.renderTypeset(cfg))
// typeset.source = setting.source
// if (typeset.nodes.length === 0) {
// return null
// }
return typeset
}
renderInitialSettings () {
const root = CSSUtils.createRoot()
const { unbounded, initial } = this.settings
;[...unbounded, ...initial].forEach(setting => {
root.append(this.renderSetting(setting))
})
return root
}
renderInitialHeadings () {
const root = CSSUtils.createRoot()
this.#headingSelectors.forEach(selector => {
root.append(this.renderHeading(selector, true, true, CONFIG.layout.width.min, this.#theme.getHeading(selector)))
})
return root
}
renderHeading (selector, includeFontSize, includeLineHeight, width, decls = []) {
const rule = CSSUtils.createRule(selector)
const fontSize = TypographyUtils.getFontSize(CONFIG.typography.baseFontSize, CONFIG.typography.headings[selector])
if (includeFontSize) {
rule.append(CSSUtils.createDecl('font-size', `${UnitUtils.pxToRelative(fontSize)}rem`))
}
if (includeLineHeight) {
rule.append(CSSUtils.createDecl('line-height', TypographyUtils.getOptimalLineHeight(fontSize, width)))
}
rule.append(decls)
return rule
}
renderTypeset ({ selector, fontSize, lineHeight, decls }) {
const rule = CSSUtils.createRule(selector)
if (fontSize) {
rule.append(`font-size: ${UnitUtils.pxToRelative(fontSize)}rem;`)
}
if (lineHeight) {
rule.append(`line-height: ${lineHeight}`)
}
if (decls) {
rule.append(decls)
}
return rule
}
renderViewports (root, addHeadings = true, includeRoot = true, settings) {
let fontSize = CONFIG.typography.baseFontSize
this.#viewports.forEach((viewport, i) => {
if (!viewport.bounds.min) {
return
}
const query = CSSUtils.createAtRule({
name: 'media',
params: `screen and (min-width: ${viewport.bounds.min}px)`
})
const columns = viewport.columns ?? 1
if (viewport.type !== 'range') {
if (viewport.bounds.max !== CONFIG.layout.width.max) {
query.params += ` and (max-width: ${viewport.bounds.max}px)`
}
return viewport.settings.forEach(setting => {
query.append(this.renderSetting(setting, viewport.bounds.min, fontSize, columns))
})
}
root.append(CSSUtils.createComment(`${viewport.name}`))
if (includeRoot) {
const rule = CSSUtils.createRule(':root')
if (viewport.fontSize) {
fontSize = viewport.fontSize
rule.append(CSSUtils.createDecl('font-size', `${fontSize}px`))
}
rule.append(CSSUtils.createDecl('line-height', `${TypographyUtils.getOptimalLineHeight(fontSize, viewport.bounds.min, columns)}`))
query.append(rule)
}
if (addHeadings) {
this.#headingSelectors.forEach(selector => {
query.append(this.renderHeading(selector, false, true, viewport.bounds.min))
})
}
(settings ?? [...this.settings.unbounded, ...viewport.settings]).forEach(setting => {
setting = this.renderSetting(setting, viewport.bounds.min, fontSize, columns)
query.append(settings ? setting.nodes[0].nodes : setting.nodes[0])
})
root.append(query)
})
return root
}
#getLayoutDecls = (type, cfg, fontSize, lineHeight, width, columns) => {
if (cfg.typeset !== 0) {
fontSize = TypographyUtils.getFontSize(CONFIG.typography.baseFontSize, cfg.typeset)
lineHeight = TypographyUtils.getOptimalLineHeight(fontSize, width, columns)
}
const process = (...args) => {
switch (type) {
case 'margin': return LayoutUtils.getMargin(...args)
case 'padding': return LayoutUtils.getPadding(...args)
// default: TODO: Throw Error
}
}
const values = {
top: cfg.top ? process(cfg.display, 'top', lineHeight, cfg.top) : null,
right: cfg.right ? process(cfg.display, 'right', lineHeight, cfg.right) : null,
bottom: cfg.bottom ? process(cfg.display, 'bottom', lineHeight, cfg.bottom) : null,
left: cfg.left ? process(cfg.display, 'left', lineHeight, cfg.left) : null
}
const decls = []
const dimensions = ['top', 'right', 'bottom', 'left']
if (dimensions.every(dimension => !!values[dimension])) {
return [CSSUtils.createDecl(type, `${values.top}em ${values.right}em ${values.bottom}em ${values.left}em`)]
}
if (values.top) {
decls.push(CSSUtils.createDecl(`${type}-top`, `${values.top}em`))
}
if (values.right) {
decls.push(CSSUtils.createDecl(`${type}-right`, `${values.right}em`))
}
if (values.bottom) {
decls.push(CSSUtils.createDecl(`${type}-bottom`, `${values.bottom}em`))
}
if (values.left) {
decls.push(CSSUtils.createDecl(`${type}-left`, `${values.left}em`))
}
return decls
}
#registerSetting = (setting, min = null, max = null) => {
const indexes = {
min: min ? this.#viewports.findIndex(viewport => viewport.name === min.name) : 0,
max: max ? this.#viewports.findIndex(viewport => viewport.name === max.name) : this.#viewports.length - 1
}
if (!setting.bounds.min) {
this.#initialSettings.push(setting)
}
this.#viewports.slice(indexes.min, indexes.max + 1).forEach((viewport, i) => {
viewport.settings.push(setting)
})
}
}