diff --git a/spec/grid_template_columns_spec.cr b/spec/grid_template_columns_spec.cr new file mode 100644 index 0000000..cc4c03d --- /dev/null +++ b/spec/grid_template_columns_spec.cr @@ -0,0 +1,35 @@ +require "./spec_helper" + +module CSS::GridTemplateColumnsSpec + class Style < CSS::Stylesheet + rule div do + grid_template_columns :none + grid_template_columns line_names(:start), 1.fr, line_names(:end) + grid_template_columns minmax(120.px, 1.fr), fit_content(240.px) + grid_template_columns repeat(3, line_names(:col_start), 1.fr, line_names(:col_end)) + grid_template_columns repeat(:auto_fit, 200.px) + grid_template_columns repeat(:auto_fit, minmax(min(380.px, 100.percent), 1.fr)) + grid_template_columns :subgrid, line_names(:alpha, :beta), repeat(2, line_names(:gamma)) + grid_template_columns :masonry + end + end + + describe "Style.to_s" do + it "renders grid-template-columns values" do + expected = <<-CSS + div { + grid-template-columns: none; + grid-template-columns: [start] 1fr [end]; + grid-template-columns: minmax(120px, 1fr) fit-content(240px); + grid-template-columns: repeat(3, [col_start] 1fr [col_end]); + grid-template-columns: repeat(auto-fit, 200px); + grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); + grid-template-columns: subgrid [alpha beta] repeat(2, [gamma]); + grid-template-columns: masonry; + } + CSS + + Style.to_s.should eq(expected) + end + end +end diff --git a/spec/grid_template_rows_spec.cr b/spec/grid_template_rows_spec.cr new file mode 100644 index 0000000..e655ca6 --- /dev/null +++ b/spec/grid_template_rows_spec.cr @@ -0,0 +1,29 @@ +require "./spec_helper" + +module CSS::GridTemplateRowsSpec + class Style < CSS::Stylesheet + rule div do + grid_template_rows :none + grid_template_rows line_names(:start), 1.fr, line_names(:end) + grid_template_rows repeat(:auto_fit, minmax(min(380.px, 100.percent), 1.fr)) + grid_template_rows :subgrid, line_names(:alpha, :beta), repeat(2, line_names(:gamma)) + grid_template_rows :masonry + end + end + + describe "Style.to_s" do + it "renders grid-template-rows values" do + expected = <<-CSS + div { + grid-template-rows: none; + grid-template-rows: [start] 1fr [end]; + grid-template-rows: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); + grid-template-rows: subgrid [alpha beta] repeat(2, [gamma]); + grid-template-rows: masonry; + } + CSS + + Style.to_s.should eq(expected) + end + end +end diff --git a/src/css/calc_function_call.cr b/src/css/calc_function_call.cr index 20dc914..e13d88b 100644 --- a/src/css/calc_function_call.cr +++ b/src/css/calc_function_call.cr @@ -2,7 +2,8 @@ require "./function_call" require "./calculation" module CSS - struct CalcFunctionCall < FunctionCall + struct CalcFunctionCall + include FunctionCall getter calculation : Calculation def initialize(@calculation) diff --git a/src/css/enums/grid_auto_repeat.cr b/src/css/enums/grid_auto_repeat.cr new file mode 100644 index 0000000..b233902 --- /dev/null +++ b/src/css/enums/grid_auto_repeat.cr @@ -0,0 +1,4 @@ +css_enum GridAutoRepeat do + AutoFill + AutoFit +end diff --git a/src/css/enums/grid_track_keyword.cr b/src/css/enums/grid_track_keyword.cr new file mode 100644 index 0000000..b084555 --- /dev/null +++ b/src/css/enums/grid_track_keyword.cr @@ -0,0 +1,5 @@ +css_enum GridTrackKeyword do + MinContent + MaxContent + Auto +end diff --git a/src/css/enums/masonry.cr b/src/css/enums/masonry.cr new file mode 100644 index 0000000..efe8be6 --- /dev/null +++ b/src/css/enums/masonry.cr @@ -0,0 +1,3 @@ +css_enum Masonry do + Masonry +end diff --git a/src/css/enums/subgrid.cr b/src/css/enums/subgrid.cr new file mode 100644 index 0000000..31bd05f --- /dev/null +++ b/src/css/enums/subgrid.cr @@ -0,0 +1,3 @@ +css_enum Subgrid do + Subgrid +end diff --git a/src/css/function_call.cr b/src/css/function_call.cr index fea1d75..93bf5da 100644 --- a/src/css/function_call.cr +++ b/src/css/function_call.cr @@ -1,5 +1,5 @@ module CSS - abstract struct FunctionCall + module FunctionCall abstract def function_name : String abstract def arguments : String diff --git a/src/css/grid_template_columns.cr b/src/css/grid_template_columns.cr new file mode 100644 index 0000000..44d6df3 --- /dev/null +++ b/src/css/grid_template_columns.cr @@ -0,0 +1,181 @@ +require "./function_call" +require "./min_function_call" + +module CSS + alias GridLineName = String | Symbol + + struct GridLineNames + getter names : Array(String) + + def initialize(*names : GridLineName) + @names = names.map(&.to_s).to_a + end + + def to_s(io : IO) + io << "[" + io << names.join(" ") + io << "]" + end + + def to_css_value + to_s + end + end + + alias GridLengthUnit = CmValue | MmValue | InValue | PxValue | PtValue | PcValue | EmValue | RemValue | ExValue | ChValue | LhValue | RlhValue | VhValue | VwValue | VmaxValue | VminValue | SvwValue | SvhValue | LvwValue | LvhValue | DvwValue | DvhValue + alias GridLength = GridLengthUnit | Int32 | CalcFunctionCall + alias GridLengthPercentage = GridLength | PercentValue | CalcFunctionCall | MinFunctionCall + + alias GridTrackBreadth = GridLengthPercentage | CSS::FrValue | CSS::Enums::GridTrackKeyword + alias GridInflexibleBreadth = GridLengthPercentage | CSS::Enums::GridTrackKeyword + alias GridFixedBreadth = GridLengthPercentage + + struct GridMinmaxFunctionCall + include FunctionCall + getter min : GridInflexibleBreadth + getter max : GridTrackBreadth + + def initialize(@min : GridInflexibleBreadth, @max : GridTrackBreadth) + end + + def function_name : String + "minmax" + end + + def arguments : String + "#{min.to_css_value}, #{max.to_css_value}" + end + end + + struct GridMinmaxFixedFunctionCall + include FunctionCall + getter min : GridFixedBreadth | GridInflexibleBreadth + getter max : GridTrackBreadth | GridFixedBreadth + + def initialize(@min : GridFixedBreadth, @max : GridTrackBreadth) + end + + def initialize(@min : GridInflexibleBreadth, @max : GridFixedBreadth) + end + + def function_name : String + "minmax" + end + + def arguments : String + "#{min.to_css_value}, #{max.to_css_value}" + end + end + + struct GridFitContentFunctionCall + include FunctionCall + getter size : GridLengthPercentage + + def initialize(@size : GridLengthPercentage) + end + + def function_name : String + "fit-content" + end + + def arguments : String + size.to_css_value.to_s + end + end + + alias GridTrackSize = GridTrackBreadth | GridMinmaxFunctionCall | GridMinmaxFixedFunctionCall | GridFitContentFunctionCall + alias GridFixedSize = GridFixedBreadth | GridMinmaxFixedFunctionCall + + alias GridRepeatTrackItem = GridLineNames | GridTrackSize + alias GridRepeatFixedItem = GridLineNames | GridFixedSize + + struct GridTrackRepeatFunctionCall + include FunctionCall + getter count : Int32 + getter tracks : Array(GridRepeatTrackItem) + + def initialize(@count : Int32, *tracks : GridRepeatTrackItem) + @tracks = tracks.map(&.as(GridRepeatTrackItem)).to_a + validate_tracks + end + + def function_name : String + "repeat" + end + + def arguments : String + "#{count.to_css_value}, #{format_tracks(tracks)}" + end + + private def validate_tracks + has_track_size = tracks.any? { |item| item.is_a?(GridTrackSize) } + raise ArgumentError.new("repeat() requires at least one track size") unless has_track_size + end + + private def format_tracks(values) + values.map(&.to_css_value).join(" ") + end + end + + struct GridAutoRepeatFunctionCall + include FunctionCall + getter count : CSS::Enums::GridAutoRepeat + getter tracks : Array(GridRepeatFixedItem) + + def initialize(@count : CSS::Enums::GridAutoRepeat, *tracks : GridRepeatFixedItem) + @tracks = tracks.map(&.as(GridRepeatFixedItem)).to_a + validate_tracks + end + + def function_name : String + "repeat" + end + + def arguments : String + "#{count.to_css_value}, #{format_tracks(tracks)}" + end + + private def validate_tracks + has_track_size = tracks.any? { |item| item.is_a?(GridFixedSize) } + raise ArgumentError.new("repeat(auto-fill/auto-fit, ...) requires at least one fixed track size") unless has_track_size + end + + private def format_tracks(values) + values.map(&.to_css_value).join(" ") + end + end + + struct GridNameRepeatFunctionCall + include FunctionCall + getter count : Int32 | CSS::Enums::GridAutoRepeat + getter names : Array(GridLineNames) + + def initialize(@count, *names : GridLineNames) + @names = names.map(&.as(GridLineNames)).to_a + validate_names + end + + def function_name : String + "repeat" + end + + def arguments : String + "#{count.to_css_value}, #{format_tracks(names)}" + end + + private def validate_names + raise ArgumentError.new("repeat() requires at least one line name group") if names.empty? + + if count.is_a?(CSS::Enums::GridAutoRepeat) && count == CSS::Enums::GridAutoRepeat::AutoFit + raise ArgumentError.new("repeat(auto-fit, +) is not allowed") + end + end + + private def format_tracks(values) + values.map(&.to_css_value).join(" ") + end + end + + alias GridTrackListValue = GridLineNames | GridTrackSize | GridTrackRepeatFunctionCall | GridAutoRepeatFunctionCall | GridNameRepeatFunctionCall + alias GridLineNameListItem = GridLineNames | GridNameRepeatFunctionCall +end diff --git a/src/css/linear_gradient_function_call.cr b/src/css/linear_gradient_function_call.cr index e380aca..f79721e 100644 --- a/src/css/linear_gradient_function_call.cr +++ b/src/css/linear_gradient_function_call.cr @@ -7,7 +7,8 @@ module CSS # comma-separated segment. To group multiple tokens inside a single # segment (e.g. a color stop like `red 10%`), pass a `Tuple`; its items # are joined with spaces. - struct LinearGradientFunctionCall < FunctionCall + struct LinearGradientFunctionCall + include FunctionCall getter arguments : String def initialize(*values) diff --git a/src/css/local_function_call.cr b/src/css/local_function_call.cr index 31e41e4..6d7937b 100644 --- a/src/css/local_function_call.cr +++ b/src/css/local_function_call.cr @@ -1,7 +1,8 @@ require "./function_call" module CSS - struct LocalFunctionCall < FunctionCall + struct LocalFunctionCall + include FunctionCall getter name : String def initialize(@name) diff --git a/src/css/min_function_call.cr b/src/css/min_function_call.cr new file mode 100644 index 0000000..4239207 --- /dev/null +++ b/src/css/min_function_call.cr @@ -0,0 +1,29 @@ +require "./function_call" + +module CSS + # Represents a `min()` function call. + struct MinFunctionCall + include FunctionCall + + getter arguments : String + + def initialize(*values) + @arguments = build_arguments(values) + end + + def function_name : String + "min" + end + + private def build_arguments(values : Tuple) : String + values.map { |value| format(value) }.join(", ") + end + + private def format(value) : String + return value if value.is_a?(String) + return value.to_css_value.to_s if value.responds_to?(:to_css_value) + + value.to_s + end + end +end diff --git a/src/css/radial_gradient_function_call.cr b/src/css/radial_gradient_function_call.cr index 0efe5de..8f97c72 100644 --- a/src/css/radial_gradient_function_call.cr +++ b/src/css/radial_gradient_function_call.cr @@ -7,7 +7,8 @@ module CSS # comma-separated segment. To group multiple tokens inside a single # segment (e.g. a color stop like `red 10%`), pass a `Tuple`; its items # are joined with spaces. - struct RadialGradientFunctionCall < FunctionCall + struct RadialGradientFunctionCall + include FunctionCall getter arguments : String def initialize(*values) diff --git a/src/css/rgb_function_call.cr b/src/css/rgb_function_call.cr index 084e01c..d451c80 100644 --- a/src/css/rgb_function_call.cr +++ b/src/css/rgb_function_call.cr @@ -1,7 +1,8 @@ require "./function_call" module CSS - struct RgbFunctionCall < FunctionCall + struct RgbFunctionCall + include FunctionCall getter red : Int32 getter green : Int32 getter blue : Int32 diff --git a/src/css/transform_function_call.cr b/src/css/transform_function_call.cr index 967b631..5c6596b 100644 --- a/src/css/transform_function_call.cr +++ b/src/css/transform_function_call.cr @@ -2,7 +2,8 @@ require "./function_call" module CSS # Represents a CSS transform function call such as rotateX() or translate(). - struct TransformFunctionCall < FunctionCall + struct TransformFunctionCall + include FunctionCall getter function_name : String getter arguments : String diff --git a/src/css/url_function_call.cr b/src/css/url_function_call.cr index d6029d8..b032065 100644 --- a/src/css/url_function_call.cr +++ b/src/css/url_function_call.cr @@ -1,7 +1,8 @@ require "./function_call" module CSS - struct UrlFunctionCall < FunctionCall + struct UrlFunctionCall + include FunctionCall getter url : String def initialize(@url) diff --git a/src/stylesheet.cr b/src/stylesheet.cr index 6274569..ff9de7e 100644 --- a/src/stylesheet.cr +++ b/src/stylesheet.cr @@ -13,10 +13,12 @@ require "./css/linear_gradient_direction" require "./css/linear_gradient_function_call" require "./css/radial_gradient_at" require "./css/radial_gradient_function_call" +require "./css/min_function_call" require "./css/url_function_call" require "./css/transform_functions" require "./css/transform_function_call" require "./css/ratio" +require "./css/grid_template_columns" require "./font_face" module CSS @@ -1002,8 +1004,67 @@ module CSS prop grid_row_start, Int prop grid_template, String prop grid_template_areas, String - prop grid_template_columns, String - prop grid_template_rows, String + + macro grid_template_rows(*values) + {% if values.empty? %} + {{ raise "grid_template_rows requires at least one value" }} + {% end %} + + {% for value in values %} + {% if value.is_a?(NumberLiteral) && value != 0 %} + {{ value.raise "Non-zero number values have to be specified with a unit, for example: #{value}.px" }} + {% end %} + {% end %} + + _grid_template_rows({{values.splat}}) + end + + def self._grid_template_rows(value : CSS::Enums::None | CSS::Enums::Masonry | CSS::Enums::Global) + property("grid-template-rows", value.to_css_value) + end + + def self._grid_template_rows(value : CSS::Enums::Subgrid, *names : CSS::GridLineNameListItem) + if names.empty? + property("grid-template-rows", value.to_css_value) + else + property("grid-template-rows", "#{value.to_css_value} #{names.map(&.to_css_value).join(" ")}") + end + end + + def self._grid_template_rows(*values : CSS::GridTrackListValue) + property("grid-template-rows", values.map(&.to_css_value).join(" ")) + end + + macro grid_template_columns(*values) + {% if values.empty? %} + {{ raise "grid_template_columns requires at least one value" }} + {% end %} + + {% for value in values %} + {% if value.is_a?(NumberLiteral) && value != 0 %} + {{ value.raise "Non-zero number values have to be specified with a unit, for example: #{value}.px" }} + {% end %} + {% end %} + + _grid_template_columns({{values.splat}}) + end + + def self._grid_template_columns(value : CSS::Enums::None | CSS::Enums::Masonry | CSS::Enums::Global) + property("grid-template-columns", value.to_css_value) + end + + def self._grid_template_columns(value : CSS::Enums::Subgrid, *names : CSS::GridLineNameListItem) + if names.empty? + property("grid-template-columns", value.to_css_value) + else + property("grid-template-columns", "#{value.to_css_value} #{names.map(&.to_css_value).join(" ")}") + end + end + + def self._grid_template_columns(*values : CSS::GridTrackListValue) + property("grid-template-columns", values.map(&.to_css_value).join(" ")) + end + prop hanging_punctuation, String prop height, CSS::LengthPercentage | CSS::Enums::Size | CSS::Enums::Auto prop hyphenate_character, String @@ -1331,6 +1392,93 @@ module CSS Ratio.new(numerator, denominator) end + macro min(*values) + {% if values.size < 2 %} + {{ raise "min requires at least two values" }} + {% end %} + + {% for value in values %} + {% if value.is_a?(NumberLiteral) && value != 0 %} + {{ value.raise "Non-zero number values have to be specified with a unit, for example: #{value}.px" }} + {% end %} + {% end %} + + _min({{values.splat}}) + end + + def self._min(*values : CSS::GridLengthPercentage) + CSS::MinFunctionCall.new(*values) + end + + def self.line_names(*names : CSS::GridLineName) + CSS::GridLineNames.new(*names) + end + + macro fit_content(value) + {% if value.is_a?(NumberLiteral) && value != 0 %} + {{ value.raise "Non-zero number values have to be specified with a unit, for example: #{value}.px" }} + {% end %} + + _fit_content({{value}}) + end + + def self._fit_content(value : CSS::GridLengthPercentage) + CSS::GridFitContentFunctionCall.new(value) + end + + macro minmax(min, max) + {% if min.is_a?(NumberLiteral) && min != 0 %} + {{ min.raise "Non-zero number values have to be specified with a unit, for example: #{min}.px" }} + {% end %} + {% if max.is_a?(NumberLiteral) && max != 0 %} + {{ max.raise "Non-zero number values have to be specified with a unit, for example: #{max}.px" }} + {% end %} + + _minmax({{min}}, {{max}}) + end + + def self._minmax(min : CSS::GridFixedBreadth, max : CSS::GridTrackBreadth) + CSS::GridMinmaxFixedFunctionCall.new(min, max) + end + + def self._minmax(min : CSS::GridInflexibleBreadth, max : CSS::GridFixedBreadth) + CSS::GridMinmaxFixedFunctionCall.new(min, max) + end + + def self._minmax(min : CSS::GridInflexibleBreadth, max : CSS::GridTrackBreadth) + CSS::GridMinmaxFunctionCall.new(min, max) + end + + macro repeat(count, *tracks) + {% if tracks.empty? %} + {{ raise "repeat requires at least one track value" }} + {% end %} + + {% for track in tracks %} + {% if track.is_a?(NumberLiteral) && track != 0 %} + {{ track.raise "Non-zero number values have to be specified with a unit, for example: #{track}.px" }} + {% end %} + {% end %} + + _repeat({{count}}, {{tracks.splat}}) + end + + def self._repeat(count : Int32, *names : CSS::GridLineNames) + CSS::GridNameRepeatFunctionCall.new(count, *names) + end + + def self._repeat(count : CSS::Enums::GridAutoRepeat, *names : CSS::GridLineNames) + CSS::GridNameRepeatFunctionCall.new(count, *names) + end + + def self._repeat(count : CSS::Enums::GridAutoRepeat, *tracks : CSS::GridRepeatFixedItem) + CSS::GridAutoRepeatFunctionCall.new(count, *tracks) + end + + def self._repeat(count : Int32, *tracks : CSS::GridRepeatTrackItem) + CSS::GridTrackRepeatFunctionCall.new(count, *tracks) + end + def self.rgb(r, g, b, *, alpha = nil, from = nil) RgbFunctionCall.new(r, g, b, alpha: alpha, from: from) end