lynx-whiskers v0.1.0
Lynx Whiskers
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.
Installation
To install the whiskers command-line tool:
npm install -g lynx-whiskersTo install locally:
npm install lynx-whiskers --save-devTemplates
Hello, World!
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.
Hints
There are a few ways to describe a value with hints. The most basic is by inference:
Hello, World!Here a
texthint is inferred based on the fact that the value is a string.In general,
text,container,link,submit, andcontenthints 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" ]
  }
}Hint Inference
Text
A string, number, true, false, or null value implies a text hint:
trueThe result:
{
  "value": true,
  "spec": {
    "hints": [ "text" ]
  }
}Container
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" ]
      }
    ]
  }
}Link
A value with an href property implies a link hint:
href: http://example.comThe result:
{
  "href": "http://example.com",
  "spec": {
    "hints": [ "link" ]
  }
}Submit
A value with an action property implies a submit hint:
action: http://example.comThe result:
{
  "action": "http://example.com",
  "spec": {
    "hints": [ "submit" ]
  }
}Content
A value with an src or data property implies a content hint:
src: http://example.comThe result:
{
  "src": "http://example.com",
  "spec": {
    "hints": [ "content" ]
  }
}Shorthand for Common Hints
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: 50The 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
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" ]
      }
    ]
  }
}Data Properties
Describe a data property (a property with no lynx specification)
with the ~data whisker:
id~data: 1
label~label: A ThingThe 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+jsonhref~data: http://example.com
type~data: application/lynx+jsonAs are these submits:
action: http://example.com
method: POSTaction~data: http://example.com
method~data: POSTAs 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/jpegAs 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/"~data=false
If you need to specify a normally unspecified property, you can use ~data=false:
~image:
  src~data=false: "http://example.com",
  width: 100,
  height: 100Array Values
Array values can be described by whiskers as well:
- ~hint=color: Red
- Green
- BlueSince 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~inlineas described below.
The result:
{
  "value": [ "Red", "Green", "Blue" ],
  "spec": {
    "hints": [ "container" ],
    "children": {
      "hints": [ "color", "text" ]
    }
  }
}Input
Describe an input value with the ~input whisker:
~form:
  firstName~line~input: ""This can also be expressed as
~input=trueor~input=firstName.
The result:
{
  "firstName": "",
  "spec": {
    "hints": [ "form" ],
    "children": [
      {
        "name": "firstName",
        "hints": [ "line", "text" ],
        "input": true
      }
    ]
  }
}Visibility
Describe visibility with the ~visibility whisker:
~visibility=hidden: "hidden value"Or with shorthand:
~hidden: "hidden value"~concealed: "secret"Emphasis
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"
~emmaps to~emphasis=1and~strongmaps to~emphasis=2.
Options
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"Labels
Reference a label with the ~labeledBy whisker:
firstNameLabel~label: First Name
firstName~input~labeledBy=firstNameLabel: BobThe result:
{
  "firstNameLabel": "First Name",
  "firstName": "Bob",
  "spec": {
    "hints": [ "container" ],
    "children": [
      {
        "name": "firstNameLabel",
        "hints": [ "label", "text" ]
      },
      {
        "name": "firstName",
        "hints": [ "text" ],
        "input": true,
        "labeledBy": "firstNameLabel"
      }
    ]
  }
}Follow
Describe an auto-follow link with the ~follow whisker:
~follow:
  href: http://example.comThis is the same as
~follow=0. You can specify any number of milliseconds like this:~follow=1000.
Spec Longhand
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" ]
      }
    ]
  }
}Inline Specs
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.
Mixed Arrays
  - ~header~inline: Colors
  - ~hint=color: Red
  - Green
  - BlueThe result:
{
  "value": [
    {
      "value": "Colors",
      "spec": {
        "hints": [ "header", "label", "text" ]
      }
    },
    "Red",
    "Green",
    "Blue"
  ],
  "spec": {
    "hints": [ "container" ],
    "children": {
      "hints": [ "color", "text" ]
    }
  }
}Handlebars
Hello, {{{name}}}
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: BobThe result:
{
  "value": "Hello, Bob",
  "spec": {
    "hints": [ "text" ]
  }
}Literals
To generate a boolean or numeric literal value (without quotes), use the ~literal whisker.
The template:
~literal: "{{{isSomething}}}"The data:
isSomething: trueThe result:
{
  "value": true,
  "spec": {
    "hints": [ "text" ]
  }
}Sections and Inverted Sections
The whiskers Handlebars generator provides support for sections and inverted sections.
The template:
~#name: "Hello, {{{.}}}"
~^name: "Hello, World!"The data:
name: nullThe result:
{
  "value": "Hello, World!",
  "spec": {
    "hints": [ "text" ]
  }
}Implicit null Inverse
If no inverse is provided, a null inverse is generated by default.
The template:
~#name: "Hello, {{{.}}}"The data:
name: nullThe result:
{
  "value": null,
  "spec": {
    "hints": [ "text" ]
  }
}Array Sections
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: SmithThe 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" ]
        }
      ]
    }
  }
}Partial Templates
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: lastNameThe 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: InvalidThe 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: InvalidPartial Naming and Location
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.
Partials as Layouts
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, 2016The 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: WelcomeThe result:
banner: Lynx Whiskers
main~section: 
  header~header~label: Welcome
footer: Copyright © John Howes, 2016Realm
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.
9 years ago