diff --git a/.run/renlin_sample [allTests].run.xml b/.run/renlin_sample [allTests].run.xml new file mode 100644 index 0000000..831ceae --- /dev/null +++ b/.run/renlin_sample [allTests].run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/renlin_sample [jsBrowserDevelopmentRun].run.xml b/.run/renlin_sample [jsBrowserDevelopmentRun].run.xml new file mode 100644 index 0000000..1db4ee6 --- /dev/null +++ b/.run/renlin_sample [jsBrowserDevelopmentRun].run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBackground.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBackground.kt new file mode 100644 index 0000000..5b0f026 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBackground.kt @@ -0,0 +1,63 @@ +package net.kigawa.renlin.css + +/** + * Background Repeat プロパティ + */ +@Suppress("unused") +enum class BackgroundRepeat(private val value: String) : CssValue { + REPEAT("repeat"), + REPEAT_X("repeat-x"), + REPEAT_Y("repeat-y"), + NO_REPEAT("no-repeat"), + SPACE("space"), + ROUND("round"); + + override fun toCssString(): String = value +} + +/** + * Background Position プロパティ + */ +@Suppress("unused") +enum class BackgroundPosition(private val value: String) : CssValue { + LEFT("left"), + CENTER("center"), + RIGHT("right"), + TOP("top"), + BOTTOM("bottom"), + LEFT_TOP("left top"), + LEFT_CENTER("left center"), + LEFT_BOTTOM("left bottom"), + CENTER_TOP("center top"), + CENTER_CENTER("center center"), + CENTER_BOTTOM("center bottom"), + RIGHT_TOP("right top"), + RIGHT_CENTER("right center"), + RIGHT_BOTTOM("right bottom"); + + override fun toCssString(): String = value +} + +/** + * Background Size プロパティ + */ +@Suppress("unused") +enum class BackgroundSize(private val value: String) : CssValue { + AUTO("auto"), + COVER("cover"), + CONTAIN("contain"); + + override fun toCssString(): String = value +} + +/** + * Background Attachment プロパティ + */ +@Suppress("unused") +enum class BackgroundAttachment(private val value: String) : CssValue { + SCROLL("scroll"), + FIXED("fixed"), + LOCAL("local"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBorder.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBorder.kt new file mode 100644 index 0000000..69b4c82 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssBorder.kt @@ -0,0 +1,31 @@ +package net.kigawa.renlin.css + +/** + * Border Style プロパティ + */ +@Suppress("unused") +enum class BorderStyle(private val value: String) : CssValue { + NONE("none"), + HIDDEN("hidden"), + DOTTED("dotted"), + DASHED("dashed"), + SOLID("solid"), + DOUBLE("double"), + GROOVE("groove"), + RIDGE("ridge"), + INSET("inset"), + OUTSET("outset"); + + override fun toCssString(): String = value +} + +/** + * Box Sizing プロパティ + */ +@Suppress("unused") +enum class BoxSizing(private val value: String) : CssValue { + CONTENT_BOX("content-box"), + BORDER_BOX("border-box"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssColor.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssColor.kt new file mode 100644 index 0000000..a7103fb --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssColor.kt @@ -0,0 +1,107 @@ +package net.kigawa.renlin.css + +/** + * CSS色の基底インターフェース + */ +sealed interface CssColor : CssValue + +/** + * 名前付きの色 + */ +@Suppress("unused") +enum class NamedColor(private val colorName: String) : CssColor { + RED("red"), + GREEN("green"), + BLUE("blue"), + BLACK("black"), + WHITE("white"), + TRANSPARENT("transparent"), + GRAY("gray"), + LIGHTGRAY("lightgray"), + DARKGRAY("darkgray"), + YELLOW("yellow"), + ORANGE("orange"), + PURPLE("purple"), + PINK("pink"), + BROWN("brown"), + CYAN("cyan"), + MAGENTA("magenta"); + + override fun toCssString(): String = colorName +} + +/** + * 16進数表記 + */ +data class HexColor(val hex: String) : CssColor { + init { + require(hex.matches(Regex("^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$"))) { + "Invalid hex color format: $hex" + } + } + + override fun toCssString(): String = hex +} + +/** + * RGB表記 + */ +data class RgbColor(val r: Int, val g: Int, val b: Int) : CssColor { + init { + require(r in 0..255 && g in 0..255 && b in 0..255) { + "RGB values must be between 0 and 255" + } + } + + override fun toCssString(): String = "rgb($r, $g, $b)" +} + +/** + * RGBA表記(透明度付き) + */ +data class RgbaColor(val r: Int, val g: Int, val b: Int, val a: Double) : CssColor { + init { + require(r in 0..255 && g in 0..255 && b in 0..255) { + "RGB values must be between 0 and 255" + } + require(a in 0.0..1.0) { + "Alpha value must be between 0.0 and 1.0" + } + } + + override fun toCssString(): String = "rgba($r, $g, $b, $a)" +} + +/** + * 色のファクトリオブジェクト + */ +@Suppress("unused") +object Color { + // よく使う色の定数 + val RED = NamedColor.RED + val GREEN = NamedColor.GREEN + val BLUE = NamedColor.BLUE + val BLACK = NamedColor.BLACK + val WHITE = NamedColor.WHITE + val TRANSPARENT = NamedColor.TRANSPARENT + val GRAY = NamedColor.GRAY + val LIGHTGRAY = NamedColor.LIGHTGRAY + val DARKGRAY = NamedColor.DARKGRAY + val YELLOW = NamedColor.YELLOW + val ORANGE = NamedColor.ORANGE + val PURPLE = NamedColor.PURPLE + val PINK = NamedColor.PINK + val BROWN = NamedColor.BROWN + val CYAN = NamedColor.CYAN + val MAGENTA = NamedColor.MAGENTA + + // ファクトリメソッド + fun hex(hex: String): HexColor = HexColor(hex) + fun rgb(r: Int, g: Int, b: Int): RgbColor = RgbColor(r, g, b) + fun rgba(r: Int, g: Int, b: Int, a: Double): RgbaColor = RgbaColor(r, g, b, a) +} + +// 文字列からの便利な変換 +@Suppress("unused") +val String.color: HexColor + get() = HexColor(if (startsWith("#")) this else "#$this") \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssDsl.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssDsl.kt new file mode 100644 index 0000000..447d39c --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssDsl.kt @@ -0,0 +1,675 @@ +package net.kigawa.renlin.css + +import net.kigawa.renlin.Html + +/** + * CSS記述用のDSL(全プロパティ対応 + 疑似クラス対応) + */ +@Html +@Suppress("unused") +class CssDsl { + private val properties = mutableMapOf() + private val pseudoClasses = mutableListOf() + + // === 疑似クラスメソッド === + + /** + * :hover 疑似クラス + */ + fun hover(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("hover", pseudoDsl.getProperties())) + } + + /** + * :focus 疑似クラス + */ + fun focus(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("focus", pseudoDsl.getProperties())) + } + + /** + * :active 疑似クラス + */ + fun active(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("active", pseudoDsl.getProperties())) + } + + /** + * :disabled 疑似クラス + */ + fun disabled(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("disabled", pseudoDsl.getProperties())) + } + + /** + * :first-child 疑似クラス + */ + fun firstChild(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("first-child", pseudoDsl.getProperties())) + } + + /** + * :last-child 疑似クラス + */ + fun lastChild(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("last-child", pseudoDsl.getProperties())) + } + + /** + * :nth-child(n) 疑似クラス + */ + fun nthChild(n: Int, block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("nth-child($n)", pseudoDsl.getProperties())) + } + + /** + * :nth-child(odd) 疑似クラス + */ + fun nthChildOdd(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("nth-child(odd)", pseudoDsl.getProperties())) + } + + /** + * :nth-child(even) 疑似クラス + */ + fun nthChildEven(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("nth-child(even)", pseudoDsl.getProperties())) + } + + /** + * :visited 疑似クラス(リンク用) + */ + fun visited(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("visited", pseudoDsl.getProperties())) + } + + /** + * :checked 疑似クラス(フォーム要素用) + */ + fun checked(block: CssPseudoDsl.() -> Unit) { + val pseudoDsl = CssPseudoDsl() + pseudoDsl.block() + pseudoClasses.add(PseudoClassRule("checked", pseudoDsl.getProperties())) + } + + // === Color Properties === + var color: CssColor? + get() = properties["color"] as? CssColor + set(value) { if (value != null) properties["color"] = value } + + var backgroundColor: CssColor? + get() = properties["background-color"] as? CssColor + set(value) { if (value != null) properties["background-color"] = value } + + // === Size Properties === + var width: CssValue? + get() = properties["width"] + set(value) { if (value != null) properties["width"] = value } + + var height: CssValue? + get() = properties["height"] + set(value) { if (value != null) properties["height"] = value } + + var minWidth: CssValue? + get() = properties["min-width"] + set(value) { if (value != null) properties["min-width"] = value } + + var minHeight: CssValue? + get() = properties["min-height"] + set(value) { if (value != null) properties["min-height"] = value } + + var maxWidth: CssValue? + get() = properties["max-width"] + set(value) { if (value != null) properties["max-width"] = value } + + var maxHeight: CssValue? + get() = properties["max-height"] + set(value) { if (value != null) properties["max-height"] = value } + + // === Font Properties === + var fontSize: CssValue? + get() = properties["font-size"] + set(value) { if (value != null) properties["font-size"] = value } + + var fontWeight: FontWeight? + get() = properties["font-weight"] as? FontWeight + set(value) { if (value != null) properties["font-weight"] = value } + + var fontStyle: FontStyle? + get() = properties["font-style"] as? FontStyle + set(value) { if (value != null) properties["font-style"] = value } + + var fontVariant: FontVariant? + get() = properties["font-variant"] as? FontVariant + set(value) { if (value != null) properties["font-variant"] = value } + + var fontFamily: CssValue? + get() = properties["font-family"] + set(value) { if (value != null) properties["font-family"] = value } + + var lineHeight: CssValue? + get() = properties["line-height"] + set(value) { if (value != null) properties["line-height"] = value } + + // === Text Properties === + var textAlign: TextAlign? + get() = properties["text-align"] as? TextAlign + set(value) { if (value != null) properties["text-align"] = value } + + var textDecoration: TextDecoration? + get() = properties["text-decoration"] as? TextDecoration + set(value) { if (value != null) properties["text-decoration"] = value } + + var textTransform: TextTransform? + get() = properties["text-transform"] as? TextTransform + set(value) { if (value != null) properties["text-transform"] = value } + + var letterSpacing: CssValue? + get() = properties["letter-spacing"] + set(value) { if (value != null) properties["letter-spacing"] = value } + + var wordSpacing: CssValue? + get() = properties["word-spacing"] + set(value) { if (value != null) properties["word-spacing"] = value } + + var whiteSpace: WhiteSpace? + get() = properties["white-space"] as? WhiteSpace + set(value) { if (value != null) properties["white-space"] = value } + + var wordBreak: WordBreak? + get() = properties["word-break"] as? WordBreak + set(value) { if (value != null) properties["word-break"] = value } + + var textOverflow: TextOverflow? + get() = properties["text-overflow"] as? TextOverflow + set(value) { if (value != null) properties["text-overflow"] = value } + + // === Margin Properties === + var margin: CssValue? + get() = properties["margin"] + set(value) { if (value != null) properties["margin"] = value } + + var marginTop: CssValue? + get() = properties["margin-top"] + set(value) { if (value != null) properties["margin-top"] = value } + + var marginRight: CssValue? + get() = properties["margin-right"] + set(value) { if (value != null) properties["margin-right"] = value } + + var marginBottom: CssValue? + get() = properties["margin-bottom"] + set(value) { if (value != null) properties["margin-bottom"] = value } + + var marginLeft: CssValue? + get() = properties["margin-left"] + set(value) { if (value != null) properties["margin-left"] = value } + + // === Padding Properties === + var padding: CssValue? + get() = properties["padding"] + set(value) { if (value != null) properties["padding"] = value } + + var paddingTop: CssValue? + get() = properties["padding-top"] + set(value) { if (value != null) properties["padding-top"] = value } + + var paddingRight: CssValue? + get() = properties["padding-right"] + set(value) { if (value != null) properties["padding-right"] = value } + + var paddingBottom: CssValue? + get() = properties["padding-bottom"] + set(value) { if (value != null) properties["padding-bottom"] = value } + + var paddingLeft: CssValue? + get() = properties["padding-left"] + set(value) { if (value != null) properties["padding-left"] = value } + + // === Border Properties === + var border: CssValue? + get() = properties["border"] + set(value) { if (value != null) properties["border"] = value } + + var borderWidth: CssValue? + get() = properties["border-width"] + set(value) { if (value != null) properties["border-width"] = value } + + var borderStyle: BorderStyle? + get() = properties["border-style"] as? BorderStyle + set(value) { if (value != null) properties["border-style"] = value } + + var borderColor: CssColor? + get() = properties["border-color"] as? CssColor + set(value) { if (value != null) properties["border-color"] = value } + + var borderRadius: CssValue? + get() = properties["border-radius"] + set(value) { if (value != null) properties["border-radius"] = value } + + // Border individual sides + var borderTop: CssValue? + get() = properties["border-top"] + set(value) { if (value != null) properties["border-top"] = value } + + var borderRight: CssValue? + get() = properties["border-right"] + set(value) { if (value != null) properties["border-right"] = value } + + var borderBottom: CssValue? + get() = properties["border-bottom"] + set(value) { if (value != null) properties["border-bottom"] = value } + + var borderLeft: CssValue? + get() = properties["border-left"] + set(value) { if (value != null) properties["border-left"] = value } + + // === Layout Properties === + var display: Display? + get() = properties["display"] as? Display + set(value) { if (value != null) properties["display"] = value } + + var position: Position? + get() = properties["position"] as? Position + set(value) { if (value != null) properties["position"] = value } + + var top: CssValue? + get() = properties["top"] + set(value) { if (value != null) properties["top"] = value } + + var right: CssValue? + get() = properties["right"] + set(value) { if (value != null) properties["right"] = value } + + var bottom: CssValue? + get() = properties["bottom"] + set(value) { if (value != null) properties["bottom"] = value } + + var left: CssValue? + get() = properties["left"] + set(value) { if (value != null) properties["left"] = value } + + var zIndex: CssValue? + get() = properties["z-index"] + set(value) { if (value != null) properties["z-index"] = value } + + var overflow: Overflow? + get() = properties["overflow"] as? Overflow + set(value) { if (value != null) properties["overflow"] = value } + + var overflowX: Overflow? + get() = properties["overflow-x"] as? Overflow + set(value) { if (value != null) properties["overflow-x"] = value } + + var overflowY: Overflow? + get() = properties["overflow-y"] as? Overflow + set(value) { if (value != null) properties["overflow-y"] = value } + + var visibility: Visibility? + get() = properties["visibility"] as? Visibility + set(value) { if (value != null) properties["visibility"] = value } + + var float: Float? + get() = properties["float"] as? Float + set(value) { if (value != null) properties["float"] = value } + + var clear: Clear? + get() = properties["clear"] as? Clear + set(value) { if (value != null) properties["clear"] = value } + + var boxSizing: BoxSizing? + get() = properties["box-sizing"] as? BoxSizing + set(value) { if (value != null) properties["box-sizing"] = value } + + // === Flexbox Properties === + var flex: CssValue? + get() = properties["flex"] + set(value) { if (value != null) properties["flex"] = value } + + var flexDirection: FlexDirection? + get() = properties["flex-direction"] as? FlexDirection + set(value) { if (value != null) properties["flex-direction"] = value } + + var flexWrap: FlexWrap? + get() = properties["flex-wrap"] as? FlexWrap + set(value) { if (value != null) properties["flex-wrap"] = value } + + var justifyContent: JustifyContent? + get() = properties["justify-content"] as? JustifyContent + set(value) { if (value != null) properties["justify-content"] = value } + + var alignItems: AlignItems? + get() = properties["align-items"] as? AlignItems + set(value) { if (value != null) properties["align-items"] = value } + + var alignContent: AlignContent? + get() = properties["align-content"] as? AlignContent + set(value) { if (value != null) properties["align-content"] = value } + + var alignSelf: AlignSelf? + get() = properties["align-self"] as? AlignSelf + set(value) { if (value != null) properties["align-self"] = value } + + var flexGrow: CssValue? + get() = properties["flex-grow"] + set(value) { if (value != null) properties["flex-grow"] = value } + + var flexShrink: CssValue? + get() = properties["flex-shrink"] + set(value) { if (value != null) properties["flex-shrink"] = value } + + var flexBasis: CssValue? + get() = properties["flex-basis"] + set(value) { if (value != null) properties["flex-basis"] = value } + + var order: CssValue? + get() = properties["order"] + set(value) { if (value != null) properties["order"] = value } + + // === Grid Properties === + var grid: CssValue? + get() = properties["grid"] + set(value) { if (value != null) properties["grid"] = value } + + var gridTemplate: CssValue? + get() = properties["grid-template"] + set(value) { if (value != null) properties["grid-template"] = value } + + var gridTemplateColumns: CssValue? + get() = properties["grid-template-columns"] + set(value) { if (value != null) properties["grid-template-columns"] = value } + + var gridTemplateRows: CssValue? + get() = properties["grid-template-rows"] + set(value) { if (value != null) properties["grid-template-rows"] = value } + + var gridTemplateAreas: CssValue? + get() = properties["grid-template-areas"] + set(value) { if (value != null) properties["grid-template-areas"] = value } + + var gridColumn: CssValue? + get() = properties["grid-column"] + set(value) { if (value != null) properties["grid-column"] = value } + + var gridRow: CssValue? + get() = properties["grid-row"] + set(value) { if (value != null) properties["grid-row"] = value } + + var gridColumnStart: CssValue? + get() = properties["grid-column-start"] + set(value) { if (value != null) properties["grid-column-start"] = value } + + var gridColumnEnd: CssValue? + get() = properties["grid-column-end"] + set(value) { if (value != null) properties["grid-column-end"] = value } + + var gridRowStart: CssValue? + get() = properties["grid-row-start"] + set(value) { if (value != null) properties["grid-row-start"] = value } + + var gridRowEnd: CssValue? + get() = properties["grid-row-end"] + set(value) { if (value != null) properties["grid-row-end"] = value } + + var gridArea: CssValue? + get() = properties["grid-area"] + set(value) { if (value != null) properties["grid-area"] = value } + + var gridAutoFlow: GridAutoFlow? + get() = properties["grid-auto-flow"] as? GridAutoFlow + set(value) { if (value != null) properties["grid-auto-flow"] = value } + + var gridAutoColumns: CssValue? + get() = properties["grid-auto-columns"] + set(value) { if (value != null) properties["grid-auto-columns"] = value } + + var gridAutoRows: CssValue? + get() = properties["grid-auto-rows"] + set(value) { if (value != null) properties["grid-auto-rows"] = value } + + var gap: CssValue? + get() = properties["gap"] + set(value) { if (value != null) properties["gap"] = value } + + var columnGap: CssValue? + get() = properties["column-gap"] + set(value) { if (value != null) properties["column-gap"] = value } + + var rowGap: CssValue? + get() = properties["row-gap"] + set(value) { if (value != null) properties["row-gap"] = value } + + var justifyItems: JustifyItems? + get() = properties["justify-items"] as? JustifyItems + set(value) { if (value != null) properties["justify-items"] = value } + + var justifySelf: JustifySelf? + get() = properties["justify-self"] as? JustifySelf + set(value) { if (value != null) properties["justify-self"] = value } + + var placeItems: PlaceItems? + get() = properties["place-items"] as? PlaceItems + set(value) { if (value != null) properties["place-items"] = value } + + var placeSelf: PlaceSelf? + get() = properties["place-self"] as? PlaceSelf + set(value) { if (value != null) properties["place-self"] = value } + + // === Background Properties === + var background: CssValue? + get() = properties["background"] + set(value) { if (value != null) properties["background"] = value } + + var backgroundImage: CssValue? + get() = properties["background-image"] + set(value) { if (value != null) properties["background-image"] = value } + + var backgroundRepeat: BackgroundRepeat? + get() = properties["background-repeat"] as? BackgroundRepeat + set(value) { if (value != null) properties["background-repeat"] = value } + + var backgroundPosition: BackgroundPosition? + get() = properties["background-position"] as? BackgroundPosition + set(value) { if (value != null) properties["background-position"] = value } + + var backgroundSize: BackgroundSize? + get() = properties["background-size"] as? BackgroundSize + set(value) { if (value != null) properties["background-size"] = value } + + var backgroundAttachment: BackgroundAttachment? + get() = properties["background-attachment"] as? BackgroundAttachment + set(value) { if (value != null) properties["background-attachment"] = value } + + // === Transform & Animation Properties === + var transform: CssValue? + get() = properties["transform"] + set(value) { if (value != null) properties["transform"] = value } + + var transformOrigin: CssValue? + get() = properties["transform-origin"] + set(value) { if (value != null) properties["transform-origin"] = value } + + var transition: CssValue? + get() = properties["transition"] + set(value) { if (value != null) properties["transition"] = value } + + var transitionProperty: CssValue? + get() = properties["transition-property"] + set(value) { if (value != null) properties["transition-property"] = value } + + var transitionDuration: CssValue? + get() = properties["transition-duration"] + set(value) { if (value != null) properties["transition-duration"] = value } + + var transitionTimingFunction: CssValue? + get() = properties["transition-timing-function"] + set(value) { if (value != null) properties["transition-timing-function"] = value } + + var transitionDelay: CssValue? + get() = properties["transition-delay"] + set(value) { if (value != null) properties["transition-delay"] = value } + + var animation: CssValue? + get() = properties["animation"] + set(value) { if (value != null) properties["animation"] = value } + + var animationName: CssValue? + get() = properties["animation-name"] + set(value) { if (value != null) properties["animation-name"] = value } + + var animationDuration: CssValue? + get() = properties["animation-duration"] + set(value) { if (value != null) properties["animation-duration"] = value } + + var animationTimingFunction: CssValue? + get() = properties["animation-timing-function"] + set(value) { if (value != null) properties["animation-timing-function"] = value } + + var animationDelay: CssValue? + get() = properties["animation-delay"] + set(value) { if (value != null) properties["animation-delay"] = value } + + var animationIterationCount: CssValue? + get() = properties["animation-iteration-count"] + set(value) { if (value != null) properties["animation-iteration-count"] = value } + + var animationDirection: CssValue? + get() = properties["animation-direction"] + set(value) { if (value != null) properties["animation-direction"] = value } + + var animationFillMode: CssValue? + get() = properties["animation-fill-mode"] + set(value) { if (value != null) properties["animation-fill-mode"] = value } + + var animationPlayState: CssValue? + get() = properties["animation-play-state"] + set(value) { if (value != null) properties["animation-play-state"] = value } + + // === Miscellaneous Properties === + var opacity: CssValue? + get() = properties["opacity"] + set(value) { if (value != null) properties["opacity"] = value } + + var cursor: Cursor? + get() = properties["cursor"] as? Cursor + set(value) { if (value != null) properties["cursor"] = value } + + var userSelect: UserSelect? + get() = properties["user-select"] as? UserSelect + set(value) { if (value != null) properties["user-select"] = value } + + var pointerEvents: PointerEvents? + get() = properties["pointer-events"] as? PointerEvents + set(value) { if (value != null) properties["pointer-events"] = value } + + var boxShadow: CssValue? + get() = properties["box-shadow"] + set(value) { if (value != null) properties["box-shadow"] = value } + + var textShadow: CssValue? + get() = properties["text-shadow"] + set(value) { if (value != null) properties["text-shadow"] = value } + + var outline: CssValue? + get() = properties["outline"] + set(value) { if (value != null) properties["outline"] = value } + + var outlineWidth: CssValue? + get() = properties["outline-width"] + set(value) { if (value != null) properties["outline-width"] = value } + + var outlineStyle: CssValue? + get() = properties["outline-style"] + set(value) { if (value != null) properties["outline-style"] = value } + + var outlineColor: CssColor? + get() = properties["outline-color"] as? CssColor + set(value) { if (value != null) properties["outline-color"] = value } + + var outlineOffset: CssValue? + get() = properties["outline-offset"] + set(value) { if (value != null) properties["outline-offset"] = value } + + var objectFit: ObjectFit? + get() = properties["object-fit"] as? ObjectFit + set(value) { if (value != null) properties["object-fit"] = value } + + var objectPosition: CssValue? + get() = properties["object-position"] + set(value) { if (value != null) properties["object-position"] = value } + + var verticalAlign: VerticalAlign? + get() = properties["vertical-align"] as? VerticalAlign + set(value) { if (value != null) properties["vertical-align"] = value } + + var resize: CssValue? + get() = properties["resize"] + set(value) { if (value != null) properties["resize"] = value } + + var content: CssValue? + get() = properties["content"] + set(value) { if (value != null) properties["content"] = value } + + var listStyle: CssValue? + get() = properties["list-style"] + set(value) { if (value != null) properties["list-style"] = value } + + var listStyleType: CssValue? + get() = properties["list-style-type"] + set(value) { if (value != null) properties["list-style-type"] = value } + + var listStylePosition: CssValue? + get() = properties["list-style-position"] + set(value) { if (value != null) properties["list-style-position"] = value } + + var listStyleImage: CssValue? + get() = properties["list-style-image"] + set(value) { if (value != null) properties["list-style-image"] = value } + + // === Helper Methods === + fun fontFamily(value: String) { + fontFamily = CssString("\"$value\"") + } + + fun border(width: CssValue, style: BorderStyle, color: CssColor) { + border = CssString("${width.toCssString()} ${style.toCssString()} ${color.toCssString()}") + } + + fun boxShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, color: CssColor) { + boxShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${color.toCssString()}") + } + + fun boxShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, spreadRadius: CssValue, color: CssColor) { + boxShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${spreadRadius.toCssString()} ${color.toCssString()}") + } + + fun textShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, color: CssColor) { + textShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${color.toCssString()}") + } + + // 内部で使用:プロパティマップを取得 + internal fun getProperties(): Map = properties.toMap() + + // 内部で使用:CSS規則セットを取得 + internal fun getRuleSet(): CssRuleSet = CssRuleSet(properties.toMap(), pseudoClasses.toList()) +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssExtensions.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssExtensions.kt new file mode 100644 index 0000000..98ba88c --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssExtensions.kt @@ -0,0 +1,29 @@ +package net.kigawa.renlin.css + +import net.kigawa.renlin.dsl.StatedDsl +import net.kigawa.renlin.w3c.category.ContentCategory + +/** + * DSLにCSS機能を追加する拡張関数(疑似クラス対応) + */ +fun StatedDsl.css(block: CssDsl.() -> Unit) { + val cssDsl = CssDsl() + cssDsl.block() + val ruleSet = cssDsl.getRuleSet() + + if (!ruleSet.isEmpty()) { + // dslStateがnullの場合は、CSS情報を一時保存 + if (this is CssCapable) { + this.pendingCssRuleSet = ruleSet + } + } +} + +/** + * CSS対応可能なDSLを示すインターフェース + */ +interface CssCapable { + var cssClassName: String? + var pendingCssProperties: Map? + var pendingCssRuleSet: CssRuleSet? +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFlexbox.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFlexbox.kt new file mode 100644 index 0000000..654cc8b --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFlexbox.kt @@ -0,0 +1,85 @@ +package net.kigawa.renlin.css + +/** + * Flex Direction プロパティ + */ +@Suppress("unused") +enum class FlexDirection(private val value: String) : CssValue { + ROW("row"), + ROW_REVERSE("row-reverse"), + COLUMN("column"), + COLUMN_REVERSE("column-reverse"); + + override fun toCssString(): String = value +} + +/** + * Flex Wrap プロパティ + */ +@Suppress("unused") +enum class FlexWrap(private val value: String) : CssValue { + NOWRAP("nowrap"), + WRAP("wrap"), + WRAP_REVERSE("wrap-reverse"); + + override fun toCssString(): String = value +} + +/** + * Justify Content プロパティ + */ +@Suppress("unused") +enum class JustifyContent(private val value: String) : CssValue { + FLEX_START("flex-start"), + FLEX_END("flex-end"), + CENTER("center"), + SPACE_BETWEEN("space-between"), + SPACE_AROUND("space-around"), + SPACE_EVENLY("space-evenly"); + + override fun toCssString(): String = value +} + +/** + * Align Items プロパティ + */ +@Suppress("unused") +enum class AlignItems(private val value: String) : CssValue { + STRETCH("stretch"), + FLEX_START("flex-start"), + FLEX_END("flex-end"), + CENTER("center"), + BASELINE("baseline"); + + override fun toCssString(): String = value +} + +/** + * Align Content プロパティ + */ +@Suppress("unused") +enum class AlignContent(private val value: String) : CssValue { + STRETCH("stretch"), + FLEX_START("flex-start"), + FLEX_END("flex-end"), + CENTER("center"), + SPACE_BETWEEN("space-between"), + SPACE_AROUND("space-around"); + + override fun toCssString(): String = value +} + +/** + * Align Self プロパティ + */ +@Suppress("unused") +enum class AlignSelf(private val value: String) : CssValue { + AUTO("auto"), + STRETCH("stretch"), + FLEX_START("flex-start"), + FLEX_END("flex-end"), + CENTER("center"), + BASELINE("baseline"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFont.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFont.kt new file mode 100644 index 0000000..22bd8e7 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssFont.kt @@ -0,0 +1,46 @@ +package net.kigawa.renlin.css + +/** + * フォントウェイト + */ +@Suppress("unused") +enum class FontWeight(private val value: String) : CssValue { + NORMAL("normal"), + BOLD("bold"), + BOLDER("bolder"), + LIGHTER("lighter"), + W100("100"), + W200("200"), + W300("300"), + W400("400"), + W500("500"), + W600("600"), + W700("700"), + W800("800"), + W900("900"); + + override fun toCssString(): String = value +} + +/** + * フォントスタイル + */ +@Suppress("unused") +enum class FontStyle(private val value: String) : CssValue { + NORMAL("normal"), + ITALIC("italic"), + OBLIQUE("oblique"); + + override fun toCssString(): String = value +} + +/** + * フォントバリアント + */ +@Suppress("unused") +enum class FontVariant(private val value: String) : CssValue { + NORMAL("normal"), + SMALLCAPS("small-caps"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssGrid.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssGrid.kt new file mode 100644 index 0000000..9445848 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssGrid.kt @@ -0,0 +1,68 @@ +package net.kigawa.renlin.css + +/** + * Grid Auto Flow プロパティ + */ +@Suppress("unused") +enum class GridAutoFlow(private val value: String) : CssValue { + ROW("row"), + COLUMN("column"), + ROW_DENSE("row dense"), + COLUMN_DENSE("column dense"); + + override fun toCssString(): String = value +} + +/** + * Justify Items プロパティ + */ +@Suppress("unused") +enum class JustifyItems(private val value: String) : CssValue { + START("start"), + END("end"), + CENTER("center"), + STRETCH("stretch"); + + override fun toCssString(): String = value +} + +/** + * Justify Self プロパティ + */ +@Suppress("unused") +enum class JustifySelf(private val value: String) : CssValue { + AUTO("auto"), + START("start"), + END("end"), + CENTER("center"), + STRETCH("stretch"); + + override fun toCssString(): String = value +} + +/** + * Place Items プロパティ + */ +@Suppress("unused") +enum class PlaceItems(private val value: String) : CssValue { + START("start"), + END("end"), + CENTER("center"), + STRETCH("stretch"); + + override fun toCssString(): String = value +} + +/** + * Place Self プロパティ + */ +@Suppress("unused") +enum class PlaceSelf(private val value: String) : CssValue { + AUTO("auto"), + START("start"), + END("end"), + CENTER("center"), + STRETCH("stretch"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssLayout.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssLayout.kt new file mode 100644 index 0000000..127974b --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssLayout.kt @@ -0,0 +1,85 @@ +package net.kigawa.renlin.css + +/** + * Display プロパティ + */ +@Suppress("unused") +enum class Display(private val value: String) : CssValue { + BLOCK("block"), + INLINE("inline"), + INLINE_BLOCK("inline-block"), + FLEX("flex"), + INLINE_FLEX("inline-flex"), + GRID("grid"), + INLINE_GRID("inline-grid"), + TABLE("table"), + TABLE_CELL("table-cell"), + TABLE_ROW("table-row"), + NONE("none"); + + override fun toCssString(): String = value +} + +/** + * Position プロパティ + */ +@Suppress("unused") +enum class Position(private val value: String) : CssValue { + STATIC("static"), + RELATIVE("relative"), + ABSOLUTE("absolute"), + FIXED("fixed"), + STICKY("sticky"); + + override fun toCssString(): String = value +} + +/** + * Overflow プロパティ + */ +@Suppress("unused") +enum class Overflow(private val value: String) : CssValue { + VISIBLE("visible"), + HIDDEN("hidden"), + SCROLL("scroll"), + AUTO("auto"); + + override fun toCssString(): String = value +} + +/** + * Float プロパティ + */ +@Suppress("unused") +enum class Float(private val value: String) : CssValue { + NONE("none"), + LEFT("left"), + RIGHT("right"); + + override fun toCssString(): String = value +} + +/** + * Clear プロパティ + */ +@Suppress("unused") +enum class Clear(private val value: String) : CssValue { + NONE("none"), + LEFT("left"), + RIGHT("right"), + BOTH("both"); + + override fun toCssString(): String = value +} + +/** + * Visibility プロパティ + */ +@Suppress("unused") +enum class Visibility(private val value: String) : CssValue { + VISIBLE("visible"), + HIDDEN("hidden"), + COLLAPSE("collapse"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssManager.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssManager.kt new file mode 100644 index 0000000..71755f7 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssManager.kt @@ -0,0 +1,94 @@ +package net.kigawa.renlin.css + +/** + * CSS疑似クラス情報を保持するデータクラス + */ +data class PseudoClassRule( + val pseudoClass: String, + val properties: Map +) + +/** + * CSS規則(ベース + 疑似クラス)を管理するデータクラス + */ +data class CssRuleSet( + val baseProperties: Map, + val pseudoClasses: List +) { + fun isEmpty(): Boolean = baseProperties.isEmpty() && pseudoClasses.isEmpty() +} + +/** + * CSSクラス管理のインターフェース + */ +interface CssManager { + fun getOrCreateClass(ruleSet: CssRuleSet): String + fun updateStyles() +} + +/** + * プラットフォーム固有のCssManagerを作成するファクトリ関数 + */ +expect fun createCssManager(): CssManager + +/** + * CSS管理のユーティリティ関数 + */ +object CssUtils { + fun generateCssString(properties: Map): String { + return properties.entries.joinToString("; ") { (key, value) -> + "$key: ${value.toCssString()}" + } + } + + /** + * CSSルールセットからReact風のクラス名を生成 + */ + fun generateClassName(ruleSet: CssRuleSet): String { + // ベースプロパティと疑似クラスを含めた内容でハッシュ値を生成 + val baseContent = ruleSet.baseProperties.entries + .sortedBy { it.key } + .joinToString("|") { "${it.key}:${it.value.toCssString()}" } + + val pseudoContent = ruleSet.pseudoClasses + .sortedBy { it.pseudoClass } + .joinToString("|") { rule -> + val props = rule.properties.entries + .sortedBy { it.key } + .joinToString(",") { "${it.key}:${it.value.toCssString()}" } + "${rule.pseudoClass}($props)" + } + + val content = listOf(baseContent, pseudoContent) + .filter { it.isNotEmpty() } + .joinToString("||") + + val hash = content.hashCode() + val hashString = hash.toUInt().toString(36) + + return "renlin-$hashString" + } + + /** + * CSS規則セットから完全なCSS文字列を生成 + */ + fun generateFullCssString(className: String, ruleSet: CssRuleSet): String { + val cssRules = mutableListOf() + + // ベースルール + if (ruleSet.baseProperties.isNotEmpty()) { + val baseRule = ".$className { ${generateCssString(ruleSet.baseProperties)} }" + cssRules.add(baseRule) + } + + // 疑似クラスルール + ruleSet.pseudoClasses.forEach { pseudoRule -> + if (pseudoRule.properties.isNotEmpty()) { + val pseudoCssRule = ".$className:${pseudoRule.pseudoClass} { ${generateCssString(pseudoRule.properties)} }" + cssRules.add(pseudoCssRule) + } + } + + return cssRules.joinToString("\n") + } +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssMisc.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssMisc.kt new file mode 100644 index 0000000..817f3ce --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssMisc.kt @@ -0,0 +1,84 @@ +package net.kigawa.renlin.css + +/** + * Cursor プロパティ + */ +@Suppress("unused") +enum class Cursor(private val value: String) : CssValue { + AUTO("auto"), + DEFAULT("default"), + POINTER("pointer"), + TEXT("text"), + WAIT("wait"), + HELP("help"), + CROSSHAIR("crosshair"), + MOVE("move"), + NOT_ALLOWED("not-allowed"), + GRAB("grab"), + GRABBING("grabbing"), + RESIZE_E("e-resize"), + RESIZE_N("n-resize"), + RESIZE_NE("ne-resize"), + RESIZE_NW("nw-resize"), + RESIZE_S("s-resize"), + RESIZE_SE("se-resize"), + RESIZE_SW("sw-resize"), + RESIZE_W("w-resize"); + + override fun toCssString(): String = value +} + +/** + * User Select プロパティ + */ +@Suppress("unused") +enum class UserSelect(private val value: String) : CssValue { + AUTO("auto"), + NONE("none"), + TEXT("text"), + ALL("all"); + + override fun toCssString(): String = value +} + +/** + * Pointer Events プロパティ + */ +@Suppress("unused") +enum class PointerEvents(private val value: String) : CssValue { + AUTO("auto"), + NONE("none"); + + override fun toCssString(): String = value +} + +/** + * Object Fit プロパティ + */ +@Suppress("unused") +enum class ObjectFit(private val value: String) : CssValue { + FILL("fill"), + CONTAIN("contain"), + COVER("cover"), + NONE("none"), + SCALE_DOWN("scale-down"); + + override fun toCssString(): String = value +} + +/** + * Vertical Align プロパティ + */ +@Suppress("unused") +enum class VerticalAlign(private val value: String) : CssValue { + BASELINE("baseline"), + TOP("top"), + MIDDLE("middle"), + BOTTOM("bottom"), + TEXT_TOP("text-top"), + TEXT_BOTTOM("text-bottom"), + SUB("sub"), + SUPER("super"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssPseudoDsl.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssPseudoDsl.kt new file mode 100644 index 0000000..a60f5d2 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssPseudoDsl.kt @@ -0,0 +1,211 @@ +package net.kigawa.renlin.css + +import net.kigawa.renlin.Html + +/** + * 疑似クラス用のDSL + */ +@Html +@Suppress("unused") +class CssPseudoDsl { + private val properties = mutableMapOf() + + // === Color Properties === + var color: CssColor? + get() = properties["color"] as? CssColor + set(value) { if (value != null) properties["color"] = value } + + var backgroundColor: CssColor? + get() = properties["background-color"] as? CssColor + set(value) { if (value != null) properties["background-color"] = value } + + // === Size Properties === + var width: CssValue? + get() = properties["width"] + set(value) { if (value != null) properties["width"] = value } + + var height: CssValue? + get() = properties["height"] + set(value) { if (value != null) properties["height"] = value } + + // === Font Properties === + var fontSize: CssValue? + get() = properties["font-size"] + set(value) { if (value != null) properties["font-size"] = value } + + var fontWeight: FontWeight? + get() = properties["font-weight"] as? FontWeight + set(value) { if (value != null) properties["font-weight"] = value } + + var fontStyle: FontStyle? + get() = properties["font-style"] as? FontStyle + set(value) { if (value != null) properties["font-style"] = value } + + // === Transform & Animation === + var transform: CssValue? + get() = properties["transform"] + set(value) { if (value != null) properties["transform"] = value } + + var transition: CssValue? + get() = properties["transition"] + set(value) { if (value != null) properties["transition"] = value } + + // === Border Properties === + var border: CssValue? + get() = properties["border"] + set(value) { if (value != null) properties["border"] = value } + + var borderColor: CssColor? + get() = properties["border-color"] as? CssColor + set(value) { if (value != null) properties["border-color"] = value } + + var borderWidth: CssValue? + get() = properties["border-width"] + set(value) { if (value != null) properties["border-width"] = value } + + var borderRadius: CssValue? + get() = properties["border-radius"] + set(value) { if (value != null) properties["border-radius"] = value } + + // === Layout Properties === + var display: Display? + get() = properties["display"] as? Display + set(value) { if (value != null) properties["display"] = value } + + var position: Position? + get() = properties["position"] as? Position + set(value) { if (value != null) properties["position"] = value } + + var top: CssValue? + get() = properties["top"] + set(value) { if (value != null) properties["top"] = value } + + var left: CssValue? + get() = properties["left"] + set(value) { if (value != null) properties["left"] = value } + + var right: CssValue? + get() = properties["right"] + set(value) { if (value != null) properties["right"] = value } + + var bottom: CssValue? + get() = properties["bottom"] + set(value) { if (value != null) properties["bottom"] = value } + + // === Visual Effects === + var opacity: CssValue? + get() = properties["opacity"] + set(value) { if (value != null) properties["opacity"] = value } + + var boxShadow: CssValue? + get() = properties["box-shadow"] + set(value) { if (value != null) properties["box-shadow"] = value } + + var textShadow: CssValue? + get() = properties["text-shadow"] + set(value) { if (value != null) properties["text-shadow"] = value } + + // === Outline Properties === + var outline: CssValue? + get() = properties["outline"] + set(value) { if (value != null) properties["outline"] = value } + + var outlineColor: CssColor? + get() = properties["outline-color"] as? CssColor + set(value) { if (value != null) properties["outline-color"] = value } + + var outlineWidth: CssValue? + get() = properties["outline-width"] + set(value) { if (value != null) properties["outline-width"] = value } + + var outlineOffset: CssValue? + get() = properties["outline-offset"] + set(value) { if (value != null) properties["outline-offset"] = value } + + // === Spacing Properties === + var margin: CssValue? + get() = properties["margin"] + set(value) { if (value != null) properties["margin"] = value } + + var marginTop: CssValue? + get() = properties["margin-top"] + set(value) { if (value != null) properties["margin-top"] = value } + + var marginRight: CssValue? + get() = properties["margin-right"] + set(value) { if (value != null) properties["margin-right"] = value } + + var marginBottom: CssValue? + get() = properties["margin-bottom"] + set(value) { if (value != null) properties["margin-bottom"] = value } + + var marginLeft: CssValue? + get() = properties["margin-left"] + set(value) { if (value != null) properties["margin-left"] = value } + + var padding: CssValue? + get() = properties["padding"] + set(value) { if (value != null) properties["padding"] = value } + + var paddingTop: CssValue? + get() = properties["padding-top"] + set(value) { if (value != null) properties["padding-top"] = value } + + var paddingRight: CssValue? + get() = properties["padding-right"] + set(value) { if (value != null) properties["padding-right"] = value } + + var paddingBottom: CssValue? + get() = properties["padding-bottom"] + set(value) { if (value != null) properties["padding-bottom"] = value } + + var paddingLeft: CssValue? + get() = properties["padding-left"] + set(value) { if (value != null) properties["padding-left"] = value } + + // === UI Properties === + var cursor: Cursor? + get() = properties["cursor"] as? Cursor + set(value) { if (value != null) properties["cursor"] = value } + + var userSelect: UserSelect? + get() = properties["user-select"] as? UserSelect + set(value) { if (value != null) properties["user-select"] = value } + + var pointerEvents: PointerEvents? + get() = properties["pointer-events"] as? PointerEvents + set(value) { if (value != null) properties["pointer-events"] = value } + + // === Text Properties === + var textAlign: TextAlign? + get() = properties["text-align"] as? TextAlign + set(value) { if (value != null) properties["text-align"] = value } + + var textDecoration: TextDecoration? + get() = properties["text-decoration"] as? TextDecoration + set(value) { if (value != null) properties["text-decoration"] = value } + + var textTransform: TextTransform? + get() = properties["text-transform"] as? TextTransform + set(value) { if (value != null) properties["text-transform"] = value } + + // === Helper Methods === + fun border(width: CssValue, style: BorderStyle, color: CssColor) { + border = CssString("${width.toCssString()} ${style.toCssString()} ${color.toCssString()}") + } + + fun boxShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, color: CssColor) { + boxShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${color.toCssString()}") + } + + fun boxShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, spreadRadius: CssValue, color: CssColor) { + boxShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${spreadRadius.toCssString()} ${color.toCssString()}") + } + + fun textShadow(offsetX: CssValue, offsetY: CssValue, blurRadius: CssValue, color: CssColor) { + textShadow = CssString("${offsetX.toCssString()} ${offsetY.toCssString()} ${blurRadius.toCssString()} ${color.toCssString()}") + } + + // 内部で使用:プロパティマップを取得 + internal fun getProperties(): Map = properties.toMap() +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssText.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssText.kt new file mode 100644 index 0000000..a64022d --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssText.kt @@ -0,0 +1,80 @@ +package net.kigawa.renlin.css + +/** + * Text Align プロパティ + */ +@Suppress("unused") +enum class TextAlign(private val value: String) : CssValue { + LEFT("left"), + RIGHT("right"), + CENTER("center"), + JUSTIFY("justify"), + START("start"), + END("end"); + + override fun toCssString(): String = value +} + +/** + * Text Decoration プロパティ + */ +@Suppress("unused") +enum class TextDecoration(private val value: String) : CssValue { + NONE("none"), + UNDERLINE("underline"), + OVERLINE("overline"), + LINE_THROUGH("line-through"); + + override fun toCssString(): String = value +} + +/** + * Text Transform プロパティ + */ +@Suppress("unused") +enum class TextTransform(private val value: String) : CssValue { + NONE("none"), + UPPERCASE("uppercase"), + LOWERCASE("lowercase"), + CAPITALIZE("capitalize"); + + override fun toCssString(): String = value +} + +/** + * White Space プロパティ + */ +@Suppress("unused") +enum class WhiteSpace(private val value: String) : CssValue { + NORMAL("normal"), + NOWRAP("nowrap"), + PRE("pre"), + PRE_LINE("pre-line"), + PRE_WRAP("pre-wrap"); + + override fun toCssString(): String = value +} + +/** + * Word Break プロパティ + */ +@Suppress("unused") +enum class WordBreak(private val value: String) : CssValue { + NORMAL("normal"), + BREAK_ALL("break-all"), + KEEP_ALL("keep-all"), + BREAK_WORD("break-word"); + + override fun toCssString(): String = value +} + +/** + * Text Overflow プロパティ + */ +@Suppress("unused") +enum class TextOverflow(private val value: String) : CssValue { + CLIP("clip"), + ELLIPSIS("ellipsis"); + + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssUnit.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssUnit.kt new file mode 100644 index 0000000..372e4a8 --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssUnit.kt @@ -0,0 +1,44 @@ +package net.kigawa.renlin.css + +/** + * 単位付きの値 + */ +data class CssUnit(val value: Number, val unit: String) : CssValue { + override fun toCssString(): String = "$value$unit" +} + +// 便利な拡張プロパティ +@Suppress("unused") +val Int.px: CssUnit get() = CssUnit(this, "px") +@Suppress("unused") +val Double.px: CssUnit get() = CssUnit(this, "px") +@Suppress("unused") +val Int.em: CssUnit get() = CssUnit(this, "em") +@Suppress("unused") +val Double.em: CssUnit get() = CssUnit(this, "em") +@Suppress("unused") +val Int.rem: CssUnit get() = CssUnit(this, "rem") +@Suppress("unused") +val Double.rem: CssUnit get() = CssUnit(this, "rem") +@Suppress("unused") +val Int.percent: CssUnit get() = CssUnit(this, "%") +@Suppress("unused") +val Double.percent: CssUnit get() = CssUnit(this, "%") +@Suppress("unused") +val Int.vh: CssUnit get() = CssUnit(this, "vh") +@Suppress("unused") +val Double.vh: CssUnit get() = CssUnit(this, "vh") +@Suppress("unused") +val Int.vw: CssUnit get() = CssUnit(this, "vw") +@Suppress("unused") +val Double.vw: CssUnit get() = CssUnit(this, "vw") + +// 文字列をCssValueに変換する拡張プロパティ +@Suppress("unused") +val String.cssValue: CssValue get() = CssString(this) + +// 数値をCssValueに変換する拡張プロパティ +@Suppress("unused") +val Int.cssValue: CssValue get() = CssString(this.toString()) +@Suppress("unused") +val Double.cssValue: CssValue get() = CssString(this.toString()) \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssValue.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssValue.kt new file mode 100644 index 0000000..6e523ef --- /dev/null +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/css/CssValue.kt @@ -0,0 +1,15 @@ +package net.kigawa.renlin.css + +/** + * CSS値の基底インターフェース + */ +sealed interface CssValue { + fun toCssString(): String +} + +/** + * 文字列値 + */ +data class CssString(val value: String) : CssValue { + override fun toCssString(): String = value +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/dsl/DslBase.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/dsl/DslBase.kt index 1224348..91c244b 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/dsl/DslBase.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/dsl/DslBase.kt @@ -1,13 +1,19 @@ package net.kigawa.renlin.dsl import net.kigawa.hakate.api.state.State +import net.kigawa.renlin.css.CssCapable +import net.kigawa.renlin.css.CssRuleSet +import net.kigawa.renlin.css.CssValue import net.kigawa.renlin.state.DslState import net.kigawa.renlin.state.DslStateData import net.kigawa.renlin.w3c.category.ContentCategory abstract class DslBase( override val dslState: DslState, -) : StatedDsl { +) : StatedDsl, CssCapable { + override var cssClassName: String? = null + override var pendingCssProperties: Map? = null + override var pendingCssRuleSet: CssRuleSet? = null private val subDsls = mutableListOf() override val states = mutableSetOf>() override val dslStateData: DslStateData? = dslState.dslStateData() @@ -26,6 +32,10 @@ abstract class DslBase( } override fun applyToDslState(state: DslState, registeredDslData: RegisteredDslData) { + + // dslStateが設定されたタイミングでpendingCssPropertiesを処理 + processPendingCss() + subDsls.forEach { it.dsl.applyToDslState( state.getOrCreateSubDslState(it.key, it.component), it @@ -39,6 +49,31 @@ abstract class DslBase( states.add(this) return this.currentValue() } -} + /** + * 保留中のCSS情報を処理 + */ + private fun processPendingCss() { + // 新しいCssRuleSet形式を優先的に処理 + pendingCssRuleSet?.let { ruleSet -> + val cssManager = dslState.cssManager + if (cssManager != null) { + cssClassName = cssManager.getOrCreateClass(ruleSet) + // 処理完了後はクリア + pendingCssRuleSet = null + return + } + } + // 後方互換性のため、古いproperties形式も処理 + pendingCssProperties?.let { properties -> + val cssManager = dslState.cssManager + if (cssManager != null) { + val ruleSet = CssRuleSet(properties, emptyList()) + cssClassName = cssManager.getOrCreateClass(ruleSet) + // 処理完了後はクリア + pendingCssProperties = null + } + } + } +} \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/BasicDslStateBase.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/BasicDslStateBase.kt index afb0312..7e4d313 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/BasicDslStateBase.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/BasicDslStateBase.kt @@ -1,17 +1,23 @@ package net.kigawa.renlin.state import net.kigawa.hakate.api.state.StateContext -import net.kigawa.renlin.dsl.StatedDsl +import net.kigawa.renlin.css.CssCapable +import net.kigawa.renlin.css.CssManager +import net.kigawa.renlin.css.createCssManager import net.kigawa.renlin.dsl.RegisteredDslData -import net.kigawa.renlin.w3c.element.TagNode +import net.kigawa.renlin.dsl.StatedDsl import net.kigawa.renlin.tag.Tag import net.kigawa.renlin.tag.component.Component +import net.kigawa.renlin.w3c.element.TagNode abstract class BasicDslStateBase( protected val stateContext: StateContext, ) : DslState { protected var subStates = mutableListOf() abstract override val ownElement: TagNode? + protected var internalCssManager: CssManager? = null + override val cssManager: CssManager? + get() = internalCssManager override fun getOrCreateSubDslState(key: String, second: Component): DslState { return subStates.firstOrNull { it.key == key } ?: SubBasicDslState( @@ -57,11 +63,19 @@ abstract class BasicDslStateBase( subStates.forEach { it.remove() } } - override fun applyDsl(dsl: StatedDsl<*>, registeredDslData: RegisteredDslData) { + // CSS適用処理を追加 + if (dsl is CssCapable && dsl.cssClassName != null) { + ownElement?.setClassName(dsl.cssClassName!!) + } throw NotImplementedError("BasicDslState not implemented.") } abstract fun newElement(tag: Tag<*>): TagNode + protected fun initializeCssManager() { + if (internalCssManager == null) { + internalCssManager = createCssManager() + } + } } \ No newline at end of file diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/DslState.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/DslState.kt index f2112cb..b3ba2e1 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/DslState.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/DslState.kt @@ -1,9 +1,10 @@ package net.kigawa.renlin.state -import net.kigawa.renlin.dsl.StatedDsl +import net.kigawa.renlin.css.CssManager import net.kigawa.renlin.dsl.RegisteredDslData -import net.kigawa.renlin.w3c.element.TagNode +import net.kigawa.renlin.dsl.StatedDsl import net.kigawa.renlin.tag.component.Component +import net.kigawa.renlin.w3c.element.TagNode /** * `DslState` インターフェースは、DSLの状態を管理するための機能を定義します。 @@ -53,4 +54,7 @@ interface DslState { */ fun applyDsl(dsl: StatedDsl<*>, registeredDslData: RegisteredDslData) fun dslStateData(): DslStateData? + + // CSS機能の追加 + val cssManager: CssManager? } diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/RootDslStateBase.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/RootDslStateBase.kt index 6f57dde..ea9dc8e 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/RootDslStateBase.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/RootDslStateBase.kt @@ -9,6 +9,11 @@ class RootDslStateBase( override val ownElement: TagNode, stateContext: StateContext, ) : BasicDslStateBase(stateContext) { + init { + // ルートではCssManagerを初期化 + initializeCssManager() + } + override fun newElement(tag: Tag<*>): TagNode { return ownElement.newNode(tag) } diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/SubBasicDslState.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/SubBasicDslState.kt index 907af5e..07b923e 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/SubBasicDslState.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/state/SubBasicDslState.kt @@ -3,6 +3,8 @@ package net.kigawa.renlin.state import net.kigawa.hakate.api.multi.mergeState import net.kigawa.hakate.api.state.State import net.kigawa.hakate.api.state.StateContext +import net.kigawa.renlin.css.CssCapable +import net.kigawa.renlin.css.CssManager import net.kigawa.renlin.dsl.RegisteredDslData import net.kigawa.renlin.dsl.StatedDsl import net.kigawa.renlin.tag.Tag @@ -22,11 +24,19 @@ class SubBasicDslState( override var latestRegisteredDslData: RegisteredDslData? = null private var latestDslStateData: DslStateData? = DslStateData(key) + override val cssManager: CssManager? + get() = parent. cssManager var latestStateContext: StateContext? = null override fun applyDsl(dsl: StatedDsl<*>, registeredDslData: RegisteredDslData) { latestRegisteredDslData = registeredDslData val index = parent.getIndex(this) + + // CSS適用処理を追加 + if (dsl is CssCapable && dsl.cssClassName != null) { + ownElement?.setClassName(dsl.cssClassName!!) + } + if (ownElement != null) { dsl.applyElement(ownElement) ownElement.setDslStateData(latestDslStateData, dsl.dslStateData) diff --git a/renlin/src/commonMain/kotlin/net/kigawa/renlin/w3c/element/TagNodeCommon.kt b/renlin/src/commonMain/kotlin/net/kigawa/renlin/w3c/element/TagNodeCommon.kt index e3267f4..020bd95 100644 --- a/renlin/src/commonMain/kotlin/net/kigawa/renlin/w3c/element/TagNodeCommon.kt +++ b/renlin/src/commonMain/kotlin/net/kigawa/renlin/w3c/element/TagNodeCommon.kt @@ -23,4 +23,6 @@ interface TagNodeCommon { dslStateData.onClick?.let { addEventListener(EventNames.click, it) } ).let { dslStateData.setAdditionalData(this::class, it) } } + + fun setClassName(className: String) } \ No newline at end of file diff --git a/renlin/src/jsMain/kotlin/net/kigawa/renlin/css/JsCssManager.kt b/renlin/src/jsMain/kotlin/net/kigawa/renlin/css/JsCssManager.kt new file mode 100644 index 0000000..b11620b --- /dev/null +++ b/renlin/src/jsMain/kotlin/net/kigawa/renlin/css/JsCssManager.kt @@ -0,0 +1,36 @@ +package net.kigawa.renlin.css + +import kotlinx.browser.document +import org.w3c.dom.HTMLStyleElement + +class JsCssManager : CssManager { + private val styleElement: HTMLStyleElement = document.createElement("style") as HTMLStyleElement + private val cssRules = mutableMapOf() + + init { + styleElement.id = "renlin-css-styles" + document.head?.appendChild(styleElement) + } + + override fun getOrCreateClass(ruleSet: CssRuleSet): String { + if (ruleSet.isEmpty()) return "" + + val className = CssUtils.generateClassName(ruleSet) + + return if (cssRules.containsKey(className)) { + className + } else { + val cssString = CssUtils.generateFullCssString(className, ruleSet) + cssRules[className] = cssString + updateStyles() + className + } + } + + override fun updateStyles() { + val cssText = cssRules.values.joinToString("\n") + styleElement.textContent = cssText + } +} + +actual fun createCssManager(): CssManager = JsCssManager() \ No newline at end of file diff --git a/renlin/src/jsMain/kotlin/net/kigawa/renlin/w3c/element/DomTagElement.kt b/renlin/src/jsMain/kotlin/net/kigawa/renlin/w3c/element/DomTagElement.kt index 7b26946..d8b8304 100644 --- a/renlin/src/jsMain/kotlin/net/kigawa/renlin/w3c/element/DomTagElement.kt +++ b/renlin/src/jsMain/kotlin/net/kigawa/renlin/w3c/element/DomTagElement.kt @@ -7,6 +7,7 @@ import net.kigawa.renlin.tag.TextTag import net.kigawa.renlin.w3c.event.RegisteredEvent import net.kigawa.renlin.w3c.event.WebEvent import net.kigawa.renlin.w3c.event.name.EventName +import org.w3c.dom.Element import org.w3c.dom.Node import org.w3c.dom.Text @@ -59,4 +60,14 @@ class DomTagElement( node.removeEventListener(registeredEvent.name.name, registeredEvent.listener) } + + override fun setClassName(className: String) { + if (node is Element) { + // 既存のクラス名から renlin- プレフィックスのクラス名を削除 + val existingClasses = node.className.split(" ").filter { it.isNotEmpty() } + val nonRenlinClasses = existingClasses.filter { !it.startsWith("renlin-") } + val newClasses = (nonRenlinClasses + className).distinct() + node.className = newClasses.joinToString(" ") + } + } } \ No newline at end of file diff --git a/renlin/src/jvmMain/kotlin/net/kigawa/renlin/css/JvmCssManager.kt b/renlin/src/jvmMain/kotlin/net/kigawa/renlin/css/JvmCssManager.kt new file mode 100644 index 0000000..0a04aac --- /dev/null +++ b/renlin/src/jvmMain/kotlin/net/kigawa/renlin/css/JvmCssManager.kt @@ -0,0 +1,78 @@ +package net.kigawa.renlin.css + +/** + * JVM版のCSSマネージャー実装(疑似クラス対応) + */ +class JvmCssManager : CssManager { + private val cssRules = mutableMapOf() // クラス名 -> 完全なCSS文字列 + + override fun getOrCreateClass(ruleSet: CssRuleSet): String { + if (ruleSet.isEmpty()) return "" + + // React風のクラス名を生成 + val className = CssUtils.generateClassName(ruleSet) + + // 既に同じクラス名が存在する場合はそれを返す + return if (cssRules.containsKey(className)) { + className + } else { + val cssString = CssUtils.generateFullCssString(className, ruleSet) + cssRules[className] = cssString + updateStyles() + className + } + } + + override fun updateStyles() { + // JVM版では何もしない(HTMLファイル生成時にまとめて出力) + } + + /** + * スタイルタグを生成 + */ + fun generateStyleTag(): String { + return if (cssRules.isEmpty()) { + "" + } else { + "" + } + } + + /** + * HTMLにスタイルを埋め込んだ完全なHTMLを生成 + */ + fun generateHtmlWithStyles(htmlContent: String): String { + val styleTag = generateStyleTag() + return if (styleTag.isNotEmpty()) { + """ + + + + + $styleTag + + + $htmlContent + + + """.trimIndent() + } else { + """ + + + + + + + $htmlContent + + + """.trimIndent() + } + } +} + +/** + * JVM版のCssManagerファクトリ関数 + */ +actual fun createCssManager(): CssManager = JvmCssManager() \ No newline at end of file diff --git a/sample/src/commonMain/kotlin/net/kigawa/renlin/sample/Sub.kt b/sample/src/commonMain/kotlin/net/kigawa/renlin/sample/Sub.kt index 5bedbab..08ed7ad 100644 --- a/sample/src/commonMain/kotlin/net/kigawa/renlin/sample/Sub.kt +++ b/sample/src/commonMain/kotlin/net/kigawa/renlin/sample/Sub.kt @@ -1,9 +1,8 @@ -@file:Suppress("unused") - package net.kigawa.renlin.sample import net.kigawa.hakate.api.HakateInitializer import net.kigawa.hakate.api.state.MutableState +import net.kigawa.renlin.css.* import net.kigawa.renlin.tag.div import net.kigawa.renlin.tag.fragment import net.kigawa.renlin.tag.p @@ -14,11 +13,8 @@ import net.kigawa.renlin.w3c.category.native.PhrasingContent import net.kigawa.renlin.w3c.category.t -interface MarginValue - - class Sub { - val state: MutableState = HakateInitializer().newStateDispatcher().newState("state") + val state: MutableState = HakateInitializer().newStateDispatcher().newState("state 0") val display = div.component { t("display") @@ -34,6 +30,13 @@ class Sub { text { margin = "asd" } + css { + color = if (value.last().digitToInt() % 2 == 0) Color.RED else Color.BLUE + backgroundColor = if (value.last().digitToInt() % 2 == 0) Color.BLUE else Color.RED + hover { + cursor = Cursor.GRABBING + } + } } } } @@ -43,10 +46,28 @@ class Sub { div("uuid aadaaaaaaa") { t("display2-1") + css { + userSelect = UserSelect.NONE + hover { + cursor = Cursor.POINTER + backgroundColor = Color.rgba(0, 255, 255, 0.3) + } + active { + color = Color.RED + fontWeight = FontWeight.BOLD + } + } } } div("uuid aawaaaaaaaa") { t("display3") + css { + color = Color.YELLOW + backgroundColor = Color.BLUE + fontSize = 24.px + height = 100.px + padding = 1.percent + } } }