# glyf-core

[![Crates.io](https://img.shields.io/crates/v/glyf-core.svg)](https://crates.io/crates/glyf-core)
[![docs.rs](https://docs.rs/glyf-core/badge.svg)](https://docs.rs/glyf-core)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A compact abbreviation parser and HTML/JSX expander inspired by [Emmet](https://emmet.io).

Write a short symbolic abbreviation — get back a full, correctly indented HTML or JSX structure.

```
ul>li.item*3
```
```html
<ul>
	<li class="item"></li>
	<li class="item"></li>
	<li class="item"></li>
</ul>
```

---

## Installation

```toml
[dependencies]
glyf-core = "0.5.0"
```

---

## Usage

### Expanding an abbreviation

The main entry point is `expand`. It validates the input, parses it, and returns
the rendered string.

```rust
use glyf_core::expand;

let html = expand("div.container>h1+p", None, None).unwrap();
```

```html
<div class="container">
	<h1></h1>
	<p></p>
</div>
```

### Indentation level

Pass `Some(n)` as `base_level` when inserting the expansion inside an
already-indented block. The root element and all its descendants are shifted
by `n` tabs.

```rust
use glyf_core::expand;

let html = expand("div>p", Some(1), None).unwrap();
```

```html

	<div>
		<p></p>
	</div>
```

### JSX mode

By default the library renders standard HTML. Pass a `Config` with
`ParserMode::JSX(None)` to switch to JSX output:

- `class` attributes render as `className`
- `{expr}` prop values are kept unquoted (JSX expressions)
- The `e` abbreviation expands to a JSX fragment `<></>`

```rust
use glyf_core::expand;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(ParserMode::JSX(None), HashMap::new());
expand("div.container", None, Some(config));
// <div className="container"></div>

let jsx_config = Config::new(ParserMode::JSX(None), HashMap::new());
expand("e>p", None, Some(jsx_config));
// <><p></p></>
```

#### Custom class attribute name

Some JSX frameworks use `class` instead of `className` (Qwik, Solid). Pass the
desired name as the inner value of `JSX`:

```rust
use glyf_core::expand;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(ParserMode::JSX(Some("class".into())), HashMap::new());
expand("div.container", None, Some(config));
// <div class="container"></div>
```

#### Conditional rendering

In JSX mode, the `->` separator triggers conditional rendering output.
The condition can be any JavaScript expression; `->` cannot appear in valid JS
so it is an unambiguous delimiter even when the condition contains `>`, `&&`,
arrows, or function calls.

```rust
use glyf_core::expand;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(ParserMode::JSX(None), HashMap::new());

// Two-part: condition && element
expand("isLoading->Spinner", None, Some(config.clone()));
// {isLoading && <Spinner />}

// Three-part: ternary
expand("isLoading->Spinner->div.empty", None, Some(config.clone()));
// {isLoading ? <Spinner /> : <div className="empty"></div>}

// Complex condition — parens protect the > from being read as child operator
expand("(count > 0)->ul>li", None, Some(config));
// {(count > 0) && <ul>\n\t<li></li>\n</ul>}
```

A condition can itself carry a child tree on the true/false branch:

```
isLoading->div>(span+p)->div.fallback
→  {isLoading ? <div>\n\t<span></span>\n\t<p></p>\n</div> : <div className="fallback"></div>}
```

### Custom snippets

Provide a `Config` with a `snippets` map to define aliases that expand before
parsing. Custom snippets support the full glyf syntax, including multi-element
expansions containing `>` or `+`.

```rust
use glyf_core::expand;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(
    ParserMode::HTML,
    HashMap::from([
        ("mc".to_string(),   "MyComponent".to_string()),
        ("card".to_string(), "div.card>p.card-header+p.card-body".to_string()),
    ]),
);

expand("mc", None, Some(config.clone()));
// <MyComponent></MyComponent>

expand("card", None, Some(config));
// <div class="card">
//     <p class="card-header"></p>
//     <p class="card-body"></p>
// </div>
```


### Compressing HTML to an abbreviation

`compress` is the inverse of `expand` — it converts HTML markup back to the
shortest Glyf abbreviation that would regenerate equivalent output.

```rust
use glyf_core::compress;

assert_eq!(compress("<div class=\"card\"><p></p></div>").unwrap(), "div.card>p");
assert_eq!(compress("<div></div><span></span>").unwrap(), "div+span");
assert_eq!(compress("<div><p></p></div><span></span>").unwrap(), "(div>p)+span");
```

### Working with the AST

Use `expand_to_tree` or `compress_to_tree` when you need the parsed
[`Element`](https://docs.rs/glyf-core/latest/glyf_core/parser/struct.Element.html)
tree rather than a string. `Element` implements `Display` for rendering back
to HTML and `to_glyf()` for serialising back to an abbreviation.

Every `Element` has a `kind: ElementKind` field that describes what it is:

| Variant | Produced by |
|---|---|
| `ElementKind::Tag { identifier, self_closing, attributes }` | Any concrete tag: `div`, `span`, `MyComponent` |
| `ElementKind::Fragment` | The `e` abbreviation (JSX mode only) |
| `ElementKind::Group(inner)` | A `(…)` group or multi-element snippet expansion |
| `ElementKind::Condition { condition, true_part, false_part }` | A `cond->el` or `cond->el->el` expression (JSX mode only) |

```rust
use glyf_core::expand_to_tree;
use glyf_core::parser::ElementKind;

let element = expand_to_tree("ul>li*3", None, None).unwrap();

if let ElementKind::Tag { ref identifier, .. } = element.kind {
    println!("tag:        {}", identifier); // ul
}
println!("multiplier: {}", element.multiplier);
println!("has child:  {}", element.node.is_some());
println!("rendered:\n{}", element);
```

```rust
use glyf_core::compress_to_tree;
use glyf_core::parser::{ElementKind, Node};

let el = compress_to_tree("<ul><li></li></ul>", None).unwrap();
if let ElementKind::Tag { ref identifier, .. } = el.kind {
    assert_eq!(identifier, "ul");
}
assert!(matches!(*el.node.unwrap(), Node::Children(_)));
```

Inspecting a JSX conditional:

```rust
use glyf_core::expand_to_tree;
use glyf_core::parser::ElementKind;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(ParserMode::JSX(None), HashMap::new());
let el = expand_to_tree("isLoading->Spinner->div.empty", None, Some(config)).unwrap();

if let ElementKind::Condition { ref condition, ref true_part, ref false_part } = el.kind {
    println!("condition:  {}", condition);                // isLoading
    println!("true part:  {}", true_part);               // <Spinner />
    println!("false part: {}", false_part.as_deref().unwrap()); // <div className="empty"></div>
}
```

### Error handling

`expand` returns `Result<String, GlyfError>` and never panics.
`GlyfError` implements both `Display` and `std::error::Error`, so it
composes naturally with `?` and error-handling crates.

```rust
use glyf_core::{expand, parser::GlyfError};

match expand("abbr", None, None) {
    Ok(html)                           => { let _ = html; }
    Err(GlyfError::UnmatchedBrackets)  => { /* user is still typing */ }
    Err(GlyfError::NoIdentifier)       => { /* empty or operator-only input */ }
    Err(GlyfError::EmptyInput)         => { /* blank string */ }
    Err(GlyfError::MalformedHtml(_))   => { /* invalid HTML passed to compress */ }
}
```

| Variant | Triggered when |
|---|---|
| `GlyfError::UnmatchedBrackets` | Input contains unclosed `(` or `)` |
| `GlyfError::NoIdentifier` | Input has no valid tag name (e.g. bare `">"`) |
| `GlyfError::EmptyInput` | Input is an empty string |
| `GlyfError::MalformedHtml` | HTML passed to `compress` / `compress_to_tree` is not parseable |

---

## Syntax reference

| Syntax | Meaning | Example | Output |
|--------|---------|---------|--------|
| `tag` | Element | `div` | `<div></div>` |
| `tag/` | Self-closing | `br/` | `<br />` |
| `a>b` | Child | `ul>li` | `<ul>` with `<li>` inside |
| `a+b` | Sibling | `div+p` | `<div>` followed by `<p>` |
| `a^b` | Climb-up | `div>p^span` | `<div><p></p></div>` followed by `<span>` |
| `(a+b)*N` | Group × N | `(li)*3` | three `<li>` elements |
| `tag*N` | Repeat | `li*3` | three `<li>` elements |
| `tag.cls` | Class | `div.foo` | `<div class="foo">` |
| `tag#id` | Id | `div#app` | `<div id="app">` |
| `tag:key=val` | Prop | `a:href=url` | `<a href="url">` |
| `tag:key={expr}` | JSX prop | `div:onClick={fn}` | `<div onClick={fn}>` |
| `tag>>text` | Text content | `p>>Hello` | `<p>Hello</p>` |
| `tag#{expr}` | JSX dynamic id | `div#{myId}` | `<div id={myId}>` |
| `.cls` / `#id` / `:prop` / `>child` | Implicit `div` | `.foo` | `<div class="foo">` |
| `e` | JSX fragment (JSX mode only) | `e>p` | `<>` with `<p>` inside |
| `cond->el` | JSX `&&` condition (JSX mode only) | `isLoading->Spinner` | `{isLoading && <Spinner />}` |
| `cond->el->el` | JSX ternary (JSX mode only) | `ok->p->span` | `{ok ? <p></p> : <span></span>}` |

### Operator precedence

When `>` and `+` appear at the same level, the **leftmost operator wins** at the
top of the tree.

```
div>p+span   →  <div><p></p><span></span></div>   (> comes first → child)
div+p>span   →  <div></div><p><span></span></p>   (+ comes first → sibling)
```

### Climb-up

`^` moves the attachment point up one level in the tree. Multiple `^` climb
multiple levels. This lets you add siblings at a higher depth without wrapping
everything in a group manually.

```
div>p>span^button    →  div>(p>span)+button
div>p>span^^button   →  (div>p>span)+button
```

---

## Snippets

glyf-core has no built-in snippet table — snippets are entirely user-defined
through `Config`. This keeps the core library small and lets each consumer
(LSP server, CLI tool, etc.) ship its own vocabulary.

A snippet alias maps a short key to any valid glyf abbreviation, including
multi-element structures:

| Key | Value | Expands to |
|-----|-------|-----------|
| `"btn"` | `"MyButton"` | `<MyButton></MyButton>` |
| `"card"` | `"div.card>p.card-body"` | `<div class="card"><p class="card-body">…</p></div>` |
| `"img"` | `"img:src=:alt=/"` | `<img src="" alt="" />` |

When a key is a prefix of the input and is followed by a boundary character
(`.`, `:`, `>`, `+`, etc.), the expansion fires and the tail is appended. If
the expansion introduces a duplicate attribute name, the last occurrence wins —
so user-supplied values always override snippet defaults.

```
a:href=abc  →  (snippet "a" → "a:href=")  →  a:href=:href=abc
            →  parse_attribute deduplicates →  <a href="abc"></a>
```

### Parametric snippet keys

A key may contain a single `$` as a digit wildcard. The `$` matches one or more
consecutive digits in the input; that number is substituted for every `$` in the
expansion. Only one `$` per key is supported; longest-key-wins applies.

| Key | Expansion | Input | Result |
|-----|-----------|-------|--------|
| `"col$"` | `"div.flex.flex-col.gap-$"` | `col3` | `<div class="flex flex-col gap-3">` |
| `"col$"` | `"div.flex.flex-col.gap-$"` | `col12` | `<div class="flex flex-col gap-12">` |
| `"p$px"` | `"div.p-$px"` | `p3px` | `<div class="p-3px">` |

```rust
use glyf_core::expand;
use glyf_core::config::{Config, ParserMode};
use std::collections::HashMap;

let config = Config::new(
    ParserMode::HTML,
    HashMap::from([("col$".to_string(), "div.flex.flex-col.gap-$".to_string())]),
);

assert_eq!(
    expand("col3", None, Some(config)).unwrap(),
    "<div class=\"flex flex-col gap-3\"></div>"
);
```

---

## License

MIT — see [LICENSE](LICENSE).
