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
86 changes: 86 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## プロジェクト概要

RenlinはHTML UIを型安全なDSLアプローチで構築するためのKotlinマルチプラットフォームライブラリです。主にJavaScript/ブラウザターゲット向けのWeb開発用に設計されていますが、JVMもサポートしています。HakateステートマネジメントシステムとCSS-in-JS機能を統合しています。

## 主要アーキテクチャ

### モジュール構造
- **renlin/**: マルチプラットフォームソースセット(commonMain、jsMain、jvmMain)を持つメインライブラリモジュール
- **sample/**: ライブラリの使用パターンを示すサンプル実装
- **generate/**: HTML タグDSL作成用のコード生成ツール
- **convention-plugins/**: 一貫したビルド設定のためのGradle規約プラグイン

### 主要概念
- **コンポーネントシステム**: レンダー関数を持つ`Component<TAG>`インターフェースを使用した型安全なコンポーネント
- **DSLアーキテクチャ**: `@Html`マーカーアノテーションと型安全なコンテンツカテゴリを使用したHTML DSL構築
- **ステート管理**: `StateDispatcher`を介したリアクティブステートハンドリングのためのHakateライブラリとの統合
- **CSS管理**: 自動クラス生成と疑似クラスサポートを持つプラットフォーム固有のCSSマネージャー
- **コンテンツカテゴリ**: W3C準拠のコンテンツモデル強制(FlowContent、PhrasingContentなど)

### プラットフォームターゲット
- **JavaScript**: DOM操作によるブラウザベースレンダリング
- **JVM**: サーバーサイドHTML生成機能

## 開発コマンド

### ビルド
```bash
./gradlew build # 全モジュールをビルド
./gradlew :renlin:build # メインライブラリのみをビルド
./gradlew :sample:build # サンプルアプリケーションをビルド
```

### テスト
```bash
./gradlew test # 全テストを実行
./gradlew :renlin:test # メインライブラリをテスト
./gradlew :renlin:jsTest # JS固有のテストを実行
./gradlew :renlin:jvmTest # JVM固有のテストを実行
```

### サンプル開発
```bash
./gradlew :sample:jsBrowserRun # ブラウザでサンプルを実行(開発用)
./gradlew :sample:jsBrowserDevelopmentExecutableDistribution # サンプル配布版をビルド
```

### コード生成
```bash
./gradlew :generate:run # HTML タグDSLコードを生成
```

### パブリッシング
```bash
./gradlew publishToMavenLocal # ローカルMavenリポジトリに公開
./gradlew publish # 設定されたリポジトリに公開
```

## 主要実装パターン

### コンポーネント作成
コンポーネントは`Component<TAG>`を継承し、`.component {}`DSLビルダーパターンを使用します。ステート統合は`StateDispatcher`を通じて行われ、`useValue()`によるリアクティブレンダリングが可能です。

### エントリーポイントパターン
JSアプリケーションは`Entrypoint(domElement).render(component, dispatcher)`を使用してコンポーネントをDOM要素にマウントします。

### CSS統合
自動クラス生成のために`cssManager`プロパティを使用してスタイリングを行います。CSSプロパティは型安全で疑似クラスをサポートしています。

### コンテンツ型安全性
DSLはW3Cコンテンツカテゴリをコンパイル時に強制します - FlowContentはPhrasingContentを含むことができますが、その逆はできません。

## 理解するための重要ファイル

- `renlin/src/commonMain/kotlin/net/kigawa/renlin/component/Component.kt` - コアコンポーネントインターフェース
- `renlin/src/commonMain/kotlin/net/kigawa/renlin/dsl/Dsl.kt` - DSL基盤
- `renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssManager.kt` - CSS管理システム
- `renlin/src/jsMain/kotlin/net/kigawa/renlin/Entrypoint.kt` - ブラウザエントリーポイント
- `sample/src/jsMain/kotlin/net/kigawa/renlin/sample/Main.kt` - 使用例

## ステート管理統合

ライブラリはステート管理にHakateが必要です。コンポーネントは`MutableState<T>`を通じてリアクティブステートにアクセスし、`useValue()`を介して再レンダリングをトリガーします。ステートの変更は自動的にコンポーネントツリー全体に伝播されます。
4 changes: 2 additions & 2 deletions generate/src/jvmMain/kotlin/generator/TagGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class TagGenerator(
}
import net.kigawa.renlin.dsl.DslBase
import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.component.TagComponent1
import net.kigawa.renlin.component.TagComponent1
import net.kigawa.renlin.w3c.element.TagNode
import net.kigawa.renlin.state.DslState
${
Expand All @@ -56,7 +56,7 @@ class TagGenerator(
}
}

val ${tagInfo.escapement} = TagComponent1<${tagInfo.className}, ${tagInfo.className}Dsl>(${tagInfo.className}, ::${tagInfo.className}Dsl)
val ${tagInfo.escapement} = TagComponent1(${tagInfo.className}, ::${tagInfo.className}Dsl)

object ${tagInfo.className} : Tag<${tagInfo.tagCategories.connectedStr("Union")}> {
override val name: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.Html
import net.kigawa.renlin.tag.Tag

@Html
interface Component<out TAG: Tag<*>> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

interface Component0<out TAG: Tag<in CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory>: Component<TAG> {
fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, key: String?)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

interface Component1<out TAG: Tag<in CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory, ARG1>: Component<TAG> {
fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, key: String?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

interface Component2<out TAG: Tag<in CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory, ARG1, ARG2>: Component<TAG> {
fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, key: String?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

interface Component3<out TAG: Tag<in CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory, ARG1, ARG2, ARG3>: Component<TAG> {
fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, arg3: ARG3, key: String?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

interface Component4<out TAG: Tag<in CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory, ARG1, ARG2, ARG3, ARG4>: Component<TAG> {
fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, arg3: ARG3, arg4: ARG4, key: String?)
}
163 changes: 163 additions & 0 deletions renlin/src/commonMain/kotlin/net/kigawa/renlin/component/Func.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.DslBase
import net.kigawa.renlin.dsl.RegisteredDslData
import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory
import net.kigawa.renlin.w3c.element.TagNode
import kotlin.reflect.KClass
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

fun <CONTENT_CATEGORY: ContentCategory> KClass<CONTENT_CATEGORY>.component(
block: StatedDsl<CONTENT_CATEGORY>.() -> Unit,
) = net.kigawa.renlin.component.component(block)

fun <CONTENT_CATEGORY: ContentCategory, TAG: Tag<CONTENT_CATEGORY>> Component<TAG>.component(
block: StatedDsl<CONTENT_CATEGORY>.() -> Unit,
) = net.kigawa.renlin.component.component(block)

fun <CONTENT_CATEGORY: ContentCategory> component(
block: StatedDsl<CONTENT_CATEGORY>.() -> Unit,
): Component0<Tag<in CONTENT_CATEGORY>, in CONTENT_CATEGORY> {
println("component start $block")
return object: Component0<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY> {
override fun render(
parentDsl: StatedDsl<out
CONTENT_CATEGORY>,
key: String?,
) {
println("component")
@OptIn(ExperimentalUuidApi::class)
val nonNullKey = key ?: Uuid.random().toString()
val state = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
// Create a concrete implementation of DslBase instead of using delegation
val newDsl = object: DslBase<CONTENT_CATEGORY>(state) {
override fun applyElement(element: TagNode): () -> Unit {
return parentDsl.applyElement(element)
}
}
println("component newDsl")
newDsl.block()
println("component end")
parentDsl.registerSubDsl(
RegisteredDslData(
newDsl,
this,
{ render(parentDsl, key) },
nonNullKey
)
)
}
}
}


fun <CONTENT_CATEGORY: ContentCategory, ARG1> component(
block: StatedDsl<CONTENT_CATEGORY>.(ARG1) -> Unit,
): Component1<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1> {
return object: Component1<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1> {
override fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, key: String?) {
@OptIn(ExperimentalUuidApi::class)
val nonNullKey = key ?: Uuid.random().toString()
val state = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
// Create a concrete implementation of DslBase instead of using delegation
val newDsl = object: DslBase<CONTENT_CATEGORY>(state) {
override fun applyElement(element: TagNode): () -> Unit {
return parentDsl.applyElement(element)
}
}
newDsl.block(arg1)
parentDsl.registerSubDsl(
RegisteredDslData(
newDsl,
this,
{ render(parentDsl, arg1, key) },
nonNullKey
)
)
}
}
}

fun <CONTENT_CATEGORY: ContentCategory, ARG1, ARG2> component(
block: StatedDsl<CONTENT_CATEGORY>.(ARG1, ARG2) -> Unit,
): Component2<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2> {
return object: Component2<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2> {
override fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, key: String?) {
@OptIn(ExperimentalUuidApi::class)
val nonNullKey = key ?: Uuid.random().toString()
val state = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
// Create a concrete implementation of DslBase instead of using delegation
val newDsl = object: DslBase<CONTENT_CATEGORY>(state) {
override fun applyElement(element: TagNode): () -> Unit {
return parentDsl.applyElement(element)
}
}
newDsl.block(arg1, arg2)
parentDsl.registerSubDsl(
RegisteredDslData(
newDsl,
this,
{ render(parentDsl, arg1, arg2, key) },
nonNullKey
)
)
}
}
}

fun <CONTENT_CATEGORY: ContentCategory, ARG1, ARG2, ARG3> component(
block: StatedDsl<CONTENT_CATEGORY>.(ARG1, ARG2, ARG3) -> Unit,
): Component3<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2, ARG3> {
return object: Component3<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2, ARG3> {
override fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, arg3: ARG3, key: String?) {
@OptIn(ExperimentalUuidApi::class)
val nonNullKey = key ?: Uuid.random().toString()
val state = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
// Create a concrete implementation of DslBase instead of using delegation
val newDsl = object: DslBase<CONTENT_CATEGORY>(state) {
override fun applyElement(element: TagNode): () -> Unit {
return parentDsl.applyElement(element)
}
}
newDsl.block(arg1, arg2, arg3)
parentDsl.registerSubDsl(
RegisteredDslData(
newDsl,
this,
{ render(parentDsl, arg1, arg2, arg3, key) },
nonNullKey
)
)
}
}
}

fun <CONTENT_CATEGORY: ContentCategory, ARG1, ARG2, ARG3, ARG4> component(
block: StatedDsl<CONTENT_CATEGORY>.(ARG1, ARG2, ARG3, ARG4) -> Unit,
): Component4<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2, ARG3, ARG4> {
return object: Component4<Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY, ARG1, ARG2, ARG3, ARG4> {
override fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: ARG1, arg2: ARG2, arg3: ARG3, arg4: ARG4, key: String?) {
@OptIn(ExperimentalUuidApi::class)
val nonNullKey = key ?: Uuid.random().toString()
val state = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
// Create a concrete implementation of DslBase instead of using delegation
val newDsl = object: DslBase<CONTENT_CATEGORY>(state) {
override fun applyElement(element: TagNode): () -> Unit {
return parentDsl.applyElement(element)
}
}
newDsl.block(arg1, arg2, arg3, arg4)
parentDsl.registerSubDsl(
RegisteredDslData(
newDsl,
this,
{ render(parentDsl, arg1, arg2, arg3, arg4, key) },
nonNullKey
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.RegisteredDslData
import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.state.DslState
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

interface StructuredComponent<
TAG: Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory,
DSL: StatedDsl<*>,
>: Component1<TAG, CONTENT_CATEGORY, DSL.() -> Unit> {
fun newDsl(dslState: DslState): DSL
@OptIn(ExperimentalUuidApi::class)
override fun render(parentDsl: StatedDsl<out CONTENT_CATEGORY>, arg1: DSL.() -> Unit, key: String?) {
println("render start $arg1")
val nonNullKey = key ?: Uuid.random().toString()
val dslState = parentDsl.dslState.getOrCreateSubDslState(nonNullKey, this)
val dsl = newDsl(dslState)
dsl.arg1()
parentDsl.registerSubDsl(
RegisteredDslData(
dsl,
this,
{ render(parentDsl, arg1, key) },
nonNullKey
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.tag.Tag

interface TagComponent<TAG : Tag<*>> : Component<TAG> {
val tag: TAG
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.kigawa.renlin.component

import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.state.DslState
import net.kigawa.renlin.tag.Tag
import net.kigawa.renlin.w3c.category.ContentCategory

class TagComponent1<TAG: Tag<CONTENT_CATEGORY>, CONTENT_CATEGORY: ContentCategory, DSL: StatedDsl<*>>(
override val tag: TAG,
private val newDslFunc: (DslState) -> DSL,
): TagComponent<TAG>, StructuredComponent<TAG, CONTENT_CATEGORY, DSL> {
override fun newDsl(dslState: DslState): DSL {
return newDslFunc(dslState)
}

}
Loading
Loading