diff --git a/src/main/groovy/org/groocss/GrooCSS.groovy b/src/main/groovy/org/groocss/GrooCSS.groovy index d7a46b2..05951bf 100644 --- a/src/main/groovy/org/groocss/GrooCSS.groovy +++ b/src/main/groovy/org/groocss/GrooCSS.groovy @@ -15,6 +15,8 @@ limitations under the License. */ package org.groocss +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType import org.codehaus.groovy.control.CompilerConfiguration import groovy.transform.* @@ -1650,6 +1652,45 @@ class GrooCSS extends Script implements CurrentKeyFrameHolder { final Underscore underscore = new Underscore(this) Underscore get_() { underscore } + //---> Pseudo-elements + /** Pseudo-element ::placeholder. */ + PseudoElement.StyleGroup placeholder(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('placeholder', closure) + } + /** Pseudo-element ::after. */ + PseudoElement.StyleGroup after(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('after', closure) + } + /** Pseudo-element ::before. */ + PseudoElement.StyleGroup before(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('before', closure) + } + /** Pseudo-element ::backdrop. */ + PseudoElement.StyleGroup backdrop(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('backdrop', closure) + } + /** Pseudo-element ::cue. */ + PseudoElement.StyleGroup cue(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('cue', closure) + } + /** Pseudo-element ::firstLetter. */ + PseudoElement.StyleGroup firstLetter(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('first-letter', closure) + } + /** Pseudo-element ::firstLine. */ + PseudoElement.StyleGroup firstLine(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('first-line', closure) + } + /** Pseudo-element ::selection. */ + PseudoElement.StyleGroup selection(@DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('selection', closure) + } + /** Pseudo-element ::slotted. */ + PseudoElement.StyleGroup slotted(String select, + @DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + withPseudoElement('slotted(' + select + ')', closure) + } + //---> Pseudo-classes /** Pseudo-class: :active. */ @@ -1793,8 +1834,7 @@ class GrooCSS extends Script implements CurrentKeyFrameHolder { PseudoClass.StyleGroup visited( @DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { withPseudoClass('visited', closure) } - @TypeChecked - PseudoClass.StyleGroup withPseudoClass(String pseudoClass, + PseudoClass.StyleGroup withPseudoClass(String pseudoClass, @DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { def sg = new PseudoClass.StyleGroup(":$pseudoClass", this.config, currentCss) @@ -1804,6 +1844,18 @@ class GrooCSS extends Script implements CurrentKeyFrameHolder { currentCss.add sg sg } + + PseudoElement.StyleGroup withPseudoElement(String pseudoElement, + @ClosureParams(value = SimpleType, options = "org.groocss.StyleGroup") + @DelegatesTo(value = StyleGroup, strategy = Closure.DELEGATE_FIRST) Closure closure) { + + def sg = new PseudoElement.StyleGroup("::$pseudoElement", this.config, currentCss) + closure.delegate = sg + closure.resolveStrategy = Closure.DELEGATE_FIRST + closure(sg) + currentCss.add sg + sg + } /** Pseudo-class: :active. */ PseudoClass getActive() { newPseudoClass('active') } @@ -1914,6 +1966,34 @@ class GrooCSS extends Script implements CurrentKeyFrameHolder { new PseudoClass(value) } + // -----> Pseudo-elements + /** Pseudo-element ::placeholder. */ + PseudoElement getPlaceholder() { new PseudoElement('placeholder') } + + /** Pseudo-element ::after. */ + PseudoElement getAfter() { new PseudoElement('after') } + + /** Pseudo-element ::before. */ + PseudoElement getBefore() { new PseudoElement('before') } + + /** Pseudo-element ::backdrop. */ + PseudoElement getBackdrop() { new PseudoElement('backdrop') } + + /** Pseudo-element ::cue. */ + PseudoElement getCue() { new PseudoElement('cue') } + + /** Pseudo-element ::firstLetter. */ + PseudoElement getFirstLetter() { new PseudoElement('first-letter') } + + /** Pseudo-element ::firstLine. */ + PseudoElement getFirstLine() { new PseudoElement('first-line') } + + /** Pseudo-element ::selection. */ + PseudoElement getSelection() { new PseudoElement('selection') } + + /** Pseudo-element ::slotted. */ + PseudoElement slotted(String select) { new PseudoElement('slotted(' + select + ')') } + Raw raw(String raw) { def r = new Raw(raw) currentCss << r diff --git a/src/main/groovy/org/groocss/MediaCSS.groovy b/src/main/groovy/org/groocss/MediaCSS.groovy index 41f4d97..d457474 100644 --- a/src/main/groovy/org/groocss/MediaCSS.groovy +++ b/src/main/groovy/org/groocss/MediaCSS.groovy @@ -112,16 +112,18 @@ class MediaCSS implements CSSPart { @CompileStatic private List doProcessingOf(Processor.Phase phase) { List errors = [] - List parts = [] + config.processors.forEach { Processor proc -> + List parts = [] Class type = ((ParameterizedType) proc.class.genericInterfaces[0]).actualTypeArguments[0] + switch (type) { case Style: groups.findAll{it instanceof StyleGroup}.each { parts.addAll(((StyleGroup)it).styleList) } break case StyleGroup: parts.addAll groups.findAll{it instanceof StyleGroup}; break case Raw: parts.addAll groups.findAll{it instanceof Raw}; break case Comment: parts.addAll groups.findAll{it instanceof Comment}; break - case MediaCSS: parts.addAll otherCss; break + case MediaCSS: parts.add(this); parts.addAll otherCss; break case FontFace: parts.addAll fonts; break case KeyFrames: parts.addAll kfs; break default: parts.addAll groups; break diff --git a/src/main/groovy/org/groocss/PseudoClass.groovy b/src/main/groovy/org/groocss/PseudoClass.groovy index 295a421..07cea40 100644 --- a/src/main/groovy/org/groocss/PseudoClass.groovy +++ b/src/main/groovy/org/groocss/PseudoClass.groovy @@ -27,8 +27,9 @@ import groovy.transform.* * * @see GrooCSS */ -@TypeChecked +@CompileStatic @TupleConstructor +@EqualsAndHashCode class PseudoClass { /** Only here to restrict the DSL so that pseudo-class is used properly. */ diff --git a/src/main/groovy/org/groocss/PseudoElement.groovy b/src/main/groovy/org/groocss/PseudoElement.groovy new file mode 100644 index 0000000..d2e6ee3 --- /dev/null +++ b/src/main/groovy/org/groocss/PseudoElement.groovy @@ -0,0 +1,35 @@ +package org.groocss + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors + +/** + * Represents a CSS3 Pseudo-element such as "::before", "::after", "::first-letter", "::first-line", + * and "::placeholder". "::placeholder" is NOT supported by IE as of July 11 2019. + * + * @see GrooCSS + * @since 1.0-M4 + */ +@InheritConstructors +@CompileStatic +class PseudoElement extends PseudoClass { + + /** Only here to restrict the DSL so that pseudo-element is used properly. */ + @InheritConstructors + static class StyleGroup extends org.groocss.StyleGroup {} + + /** Allows this to be chainable to pseudo-classes. */ + PseudoElement mod(PseudoClass other) { + new PseudoElement(this.value + "$other") + } + + /** Allows this to be chainable to pseudo-classes. */ + PseudoElement.StyleGroup mod(PseudoClass.StyleGroup other) { + new PseudoElement.StyleGroup(this.value + "$other", other.config, other.owner) + } + + @Override + String toString() { + return "::$value" + } +} diff --git a/src/main/groovy/org/groocss/Selector.groovy b/src/main/groovy/org/groocss/Selector.groovy index ae9370c..7165d9a 100644 --- a/src/main/groovy/org/groocss/Selector.groovy +++ b/src/main/groovy/org/groocss/Selector.groovy @@ -142,12 +142,23 @@ class Selector extends Selectable { String toString() { value } /** Adds the given PseudoClass's value to this selector. Allows syntax: input % hover. */ - def mod(PseudoClass pc) { + Selector mod(PseudoClass pc) { new Selector("$value$pc", owner) } /** Prepends the value of this selector to the given styleGroup's selector. Allows syntax: input % hover {...}. */ - def mod(PseudoClass.StyleGroup styleGroup) { + PseudoClass.StyleGroup mod(PseudoClass.StyleGroup styleGroup) { + styleGroup.resetSelector(value + styleGroup.selector) + styleGroup + } + + /** Adds the given PseudoElement's value to this selector. Allows syntax: input ** before. */ + Selector power(PseudoElement pc) { + new Selector("$value$pc", owner) + } + + /** Prepends the value of this selector to the given styleGroup's selector. Allows syntax: input ** before {...}. */ + PseudoElement.StyleGroup power(PseudoElement.StyleGroup styleGroup) { styleGroup.resetSelector(value + styleGroup.selector) styleGroup } diff --git a/src/main/groovy/org/groocss/Translator.groovy b/src/main/groovy/org/groocss/Translator.groovy index 0540fda..2ad47b7 100644 --- a/src/main/groovy/org/groocss/Translator.groovy +++ b/src/main/groovy/org/groocss/Translator.groovy @@ -80,7 +80,7 @@ class Translator { static final String STYLE_RE = /[-\w]+\s*:\s*[^\{\};]+;?\s*/ static final String FRAME_RE = /([0-9]+|from|to)%?\s*\{?\s*/ static final String FRAME_LIST_RE = /([0-9]+%,? *){2,}\s*\{?\s*/ - static final String SELECTOR_RE = /[- >\~\+*#\[,:\]="\w\.\(\)]+\s*[\{,]\s*/ + static final String SELECTOR_RE = /[- >\~\+*#\[,:\]{1,2}="\w\.\(\)]+\s*[\{,]\s*/ private static void processLineSwitch(String originalLine, String line, Printer pw, Map state) { switch (line) { @@ -180,6 +180,9 @@ class Translator { else if (line ==~ /\w+:[-+\w\.\(\)]+/) { //pseudo-class processPseudo(original.replaceAll(/ *, */, ' |')) } + else if (line ==~ /\w+::[-+\w\.\(\)]+/) { //pseudo-element + processPseudo(original.replaceAll(/ *, */, ' |')) + } else if (line ==~ /\w+(\.\w+)?(\s+\w+(\.\w+)?)*/) { /* Spaces separating simple elements. */ "$line $ending" // don't change a thing } @@ -201,10 +204,14 @@ class Translator { static String processPseudo(String line) { String result = line + Matcher pseudoElement = line =~ /::([-\w]+)/ Matcher pseudo = line =~ /:([-\w]+)/ Matcher parens = line =~ /\(([-+\w\.]+)\)/ boolean hasParens = parens.find() + if (pseudoElement.find()) + result = result.replace( '::'+ pseudoElement.group(1), '**' + nameToCamel(pseudoElement.group(1)) ) + if (pseudo.find()) result = result.replace( ':'+ pseudo.group(1), '%' + nameToCamel(pseudo.group(1)) ) diff --git a/src/main/groovy/org/groocss/Underscore.groovy b/src/main/groovy/org/groocss/Underscore.groovy index 2af1e7f..26970a9 100644 --- a/src/main/groovy/org/groocss/Underscore.groovy +++ b/src/main/groovy/org/groocss/Underscore.groovy @@ -30,6 +30,16 @@ class Underscore { new Selector(".$name", grooCSS.currentCss) } + /** Allows _** syntax for pseudo-elements like ::before and ::after. */ + StyleGroup power(PseudoElement.StyleGroup styleGroup) { + return styleGroup + } + + /** Allows _% syntax for pseudo-classes like :active and :hover. */ + StyleGroup mod(PseudoClass.StyleGroup styleGroup) { + return styleGroup + } + def methodMissing(String name, args) { if (args.length > 0 && args[0] instanceof Closure) grooCSS.sg(".$name", (Closure) args[0]) diff --git a/src/main/groovy/org/groocss/proc/PlaceholderProcessor.groovy b/src/main/groovy/org/groocss/proc/PlaceholderProcessor.groovy new file mode 100644 index 0000000..897f485 --- /dev/null +++ b/src/main/groovy/org/groocss/proc/PlaceholderProcessor.groovy @@ -0,0 +1,37 @@ +package org.groocss.proc + +import groovy.transform.CompileStatic +import org.groocss.MediaCSS +import org.groocss.StyleGroup + +/** + * Finds any StyleGroup with placeholder in it and adds a copy of it with -webkit-input- prefix added to placeholder. + */ +@CompileStatic +class PlaceholderProcessor implements Processor { + + static final String webkitInputPrefix = '-webkit-input-' + static final String placeholder = 'placeholder' + + @Override + Optional process(MediaCSS cssPart, Processor.Phase phase) { + + if (phase == Processor.Phase.POST_VALIDATE) { + cssPart.groups.findAll { + it instanceof StyleGroup && ((StyleGroup) it).selector.contains('placeholder') + }.each { + println "WARNING: $placeholder not supported by IE" + def sg = (StyleGroup) it + def selector = sg.selector + def newSelector = selector.replace(placeholder, webkitInputPrefix + placeholder) + + def newSg = new StyleGroup(newSelector, sg.config, sg.owner) + + newSg.styleList.addAll sg.styleList + + cssPart.groups << newSg + } + } + return Optional.empty() + } +} diff --git a/src/test/groovy/org/groocss/PseudoElementSpec.groovy b/src/test/groovy/org/groocss/PseudoElementSpec.groovy new file mode 100644 index 0000000..97747ac --- /dev/null +++ b/src/test/groovy/org/groocss/PseudoElementSpec.groovy @@ -0,0 +1,103 @@ +package org.groocss + +import org.groocss.proc.PlaceholderProcessor +import spock.lang.Specification +import spock.lang.Unroll + +/** Tests for pseudo-elements such as ::before and ::after. */ +class PseudoElementSpec extends Specification { + + def "should create style group with _**placeholder"() { + when: + def css = GrooCSS.process { + get_()**placeholder { color blue } + }.css + then: + "$css" == "::placeholder{color: Blue;}" + } + + def "should create style group with _**before"() { + when: + def css = GrooCSS.process { get_()**before { color blue } }.css + then: + "$css" == "::before{color: Blue;}" + } + + def "should create style group with _**after"() { + when: + def css = GrooCSS.process { get_()**after { color blue } }.css + then: + "$css" == "::after{color: Blue;}" + } + + def "should create style group with _%before"() { + when: + def css = GrooCSS.process { get_()%before { color blue } }.css + then: + "$css" == "::before{color: Blue;}" + } + + def "should create style group with _%after"() { + when: + def css = GrooCSS.process { get_()%after { color blue } }.css + then: + "$css" == "::after{color: Blue;}" + } + + def "should create style group with p**firstLine"() { + when: + def css = GrooCSS.process { p**firstLine { color red } }.css + then: + "$css" == "p::first-line{color: Red;}" + } + + @Unroll + def "should create a pseudo-element with #name"() { + expect: + GrooCSS.process closure + where: + name || closure + 'after' || { after instanceof PseudoElement } + 'before' || { before instanceof PseudoElement } + 'placeholder' || { placeholder instanceof PseudoElement } + 'cue' || { cue instanceof PseudoElement } + 'backdrop' || { backdrop instanceof PseudoElement } + 'firstLine' || { firstLine instanceof PseudoElement } + 'firstLetter' || { firstLetter instanceof PseudoElement } + 'selection' || { selection instanceof PseudoElement } + } + + + def "should be able to chain pseudo-element and pseudo-class"() { + when: + def css = GrooCSS.process { p **firstLine %hover { color red } }.css + then: + "$css" == "p::first-line:hover{color: Red;}" + } + + def "should be able to chain pseudo-element and last-child"() { + when: + def css = GrooCSS.process { p **firstLine %lastChild { color red } }.css + then: + "$css" == "p::first-line:last-child{color: Red;}" + } + + def "should be able to chain pseudo-element and last-of-type"() { + when: + def css = GrooCSS.process { p **firstLine %lastOfType { color red } }.css + then: + "$css" == "p::first-line:last-of-type{color: Red;}" + } + + def "should make copy of placeholder with -webkit-input- prefix with PlaceholderProcessor"() { + when: + def css = GrooCSS.process(new Config().withProcessors([new PlaceholderProcessor()])) { + + input**placeholder { color red } + + }.css + then: + "$css" == "input::placeholder{color: Red;}\ninput::-webkit-input-placeholder{color: Red;}" + } + +} diff --git a/src/test/groovy/org/groocss/TranslatorSpec.groovy b/src/test/groovy/org/groocss/TranslatorSpec.groovy index f5c0940..faa12eb 100644 --- a/src/test/groovy/org/groocss/TranslatorSpec.groovy +++ b/src/test/groovy/org/groocss/TranslatorSpec.groovy @@ -121,6 +121,16 @@ class TranslatorSpec extends Specification { "body div.test p li a{text-decoration: none;}" | "body div.test p li a {\n textDecoration 'none'\n}" } + + def "should replace :: with ** now"() { + expect: + groo == Translator.convertFromCSS(css).trim() + where: + css | groo + "div::before{\ncontent: 'a';\n}" | "div**before{\n content '\\'a\\''\n}" + "p::after{\ncontent: 'a';\n}" | "p**after{\n content '\\'a\\''\n}" + } + def "should_translate_file"() { given: def inf = new File('index.css')