Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@
.nrepl-port
README.html
target

# Specs UI (generated by /specs ui)
specs/index.html
specs/_sidebar.md
specs/.nojekyll
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `element` - lookup element metadata by keyword tag (e.g., `:ns/button`)
- `element-tags` - list all registered element keywords
- `elements` - list elements with optional namespace filtering (symbol or regex)
- `search-elements` - search elements by documentation text (string or regex)

### Removed

- **BREAKING:** `describe` function (use `element` instead)
- **BREAKING:** `attributes` function (use `(:attributes (element :ns/tag))` instead)

### Migration

The new API uses keyword tags instead of symbols, matching how elements are used in hiccup markup:

```clojure
;; Old (removed)
(yeah/describe 'my-button)
(yeah/attributes 'my-button)

;; New
(yeah/element :my-ns/my-button)
(:attributes (yeah/element :my-ns/my-button))
```

## [0.1.0] - 2024-12-19

### Added

- Initial release
- `defelem` macro for defining Chassis alias elements with malli schemas
- `children` macro for placeholder in element definitions
- Attribute options via `html.yeah.attrs/option` multimethod
- Schema properties via `html.yeah/property` multimethod
- Support for `let`, `when-let`, `when-some`, `if-let`, `if-some` binding hoisting
- Child schema support via `::html.yeah/children` property
- Integration with `malli.instrument` and `malli.dev`

[Unreleased]: https://github.com/brianium/html.yeah/compare/0.1.0...HEAD
[0.1.0]: https://github.com/brianium/html.yeah/releases/tag/0.1.0
89 changes: 70 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ schema. Schema syntax follows [malli vector syntax](https://github.com/metosin/m
(children)])

;;; Inspect it brother
(ya/attributes 'daisy-button) ; => get the attached malli schema
(:attributes (ya/element ::daisy-button)) ; => get the attached malli schema

;;; Render it brother
(c/html [daisy-button {:color :neutral} "Html Yeah Brother"])
Expand All @@ -51,7 +51,8 @@ See [dev.clj](dev/src/dev.clj) or [yeah_test.clj](test/html/yeah_test.clj) for m
- [Why?](#why)
- [The defelem macro](#the-defelem-macro)
- [Everything is compiled](#everything-is-compiled)
- [Schemas](#schemas)
- [Schemas](#schemas)
- [Discoverability API](#discoverability-api)
- [Extending defelem](#extending-defelem)
- [Attribute options](#attribute-options)
- [Properties](#properties)
Expand Down Expand Up @@ -92,17 +93,20 @@ Destructuring forms are stripped from schema metadata.

*Note:* These bindings cannot be used in the schema itself, just the element body.

Given an element's symbol, you can inspect its attribute schema from the REPL:
Given an element's keyword tag, you can inspect its metadata from the REPL:

``` clojure
(yeah/attributes 'simple-button) ; => [:map [:type [:enum :submit :button]]]
```

You can look at *ALL* metadata for the element via `describe`:

``` clojure
(yeah/describe 'simple-button)
; => {:html.yeah/attributes [map...], :html.yeah/children [:* :any]}
(yeah/element :dev/simple-button)
; => {:tag :dev/simple-button
; :attributes [:map [:type [:enum :submit :button]]]
; :children [:* :any]
; :doc "A simple button with a schema for type"
; :var #'dev/simple-button
; :ns 'dev}

;; Get just the attribute schema
(:attributes (yeah/element :dev/simple-button))
; => [:map [:type [:enum :submit :button]]]
```

If you evaluate the var you will get the namespace qualified tag used by Chassis:
Expand Down Expand Up @@ -212,6 +216,53 @@ The shape of `(children)` mirrors whatever content is passed to the Chassis alia

This ensures render functions are picked up by `malli.instrument` and `malli.dev`.

## Discoverability API

`html.yeah` provides functions to discover and inspect elements at runtime using keyword tags (the same keywords used in hiccup markup).

### `element` - Lookup by keyword

``` clojure
(yeah/element :dev/simple-button)
; => {:tag :dev/simple-button
; :attributes [:map [:type [:enum :submit :button]]]
; :children [:* :any]
; :doc "A simple button with a schema for type"
; :var #'dev/simple-button
; :render-var #'dev/render-simple-button-html
; :ns 'dev}
```

### `element-tags` - List all element keywords

``` clojure
(yeah/element-tags)
; => (:dev/simple-button :dev/daisy-button :ui/alert ...)
```

### `elements` - List with optional filtering

``` clojure
;; All elements
(yeah/elements)

;; Filter by namespace (symbol)
(yeah/elements {:ns 'dev})

;; Filter by namespace (regex)
(yeah/elements {:ns #"ui\..*"})
```

### `search-elements` - Search by documentation

``` clojure
;; Case-insensitive string search
(yeah/search-elements "button")

;; Regex search
(yeah/search-elements #"[Bb]utton")
```

## Extending defelem

`html.yeah` supports extending elements 2 different ways: attribute options and properties. Attribute options are used to affect attribute transformation, and properties affect schema transformation. Both occur at compile time.
Expand Down Expand Up @@ -307,7 +358,7 @@ We can create userland inheritance with a custom merge property:

(defmethod yeah/property ::merge [_ schema element-syms]
(m/form
(->> (mapv yeah/attributes element-syms)
(->> (mapv #(:attributes (yeah/element @(resolve %))) element-syms)
(reduce
(fn [result target]
(mu/merge target result)) schema))))
Expand Down Expand Up @@ -359,10 +410,10 @@ Attempting to render an element with invalid attributes or children will produce

![malli error](docs/prettyerror.png)

You can use `html.yeah/describe` on an element symbol if you want direct access to the render function for instrumenting purposes.
You can use `html.yeah/element` to get direct access to the render function for instrumenting purposes.

``` clojure
(html.yeah/describe 'daisy-button) ; => {:html.yeah/render dev/render-daisy-button-html}
(:render-var (yeah/element :dev/daisy-button)) ; => #'dev/render-daisy-button-html
```

## Generating elements
Expand All @@ -376,9 +427,9 @@ Another cool outcome of having a schema handy is the ability to generate valid s

(defn generate
"We can make custom fun with the fact that a schema is attached to the element"
[symbol & children]
(when-some [s (yeah/attributes symbol)]
[@(resolve symbol) (mg/generate s) children]))
(generate 'daisy-button "Click me") ; => [:dev/daisy-button {:color :neutral :size :xl} "Click me"]
[tag & children]
(when-some [{:keys [attributes]} (yeah/element tag)]
[tag (mg/generate attributes) children]))

(generate :dev/daisy-button "Click me") ; => [:dev/daisy-button {:color :neutral :size :xl} "Click me"]
```
25 changes: 17 additions & 8 deletions dev/src/dev.clj
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,33 @@

(defn generate
"We can make custom fun with the fact that a schema is attached to the element"
[symbol & children]
(when-some [s (yeah/attributes symbol)]
[@(resolve symbol) (mg/generate s) children]))
[tag & children]
(when-some [{:keys [attributes]} (yeah/element tag)]
[tag (mg/generate attributes) children]))

(comment
;;; Describe the entire element
(yeah/describe 'daisy-button)
;;; Get full element metadata
(yeah/element :dev/daisy-button)

;;; Get the attribute schema for an element
(yeah/attributes 'daisy-button)
(:attributes (yeah/element :dev/daisy-button))

;;; List all element tags
(yeah/element-tags)

;;; List elements in this namespace
(yeah/elements {:ns 'dev})

;;; Search elements by doc
(yeah/search-elements "button")

;;; Generate a sample element
(c/html
(generate 'daisy-button "Click Me"))
(generate :dev/daisy-button "Click Me"))

;;; Render this invalid button and check the error output in the REPL
(c/html
[daisy-button {:color :mauve} "Click Me"])
[:dev/daisy-button {:color :mauve} "Click Me"])

(do "not make me create a new line just to evaluate my comments"))

Expand Down
7 changes: 7 additions & 0 deletions specs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Specifications

*Auto-generated on 2026-01-24 20:21*

## Completed

- [Better Discoverability API](better-discoverability-api/README.md)
55 changes: 55 additions & 0 deletions specs/better-discoverability-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: "Better Discoverability API"
status: completed
date: 2026-01-24
priority: 50
---

# Better Discoverability API

## Overview

Add a discoverability API to html.yeah that enables:
- Keyword-based metadata lookup (`:ui/alert` -> full schema and metadata)
- Listing all components defined with `defelem`
- Searching/grepping by `:doc` metadata

This replaces the current symbol-based `describe`/`attributes` functions with a simpler keyword-only API that matches how elements are used in hiccup markup.

## Goals

- Look up rich metadata by chassis alias keyword (e.g., `:ui/alert` -> schema info)
- List all `defelem`-defined components with their chassis alias keywords
- Search elements by `:doc` metadata (string or regex)
- Zero new mutable state - leverage existing infrastructure
- Minimal API surface - one lookup function, users destructure what they need
- REPL-friendly - no stale entries on reload

## Non-Goals

- Complex indexing or search infrastructure (linear scan is sufficient)
- External persistence (in-memory via Chassis multimethod is sufficient)
- Schema validation during lookup (defer to malli.instrument)
- Backward compatibility with symbol-based `describe`/`attributes` functions

## Key Decisions

See `research.md` for detailed analysis.

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Storage approach | Hybrid (Chassis multimethod + var metadata) | Zero new mutable state, no stale entries |
| Enumeration | `(keys (methods c/resolve-alias))` | Chassis already tracks all aliases |
| Lookup | Resolve keyword → var → metadata | `defelem` already attaches metadata |
| Symbol API | Remove entirely | Keywords match hiccup usage; simpler API |
| `describe`/`attributes` | Remove | `element` returns full map; users destructure |
| Namespace filtering | By symbol or regex | Flexible for different use cases |

## Implementation Status

See `implementation-plan.md` for detailed task breakdown.

- [x] Phase 1: Internal helper function (`tag->element`)
- [x] Phase 2: New API functions
- [x] Phase 3: Remove old API functions
- [x] Phase 4: Testing
85 changes: 85 additions & 0 deletions specs/better-discoverability-api/implementation-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Better Discoverability API - Implementation Plan

## Overview

Add API functions for element discovery using a hybrid approach: enumerate via Chassis multimethod, lookup via var metadata. **No new mutable state required** - leverages existing `defelem` infrastructure.

## Prerequisites

- [x] Understand current defelem macro structure (lines 119-228)
- [x] Confirm Chassis multimethod can be enumerated via `(keys (methods c/resolve-alias))`
- [x] Confirm defelem already attaches `::attributes`, `::children`, `:doc` to var metadata
- [x] Design hybrid API approach

## Phase 1: Internal Helper Function

Add helper to resolve keyword tag back to var metadata.

- [x] Add `tag->element` internal function to `src/html/yeah.clj` (around line 105)

## Phase 2: New API Functions

Add public functions for element discovery. Replace existing `describe`/`attributes` (lines 105-117).

- [x] Add `element` function - lookup by keyword
- [x] Add `element-tags` function - list all keywords
- [x] Add `elements` function - list with optional filtering
- [x] Add `search-elements` function - search by doc

## Phase 3: Remove Old API Functions

Remove `describe` and `attributes` functions - they're superseded by `element`.

- [x] Delete `describe` function (lines 105-112)
- [x] Delete `attributes` function (lines 114-117)

## Phase 4: Testing

Add tests in `test/html/yeah_test.clj`.

- [x] Test `element` lookup by keyword
- [x] Test `element` returns nil for unknown keyword
- [x] Test `element` returns nil for keyword without namespace
- [x] Test `element-tags` returns all defined elements
- [x] Test `elements` returns full metadata
- [x] Test `elements` with namespace filter (symbol)
- [x] Test `elements` with namespace filter (regex)
- [x] Test `search-elements` with string pattern
- [x] Test `search-elements` with regex pattern
- [x] Test `search-elements` returns empty for no matches
- [x] Update `::merge` property to use new API

## Verification

1. **Run tests**: `clojure -M:dev:test`
2. **REPL verification**:
```clojure
(require '[html.yeah :as ya])
(require 'html.yeah-test) ;; load test elements

;; Verify keyword lookup
(ya/element :html.yeah-test/simple-button)
(:attributes (ya/element :html.yeah-test/simple-button))

;; Verify listing
(ya/element-tags)
(ya/elements {:ns 'html.yeah-test})

;; Verify search
(ya/search-elements "button")
```

3. **REPL reload test** (verify no stale entries):
```clojure
;; Define element, verify it appears
;; Remove defelem from source, reload namespace
;; Verify element no longer appears in element-tags
```

## Rollback Plan

If issues arise:
1. Remove new functions (element, element-tags, elements, search-elements, tag->element)
2. Restore describe/attributes functions from git history

Note: No defelem changes to revert - this approach doesn't modify the macro.
Loading