Kotlin og Java
Fristil for Kotlin gir deg tre ting: byggefunksjonene fra fs, der de lovlige verdiene er enum-er, den samme sjekken som fristil sjekk og editoren bruker, som en vanlig funksjon i testene dine, og CSS-en og web-komponentene som en WebJar. Alt kjører i JVM-en eller serveres av appen. Ingen Node, ingen npm, ingen prosess å starte og ingenting å installere ved siden av.
dependencies { implementation("io.github.markusaugust:fristil:0.35.0")}Versjonen er den samme som npm-pakkens, så et versjonsnummer betyr det samme Fristil i Maven og på npm. Men ikke hver npm-versjon går til Maven Central. Finnes ikke versjonen over på Central, bruk den nyeste som gjør det. Pakken har én avhengighet, com.dylibso.chicory:runtime, og krever Java 17 eller nyere.
CSS-en og web-komponentene
Section titled “CSS-en og web-komponentene”Pakken er også en WebJar: fristil.css, stilarket for hver komponent og JavaScript-modulene for nettleseren ligger under META-INF/resources/webjars/fristil/0.35.0/, med de samme stiene som i npm-pakken. Spring Boot og Servlet-containere serverer den mappa uten oppsett. I Ktor er det én linje:
routing { staticResources("/webjars", "META-INF/resources/webjars")}FristilWebJar har stiene, med versjonen i, så malen aldri peker på en annen versjon enn den pakken har:
"""<link rel="stylesheet" href="${FristilWebJar.CSS}"><script type="module"> import { defineFs } from "${FristilWebJar.REGISTER}" defineFs()</script>"""En adresse fra komponentsidene beholder stien og bytter bare begynnelsen. Denne:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.35.0/src/components/css/button/button.css">blir ${FristilWebJar.ROOT}/src/components/css/button/button.css. Det som bare er for Node eller React, er ikke med.
Byggefunksjonene
Section titled “Byggefunksjonene”Fs har de samme byggefunksjonene og valgene som fs i TypeScript. Hver gir attributtene du legger på elementet:
import no.fristil.*
val lagre = Fs.button(variant = ButtonVariant.SECONDARY)
"""<button ${lagre.toHtml()}>Lagre utkast</button>"""// <button class="fs-button" data-variant="secondary">Lagre utkast</button>De lovlige verdiene er enum-er, så ButtonVariant.SECUNDARY er en kompileringsfeil, og IDE-en viser hva som finnes. Verdier som går igjen, har ett felles navn: Size, FieldState og RequiredMarker brukes av alle byggefunksjonene som har dem. Hver enum har value, teksten som havner i HTML-en, og of(tekst), som gir verdien eller null for en tekst utenfra.
Svaret er et Attributes, som er et vanlig Map<String, String>. Det kan gis til hvilken som helst malmotor. Uten malmotor skriver toHtml() det ut med verdiene escapet, og + legger to sett sammen.
Byggefunksjonene som gir flere sett, gir en dataklasse med ett felt per sett. Fs.field kobler ledetekst, kontroll, hjelpetekst og feilmelding:
val felt = Fs.field(id = "epost", required = RequiredMarker.TEXT, help = true, error = true, invalid = ugyldig)
"""<label ${felt.label.toHtml()}>E-postadresse</label><input ${(Fs.input(type = InputType.EMAIL, state = felt.state) + felt.control).toHtml()}><p ${(Fs.helpText() + felt.help).toHtml()}>Vi sender kvittering hit.</p><p ${(Fs.errorText() + felt.error).toHtml()}>Skriv en gyldig adresse.</p>"""Et flagg som hidden har den tomme teksten som verdi, og skrives som navnet alene.
At de svarer det samme som i TypeScript
Section titled “At de svarer det samme som i TypeScript”Byggefunksjonene genereres fra manifestet Fristil skriver fra TypeScript-koden, og de få der valgene virker sammen, som field og tabs, er skrevet for hånd. Alle testes mot over tusen byggetilfeller med svaret fra TypeScript. Svarer én byggefunksjon ett attributt annerledes, slippes ikke pakken.
kotlinx.html
Section titled “kotlinx.html”io.github.markusaugust:fristil-kotlinx-html har én utvidelse, fs(…), som legger attributtene fra byggefunksjonene på en tagg. Den er en egen pakke, så hovedpakken ikke tar med kotlinx.html til den som ikke bruker det.
import kotlinx.html.*import no.fristil.*import no.fristil.InputTypeimport no.fristil.html.fs
fun FlowContent.epost(ugyldig: Boolean) { val felt = Fs.field(id = "epost", required = RequiredMarker.TEXT, help = true, error = true, invalid = ugyldig) label { fs(felt.label); +"E-postadresse" } input { fs(Fs.input(type = InputType.EMAIL, state = felt.state), felt.control); name = "epost" } p { fs(Fs.helpText(), felt.help); +"Vi sender kvittering hit." } p { fs(Fs.errorText(), felt.error); +"Skriv en gyldig adresse." }}Flere sett legges på i rekkefølge, og et senere vinner, som {...a} {...b} i JSX. Klassene legges til dem taggen har, så div(classes = "min-egen") { fs(Fs.card()) } gir begge. InputType importeres for seg, siden kotlinx.html har en med samme navn.
Med Ktor
Section titled “Med Ktor”routing { staticResources("/webjars", "META-INF/resources/webjars") get("/soknad") { call.respondHtml { head { link(rel = "stylesheet", href = FristilWebJar.CSS) } body { form(action = "/soknad", method = FormMethod.post) { epost(ugyldig = false) button { fs(Fs.button()); +"Send" } } script(type = "module") { unsafe { +"import { defineFs } from \"${FristilWebJar.REGISTER}\"; defineFs()" } } } } }}Maler: JTE, Thymeleaf og de andre
Section titled “Maler: JTE, Thymeleaf og de andre”En mal kan ikke kalle Fs for hver verdi, og da er det sjekken som passer på. Skriv markupen som på komponentsidene, og sjekk det malen gir i en test. Med JTE:
@Testfun `søknaden er riktig markup`() { val ut = StringOutput() templateEngine.render("soknad.kte", mapOf("ugyldig" to true), ut) assertFristil(ut.toString())}Det samme gjelder Thymeleaf, Freemarker og Pebble: render malen til en tekst, og gi den til assertFristil. Malsyntaksen i selve malfila, som ${…}, th:… og @if, forstår sjekken også, så editoren kan si fra mens du skriver.
Sjekken i testene
Section titled “Sjekken i testene”assertFristil sjekker HTML-en serveren sender, og feiler med funnene som melding:
@Testfun `skjemaet er koblet riktig`() { assertFristil(app.render("/skjema"))}Fristil fant 2 funn:12:30: feil: for="epost" peker på id-en «epost», som ikke finnes på siden. … [id-finnes-ikke]18:9: advarsel: Klassen «fs-buton» finnes ikke i Fristil. Mente du fs-button? [ukjent-klasse]Den kaster en AssertionError, som JUnit, Kotest og TestNG alle forstår. Som standard sjekkes en hel side: at hver id det pekes på finnes, og at hvert felt er koblet. Er HTML-en en bit av en side, som en komponent, sjekker assertFristil(html, fragment = true) bare ordforrådet.
Det virker likt for alt som lager HTML på JVM-en: strenger, kotlinx.html, Thymeleaf, JTE, Ktor og Spring. Sjekken leser HTML-en som kommer ut, ikke koden som lager den.
Trenger du funnene selv, gir Fristil.diagnosePage(html) og Fristil.diagnoseMarkup(html) dem som en liste. Hvert funn har start og end, line og column, severity, rule, message, link og noen ganger en fix. Posisjonene er indekser i en Kotlin-String, så html.substring(funn.start, funn.end) gir nøyaktig teksten funnet gjelder, også med «æøå» og emoji foran.
Et funn du vil beholde, undertrykkes med en kommentar over taggen:
<!-- fristil-ignore-next ukjent-klasse --><div class="fs-eksperiment">…</div>Uten regelnavn gjelder kommentaren alle reglene for den neste taggen.
Den rendrede siden
Section titled “Den rendrede siden”Med Playwright for Java sjekkes siden slik nettleseren har rendret den, med
markupen web-komponentene la til og stilarkene den lastet. Pakken avhenger
ikke av Playwright: assertFristilRendered tar en funksjon som kjører et
skript på siden.
@Testfun `skjemaet er riktig slik det vises`() { page.navigate("http://localhost:8080/skjema") assertFristilRendered { script -> page.evaluate(script) }}Den feiler på det assertFristil feiler på, og i tillegg på en klasse ingen
stilark styler og en verdi uten regel. Har du stilarkene som tekst, gir
assertFristil(html, css = listOf(stilark)) og
Fristil.diagnosePage(html, css) den samme sjekken uten nettleser.
Har prosjektet overtatt komponenter, tar diagnoseMarkup og diagnosePage
manifestet som tekst i manifest, og sjekker da mot det samme som
Gradle-pluginen og editoren. Uten valget gjelder manifestet i pakken.
assertFristil og assertFristilRendered tar det ikke ennå.
val manifest = File("build/fristil/manifest.json").readText()assertEquals(emptyList(), Fristil.diagnosePage(html, manifest = manifest))Fila skrives av fristilManifest, som kjøres av assemble og ikke av test.
Skal testene lese den, må de vente på oppgaven:
tasks.test { dependsOn("fristilManifest") }.
Temaet
Section titled “Temaet”Fristil.buildTheme(oppskrift) bygger det samme fargetemaet som fristil tema, fra JSON-teksten i en oppskriftsfil, og Fristil.inspectTheme(css) kontrollerer et tema du har skrevet selv, som fristil sjekk-tema. Oppskriften har de ni fargene med norske eller engelske nøkler, typografi og form:
val tema = Fristil.buildTheme("""{"aksent": "#7c3aed", "fare": "#b3261e"}""")File("build/fristil/tema.css").writeText(tema.css)Svaret har light og dark, hvert --fs-color-*-token med verdien sin, violations og css. Løftene holder av konstruksjon, så violations er tom for gyldige farger, men sjekk den før du skriver fila: fristil tema nekter å skrive når den ikke er tom, mens buildTheme gir temaet tilbake med lista, som i TypeScript. En nøkkel som ikke finnes, og en oppskrift kjernen avviser, kaster IllegalArgumentException med forklaringen. inspectTheme gir problems med selektor og melding, og tallene blocks, declarations og promises, som sier hvor mye som faktisk ble lest. Oppskriften leses som fristil tema leser den, og begge svarer det kjernen svarer i TypeScript, testet mot den samme fasiten.
Gradle-pluginen
Section titled “Gradle-pluginen”Malfilene selv kan sjekkes som en del av bygget, uten en test som rendrer dem. Pluginen kjører den samme kommandolinja som fristil sjekk og fristil tema, i Gradle-prosessen:
plugins { id("io.github.markusaugust.fristil") version "0.35.0"}
fristil { templates.from(fileTree("src/main/jte")) themeRecipe = file("fristil.tema.json")}| Oppgave | Gjør |
|---|---|
fristilSjekk | sjekker templates som fristil sjekk, og feiler med funnene. Kjøres av check. |
fristilTema | bygger temaet av themeRecipe til themeOutput, standard build/fristil/tema.css |
fristilManifest | skriver manifestet prosjektet sjekkes mot til build/fristil/manifest.json når manifests har fragmenter, og sletter fila når det ikke har det. Kjøres av assemble. |
> Task :fristilSjekk FAILEDMarkupen stemmer ikke med Fristil:
src/main/jte/soknad.jte:14:18: advarsel: Klassen «fs-buton» finnes ikke i Fristil. Mente du fs-button?rendered = true sjekker filene som hele sider, med koblingen, som --rendret. Har du overtatt en komponent med fristil overta, legger du fragmentet i manifests, så markupen for kopien sjekkes også, og kopien kommer med i build/fristil/manifest.json:
fristil { templates.from(fileTree("src/main/jte")) manifests.from("src/main/fristil/button/fristil-manifest.json")}Pluginen ligger på Maven Central sammen med pakken, med samme versjon. Gradle finner den der uten en egen pluginManagement-blokk, siden Gradle Plugin Portal sender videre til Maven Central for det den ikke har selv. Oppgavene virker med konfigurasjonsbufferen, og står som oppdatert når verken malene eller fragmentene er endret.
Hvordan det virker
Section titled “Hvordan det virker”Sjekken er skrevet i Rust og bygget til én WebAssembly-modul. Den samme modulen kjører i Node, i VS Code og her. På JVM-en kjøres den av Chicory, en WebAssembly-runtime skrevet i ren Java. Modulen er gjort om til JVM-bytekode da pakken ble bygget, så den kjører med JIT-en og starter på en brøkdel av et sekund. Et kall låner en instans fra en pool og gir den tilbake etterpå, så tester som kjører samtidig, venter ikke på hverandre.
I IntelliJ
Section titled “I IntelliJ”Pluginen Fristil kjører den samme sjekken mens du skriver, også i HTML i en Kotlin-streng merket med @Language("HTML") eller // language=HTML. Se Editoren.