Indent-based CSS syntax parser for PostCSS that provides an alternative to traditional CSS syntax with braces and semicolons
npx @tessl/cli install tessl/npm-sugarss@5.0.0SugarSS is an indent-based CSS syntax parser for PostCSS that provides an alternative to traditional CSS syntax with braces and semicolons. It allows developers to write CSS using indentation similar to Sass or Stylus while maintaining full compatibility with PostCSS plugins and source maps.
npm install sugarss postcss// Default import (contains parse and stringify)
import sugarss from "sugarss";
// Named imports
import { parse, stringify } from "sugarss";
// Direct module imports (as per package.json exports)
import parse from "sugarss/parse";
import stringify from "sugarss/stringify";
import tokenize from "sugarss/tokenize";// Default require (object with parse and stringify)
const sugarss = require("sugarss");
const { parse, stringify } = sugarss;
// Destructured require
const { parse, stringify } = require("sugarss");
// Direct module requires (as per package.json exports)
const parse = require("sugarss/parse");
const stringify = require("sugarss/stringify");
const tokenize = require("sugarss/tokenize");import postcss from "postcss";
import sugarss from "sugarss";
const sugarssInput = `
a
color: blue
font-size: 16px
.nested
.child
padding: 10px
`;
// Parse SugarSS to CSS
const result = await postcss()
.process(sugarssInput, { parser: sugarss });
console.log(result.css);import postcss from "postcss";
import sugarss from "sugarss";
// Use as both parser and stringifier
const result = await postcss()
.process(sugarssInput, { syntax: sugarss });import postcss from "postcss";
import sugarss from "sugarss";
const cssInput = `
a {
color: blue;
font-size: 16px;
}
`;
// Convert CSS to SugarSS format
const result = await postcss()
.process(cssInput, { stringifier: sugarss });
console.log(result.css);
// Output:
// a
// color: blue
// font-size: 16pxSugarSS is built around several key components:
Parses SugarSS syntax into PostCSS AST nodes for further processing.
/**
* Parse SugarSS syntax into PostCSS AST
* @param source - SugarSS source code string
* @param opts - PostCSS Input options (from, map, etc.)
* @returns PostCSS Root node
*/
function parse(source: string, opts?: InputOptions): Root;
interface InputOptions {
from?: string;
map?: SourceMapOptions | boolean;
to?: string;
origin?: string;
}
interface SourceMapOptions {
inline?: boolean;
annotation?: boolean | string;
sourcesContent?: boolean;
from?: string;
to?: string;
}
interface Root {
type: 'root';
nodes: ChildNode[];
source?: NodeSource;
raws?: RootRaws;
walkRules(callback: (rule: Rule) => void): void;
walkDecls(callback: (decl: Declaration) => void): void;
walkAtRules(callback: (atrule: AtRule) => void): void;
walkComments(callback: (comment: Comment) => void): void;
}
interface NodeSource {
input: Input;
start?: Position;
end?: Position;
}
interface Position {
line: number;
column: number;
offset: number;
}
interface RootRaws {
indent?: string;
after?: string;
semicolon?: boolean;
}
type ChildNode = Rule | AtRule | Declaration | Comment;Usage Examples:
import { parse } from "sugarss";
import { Input } from "postcss";
// Basic parsing
const root = parse(`
a
color: red
font-size: 14px
`, { from: 'input.sss' });
// With source maps
const rootWithMap = parse(sugarssCode, {
from: 'styles.sss',
map: { inline: false }
});
// Access parsed nodes
root.walkRules(rule => {
console.log(rule.selector); // "a"
});
root.walkDecls(decl => {
console.log(decl.prop, decl.value); // "color", "red"
});Converts PostCSS AST nodes back to SugarSS syntax format.
/**
* Stringify PostCSS AST to SugarSS format
* @param node - PostCSS AST node to stringify
* @param builder - PostCSS builder function for output
*/
function stringify(node: AnyNode, builder: Builder): void;
type Builder = (str: string, node?: AnyNode, type?: 'start' | 'end') => void;
interface Rule {
type: 'rule';
selector: string;
nodes: Declaration[];
source?: NodeSource;
raws?: RuleRaws;
}
interface AtRule {
type: 'atrule';
name: string;
params: string;
nodes?: ChildNode[];
source?: NodeSource;
raws?: AtRuleRaws;
}
interface Declaration {
type: 'decl';
prop: string;
value: string;
important?: boolean;
source?: NodeSource;
raws?: DeclRaws;
}
interface Comment {
type: 'comment';
text: string;
source?: NodeSource;
raws?: CommentRaws;
}
interface RuleRaws {
before?: string;
after?: string;
selector?: RawSelector;
}
interface AtRuleRaws {
before?: string;
after?: string;
afterName?: string;
params?: RawParams;
sssBetween?: string;
}
interface DeclRaws {
before?: string;
after?: string;
between?: string;
value?: RawValue;
}
interface CommentRaws {
before?: string;
after?: string;
left?: string;
right?: string;
}
interface RawSelector {
value: string;
raw: string;
}
interface RawParams {
value: string;
raw: string;
}
interface RawValue {
value: string;
raw: string;
}
type AnyNode = Root | AtRule | Rule | Declaration | Comment;Usage Examples:
import { stringify } from "sugarss";
import postcss from "postcss";
// Use as PostCSS stringifier
const result = await postcss()
.process(cssInput, {
parser: postcss.parse,
stringifier: stringify
});
// Direct usage with PostCSS process
const convertToCss = await postcss()
.process(sugarssInput, {
parser: parse,
stringifier: postcss.stringify
});Low-level tokenization function that converts SugarSS source into structured tokens.
/**
* Tokenize SugarSS source into structured tokens
* @param input - PostCSS Input object containing source
* @returns Array of tokens with position information
*/
function tokenize(input: Input): Token[];
interface Input {
css: string;
from?: string;
origin?: string;
error(message: string, offset: number): CssSyntaxError;
error(message: string, line: number, column: number): CssSyntaxError;
}
type Token = [
type: TokenType,
value: string,
startOffset: number,
endOffset: number,
...additional: any[]
];
type TokenType =
| 'newline' // \n, \r\n, \f, \r
| 'space' // spaces and tabs
| '{' // opening curly brace
| '}' // closing curly brace
| ':' // colon separator
| ';' // semicolon
| ',' // comma
| '(' // opening parenthesis
| ')' // closing parenthesis
| 'brackets' // matched parentheses content
| 'string' // quoted strings
| 'at-word' // @-rules like @media, @import
| 'word' // identifiers, values, selectors
| 'comment'; // /* */ and // comments
interface CssSyntaxError extends Error {
name: 'CssSyntaxError';
message: string;
file?: string;
line: number;
column: number;
source: string;
pos: number;
}Usage Examples:
import { tokenize } from "sugarss/tokenize";
import { Input } from "postcss";
const input = new Input(`
a
color: blue
`, { from: 'test.sss' });
const tokens = tokenize(input);
tokens.forEach(token => {
const [type, value, start, end] = token;
console.log(`${type}: "${value}" at ${start}-${end}`);
});
// Example output:
// newline: "\n" at 0-1
// word: "a" at 1-2
// newline: "\n" at 2-3
// space: " " at 3-5
// word: "color" at 5-10
// :: ":" at 10-11
// space: " " at 11-12
// word: "blue" at 12-16SugarSS supports comprehensive CSS syntax with indentation-based structure:
// ✅ Valid: Consistent 2-space indentation
const validIndent = `
.parent
color: blue
.child
padding: 10px
`;
// ❌ Invalid: Mixed tabs and spaces
const invalidMixed = `
.parent
color: blue // 2 spaces
\t.child // tab - will throw error
`;
// ❌ Invalid: First line cannot have indent
const invalidFirst = `
.parent // Error: First line should not have indent
color: blue
`;// Multiline selectors with consistent indentation
const multilineSelector = `
.parent >
.child,
.sibling
color: black
`;
// Multiline values with increased indentation
const multilineValue = `
.element
background:
linear-gradient(rgba(0, 0, 0, 0), black)
linear-gradient(red, rgba(255, 0, 0, 0))
box-shadow: 1px 0 9px rgba(0, 0, 0, .4),
1px 0 3px rgba(0, 0, 0, .6)
`;
// Continuation rules
const continuationRules = `
// 1. Brackets allow line breaks
@supports ( (display: flex) and
(display: grid) )
.flex-grid
display: flex
// 2. Comma at line end continues
@media (max-width: 400px),
(max-height: 800px)
.responsive
padding: 10px
// 3. Backslash before newline continues
@media screen and \\
(min-width: 600px)
.desktop
width: 100%
`;const comments = `
/*
Multiline comments
preserved in output
*/
.element
color: blue // Inline comments also preserved
// Standalone inline comment
.another
font-size: 16px
`;SugarSS works seamlessly with PostCSS plugins and tools:
import postcss from "postcss";
import sugarss from "sugarss";
import autoprefixer from "autoprefixer";
import cssnano from "cssnano";
// Complete preprocessing pipeline
const result = await postcss([
autoprefixer(),
cssnano()
])
.process(sugarssInput, {
parser: sugarss,
from: 'src/styles.sss',
to: 'dist/styles.css'
});
// Configuration file usage (.postcssrc)
const config = {
"parser": "sugarss",
"plugins": {
"postcss-simple-vars": {},
"postcss-nested": {},
"autoprefixer": {}
}
};SugarSS provides detailed error messages with precise location information:
SugarSS uses PostCSS's standard error system, throwing CssSyntaxError instances:
import { parse } from "sugarss";
// Indentation errors
try {
parse(`
.parent
color: blue
\t.child // Mixed tabs/spaces
padding: 10px
`);
} catch (error) {
console.log(error.name); // "CssSyntaxError"
console.log(error.message); // "Mixed tabs and spaces are not allowed"
console.log(`Line ${error.line}, Column ${error.column}`);
console.log(error.pos); // Character offset position
}
// Property syntax errors
try {
parse(`
.element
color:blue // Missing space after colon
`);
} catch (error) {
console.log(error.message);
// "Unexpected separator in property"
console.log(error.source); // Original source code
}
// Unclosed constructs
try {
parse(`
.element
content: "unclosed string
`);
} catch (error) {
console.log(error.message);
// "Unclosed quote"
console.log(error.file); // Input file path (if provided)
}SugarSS automatically detects and adapts to different indentation styles:
// Auto-detection of indentation
const spacesInput = `
.element
color: blue // Detects 2-space indent
font-size: 16px // Nested with 4 spaces
`;
const tabsInput = `
.element
\tcolor: blue // Detects tab indent
\t\tfont-size: 16px // Nested with 2 tabs
`;
// Both parse correctly with auto-detection
const spacesRoot = parse(spacesInput);
const tabsRoot = parse(tabsInput);
console.log(spacesRoot.raws.indent); // " " (2 spaces)
console.log(tabsRoot.raws.indent); // "\t" (tab)Full source map support for debugging and development tools:
import { parse } from "sugarss";
const result = parse(sugarssCode, {
from: 'styles.sss',
map: {
inline: false,
annotation: true,
sourcesContent: true
}
});
// Source positions are preserved
result.walkDecls(decl => {
console.log(decl.source);
// {
// input: Input { css: '...', from: 'styles.sss' },
// start: { line: 3, column: 3, offset: 25 },
// end: { line: 3, column: 15, offset: 37 }
// }
});