Datendateien
Auf dieser Seite
Datendateien
Wiederverwendbare Variablen
eignen sich hervorragend für einen flachen, einmaligen Fakt (company,
supportEmail), sind aber unhandlich für alles mit echter Form - eine
Teamliste, eine Preistabelle, eine Feature-Matrix. Datendateien
schließen genau diese Lücke: Lege eine docs/data/*.yaml-/.yml-/
.json-Datei in deinem Projekt ab, und ihr gesamter Inhalt - in
beliebiger Form, ein Objekt oder ein Array - wird von jeder Seite aus als
data.<file> erreichbar, mit derselben {{ }}-Syntax, die
variables/page bereits verwenden. Brauchst du die Daten berechnet
statt nur aus einer statischen Datei geparst - einen Rabatt, der zur
Lesezeit angewendet wird, einen Wert, der nicht in drei duplizierten
Dateien leben sollte? Lege stattdessen eine docs/data/*.bx-Klasse
ab - siehe Datenklassen weiter unten.
Die Konvention
Füge einen docs/data/-Ordner hinzu. Der Basisname jeder Datei (ohne
Dateiendung) wird zu einem Top-Level-Schlüssel unter data:
docs/
├── index.md
└── data/
├── team.yaml
└── pricing.json
- name: Luis Majano
role: CEO
- name: Jon Clausen
role: CTO
{
"free": { "price": 0, "seats": 3 },
"pro": { "price": 29, "seats": 20 }
}
data.team ist jetzt genau dieses Array, data.pricing.pro.price jene
verschachtelte Zahl - die geparste Wurzel einer Datei wird exakt so
verwendet, wie sie geparst wurde, Objekt oder Array gleichermaßen, ohne
dass sie einer festen Form entsprechen müsste. Gibt es überhaupt keinen
docs/data/-Ordner, bedeutet das schlicht kein data - dieselbe
Opt-in-durch-Vorhandensein-Form, die
docs/functions.bxs/
docs/blog/authors.yml bereits verwenden.
Referenziere davon alles in gewöhnlichem Markdown, per Punktpfad:
The Pro plan is **${{ data.pricing.pro.price }}/mo** for up to
{{ data.pricing.pro.seats }} seats.
baut zu:
<p>The Pro plan is <strong>$29/mo</strong> for up to 20 seats.</p>
Teilen sich mehrere Dateien über verschiedene Endungen hinweg denselben
Basisnamen (sowohl products.yaml als auch products.json vorhanden),
gewinnt zuerst .bx (siehe Datenklassen), dann .yaml,
dann .yml, dann .json - wähle in der Praxis lieber ein Format pro
Basisnamen, statt dich auf diese Reihenfolge zu verlassen.
Datenklassen
Eine .yaml-/.json-Datei ist statisch - einmal geparst, exakt so
verwendet, wie geschrieben. Für Daten, die berechnet werden müssen (ein
rabattierter Preis, ein aus mehreren Quellen zusammengesetzter Wert,
irgendetwas mit echter Logik dahinter), lege stattdessen eine echte
BoxLang-Klasse ab - docs/data/Pricing.bx (PascalCase, dieselbe
Klassendatei-Konvention, die dieses Modul überall sonst auch verwendet)
wird zu data.pricing - dieselbe kleingeschriebene data.*-Schlüsselform
wie jede andere Datei, nur mit dem ersten Buchstaben des Klassen-
Basisnamens kleingeschrieben:
class {
struct function getData() {
return { "free": { "price": 0 }, "pro": { "price": 29 } }
}
numeric function getDiscountedPrice( required string plan, required numeric pct ) {
var base = getData()[ arguments.plan ].price
return base - ( base * arguments.pct )
}
}
getData() ist erforderlich - jede Datenklasse braucht eine (selbst
eine triviale, die {} zurückgibt), da sie automatisch aufgerufen wird,
wann immer data.pricing bloß verwendet wird, genau wie eine geparste
YAML-/JSON-Wurzel:
The Pro plan is **${{ data.pricing.pro.price }}/mo**.
::: for plan, info in data.pricing
- {{ plan }}: ${{ info.price }}
:::
Jede andere öffentliche Methode ist ebenfalls aufrufbar, direkt aus
{{ }}, mit genau derselben Argumentsyntax, die ein Aufruf einer
magischen Funktion
bereits verwendet (Literale oder Punktpfad-Variablenreferenzen,
kommagetrennt):
Discounted for early adopters: **${{ data.pricing.getDiscountedPrice("pro", 0.2) }}/mo**
baut zu:
Discounted for early adopters: <strong>$23.2/mo</strong>
Das funktioniert auch aus ::: for/::: if, derselben <dotted.path>-
Grammatik, die diese Direktiven bereits auflösen:
::: if data.pricing.getDiscountedPrice("pro", 0.2)
Discounts are active.
:::
Ein Theme-Override oder eine magische Funktion, die bereits über das
volle BoxLang verfügen, bekommen die lebende Instanz selbst bar als
data.pricing gebunden - rufe getData() oder jede andere Methode dort
direkt auf, keine automatische Aufruf-Magie nötig (siehe
Daten verwenden weiter unten).
Nur öffentliche Methoden sind auf diese Weise erreichbar - eine
private function in derselben Klasse bleibt ein echtes
Implementierungsdetail, unerreichbar von {{ }} aus, genau wie ein nicht
mit $ präfixierter Helfer in functions.bxs direkt unerreichbar ist
(obwohl er, wie dort auch, weiterhin aus einer anderen Methode derselben
Datei aufrufbar ist).
Das lockert die Vertrauensgrenze weiter unten
nicht - eine .bx-Datei unter docs/data/ ist Code, den der
Projekt-Eigentümer schreibt, dieselbe Vertrauensstufe, die
docs/functions.bxs bereits hat, niemals etwas, das das Markdown eines
reinen Docs-Mitwirkenden erreichen kann.
Eine schmale Einschränkung, real, aber in der Praxis selten: Das Laden
einer Datenklasse braucht ihren eigenen aufgelösten Pfad als gültigen
BoxLang-Klassennamen (keine Bindestriche oder Leerzeichen irgendwo darin).
bxSites von innerhalb des Projekts selbst auszuführen - der weitaus
häufigste Fall - funktioniert immer, da nichts am eigenen Pfad des
Projekts (der beliebig viele Bindestriche haben darf, z. B. my-project/)
jemals so ausgeschrieben werden muss. Es wird nur mit einem expliziten
--projectRoot zu einer echten Einschränkung, das auf ein Projekt
außerhalb des aktuellen Verzeichnisses zeigt, dessen eigener Pfad (oder
der eines übergeordneten Verzeichnisses) einen Bindestrich oder ein
Leerzeichen enthält - siehe
BxSites.UnsupportedDataClassPath für den genauen Fehler, der
stattdessen wirft, statt kryptisch zu scheitern.
Daten verwenden
Ein skalarer {{ data.x.y }}-Verweis funktioniert überall dort, wo
{{ }} bereits funktioniert, aber echter Inhalt - ein Team-Raster, eine
Preistabelle - bedeutet meist ein Durchlaufen von data.*. Dafür gibt es
drei Wege, je nachdem, wo die Schleife hingehört:
In einem Theme-Override
Sobald ein Projekt ein theme/-Override hat (siehe
Themes), wird data auf dieselbe
Weise bar in layout.bxm/page.bxm eingebunden, wie es page/
siteConfig bereits sind - kein {{ }}, nur echtes BoxLang:
<ul class="footer-sponsors">
<bx:loop array="#data.sponsors#" index="sponsor">
<li>#encodeForHTML( sponsor )#</li>
</bx:loop>
</ul>
Das ist der natürliche Ort für Daten, die auf jede Seite gehören (eine
Sponsorenliste im Footer, ein websiteweites Nav-Badge), statt auf den
Inhalt einer bestimmten Seite. Wäre sponsors eine
Datenklasse statt einer .yaml-/.json-Datei, ist
data.sponsors hier die lebende Instanz selbst (echtes BoxLang, keine
nur-in-{{ }}-Bequemlichkeit mit automatischem Aufruf) - durchlaufe
stattdessen explizit data.sponsors.getData().
Aus einer magischen Funktion
Eine magische Funktion
kann data ebenfalls bar lesen (sie ist eine der gleichen
"unterstützenden Variablen", die page/siteConfig/etc. bereits sind)
und darüber mit echtem BoxLang schleifen/verzweigen, wobei sie ein
Markdown-/HTML-Fragment zurückgibt:
function $team() {
var html = ""
for ( item, idx in data.team ) {
html &= "- **" & encodeForHTML( item.name ) & "** - " & encodeForHTML( item.role ) & char( 10 )
}
return html
}
## Our team
{{ $team() }}
Das wird serverseitig gerendert, zur Build-Zeit - für einen Such-Crawler sichtbar, ganz ohne JavaScript, anders als das Alpine-Rezept unten.
Direkt in Markdown, mit ::: for/::: if
Für eine Schleife oder eine einfache Truthy-Prüfung, die überhaupt keine
magische Funktion braucht, funktionieren
::: for/::: if
direkt aus Markdown heraus:
::: for member, idx in data.team
{{ idx }}. **{{ member.name }}** - {{ member.role }}
:::
::: for <item>, <index> in <dotted.path> bindet <item>/<index>
mithilfe von BoxLangs eigener nativer Zwei-Variablen-for-Schleifen-
Semantik an das, was auch immer <dotted.path> auflöst - Element +
1-basierter Index für ein Array, oder Schlüssel + Wert für ein Struct,
in beiden Fällen die identische Syntax (keine Array-vs-Struct-
Verzweigung, die du selbst schreiben müsstest):
::: for name, enabled in data.flags
- {{ name }}: {{ enabled }}
:::
::: if <dotted.path> rendert seinen eigenen Inhalt nur dann, wenn der
aufgelöste Wert truthy ist (ein leeres Array/Struct/String, 0 und
false gelten allesamt als falsy):
::: if data.flags.betaBanner
Beta features are enabled on this build.
:::
Verkette ::: elseif <dotted.path> (beliebig oft) und ein abschließendes,
bloßes ::: else direkt nach einem ::: if für echte
if/elseif/else-Semantik - die erste truthy Bedingung gewinnt,
::: else fängt auf, was übrig bleibt, und die Bedingung eines späteren
Zweigs wird erst dann überhaupt aufgelöst, wenn dieser Zweig selbst an
der Reihe ist. Ein einziges abschließendes ::: schließt die gesamte
Kette - ::: elseif/::: else markieren selbst, wo der vorherige Zweig
endet, es ist also kein ::: vor jedem von ihnen nötig (auch wenn es
weiterhin funktioniert, falls du es lieber so schreiben möchtest):
::: if data.flags.darkModeDefault
Dark mode is on by default.
::: elseif data.flags.betaBanner
Beta features are enabled, though dark mode isn't on by default.
::: else
Nothing special about this build.
:::
Beide Inhalte können gewöhnliches Markdown und sogar weitere
Content-Blöcke enthalten, einschließlich eines verschachtelten
::: for/::: if. Bewusst schmale Grammatik, passend zu {{ }} selbst
- nur ein Punktpfad, keine Vergleichsoperatoren (
==,&&, ...) in dieser ersten Version. Ein echter Vergleichsbedarf führt stattdessen zu einer magischen Funktion (oben), die bereits über das volle BoxLang verfügt.
In Alpine, clientseitig (x-data)
Interaktivität mit Alpine.js behandelt bereits, wie
man rohes x-data/x-for-HTML in Markdown fallen lässt; es stattdessen
mit data.* statt mit einem handgetippten JS-Array zu speisen, braucht
nur, data.* in einen sicheren HTML-Attributwert zu verwandeln.
jsonSerialize() allein reicht nicht aus - das Ergebnis braucht
weiterhin eine HTML-Attribut-Kodierung, um sicher innerhalb eines mit
"..." gequoteten Attributs zu landen (dasselbe Zwei-Schritt-Rezept, das
ColdBoxs eigener attribute()/forAttribute()-Helfer verwendet) - also
definiere einmalig einen einzeiligen Helfer, in deiner eigenen
functions.bxs:
function $jsonAttr( required any value ) {
return encodeForHtmlAttribute( jsonSerialize( arguments.value ) )
}
encodeForHtmlAttribute() stammt aus bx-esapi, bereits eine Abhängigkeit
jedes bx-sites-Projekts - keine neue Abhängigkeit, nur dieses eine
Rezept. Dann, in Markdown:
<div x-data="{ team: {{ $jsonAttr(data.team) }} }">
<template x-for="member in team" :key="member.name">
<li x-text="member.name + ' - ' + member.role"></li>
</template>
</div>
Einfache doppelte Anführungszeichen funktionieren rund um x-data
problemlos - encodeForHtmlAttribute() behandelt den Konflikt bereits,
kein Workaround mit einfachen Anführungszeichen nötig. Das ist der eine
Weg, der ausschließlich clientseitig rendert (nichts für eine Leserin
mit deaktiviertem JavaScript oder einen Such-Crawler) - greife
stattdessen zu einer magischen Funktion oder ::: for, wenn der Inhalt
auch ohne JavaScript sichtbar sein soll.
Warum Datendateien, nicht BoxLang-Templates in Markdown?
Eine verwandte, größere Frage kam beim Entwerfen davon auf: Warum nicht
Markdown selbst zu einem echten BoxLang-Template machen (Schleifen,
Bedingungen, beliebige Logik), statt ein schmales ::: for/::: if
hinzuzufügen und sich für alles Weitere auf magische Funktionen zu
verlassen? Zwei Gründe:
- Vertrauensgrenze.
docs/**.mdist das eine Artefakt, das routinemäßig von vielen/externen/weniger vertrauenswürdigen Mitwirkenden bearbeitet wird (ein Docs-PR).docs/functions.bxsist das eine Artefakt, das der Projekt-Eigentümer explizit verfasst. Jede.md-Datei als echtes BoxLang-Template zu kompilieren, würde diese Grenze einreißen - jeder Mitwirkende, der einen Docs-PR öffnen kann, würde beliebige BoxLang-Ausführung gewinnen (Datei-I/O, Umgebungszugriff), statt nur reinen Markdown-Text. - Fehlerverhalten. Ein unaufgelöstes
{{ }}bleibt heute als reiner Text stehen - ein Tippfehler bricht einen Build nie. Ein BoxLang-Template-Kompilierfehler ist ein harter Fehler.::: for/::: ifbehalten dieselbe nachsichtige Form (ein nicht auflösbarer Pfad wirft einen klaren, tippfehlererkennenden Fehler - siehe Fehler - statt still falsch zu kompilieren).
Datendateien schließen die eigentliche Lücke (strukturierter Inhalt, und
Schleifen/Bedingungen darüber), ohne einen der beiden Kompromisse:
Markdown selbst bleibt inert-bis-{{ }}-ersetzt, und functions.bxs/eine
docs/data/*.bx-Klasse bleiben die explizit vertrauenswürdigen
Fluchttüren in echte BoxLang-Logik - beides vom Projekt-Eigentümer
verfasster Code, niemals etwas, das der eigene PR eines
Markdown-Mitwirkenden hinzufügen kann.
Geltungsbereich
docs/data/gilt projektweit, einmal geladen - derselbe Einmal-Lade-Geltungsbereich, denfunctions.bxsbereits hat. Jeder Versions-/Locale-Baum sieht dasselbedata; es gibt in dieser ersten Version kein Überschreiben oder Zusammenführen pro Version oder pro Locale. Duplizieredocs/data/nicht nachdocs/versions/<name>/oderdocs/i18n/<code>/- es wird von dort nicht gelesen.- Nur ein flaches Verzeichnis - keine Unterordner-Rekursion in
docs/data/in dieser ersten Version, dieselbe "genau eine Datei"-Form, diedocs/blog/authors.ymlbereits hat. dataist ein reservierter{{ }}-Name, genau wie espagebereits ist (siehe Reservierte Namen) - einbxsites.yaml-Eintragvariables.data, sollte ein Projekt irgendwie einen deklariert haben, wird vom eigenen Struct ausdocs/data/überschattet, statt zu gewinnen.docs/functions.bxskann ebenfalls keine Funktion namensdatadeklarieren, aus demselben Grund.
Fehler
BxSites.InvalidDataFile- einedocs/data/*.yaml-/.yml-/.json-Datei konnte nicht geparst werden (ein YAML-/JSON-Syntaxfehler), oder einedocs/data/*.bx-Klasse konnte nicht kompiliert/instanziiert werden, benennt die betroffene Datei.BxSites.MissingDataMethod- einedocs/data/*.bx-Klasse hat keine öffentlichegetData()-Methode.BxSites.UnknownDataMethod-{{ data.x.someMethod(...) }}benennt eine Methode, die auf dieser Datenklassen-Instanz nicht existiert (oder nicht öffentlich ist).BxSites.NotCallable-{{ data.x.someMethod(...) }}, wobeidata.xüberhaupt keine Datenklassen-Instanz ist (ein.yaml-/.json-basierter Schlüssel hat keine Methoden, die aufgerufen werden könnten).BxSites.UnsupportedDataClassPath- einedocs/data/*.bx-Klasse konnte nicht geladen werden, weil ihr aufgelöster Pfad ein Zeichen enthält, das in einem BoxLang-Klassennamen nicht gültig ist (ein Bindestrich oder Leerzeichen in einem übergeordneten Verzeichnisnamen) - siehe die eigene Anmerkung dazu in Datenklassen.BxSites.UnknownVariable- ein{{ data.x.y }}(oder ein::: for/::: if-Pfad) lässt sich nicht gegen das auflösen, was tatsächlich indocs/data/liegt.BxSites.InvalidForTarget- der eigene Pfad eines::: forlöste sich zu etwas auf, das weder ein Array noch ein Struct ist (kann nicht durchlaufen werden).