@codepatch/css
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

@codepatch/css

Make small changes to your CSS code the easy way

Installation

npm install @codepatch/css

IMPORTANT: @codepatch/css is an ESM-only package. Read more.

Motivation

@codepatch/css is the ideal tool for programmatically making small & simple modifications to your CSS code in JavaScript. It works by parsing the code into an AST and then overriding parts of it. Learn more about the motivation behind Codepatch in the main repository.

As an introducing example, let's rewrite all url() functions that point to resources on example.com:

import { modify } from '@codepatch/css'

const code = `body {
  background-image: url('https://example.com/image.png');
}`

const result = modify(code, (node, { source, override }) => {
  if (node.type === 'Url') {
    let url
    try {
      url = new URL(node.value)
    } catch {
      return
    }

    if (url.hostname === 'example.com') {
      override(`url('/cache${url.pathname}')`)
    }
  }
})

console.log(result.code)

Tip: To simplify following along the code above, you can have a look at the handled CSS code's AST.

Output:

body {
  background-image: url('/cache/image.png');
}

Note that Codepatch is not a transpiler, so it's not ideal for large or complex changes, you'd want something like PostCSS for that.

Usage

How it Works

function modify(code, options = {}, manipulator)

Transform the code string with the function manipulator, returning an output object.

For every node in the AST, manipulator(node, helpers) is called. The recursive walk is an in-order, depth-first traversal, so children are handled before their parents. This makes it easier to write manipulators that perform nested transformations as transforming parents often requires transforming their children first anyway.

The modify() return value is an object with two properties:

Type casting a Codepatch result object to a string will return its source code.

Pro Tip: Don't know how a CSS AST looks like? Have a look at astexplorer.net and select the "CSS" language with the "csstree" parser to get an idea.

Options

All options are, as the name says, optional. If you want to provide an options object, its place is between the code string and the manipulator function.

css-tree Options

Any options for the underlying css-tree library can be passed to options.parser:

const options = {
  parser: {
    context: 'declaration'
  }
}

// Parse a single declaration
modify('color: red', options, (node, helpers) => {
  ...
})

Source Maps

Codepatch uses magic-string under the hood to generate source maps for your code modifications. You can pass its source map options as options.sourceMap:

const options = {
  sourceMap: {
    hires: true
  }
}

modify(code, options, (node, helpers) => {
  // Create a high-resolution source map
})

Helpers

The helpers object passed to the manipulator function exposes three methods. All of these methods handle the current AST node (the one that has been passed to the manipulator as its first argument).

However, each of these methods takes an AST node as an optional first parameter if you want to access other nodes.

Example:

modify(':root { border: 1px solid red; }', (node, { source }) => {
  if (node.type === 'Declaration') {
    // `node` refers to the whole property and value
    source() // returns "border: 1px solid red"
    source(node.value) // returns "1px solid red"
  }
})

source()

Return the source code for the given node, including any modifications made to child nodes:

modify(':root {}', (node, { source, override }) => {
  if (node.type === 'Selector') {
    source() // returns ":root"
    override('body')
    source() // returns "body"
  }
})

override(replacement)

Replace the source of the affected node with the replacement string:

const result = modify('div:before {}', (node, { source, override }) => {
  if (
    node.type === 'PseudoClassSelector' &&
    ['before', 'after'].includes(node.name)
  ) {
    override(':' + source())
  }
})

console.log(result.code)

Output:

div::before {
}

parent(levels = 1)

From the starting node, climb up the syntax tree levels times. Getting an ancestor node of the program root yields undefined.

modify(':root { border: 1px solid red; }', (node, { parent }) => {
  if (node.type === 'Dimension') {
    // `node` refers to the `1` literal
    parent() // same as parent(1), refers to the `1px` dimension
    parent(2) // refers to the `1px solid red` value
    parent(3) // refers to the `border: 1px solid red` declaration
    parent(4) // refers to the `{ border: 1px solid red; }` block
    parent(5) // refers to the `:root { border: 1px solid red; }` rule
    parent(6) // refers to the stylesheet as a whole (root node)
    parent(7) // yields `undefined`, same as parent(7), parent(8) etc.
  }
})

External Helper Access

If you want to extract manipulation behavior into standalone functions, you can import the helpers directly from the @codepatch/css package, where they are not bound to a specific node:

import { override } from '@codepatch/css'

// Standalone function, converts pixel dimensions to rem
const toRem = node => {
  if (node.type === 'Dimension' && node.unit === 'px') {
    override(node, String(Number(node.value) / 16) + 'rem')
  }
}

// ...

import { modify } from '@codepatch/html'

const result = modify(':root { padding: 8px 12px; }', node => {
  toRem(node)
})

console.log(result.code)

Output:

:root {
  padding: 0.5rem 0.75rem;
}

Asynchronous Manipulations

The manipulator function may return a Promise. If it does, Codepatch will wait for that to resolve, making the whole modify() function return a Promise resolving to the result object (instead of returning the result object directly):

const code = `
:root { background-image: url('https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/cc.svg'); }
`

// Replace all SVG url()s with data URLs
const result = await modify(code, async (node, { source, override }) => {
  if (node.type === 'Url' && node.value.endsWith('.svg')) {
    // Fetch the URL's contents
    const svgCode = await fetch(node.value).then(result => result.text())

    // Replace the cUrl() call with the fetched contents
    override(
      `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgCode)}")`
    )
  }
})

// Note that we needed to `await` the result
console.log(result.code)

Output:

:root {
  background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%0A%20%20%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2243%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%229%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M50%2C42c-6-9-20-9%2C-25%2C0c-2%2C5-2%2C11%2C0%2C16c5%2C9%2C19%2C9%2C25%2C0l-6-3c-2%2C5-9%2C5-11%2C0c-1-1-1-9%2C0-10c2-5%2C9-4%2C11%2C0z%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M78%2C42c-6-9-20-9%2C-25%2C0c-2%2C5-2%2C11%2C0%2C16c5%2C9%2C19%2C9%2C25%2C0l-6-3c-2%2C5-9%2C5-11%2C0c-1-1-1-9%2C0-10c2-5%2C9-4%2C11%2C0z%22%2F%3E%0A%3C%2Fsvg%3E%0A');
}

Note: You have to return a Promise if you want to commit updates asynchronously. Once the manipulator function is done running, any override() calls originating from it will throw an error.

@codepatch/css is part of the Codepatch family of tools. Codepatch is a collection of tools that make it easy to programmatically make simple modifications to code of various languages.

Check out the Codepatch repository to find tools for other languages or information about how to write your own Codepatch modifier.

Package Sidebar

Install

npm i @codepatch/css

Weekly Downloads

1

Version

1.0.1

License

MIT

Unpacked Size

37.7 kB

Total Files

7

Last publish

Collaborators

  • loilo