# core/converters/style.converter.ts

> 191 lines of code and 70 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-style-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/style.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/structure.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/structure.converter.ts.md) and before [core/converters/surface.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/surface.converter.ts.md).

## Definitions

- `hoistTag` (lexical_declaration, line 135)
- `liftAttributes` (lexical_declaration, line 172)
- `splitDeclarations` (lexical_declaration, line 94)
- `prioritize` (lexical_declaration, line 117, exported)
- `render` (method_definition, line 46)
- `renameDerived` (lexical_declaration, line 86)
- `withClass` (lexical_declaration, line 123)
- `rootIdOf` (lexical_declaration, line 143)
- `liftBlocks` (lexical_declaration, line 149)
- `hoistStyles` (lexical_declaration, line 199, exported)
- `TAG_OPEN` (lexical_declaration, line 6)
- `STYLE_OPEN` (lexical_declaration, line 7)
- `STYLE_CLOSE` (lexical_declaration, line 8)
- `STYLE_ATTRIBUTE` (lexical_declaration, line 9)
- `CLASS_ATTRIBUTE` (lexical_declaration, line 10)
- `ID_ATTRIBUTE` (lexical_declaration, line 11)
- `ROOT_TAG` (lexical_declaration, line 12)
- `VECTOR_CLASS` (lexical_declaration, line 13)
- `RULE_CLASS_PREFIX` (lexical_declaration, line 14)
- `ID_MARK` (lexical_declaration, line 15)
- `CLASS_MARK` (lexical_declaration, line 16)
- `SPACE` (lexical_declaration, line 17)
- `LINE_END` (lexical_declaration, line 18)
- `DECLARATION_END` (lexical_declaration, line 19)
- `IMPORTANT` (lexical_declaration, line 20)
- `RULE_OPEN` (lexical_declaration, line 21)
- `RULE_CLOSE` (lexical_declaration, line 22)
- `GROUP_OPEN` (lexical_declaration, line 23)
- `GROUP_CLOSE` (lexical_declaration, line 24)
- `NON_ELEMENT_OPENERS` (lexical_declaration, line 25)
- `IDENTIFIER_CHARS` (lexical_declaration, line 26)
- `Sheet` (class_declaration, line 28)
- `blocks` (public_field_definition, line 29)
- `classes` (public_field_definition, line 30)
- `addBlock` (method_definition, line 32)
- `classFor` (method_definition, line 36)
- `known` (lexical_declaration, line 37)
- `created` (lexical_declaration, line 41)
- `rules` (lexical_declaration, line 47)
- `identifierEnd` (lexical_declaration, line 54)
- `rescope` (lexical_declaration, line 62, exported)
- `needle` (lexical_declaration, line 63, exported)
- `found` (lexical_declaration, line 71, exported)
- `end` (lexical_declaration, line 75, exported)
- `suffix` (lexical_declaration, line 76, exported)
- `parts` (lexical_declaration, line 95)
- `depth` (lexical_declaration, line 96)
- `quote` (lexical_declaration, line 97)
- `start` (lexical_declaration, line 98)
- `char` (lexical_declaration, line 100)
- `wasQuoted` (lexical_declaration, line 101)
- `present` (lexical_declaration, line 124)
- `attributes` (lexical_declaration, line 125)
- `style` (lexical_declaration, line 136)
- `stripped` (lexical_declaration, line 137)
- `rooted` (lexical_declaration, line 138)
- `declarations` (lexical_declaration, line 139)
- `derived` (lexical_declaration, line 150, exported)
- `rescoped` (lexical_declaration, line 161)
- `out` (lexical_declaration, line 173, exported)
- `at` (lexical_declaration, line 174, exported)
- `rootSeen` (lexical_declaration, line 175)
- `open` (lexical_declaration, line 177)
- `close` (lexical_declaration, line 181)
- `tag` (lexical_declaration, line 187)
- `isRoot` (lexical_declaration, line 188)
- `touched` (lexical_declaration, line 190)
- `sheet` (lexical_declaration, line 200, exported)
- `vectors` (lexical_declaration, line 201, exported)
- `lifted` (lexical_declaration, line 203, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Uses

- [core/converters/markup.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/markup.converter.ts.md)
- [core/selectors/markup.selector.ts](https://banes-lab.com/source/build-scripts/core/selectors/markup.selector.ts.md)

## Used by

- [core/stores/catalog.store.ts](https://banes-lab.com/source/build-scripts/core/stores/catalog.store.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/selectors](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-selectors.md)
- [core/stores](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-stores.md)

## Source

```typescript
import type { Hoisted, Rescoped } from "#types/diagram.types";
import { decodeEntities, nextQuote, parseTag, serializeTag, tagEnd } from "#core/converters/markup.converter";
import type { Tag } from "#types/markup.types";
import { attributeOf } from "#core/selectors/markup.selector";

const TAG_OPEN = "<";
const STYLE_OPEN = "<style>";
const STYLE_CLOSE = "</style>";
const STYLE_ATTRIBUTE = "style";
const CLASS_ATTRIBUTE = "class";
const ID_ATTRIBUTE = "id";
const ROOT_TAG = "svg";
const VECTOR_CLASS = "diagram-vector";
const RULE_CLASS_PREFIX = "diagram-style-";
const ID_MARK = "#";
const CLASS_MARK = ".";
const SPACE = " ";
const LINE_END = "\n";
const DECLARATION_END = ";";
const IMPORTANT = "!important";
const RULE_OPEN = "{";
const RULE_CLOSE = "}";
const GROUP_OPEN = "(";
const GROUP_CLOSE = ")";
const NON_ELEMENT_OPENERS = new Set(["/", "!", "?"]);
const IDENTIFIER_CHARS = new Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_");

class Sheet {
    private readonly blocks = new Set<string>();
    private readonly classes = new Map<string, string>();

    public addBlock(css: string): void {
        this.blocks.add(css);
    }

    public classFor(declarations: string): string {
        const known = this.classes.get(declarations);
        if (known !== undefined) {
            return known;
        }
        const created = RULE_CLASS_PREFIX + String(this.classes.size + 1);
        this.classes.set(declarations, created);
        return created;
    }

    public render(): string {
        const rules = [...this.classes].map(
            ([declarations, name]) => CLASS_MARK + name + RULE_OPEN + declarations + RULE_CLOSE,
        );
        return [...this.blocks, ...rules].join(LINE_END) + LINE_END;
    }
}

const identifierEnd = function identifierEnd(text: string, from: number): number {
    let at = from;
    while (at < text.length && IDENTIFIER_CHARS.has(text.charAt(at))) {
        at += 1;
    }
    return at;
};

export const rescope = function rescope(css: string, rootId: string): Rescoped {
    const needle = ID_MARK + rootId;
    const derived = new Set<string>();
    let out = "";
    let at = 0;
    if (rootId.length === 0) {
        return { css, derived };
    }
    while (at < css.length) {
        const found = css.indexOf(needle, at);
        if (found === -1) {
            return { css: out + css.slice(at), derived };
        }
        const end = identifierEnd(css, found + needle.length);
        const suffix = css.slice(found + needle.length, end);
        if (suffix.length > 0) {
            derived.add(suffix);
        }
        out += css.slice(at, found) + (suffix.length === 0 ? CLASS_MARK : ID_MARK) + VECTOR_CLASS + suffix;
        at = end;
    }
    return { css: out, derived };
};

const renameDerived = function renameDerived(markup: string, rootId: string, derived: ReadonlySet<string>): string {
    let out = markup;
    for (const suffix of derived) {
        out = out.split(rootId + suffix).join(VECTOR_CLASS + suffix);
    }
    return out;
};

const splitDeclarations = function splitDeclarations(text: string): string[] {
    const parts: string[] = [];
    let depth = 0;
    let quote = "";
    let start = 0;
    for (let at = 0; at < text.length; at += 1) {
        const char = text.charAt(at);
        const wasQuoted = quote !== "";
        quote = nextQuote(quote, char);
        if (wasQuoted || quote !== "") {
            continue;
        }
        depth += char === GROUP_OPEN ? 1 : 0;
        depth -= char === GROUP_CLOSE ? 1 : 0;
        if (char === DECLARATION_END && depth === 0) {
            parts.push(text.slice(start, at));
            start = at + 1;
        }
    }
    parts.push(text.slice(start));
    return parts.map((part) => part.trim()).filter((part) => part.length > 0);
};

export const prioritize = function prioritize(declarations: string): string {
    return splitDeclarations(declarations)
        .map((declaration) => (declaration.endsWith(IMPORTANT) ? declaration : declaration + SPACE + IMPORTANT))
        .join(DECLARATION_END);
};

const withClass = function withClass(tag: Tag, className: string): Tag {
    const present = attributeOf(tag, CLASS_ATTRIBUTE) !== undefined;
    const attributes = present
        ? tag.attributes.map((attribute) =>
              attribute.name === CLASS_ATTRIBUTE
                  ? { ...attribute, value: [attribute.value ?? "", className].join(SPACE).trim() }
                  : attribute,
          )
        : [...tag.attributes, { name: CLASS_ATTRIBUTE, quote: '"', value: className }];
    return { ...tag, attributes };
};

const hoistTag = function hoistTag(tag: Tag, sheet: Sheet, isRoot: boolean): Tag {
    const style = attributeOf(tag, STYLE_ATTRIBUTE);
    const stripped = { ...tag, attributes: tag.attributes.filter((attribute) => attribute !== style) };
    const rooted = isRoot ? withClass(stripped, VECTOR_CLASS) : stripped;
    const declarations = decodeEntities(style?.value ?? "").trim();
    return declarations.length === 0 ? rooted : withClass(rooted, sheet.classFor(prioritize(declarations)));
};

const rootIdOf = function rootIdOf(markup: string): string {
    const open = markup.indexOf(TAG_OPEN + ROOT_TAG);
    const close = open === -1 ? -1 : tagEnd(markup, open + 1);
    return close < 0 ? "" : (attributeOf(parseTag(markup.slice(open + 1, close)), ID_ATTRIBUTE)?.value ?? "");
};

const liftBlocks = function liftBlocks(markup: string, sheet: Sheet, rootId: string): string {
    const derived = new Set<string>();
    let out = "";
    let at = 0;
    while (at < markup.length) {
        const open = markup.indexOf(STYLE_OPEN, at);
        const close = open === -1 ? -1 : markup.indexOf(STYLE_CLOSE, open);
        if (close < 0) {
            out += markup.slice(at);
            at = markup.length;
            continue;
        }
        const rescoped = rescope(decodeEntities(markup.slice(open + STYLE_OPEN.length, close)), rootId);
        sheet.addBlock(rescoped.css);
        for (const suffix of rescoped.derived) {
            derived.add(suffix);
        }
        out += markup.slice(at, open);
        at = close + STYLE_CLOSE.length;
    }
    return renameDerived(out, rootId, derived);
};

const liftAttributes = function liftAttributes(markup: string, sheet: Sheet): string {
    let out = "";
    let at = 0;
    let rootSeen = false;
    while (at < markup.length) {
        const open = markup.indexOf(TAG_OPEN, at);
        if (open === -1) {
            return out + markup.slice(at);
        }
        const close = NON_ELEMENT_OPENERS.has(markup.charAt(open + 1)) ? -1 : tagEnd(markup, open + 1);
        if (close < 0) {
            out += markup.slice(at, open + 1);
            at = open + 1;
            continue;
        }
        const tag = parseTag(markup.slice(open + 1, close));
        const isRoot: boolean = !rootSeen && tag.name === ROOT_TAG;
        rootSeen ||= isRoot;
        const touched = isRoot || attributeOf(tag, STYLE_ATTRIBUTE) !== undefined;
        out +=
            markup.slice(at, open) +
            (touched ? serializeTag(hoistTag(tag, sheet, isRoot)) : markup.slice(open, close + 1));
        at = close + 1;
    }
    return out;
};

export const hoistStyles = function hoistStyles(rendered: ReadonlyMap<string, string>): Hoisted {
    const sheet = new Sheet();
    const vectors = new Map<string, string>();
    for (const [source, markup] of rendered) {
        const lifted = liftBlocks(markup, sheet, rootIdOf(markup));
        vectors.set(source, liftAttributes(lifted, sheet));
    }
    return { stylesheet: sheet.render(), vectors };
};
```
