Montag, 2. Juli 2012

Präsentationen mit reveal.js und Groovy


Gut aussehende Online-Präsentationen können mit reveal.js (https://github.com/hakimel/reveal.js) erstellt werden. Diese werden in dabei HTML geschrieben. Natürlich kann man auch Groovy's MarkupBuilder verwenden.

Der HTML-Code wird dann von Groovy erstellt. Das hat folgende Vorteile:
  • Trennung und Auslagerung von wiederkehrenden und gleichbleibenden Codeabschnitten
  • Auslagern vom Beispielcode in die entsprechenden Quellcodedateien
  • Auslagerung von Einstellungen, wie zum Beispiel die Ziel-URL
  • Der Funktionsumfang wird durch den MarkupBuilder nicht eingeschränkt
  • Builder-Code ist meiner Meinung nach etwas übersichtlicher als HTML-Code

Head-Daten, Cover und eine Seite der Präsentation können dann wie folgt aussehen:

// this script contains data of the presentation

// header data
data.head.title = 'reveal.js'
data.head.description = 'An easy to use CSS 3D slideshow tool for quickly creating good looking HTML presentations.'
data.head.author = 'Hakim El Hattab'

// title page
data.cover.title = 'Reveal.js'
data.cover.subtitle = 'CSS 3D Presentations'

// sections
sections.section {
    h2 'Heads Up'
    p """reveal.js is an easy to use, HTML based, presentation tool. You'll need a modern browser with
            support for CSS 3D transforms to see it in its full glory."""
    p {
        i {
            small {
                mkp.yield '-'
                a(href: 'http://hakim.se', 'Hakim El Hattab')
                mkp.yield ' /'
                a(href: 'http://twitter.com/hakimel', '@hakimel')
            }
        }
    }
}
...

Hierbei wird die Seite mit dem MarkupBuilder sections erstellt.

Die Initialisierung der Variablen data (Daten der Präsentation) und sections (MarkupBuilder für die einzelnen Seiten) erfolgen im allgemeinen, bei jeder Präsentation gleichbleibenden, Script index-html-builder.groovy:

...
// Map and builder initialisation (presentation data)
def data = [:]
data.head = [:]
data.cover = [:]
data.sectionWriter = new StringWriter()
def sections = new groovy.xml.MarkupBuilder(data.sectionWriter)
...

Anschließend werden die Daten der Präsentation (siehe oben) aus dem Script index-data.groovy bezogen:

...
// call script with presentation data
def scriptEngine = new GroovyScriptEngine([scriptPath] as String[])

def binding = new Binding()
binding.setVariable 'data', data
binding.setVariable 'sections', sections
binding.setVariable 'scriptPath', scriptPath
binding.setVariable 'targetName', targetName
binding.setVariable 'targetUrl', targetUrl

data = scriptEngine.run 'index-data.groovy', binding
...

Jetzt sind alle Daten der Präsentation vorhanden und die gesamte HTML-Datei wird mit dem MarkupBuilder erstellt:

...
// builder initialisation (html file)
def fileName = "$targetDir/$targetName"
def fileWriter = new FileWriter(fileName)
def builder = new groovy.xml.MarkupBuilder(fileWriter)

// presentation build
builder.html(lang: 'en') {
    head {
        meta(charset: 'UTF-8')

        title data.head.title

        meta(name: 'description', content: data.head.description)
        meta(name: 'author', content: data.head.author)

        meta(name: 'apple-mobile-web-app-capable', content: 'yes')
        meta(name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent')

        link(href: 'http://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic', rel: 'stylesheet', type: 'text/css')


        link(rel: 'stylesheet', href: 'css/reset.css')
        link(rel: 'stylesheet', href: 'css/main.css')
        link(rel: 'stylesheet', href: 'css/print.css', type: 'text/css', media: 'print')

        link(rel: 'stylesheet', href: 'lib/zenburn.css')

    }

    body {
        div(class: 'reveal') {

            // Used to fade in a background when a specific slide state is reached
            div(class: 'state-background')

            // Any section element inside of this container is displayed as a slide
            div(class: 'slides') {


                section(class: 'present', style: 'display: block;') {
                    h1 data.cover.title
                    h3(class: 'inverted', data.cover.subtitle)

                    // Delicously hacky. Look away.
                    script """if( navigator.userAgent.match( /(iPhone|iPad|iPod|Android)/i ) ) document.write( '<p style="color: rgba(0,0,0,0.3); text-shadow: none;">('+'Tap to navigate'+')</p>' );"""
                }

                mkp.yieldUnescaped data.sectionWriter
            }
...

Mit der Zeile "mkp.yieldUnescaped data.sectionWriter" werden die einzelnen Seiten aus dem Builder der Seiten in dem Builder für das gesamte HTML übernommen.

Grundlegende Einstellungen werden in der Datei gradle.properties vorgenommen.

Hier das Script index-data.groovy für die Beispielpräsentation (http://www.aonnet.de/reveal-js-groovy):

// this script contains data of the presentation

// header data
data.head.title = 'reveal.js'
data.head.description = 'An easy to use CSS 3D slideshow tool for quickly creating good looking HTML presentations.'
data.head.author = 'Hakim El Hattab'

// title page
data.cover.title = 'Reveal.js'
data.cover.subtitle = 'CSS 3D Presentations'

// sections
sections.section {
    h2 'Heads Up'
    p """reveal.js is an easy to use, HTML based, presentation tool. You'll need a modern browser with
            support for CSS 3D transforms to see it in its full glory."""
    p {
        i {
            small {
                mkp.yield '-'
                a(href: 'http://hakim.se', 'Hakim El Hattab')
                mkp.yield ' /'
                a(href: 'http://twitter.com/hakimel', '@hakimel')
            }
        }
    }
}

sections.section {
    h2 'What\'s the deal with Groovy?'
    p {
        mkp.yield 'Groovy is an agile and dynamic language for the Java Virtual Machine.'
        a(href: 'http://groovy.codehaus.org/Creating+XML+using+Groovy%27s+MarkupBuilder', "Groovy's MarkupBuilder")
        mkp.yield " supports Groovy's builder pattern with XML/HTML."
    }

    p {
        i {
            small {
                mkp.yield '-'
                a(href: 'http://www.aonnet.de', 'Thomas Westphal')
                mkp.yield ' /'
                a(href: 'http://twitter.com/twest72', '@twest72')
                mkp.yield ' /'
                a(href: 'https://github.com/twest72', 'twest72@github')
            }
        }
    }
}

sections.section {
    h2 'Pretty Groovy Code for reveal.js'
    p "The following script contains the code for this page. 'example.groovy' is a Groovy script."
    pre {
        code(class:'java', contenteditable: '') {
            mkp.yieldUnescaped new File("$scriptPath/example.groovy").text
        }
    }
    p {
        mkp.yield 'Courtesy of'
        a(href: 'http://softwaremaniacs.org/soft/highlight/en/description/', 'highlight.js')
    }
}

// Example of nested vertical slides
sections.section {
    section {
        h2 'Vertical Slides'
        p {
            mkp.yield 'Slides can be nested inside of other slides,'
            br()
            mkp.yield 'try pressing'
            a(href: '#/2/1', 'down.')
        }
        a(href: '#/2/1', class: 'image') {
            img(src: 'https://s3.amazonaws.com/hakim-static/reveal-js/arrow.png', height: '238', width: '178')
        }
    }
    section {
        h2 'Basement Level 1'

        p 'Press down or up to navigate.'
    }
    section {
        h2 'Basement Level 2'
        p 'Cornify'
        a(href: 'http://cornify.com', class: 'test') {
            img(src: 'https://s3.amazonaws.com/hakim-static/reveal-js/cornify.gif', height: '326', width: '280')
        }
    }
    section {
        h2 'Basement Level 3'
        p '''That's it, time to go back up.'''
        a(href: '#/2', class: 'image') {
            img(src: 'https://s3.amazonaws.com/hakim-static/reveal-js/arrow.png', height: '238', width: '178')
        }
    }
}


sections.section {
    h2 'Holistic Overview'
    p {
        mkp.yield 'Press'
        strong 'ESC'
        mkp.yield ' to enter the slide overview!'
    }
}


sections.section {
    h2 'Works in Mobile Safari'
    p 'Try it out! You can swipe through the slides pinch your way to the overview.'
}


sections.section {
    h2 'Transition Styles'
    p 'You can select from different transitions, like:'
    ul {
        li { a(href: "${ targetUrl }?transition=cube", 'Cube') }
        li { a(href: "${ targetUrl }?transition=page", 'Page') }
        li { a(href: "${ targetUrl }?transition=concave", 'Concave') }
        li { a(href: "${ targetUrl }?transition=linear", 'Linear') }
    }
}


sections.section {
    h2 'Marvelous Unordered List'
    ul {
        li 'No order here'
        li 'Or here'
        li 'Or here'
        li 'Or here'
    }
}


sections.section {
    h2 'Fantastic Ordered List'
    ol {
        li 'One is smaller than...'
        li 'Two is smaller than...'
        li 'Three!'
    }
}


sections.section {
    h2 'Clever Quotes'
    p {
        mkp.yield 'These guys come in two forms, inline:'
        q(cite: 'http://searchservervirtualization.techtarget.com/definition/Our-Favorite-Technology-Quotations', 'The nice thing about standards is that there are so many to choose from')
        br()
        mkp.yield 'and block:'
    }
    blockquote(cite: 'http://searchservervirtualization.techtarget.com/definition/Our-Favorite-Technology-Quotations', 'For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.')
}


sections.section {
    h2 'Pretty Code'
    pre {
        code(contenteditable: '') {
            mkp.yieldUnescaped new File("$scriptPath/example.js").text
        }
    }
    p {
        mkp.yield 'Courtesy of'
        a(href: 'http://softwaremaniacs.org/soft/highlight/en/description/', 'highlight.js')
    }
}


sections.section {
    h2 'Stellar Links'
    ul {
        li {
            a(href: 'https://github.com/hakimel/reveal.js', 'Source code on github')
        }
        li {
            a(href: 'http://hakim.se/projects/reveal-js', 'Read more on my site')
        }
        li {
            a(href: 'http://twitter.com/hakimel', 'Follow me on Twitter')
        }
    }
}


sections.section {
    h1 'THE END'
    h3(class: 'inverted', 'BY Hakim El Hattab / hakim.se')
}

// returns the presentation data
return data

Man kann sich somit auf das Wesendliche, auf den Inhalt konzentrieren.



Mittwoch, 8. Februar 2012

Groovy Beans To Json

Groovy bietet mit dem groovy.json.JsonBuilder und dem groovy.json.JsonBuilder sehr gute und einfache Möglichkeiten zur Json-Verarbeitung. Hier sollen einfache Groovy Beans (POGO's - Plain Old Groovy Objects) in Json Objekte und wieder zurück transformiert werden.

Dies ist ja dank Builder und Slurper kein Hexenwerk mehr. Das Beispiel-Bean ist folgendes (beliebig austauschbar):

class Book {
    String title
    String author
}



Bean To Json

String toJsonImpl(def bean) {

    JsonBuilder builder = new JsonBuilder()
    builder {
        // Hier folgen Schlüssel und Wert des Eintrags
        // dabei ist eine Variable als Schlüssel nicht möglich
        // deshalb "${ ... }"
        "${ bean.class.getName() }" bean
    }

    return builder.toString()
}

Nun ist folgender Aufruf möglich:

assert '{"Book":{"title":"Die Entw...","author":"G.S."}}'
    == JsonUtil.toJsonImpl(new Book(title: "Die Entw...", author: "G.S."))

Um die Klassen eindeutig identifizieren zu können, wird hier als Json-Key auf oberster Ebene der Name der Klasse benutzt. Und nun der Weg zurück...



Json To Bean

def toBeanImpl(String json) {

    JsonSlurper slurper = new JsonSlurper()
    def beanData = slurper.parseText(json)

    String beanClassName
    Map beanParameter
    beanData.eachWithIndex { key, value, index ->
        if (index == 0) {
            beanClassName = key
            beanParameter = value
        }
    }

    def beanInstance = Class.forName(beanClassName)
            .newInstance(beanParameter)
    return beanInstance
}

Damit ist der Aufruf möglich:

assert JsonUtil.toBeanImpl(
        '{"Book":{"title":"Die Entw...","author":"G.S."}}').properties
        == new Book(title: "Die Entw...", author: "G.S.").properties



Meta Programming

Jetzt können die beiden Funktionen noch an die Beispielklasse “angehängt“ werden. Dass kann zum Beispiel beim automatischen Anhängen an eine Reihe von Klassen interessant sein:

Book.metaClass.toJson = {
    return JsonUtil.toJsonImpl(delegate)
}

Book.metaClass.'static'.toBean = { String json ->
    return JsonUtil.toBeanImpl(json)
}

Die Methode “toBean“ wurde dabei statisch (an der Klasse) hinzugefügt.
Jetzt ist der Aufruf toJson() direkt an der Instanz und toBean(...) an der Klasse möglich:

Book bookBean = new Book(title: "Groovy in Action", author: "Dierk Konig")
String jsonBook = bookBean.toJson()
Book bookBean2 = Book.toBean(jsonBook)
assert bookBean.properties == bookBean2.properties


Na dann viel Spaß beim “Bauen“ und “Schlürfen“.