Skip to content

par5ul1/longhand

Repository files navigation

longhand logo
ASCII art adapted from https://www.asciiart.eu/people/body-parts/hand-gestures

longhand is a highly specialized CSS parser, focused on taking a shorthand style declaration -- such as margin: 10px -- and returning the implicit declarations that make up the shorthand (the longhands, if you will) -- such as margin-top: 10px; margin-right: 10px; margin-bottom: 10px; margin-left: 10px.

Features

  • Supports all CSS shorthand properties*
  • Can parse shorthand properties into their longhand equivalents
  • Can check if a property is a valid longhand property for a given shorthand property
  • Supports both camelCase 🐪 and kebab-case 🍢
  • TypeScript types included
  • JSDoc-style API documentation
  • No external dependencies
  • Unit tested

*on the browser. See Limitations for more details regarding server-side limitations.

Installation

pnpm

pnpm add longhand

npm

npm install longhand

yarn

yarn add longhand

Usage

Getting Started

The longhand package exports a single class, Longhand, which can be used to parse shorthand properties into their longhand equivalents. Both instance parse and static parse methods are available.

import { Longhand } from "longhand";

const { parse } = new Longhand();

{
  const parsed = parse("margin", "10px"); // Instance method
} /* or */ {
  const parsed = Longhand.parse("margin", "10px"); // Static method
} 

// `{...}` just to create a new block scope to keep the example valid

The parse method returns an instance of the LonghandStyle class, which contains the parsed longhand styles, without the original shorthand property. Those can, however, be accessed separately via originalProperty and originalValue.

import { Longhand } from "longhand";

const longhand = new Longhand();

const parsed = longhand.parse("margin", "10px");

console.log(parsed.originalProperty); // "margin"
console.log(parsed.originalValue); // "10px"
console.log(parsed.kebabCaseStyles); // { "margin-top": "10px", "margin-right": "10px", "margin-bottom": "10px", "margin-left": "10px" }
console.log(parsed.camelCaseStyles); // { "marginTop": "10px", "marginRight": "10px", "marginBottom": "10px", "marginLeft": "10px" }
console.log(parsed.styles); // { "margin-top": "10px", "margin-right": "10px", "margin-bottom": "10px", "margin-left": "10px", "marginTop": "10px", "marginRight": "10px", "marginBottom": "10px", "marginLeft": "10px" }
console.log(parsed.kebabCaseProperties); // [ "margin-top", "margin-right", "margin-bottom", "margin-left" ]
console.log(parsed.camelCaseProperties); // [ "marginTop", "marginRight", "marginBottom", "marginLeft" ]
console.log(parsed.properties); // [ "margin-top", "margin-right", "margin-bottom", "margin-left", "marginTop", "marginRight", "marginBottom", "marginLeft" ]
console.log(parsed.length); // 4
console.log(parsed.isValidLonghandProperty("margin-top")); // true
console.log(parsed.isValidLonghandProperty("margin-middle")); // false

Examples

For examples, see examples:

Documentation

For the full API, see DOCUMENTATION.md.

Running Server Side

The way it's written, longhand leverages browser methods to parse shorthands. This is to avoid reinventing the wheel. Unfortunately, this means that to work in Node (or Bun, or Deno, or the next big runtime), someone has to reinvent the wheel, or make it accessible on the server. Technically, only the CSSOM needs to be emulated, but that's accessed in the browser through a the window (element.style) so we'll need full DOM emulation through something like jsdom or happy-dom, or a full-on headless browser like puppeteer. If the former option is pursued, like in the server example, then the following code should enable functionality:

import { JSDOM } from "jsdom";
import { Longhand } from "longhand";

const dom = new JSDOM("<!DOCTYPE html>");
// @ts-expect-error
global.window = dom.window;
global.document = dom.window.document;

Running longhand in a headless browser should not need any additional configuration.

Limitations

Zero-dependency just means that no external packages are needed, but the runtime environment is effectively a dependency, and therefore, YMMV.

There are two major limitations to look out for with this package:

  1. jsdom is imperfect, and they have made some really questionable decisions particularly regarding the CSSOM -- which makes sense; who in their right mind emulates CSS 🥲? -- so you might get strange results. I have done my best to support as much as possible (see this) but as you can see by the test input shorthand-properties.json, styles marked as unsupported.property mean that jsdom doesn't support the property at all (mostly really just the --webkit-* exclusives), and those marked as unsupported.value mean that jsdom understands the property but doesn't know the longhands exist. Perhaps, this will someday improve. But for now, I recommend not using this package server-side, unless you have a contrived use-case that you can test works. (FYI, happy-dom works even worse according to the tests.)

    1.1. Another silly limitation of jsdom is that, in longhand, background: invalid throws for invalid not being a valid background value, but borderRadius: invalid is accepted as valid.

  2. As anyone who's worked on the web knows, browsers have a mind of their own, especially when it comes to CSS support. Therefore, the CSSOM also differs slightly browser to browser. This is less of an issue, and is kind of expected, but you should keep in mind that not even the people behind whatever browser you're reading this in can agree on a universal model, so this package can't do that either.

Why?

Fair question.

The short(hand) answer is that I thought I needed it for work, so I thought I'd turn it into a full-on package. Turns out, I didn't need it for work.

The long(hand) answer -- and the arguably more useful one for a README -- is that CSS shorthands are super convoluted to work with in a structured manner, so if one were to ever write a CSS parser, they would have to figure out all the different ways in which the browser unwraps a shorthand property into its individual components. All pre-existing solutions were incomplete, and they didn't need to be, so I wrote this.

License

MIT License (whatever that means)

© 2024-Present

Parsa Rahimi

Releases

No releases published

Packages

No packages published