Skip to content
Merged
  •  
  •  
  •  
42 changes: 38 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ RenlinはHTML UIを型安全なDSLアプローチで構築するためのKotlin
- **ステート管理**: `StateDispatcher`を介したリアクティブステートハンドリングのためのHakateライブラリとの統合
- **CSS管理**: 自動クラス生成と疑似クラスサポートを持つプラットフォーム固有のCSSマネージャー
- **コンテンツカテゴリ**: W3C準拠のコンテンツモデル強制(FlowContent、PhrasingContentなど)
- **属性システム**: `DslStateData`を通じた型安全な HTML 属性管理(href、onClick など)

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

## 開発コマンド
Expand Down Expand Up @@ -73,14 +74,47 @@ JSアプリケーションは`Entrypoint(domElement).render(component, dispatche
### コンテンツ型安全性
DSLはW3Cコンテンツカテゴリをコンパイル時に強制します - FlowContentはPhrasingContentを含むことができますが、その逆はできません。

### 属性とイベント管理
- **DslStateData パターン**: 属性(href など)とイベントハンドラー(onClick など)は`DslStateData`を通じて管理されます
- **型安全な属性**: `Href`クラスなどのvalue objectsを使用して属性値を型安全に扱います
- **自動DOM同期**: `TagNodeCommon.setDslStateData`が属性とイベントの DOM への同期を自動的に行います

## アーキテクチャの理解

### レイヤー構造
1. **Component レイヤー**: `Component<TAG>` - 最上位のコンポーネント抽象化
2. **DSL レイヤー**: `DslBase` - HTML構造構築とライフサイクル管理
3. **State レイヤー**: `DslState` / `DslStateData` - 状態管理と属性/イベント管理
4. **Platform レイヤー**: `TagNode` implementations - プラットフォーム固有のレンダリング

### W3C カテゴリシステム
- `w3c/category/native/` - W3C HTML仕様に基づくコンテンツカテゴリ型定義
- `w3c/category/dsl/` - 各カテゴリ用のDSLインターフェース
- `w3c/category/integration/` - カテゴリ間の統合型定義
- コンパイル時にHTMLコンテンツモデルの制約を強制

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

- `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/commonMain/kotlin/net/kigawa/renlin/dsl/DslBase.kt` - DSL基底クラスと核心機能
- `renlin/src/commonMain/kotlin/net/kigawa/renlin/state/DslStateData.kt` - 状態データと属性管理
- `renlin/src/commonMain/kotlin/net/kigawa/renlin/w3c/element/TagNodeCommon.kt` - プラットフォーム間共通のDOM抽象化
- `renlin/src/jsMain/kotlin/net/kigawa/renlin/w3c/element/DomTagElement.kt` - ブラウザ用DOM実装
- `renlin/src/jsMain/kotlin/net/kigawa/renlin/Entrypoint.kt` - ブラウザエントリーポイント
- `sample/src/jsMain/kotlin/net/kigawa/renlin/sample/Main.kt` - 使用例

## ステート管理統合

ライブラリはステート管理にHakateが必要です。コンポーネントは`MutableState<T>`を通じてリアクティブステートにアクセスし、`useValue()`を介して再レンダリングをトリガーします。ステートの変更は自動的にコンポーネントツリー全体に伝播されます。
ライブラリはステート管理にHakateが必要です。コンポーネントは`MutableState<T>`を通じてリアクティブステートにアクセスし、`useValue()`を介して再レンダリングをトリガーします。ステートの変更は自動的にコンポーネントツリー全体に伝播されます。

## 拡張とカスタマイズ

### 新しい属性の追加
1. `DslStateData`にプロパティを追加
2. `TagNodeCommon.setDslStateData`で属性をDOMに適用するロジックを追加
3. 対象DSLクラス用の拡張プロパティを`w3c/attribute/`に作成

### 新しいHTMLタグの追加
1. `generate/`モジュールのコード生成を使用するか、手動でタグクラスを作成
2. 適切なW3Cコンテンツカテゴリに従ってDSLクラスを実装
3. プラットフォーム固有の実装が必要な場合は、各プラットフォームモジュールで対応
8 changes: 8 additions & 0 deletions generate/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ kotlin {
}

}

tasks.register<JavaExec>("runGenerator") {
group = "application"
description = "Run the code generator"
dependsOn("jvmMainClasses")
classpath = kotlin.targets["jvm"].compilations["main"].output.allOutputs + kotlin.targets["jvm"].compilations["main"].runtimeDependencyFiles
mainClass.set("_Tag_generateKt")
}
2 changes: 1 addition & 1 deletion generate/src/jvmMain/kotlin/_Tag_generate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fun main() {
it.generate()
}
val nativeGenerator = NativeGenerator(categoryNativeOutputDir).also {
it.generate(integrationGenerator.nativeCategories)
it.generate()
}

println("タグのコード生成が完了しました。")
Expand Down
6 changes: 3 additions & 3 deletions generate/src/jvmMain/kotlin/generator/DslGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ class DslGenerator(
${
if (allowedCategories.categories.size > 1)
"import net.kigawa.renlin.w3c.category.integration.${
allowedCategories.connectedStr("Integration")
allowedCategories.connectedStr()
}"
else "import net.kigawa.renlin.w3c.category.native.${
allowedCategories.connectedStr("Integration")
allowedCategories.connectedStr()
}"
}


/**
* DSL for ${categories.joinToString(", ")}
*/
interface ${dslName}<CATEGORY_DSL : ${allowedCategories.connectedStr("Integration")}>${
interface ${dslName}<CATEGORY_DSL : ${allowedCategories.connectedStr()}>${
if (categories.size <= 1) ""
else (categories.filter { it.trim() != dslName.trim() }
.joinToString(separator = ",", prefix = ":")
Expand Down
28 changes: 22 additions & 6 deletions generate/src/jvmMain/kotlin/generator/IntegrationGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class IntegrationGenerator(
.filter { it.categories.size > 1 }
.toSet()
.forEach { tagCategories ->
val integrationName = tagCategories.connectedStr("Integration")
val integrationName = tagCategories.connectedStr()
if (!processedIntegrations.contains(integrationName)) {
processedIntegrations.add(integrationName)
allIntegrations[integrationName] = tagCategories.categories.toSet()
Expand All @@ -28,8 +28,20 @@ class IntegrationGenerator(
}
}

// Generate each integration with inheritance from subset integrations
// タグのCategoryインターフェースを生成対象に含める
val tagCategoryMap = mutableMapOf<String, Set<String>>()
tagCategories.forEach { tagInfo ->
val tagCategoryName = "${tagInfo.className}Category"
tagCategoryMap[tagCategoryName] = setOf(tagInfo.tagCategories.connectedStr())
}

// Generate each integration with inheritance from tag categories
allIntegrations.forEach { (integrationName, categories) ->
// Find tag categories that match this integration
val matchingTagCategories = tagCategoryMap.filter { (_, integrationSet) ->
integrationSet.contains(integrationName)
}.keys

// Find other integrations that are subsets of this integration
val subsetIntegrations = allIntegrations.filter { (otherName, otherCategories) ->
otherName != integrationName &&
Expand All @@ -38,7 +50,7 @@ class IntegrationGenerator(
otherCategories.size < categories.size
}.keys

// Generate imports for categories and subset integrations
// Generate imports for categories, subset integrations, and tag categories
val categoryImports = categories.map { category ->
"import net.kigawa.renlin.w3c.category.native.$category"
}
Expand All @@ -47,10 +59,14 @@ class IntegrationGenerator(
"import net.kigawa.renlin.w3c.category.integration.$subsetIntegration"
}

val allImports = (categoryImports + integrationImports).joinToString("\n ")
val tagCategoryImports = matchingTagCategories.map { tagCategory ->
"import net.kigawa.renlin.w3c.category.native.$tagCategory"
}

val allImports = (categoryImports + integrationImports + tagCategoryImports).joinToString("\n ")

// Generate inheritance list including categories and subset integrations
val inheritance = (categories + subsetIntegrations + "ContentCategory").joinToString(", ")
// Generate inheritance list including categories, subset integrations, and tag categories
val inheritance = (categories + subsetIntegrations + matchingTagCategories + "ContentCategory").joinToString(", ")

val fileContent = """
package net.kigawa.renlin.w3c.category.integration
Expand Down
113 changes: 94 additions & 19 deletions generate/src/jvmMain/kotlin/generator/NativeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,114 @@ import tagCategories
import categoryParents

class NativeGenerator(val categoryNativeOutputDir: String) {
fun generate(categories: Map<String, Set<String>>) {
fun generate() {
// 使用されているすべてのネイティブカテゴリを収集
val usedNativeCategories = mutableSetOf<String>()
tagCategories.forEach { tagInfo ->
usedNativeCategories.addAll(tagInfo.tagCategories.categories)
usedNativeCategories.addAll(tagInfo.allowedCategories.categories)
// 各タグ専用のCategoryインターフェースも追加
usedNativeCategories.add("${tagInfo.className}Category")
}

// categoryParentsで定義されているがタグで使用されていないカテゴリも追加(EventTargetなど)
categoryParents.keys.forEach { categoryName ->
usedNativeCategories.add(categoryName)
}

// 親カテゴリーの情報を収集
val allParentCategories = categoryParents.toMutableMap()
tagCategories.forEach { tagInfo ->
allParentCategories.putAll(tagInfo.tagCategories.parentCategories)
allParentCategories.putAll(tagInfo.allowedCategories.parentCategories)
// 各タグ専用のCategoryの親カテゴリーを設定(ContentCategoryのみ継承)
allParentCategories["${tagInfo.className}Category"] = ""
}

// FlowContent, PhrasingContentなどが継承すべきタグカテゴリを設定
val categoryToTagCategories = mutableMapOf<String, MutableSet<String>>()
tagCategories.forEach { tagInfo ->
tagInfo.tagCategories.categories.forEach { category ->
categoryToTagCategories.getOrPut(category) { mutableSetOf() }.add("${tagInfo.className}Category")
}
}

// Integrationクラスの生成
categories.forEach { (name, deps) ->
val categoryName = name

// ネイティブカテゴリの生成
usedNativeCategories.forEach { categoryName ->
// 親カテゴリーを取得
val parentCategory = allParentCategories[name]
val parentCategory = allParentCategories[categoryName]

// 継承するインターフェースのリスト
val interfaces = if (parentCategory != null) {
(deps + "ContentCategory" + parentCategory).joinToString(", ")
} else {
(deps + "ContentCategory").joinToString(", ")
val interfaces = when {
parentCategory == null -> "ContentCategory"
parentCategory.isEmpty() -> "" // P, Divの場合は継承なし
else -> "ContentCategory, $parentCategory"
}

val fileContent = """
package net.kigawa.renlin.w3c.category.native
val fileContent = when {
parentCategory?.isEmpty() == true && !categoryName.endsWith("Category") -> {
// P, Div, EventTargetの場合
if (categoryName == "EventTarget") {
// EventTargetはContentCategoryを継承
"""
package net.kigawa.renlin.w3c.category.native

import net.kigawa.renlin.w3c.category.ContentCategory

interface $categoryName : ContentCategory
""".trimIndent()
} else {
// P, Divの場合は基本インターフェースのみ
"""
package net.kigawa.renlin.w3c.category.native

interface $categoryName
""".trimIndent()
}
}
categoryName.endsWith("Category") -> {
// タグ専用カテゴリはContentCategoryのみ継承
"""
package net.kigawa.renlin.w3c.category.native

import net.kigawa.renlin.w3c.category.ContentCategory
import net.kigawa.renlin.w3c.category.ContentCategory

/**
* Integration to ${deps.joinToString(", ")}
* ${if (parentCategory != null) "Parent: $parentCategory" else ""}
*/
interface $categoryName : $interfaces
""".trimIndent()
/**
* ${categoryName} represents elements that are part of the ${categoryName.replace("Category", "").lowercase()} content category.
*/
interface $categoryName : ContentCategory
""".trimIndent()
}
else -> {
// 標準カテゴリ(FlowContent、PhrasingContentなど)の場合、関連するタグカテゴリを継承
val tagCategoriesToInherit = categoryToTagCategories[categoryName] ?: emptySet()
val tagCategoryImports = tagCategoriesToInherit.map { "import net.kigawa.renlin.w3c.category.native.$it" }
val allTagCategoriesInheritance = if (tagCategoriesToInherit.isNotEmpty()) {
tagCategoriesToInherit.joinToString(", ")
} else {
""
}

val finalInterfaces = if (allTagCategoriesInheritance.isNotEmpty()) {
if (interfaces.isNotEmpty()) "$interfaces, $allTagCategoriesInheritance" else allTagCategoriesInheritance
} else {
interfaces
}

"""
package net.kigawa.renlin.w3c.category.native

import net.kigawa.renlin.w3c.category.ContentCategory
${tagCategoryImports.joinToString("\n ")}

/**
* ${categoryName} represents elements that are part of the ${categoryName.replace("Content", "").lowercase()} content category.
* ${if (parentCategory != null && parentCategory.isNotEmpty()) "Parent: $parentCategory" else ""}
*/
interface $categoryName : $finalInterfaces
""".trimIndent()
}
}

val file = File("$categoryNativeOutputDir/${categoryName}.kt")
file.writeText(fileContent)
Expand Down
20 changes: 10 additions & 10 deletions generate/src/jvmMain/kotlin/generator/TagGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@ class TagGenerator(
if (tagInfo.tagCategories.categories.size > 1)
imports.add(
"import net.kigawa.renlin.w3c.category.integration.${
tagInfo.tagCategories.connectedStr(
"Integration"
)
tagInfo.tagCategories.connectedStr()
}"
)
else imports.add("import net.kigawa.renlin.w3c.category.native.${tagInfo.tagCategories.connectedStr()}")
if (tagInfo.allowedCategories.categories.size > 1)
imports.add(
"import net.kigawa.renlin.w3c.category.integration.${
tagInfo.allowedCategories.connectedStr("Integration")
tagInfo.allowedCategories.connectedStr()
}"
)
else if (
tagInfo.allowedCategories.categories.isNotEmpty() &&
tagInfo.tagCategories.connectedStr("Integration") !=
tagInfo.allowedCategories.connectedStr("Integration")
tagInfo.tagCategories.connectedStr() !=
tagInfo.allowedCategories.connectedStr()
) imports.add(
"import net.kigawa.renlin.w3c.category.native.${
tagInfo.allowedCategories.connectedStr()
Expand All @@ -43,8 +41,10 @@ class TagGenerator(
import net.kigawa.renlin.dsl.DslBase
import net.kigawa.renlin.dsl.StatedDsl
import net.kigawa.renlin.component.TagComponent1
import net.kigawa.renlin.component.Component
import net.kigawa.renlin.w3c.element.TagNode
import net.kigawa.renlin.state.DslState
import net.kigawa.renlin.w3c.category.native.${tagInfo.className}Category
${
if (tagInfo.allowedCategories.categories.isEmpty())
"import net.kigawa.renlin.w3c.category.ContentCategory"
Expand All @@ -57,11 +57,11 @@ class TagGenerator(
* model.Categories: ${tagInfo.tagCategories.categories.joinToString(", ")}
*/
class ${tagInfo.className}Dsl(dslState: DslState):
DslBase<${tagInfo.allowedCategories.connectedStr("Integration")}>(dslState),
StatedDsl<${tagInfo.allowedCategories.connectedStr("Integration")}>${
DslBase<${tagInfo.allowedCategories.connectedStr()}>(dslState),
StatedDsl<${tagInfo.allowedCategories.connectedStr()}>${
if (tagInfo.allowedCategories.categories.isEmpty()) ""
else ",\n ${tagInfo.allowedCategories.connectedStr()}" +
"Dsl<${tagInfo.allowedCategories.connectedStr("Integration")}>"
"Dsl<${tagInfo.allowedCategories.connectedStr()}>"
} {
override fun applyElement(element: TagNode): ()->Unit {
return {}
Expand All @@ -70,7 +70,7 @@ class TagGenerator(

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

object ${tagInfo.className} : Tag<${tagInfo.tagCategories.connectedStr("Integration")}> {
object ${tagInfo.className} : Tag<${tagInfo.className}Category> {
override val name: String
get() = "${tagInfo.name}"
}
Expand Down
Loading
Loading