Lynx Whiskers is a template language for writing lynx documents. Whiskers templates are YAML documents that compile to Handlebars templates, so they're supported on every platform that supports Handlebars.
To install the whiskers
command-line tool:
npm install -g lynx-whiskers
To install locally:
npm install lynx-whiskers --save-dev
This is a simple text document.
Hello, World!
The result:
{
"value": "Hello, World!",
"spec": {
"hints": [ "text" ]
}
}
We know that this is a text value because it's a string, but that's about all we
know. To describe it, we use a whisker. Whiskers begin with a ~
and provide
a shorthand notation for describing any lynx value.
There are a few ways to describe a value with hints. The most basic is by inference:
Hello, World!
Here a
text
hint is inferred based on the fact that the value is a string.In general,
text
,container
,link
,submit
, andcontent
hints can be inferred based on value and need not be specified.
Hints may also be added with a shorthand whisker:
# With a single hint:
~hint=label: Hello, World!
# With a list of hints:
~hints=label,text: Hello, World!
# With a shorthand whisker name:
~label: Hello, World!
All three of these examples result in identical output:
{
"value": "Hello, World!",
"spec": {
"hints": [ "label", "text" ]
}
}
A string, number, true
, false
, or null
value implies a text
hint:
true
The result:
{
"value": true,
"spec": {
"hints": [ "text" ]
}
}
Unless another base hint is specified or inferred, an object or array value
implies a container
hint:
message: "Hello, World!"
The result:
{
"message": "Hello, World!",
"spec": {
"hints": [ "container" ],
"children": [
{
"name": "message",
"hints": [ "text" ]
}
]
}
}
A value with an href
property implies a link
hint:
href: http://example.com
The result:
{
"href": "http://example.com",
"spec": {
"hints": [ "link" ]
}
}
A value with an action
property implies a submit
hint:
action: http://example.com
The result:
{
"action": "http://example.com",
"spec": {
"hints": [ "submit" ]
}
}
A value with an src
or data
property implies a content
hint:
src: http://example.com
The result:
{
"src": "http://example.com",
"spec": {
"hints": [ "content" ]
}
}
Other common lynx hints may be specified with shorthand whisker names:
~image: # This is the equivalent of ~hint=image
src: "http://example.com/logo.svg",
width: 300,
height: 50
The result:
{
"src": "http://example.com/logo.svg",
"width": 300,
"height": 50,
"spec": {
"hints": [ "image", "content" ]
}
}
The following hints have shorthand names:
text
,container
,link
,submit
,content
,image
,form
,section
,complement
,marker
,card
,header
,label
, andline
.
Properties can be described by whiskers:
message:
~hint=greeting: Hello, World!
Or you can just append the whisker to the property name like this:
message~hint=greeting: Hello, World!
The result, in either case:
{
"message": "Hello, World!",
"spec": {
"hints": [ "container" ],
"children": [
{
"name": "message",
"hints": [ "greeting", "text" ]
}
]
}
}
Describe a data property (a property with no lynx specification)
with the ~data
whisker:
id~data: 1
label~label: A Thing
The result:
{
"id": 1,
"label": "A Thing",
"spec": {
"hints": [ "container"],
"children": [
{
"name": "label",
"hints": [ "label", "text" ]
}
]
}
}
Well-known properties defined by the lynx specification as data properties are treated as such by convention.
These links are equivalent:
href: http://example.com
type: application/lynx+json
href~data: http://example.com
type~data: application/lynx+json
As are these submits:
action: http://example.com
method: POST
action~data: http://example.com
method~data: POST
As are these images:
~image:
src: http://example.com
width: 100
height: 100
type: image/jpeg
~image:
src~data: http://example.com
width~data: 100
height~data: 100
type~data: image/jpeg
As are these markers:
~marker:
for: "http://example.com/place-to-be/"
~marker:
for~data: "http://example.com/place-to-be/"
As are these scoped sub-windows:
subWindow:
scope: "http://example.com/sub-process/"
subWindow:
scope~data: "http://example.com/sub-process/"
If you need to specify a normally unspecified property, you can use ~data=false:
~image:
src~data=false: "http://example.com",
width: 100,
height: 100
Array values can be described by whiskers as well:
- ~hint=color: Red
- Green
- Blue
Since all the values share the same spec, the hint need only be added to the first item. If all the values do not share the same
spec
, all but one spec needs to be marked~inline
as described below.
The result:
{
"value": [ "Red", "Green", "Blue" ],
"spec": {
"hints": [ "container" ],
"children": {
"hints": [ "color", "text" ]
}
}
}
Describe an input
value with the ~input
whisker:
~form:
firstName~line~input: ""
This can also be expressed as
~input=true
or~input=firstName
.
The result:
{
"firstName": "",
"spec": {
"hints": [ "form" ],
"children": [
{
"name": "firstName",
"hints": [ "line", "text" ],
"input": true
}
]
}
}
Describe visibility with the ~visibility
whisker:
~visibility=hidden: "hidden value"
Or with shorthand:
~hidden: "hidden value"
~concealed: "secret"
Describe emphasis with the ~emphasis
whisker:
~emphasis=3: "This is really Important!"
Or with the shorthand values ~em
and ~strong
:
~em: "This is slightly emphatic"
~strong: "This is very emphatic"
~em
maps to~emphasis=1
and~strong
maps to~emphasis=2
.
Reference input options with the ~options
and ~option
whiskers:
~form:
color~input~hidden~hint=color~options=colorOptions: "#FF0000"
colorOptions:
- ~option:
label: Red
code~hint=color: "#FF0000"
- label: Green
code: "#00FF00"
- label: Blue
code: "#0000FF"
Reference a label with the ~labeledBy
whisker:
firstNameLabel~label: First Name
firstName~input~labeledBy=firstNameLabel: Bob
The result:
{
"firstNameLabel": "First Name",
"firstName": "Bob",
"spec": {
"hints": [ "container" ],
"children": [
{
"name": "firstNameLabel",
"hints": [ "label", "text" ]
},
{
"name": "firstName",
"hints": [ "text" ],
"input": true,
"labeledBy": "firstNameLabel"
}
]
}
}
Describe an auto-follow link with the ~follow
whisker:
~follow:
href: http://example.com
This is the same as
~follow=0
. You can specify any number of milliseconds like this:~follow=1000
.
Specs can also be expressed longhand:
~form:
firstName:
value: ""
spec:
input: true
validation:
required:
invalid: firstNameRequiredMessage
firstNameRequiredMessage: "Please provide a first name."
Or you can use a mix of longhand and shorthand:
~form:
firstName~input~line:
value: ""
spec:
validation:
required:
invalid: firstNameRequiredMessage
firstNameRequiredMessage: "Please provide a first name."
In either case, the result:
{
"firstName": "",
"spec": {
"hints": [ "form" ],
"children": [
{
"name": "firstName",
"spec": {
"hints": [ "text" ],
"input": true,
"validation": {
"required": {
"invalid": "firstNameRequiredMessage"
}
}
}
}, {
"name": "firstNameRequiredMessage",
"hints": [ "text" ]
}
]
}
}
In many cases, a single lynx spec
can describe an entire document, can be referenced
by URL, and can even be cached indefinitely. But in some cases (when the spec itself is
dynamic, or when the items in an array are described by different specs),
a value must be marked with the ~inline
whisker to include its spec
inline.
- ~header~inline: Colors
- ~hint=color: Red
- Green
- Blue
The result:
{
"value": [
{
"value": "Colors",
"spec": {
"hints": [ "header", "label", "text" ]
}
},
"Red",
"Green",
"Blue"
],
"spec": {
"hints": [ "container" ],
"children": {
"hints": [ "color", "text" ]
}
}
}
Whiskers templates can use simple inline Handlebars expressions:
The template:
"Hello, {{{name}}}"
Note the use of a triple mustache in "{{{name}}}", since we are not generating HTML and have no need for HTML escaping.
The data:
name: Bob
The result:
{
"value": "Hello, Bob",
"spec": {
"hints": [ "text" ]
}
}
To generate a boolean or numeric literal value (without quotes), use the ~literal
whisker.
The template:
~literal: "{{{isSomething}}}"
The data:
isSomething: true
The result:
{
"value": true,
"spec": {
"hints": [ "text" ]
}
}
The whiskers Handlebars generator provides support for sections and inverted sections.
The template:
~#name: "Hello, {{{.}}}"
~^name: "Hello, World!"
The data:
name: null
The result:
{
"value": "Hello, World!",
"spec": {
"hints": [ "text" ]
}
}
If no inverse is provided, a null inverse is generated by default.
The template:
~#name: "Hello, {{{.}}}"
The data:
name: null
The result:
{
"value": null,
"spec": {
"hints": [ "text" ]
}
}
You can iterate over array data using a section and an ~array
whisker:
- ~header~inline: "People"
- ~#people~array:
firstName: "{{{firstName}}}"
lastName: "{{{lastName}}}"
The data:
people:
- firstName: Bob
lastName: Smith
- firstName: Jim
lastName: Smith
The result:
{
"value": [
{
"value": "People",
"spec": {
"hints": [ "header", "label", "text" ]
}
},
{
"firstName": "Bob",
"lastName": "Smith"
},
{
"firstName": "Jim",
"lastName": "Smith"
}
],
"spec": {
"hints": [ "container" ],
"children": {
"hints": [ "container" ],
"children": [
{
"name": "firstName",
"hints": [ "text" ]
},
{
"name": "lastName",
"hints": [ "text" ]
}
]
}
}
}
The following template references the input-group
partial:
~form~labeledBy=header:
header~header~label: Edit User Information
firstNameGroup~include=input-group:
label: First Name
name: firstName
middleNameGroup~include=input-group:
label: Middle Name
name: middleName
lastNameGroup~include=input-group:
label: Last Name
name: lastName
The label
and name
values are parameters passed to the partial. In the
partial template, these parameters are identified with a ~~
prefix and will
be replaced with the parameter values.
# ~partials/~input-group.whiskers
~#~~name~section~labeledBy=label:
label~header~label: ~~label
~~name:
value: "{{{value}}}"
spec:
validation:
required~#required:
invalid: requiredInvalidMessage
text~#constraints:
minLength~#minLength: "{{{minLength}}}"
maxLength~#maxLength: "{{{maxLength}}}"
pattern~#pattern: "{{{pattern}}}"
format~#format: "{{{format}}}"
invalid: textInvalidMessage
requiredInvalidMessage~#required: Required
textInvalidMessage~#constraints: Invalid
The result:
~form~labeledBy=header:
header~header~label: Edit User Information
firstNameGroup~#firstName~section~labeledBy=label:
label~header~label: First Name
firstName:
value: "{{{value}}}"
spec:
validation:
required~#required:
invalid: requiredInvalidMessage
text~#constraints:
minLength~#minLength: "{{{minLength}}}"
maxLength~#maxLength: "{{{maxLength}}}"
pattern~#pattern: "{{{pattern}}}"
format~#format: "{{{format}}}"
invalid: textInvalidMessage
requiredInvalidMessage~#required: Required
textInvalidMessage~#constraints: Invalid
middleNameGroup~#middleName~section~labeledBy=label:
label~header~label: Middle Name
middleName:
value: "{{{value}}}"
spec:
validation:
required~#required:
invalid: requiredInvalidMessage
text~#constraints:
minLength~#minLength: "{{{minLength}}}"
maxLength~#maxLength: "{{{maxLength}}}"
pattern~#pattern: "{{{pattern}}}"
format~#format: "{{{format}}}"
invalid: textInvalidMessage
requiredInvalidMessage~#required: Required
textInvalidMessage~#constraints: Invalid
lastNameGroup~#lastName~section~labeledBy=label:
label~header~label: Last Name
lastName:
value: "{{{value}}}"
spec:
validation:
required~#required:
invalid: requiredInvalidMessage
text~#constraints:
minLength~#minLength: "{{{minLength}}}"
maxLength~#maxLength: "{{{maxLength}}}"
pattern~#pattern: "{{{pattern}}}"
format~#format: "{{{format}}}"
invalid: textInvalidMessage
requiredInvalidMessage~#required: Required
textInvalidMessage~#constraints: Invalid
Partial files have a ~
prefix and live in a ~partials
folder anywhere in the
app file system. Referenced by name, they're found in the
nearest ~partials
folder (starting in the referencing template directory
and searching all ancestors until a match is found).
In the previous example, we would have searched through the template's ancestor tree looking for the file
~partials/~input-group.whiskers
.
Partial templates can also be used for document layout. The following partial includes three zones: a banner, a main content zone, and a footer.
# /~partials/~site-layout.whiskers
banner~zone: Lynx Whiskers
main~zone: null
footer~zone: Copyright © John Howes, 2016
The following template includes the site-layout
and replaces its main
zone, leaving the default banner and footer intact.
~include=site-layout:
main~section:
header~header~label: Welcome
The result:
banner: Lynx Whiskers
main~section:
header~header~label: Welcome
footer: Copyright © John Howes, 2016
Notes: There should be one folder per resource. Each state of the resource should be represented with a .js file in a ~states folder. The state document should optionally reference its own template. If not, index.whiskers. Each template should have a realm, starting with a configured base realm, and adding the path to the folder and the name of the template (if not index). If a relative realm is specified, it should be completed with the configured base realm. If an absolute realm is specified, it should be used instead. Scope and for should use the same rules with rootRealm.