hurml
v2.0.2
Published
Human Readable Markup Language Parser
Readme
HURML Parser
This module parses files that are formatted using the 'hurml' format, and provides simple tools for working with the resulting data structures. The 'hurml' format is a simple plain-text file format, which is somewhat resilient to human (user) error and is especially useful for configuration files that are meant to be changed by non-developer personnel.
File Format
'Hurml' is a line-based file format. Whitespace at the start and end of lines is ignored, and indentation is for humans only.
Comments start with a # character as the first non-whitespace symbol and span to the end of the line. A # preceded by whitespace later in a line also starts a comment, unless it appears inside a quoted string.
Scalars And Tokens
Property lines use name = value. Values may be strings, numbers, booleans, null, list literals, tokens, or multiline blocks.
title = "Example"
price = 123.45
mask = 0xff
enabled = true
flag = FOO_ENABLEResult:
{
"title": "Example",
"price": 123.45,
"mask": 255,
"enabled": true,
"flag": { "token": "FOO_ENABLE" }
}Empty values and the literal null parse as undefined. Since JSON has no undefined, these properties disappear when serialized with JSON.stringify.
empty =
nothing = null
set = trueResult after JSON serialization:
{
"set": true
}Bare values separated by commas or semicolons become token arrays.
flags = FOO_ENABLE, FOO_VERBOSEResult:
{
"flags": [
{ "token": "FOO_ENABLE" },
{ "token": "FOO_VERBOSE" }
]
}Strings may use single, double, straight, or curly quotes. Inside straight-quoted strings, the escapes \n, \t, \r, \", \', \\, and \uXXXX are recognized.
name = "A quoted value"
line = "one\ntwo"Result:
{
"name": "A quoted value",
"line": "one\ntwo"
}Triple-quoted strings may span multiple lines. Their content is preserved verbatim until a line containing only """.
description = """
A short paragraph spanning multiple lines.
It can contain "quotes", = signs, and # symbols freely.
"""Result:
{
"description": "A short paragraph spanning multiple lines.\nIt can contain \"quotes\", = signs, and # symbols freely."
}Objects
Property lines at the same level form an object.
name = "Service"
port = 8080
enabled = trueResult:
{
"name": "Service",
"port": 8080,
"enabled": true
}Objects can also be written as blocks.
server = (
host = "localhost"
port = 8080
)Result:
{
"server": {
"host": "localhost",
"port": 8080
}
}The same object can be written using a section header.
[server]
host = "localhost"
port = 8080Result:
{
"server": {
"host": "localhost",
"port": 8080
}
}Dotted keys and dotted headers create nested objects as needed.
server.http.port = 8080
[server.http]
host = "localhost"Result:
{
"server": {
"http": {
"port": 8080,
"host": "localhost"
}
}
}Arrays
List literals are useful for short arrays.
tags = [one, "two", 3, true]Result:
{
"tags": [
{ "token": "one" },
"two",
3,
true
]
}Longer arrays can be written with dash items.
tags = (
- one
- two
- three
)Result:
{
"tags": [
{ "token": "one" },
{ "token": "two" },
{ "token": "three" }
]
}The same array can be written using a section header.
[tags]
- one
- two
- threeResult:
{
"tags": [
{ "token": "one" },
{ "token": "two" },
{ "token": "three" }
]
}Inside an object, dash arrays must be assigned to an explicit property.
[[catalog]]
items = (
- id = 123
- id = 456
)Result:
{
"catalog": [
{
"items": [
{ "id": 123 },
{ "id": 456 }
]
}
]
}Nested list literals are supported.
matrix = [[1, 2], [3, 4]]Result:
{
"matrix": [
[1, 2],
[3, 4]
]
}Arrays Of Objects
Arrays of objects can be written with the dash-property shorthand. Each dash followed by a property assignment starts a new object; following property lines are assigned to that object until the next dash or section header.
[items]
- name = "first"
value = 1
- name = "second"
value = 2
config.deep = trueResult:
{
"items": [
{
"name": "first",
"value": 1
},
{
"name": "second",
"value": 2,
"config": {
"deep": true
}
}
]
}The same shape can be written using double-bracket array sections. Each repeated header appends a new object to the named array.
[[items]]
name = "first"
value = 1
[[items]]
name = "second"
value = 2
config.deep = trueResult:
{
"items": [
{
"name": "first",
"value": 1
},
{
"name": "second",
"value": 2,
"config": {
"deep": true
}
}
]
}Double-bracket header names may be dotted.
[[server.routes]]
path = "/"
method = GET
[[server.routes]]
path = "/health"
method = GETResult:
{
"server": {
"routes": [
{
"path": "/",
"method": { "token": "GET" }
},
{
"path": "/health",
"method": { "token": "GET" }
}
]
}
}Nested Structures
Blocks, sections, lists, and dotted keys can be composed.
[topLevel]
list = (
- (
name = "First item"
[config]
value = 1
)
- (
name = "Second item"
[config]
value = 2
)
)Result:
{
"topLevel": {
"list": [
{
"name": "First item",
"config": {
"value": 1
}
},
{
"name": "Second item",
"config": {
"value": 2
}
}
]
}
}Single values that are not property assignments or list items are used as the value of a name property, with the exception of tokens, which are assigned to properties that have the same name as the token string.
Working with data
The hurml.parse(...) function parses a 'hurml' formatted text, and returns the corresponding data structure. This consists of an object, array, or value (string, number, or boolean), with nested properties or array elements similar to a plain JavaScript data structure. Only tokens have a separate HurmlToken type, which can be converted to a string using the .toString() method.
With this result, the following functions can be used to check and get data:
hurml.find(data, ...path) finds one or more values using (nested, case-insensitive) property names. Any nested arrays are flattened, and do not need to be specified in the property path.
hurml.test(data, ...path) tests whether the given (nested, case-insensitive) property exists and its value is truthy.
hurml.match(stringOrRegExp, data, ...path) tests whether the given (nested, case-insensitive) property's value matches given string, number, boolean, or regular expression.
