Skip to content

Add getStylesFromCss #7830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { LHS } from './LHS/LHS'
import { RHS } from './RHS/RHS'
import { CSSExercisePageContext } from './CSSExercisePageContext'
import { useCSSExercisePageStore } from './store/cssExercisePageStore'
import { getStylesFromCss } from './utils/getStylesFromCss'

export default function CSSExercisePage(data: CSSExercisePageProps) {
const {
@@ -39,6 +40,13 @@ export default function CSSExercisePage(data: CSSExercisePageProps) {
defaultCode,
} = useSetupEditors(data.exercise.slug, data.code, actualIFrameRef)

useEffect(() => {
const selector = '#flag #top #rhs div:nth-child(1)'
getStylesFromCss(DUMMY_CSS, selector).then((styles) => {
console.log(`getting styles for selector: ${selector}`, styles)
})
}, [])

return (
<CSSExercisePageContext.Provider
value={{
@@ -73,3 +81,49 @@ export default function CSSExercisePage(data: CSSExercisePageProps) {
</CSSExercisePageContext.Provider>
)
}

const DUMMY_CSS = `#flag {
background: white;
display: flex;
flex-direction: column;
}

#top,
#bottom {
flex-basis: 0;
min-height: 0;
}
#top {
display: flex;
flex-grow: 5;
align-items: stretch;
}
#star {
aspect-ratio: 1;
background: #002868;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 60%;
}
}
#rhs,
#bottom {
flex-grow: 1;
display: flex;
flex-direction: column;
}
#bottom {
flex-grow: 6;
}

#rhs, #bottom {
div {
flex-grow: 1;
}
}
#rhs div:nth-child(odd),
#bottom div:nth-child(even) {
background: #BF0A30;
}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import postcss from 'postcss'
import postcssNested from 'postcss-nested'
import { parse as parseSelector } from 'css-what'

type Styles = Record<string, string>

interface SimulatedNode {
tag: string
id?: string
classes: string[]
nthChild?: number
parent?: SimulatedNode
}

export async function getStylesFromCss(
css: string,
targetSelector: string
): Promise<Styles> {
const targetNode = buildSimulatedTree(targetSelector)
const result = await postcss([postcssNested]).process(css, {
from: undefined,
})

const styles: Styles = {}

result.root.walkRules((rule) => {
const raw = rule.selector
const selectors = raw.split(',').map((s) => s.trim())

for (const sel of selectors) {
if (selectorMatchesNode(sel, targetNode)) {
rule.walkDecls((decl) => {
styles[decl.prop] = decl.value
})
}
}
})

return styles
}

// converts a full selector string (e.g. #flag #top #rhs div) into a parent-linked tree
function buildSimulatedTree(selector: string): SimulatedNode {
const parts = selector.split(' ').filter(Boolean)
let parent: SimulatedNode | undefined = undefined
let node: SimulatedNode | undefined

for (const part of parts) {
const current = parseSelectorPart(part)
current.parent = parent
parent = current
node = current
}

if (!node) throw new Error('Invalid selector')
return node
}

// parses a single CSS selector into a node
function parseSelectorPart(part: string): SimulatedNode {
const tokens = parseSelector(part)[0]
const node: SimulatedNode = {
tag: '*',
classes: [],
}

for (const token of tokens) {
if (token.type === 'tag') node.tag = token.name
if (token.type === 'attribute') {
if (token.name === 'id') node.id = token.value
if (token.name === 'class') node.classes.push(token.value)
}
if (
token.type === 'pseudo' &&
token.name === 'nth-child' &&
typeof token.data === 'string'
) {
const parsed = parseInt(token.data)
if (!isNaN(parsed)) node.nthChild = parsed
}
}

return node
}

// matches a CSS selector against a kinda "simulated DOM" node
function selectorMatchesNode(selector: string, node: SimulatedNode): boolean {
const parts = selector.split(' ').filter(Boolean)
let current: SimulatedNode | undefined = node

for (let i = parts.length - 1; i >= 0; i--) {
if (!current) return false

const tokens = parseSelector(parts[i])[0]

for (const token of tokens) {
if (
token.type === 'tag' &&
token.name !== current.tag &&
token.name !== '*'
)
return false
if (token.type === 'attribute') {
if (token.name === 'id' && token.value !== current.id) return false
if (token.name === 'class' && !current.classes.includes(token.value))
return false
}
if (token.type === 'pseudo' && token.name === 'nth-child') {
if (!nthChildMatches(token.data as string, current.nthChild))
return false
}
}

current = current.parent
}

return true
}

// evals n-th logic
// we might need to add more of these if we want to support more pseudo-classes
function nthChildMatches(
expected: string,
actual: number | undefined
): boolean {
if (!actual) return false
if (expected === 'odd') return actual % 2 === 1
if (expected === 'even') return actual % 2 === 0
return parseInt(expected) === actual
}
Loading
Oops, something went wrong.