Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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