npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

weblang

v0.17.0

Published

Weblang programming language

Readme

Weblang

Weblang is a small programming language built around variables, pipe functions, and pipe chains. Programs describe the work; host-provided functions implement it. YAML is a convenient source notation. Program data can be stored as JSON and passed between hosts that provide the same pipe functions.

Use the recommended .w extension, such as hello.w. Select YAML as the editor language for syntax highlighting. In VS Code, add a file association to user or workspace settings:

{
  "files.associations": {
    "*.w": "yaml"
  }
}

Install globally to run a source file from the command line:

npm i -g weblang
weblang app.w

For example, app.w:

=name: Vidar
=message: Hello $name

This prints Hello Vidar. The CLI prints the program's return value (the last assigned result by default). Errors print to stderr and exit with status 1. The CLI does not register pipe functions; programs using custom pipes need the JavaScript host API below.

For programmatic use:

npm i weblang
var weblang = require('weblang')
var pipes = {
  up: {
    handler: function (ctx, value) {
      return value.toUpperCase()
    }
  },
  slice: {
    handler: function (ctx, value, start, end) {
      return value.slice(start, end)
    }
  },
  or: {
    handler: function (ctx, value, fallback) {
      return typeof value === 'undefined' ? fallback : value
    }
  }
}

async function main() {
  var code = '=message: Hello $name |> up'
  var ast = weblang.compile(code, { file: 'hello.w' })
  var result = await weblang.run(ast, { vars: { name: 'Vidar' }, pipes })
  console.log(result.state.vars.message) // HELLO VIDAR
  console.log(result.state.return) // HELLO VIDAR
}

main().catch(console.error)

The host supplies source text and registers every pipe by name. Examples use these handlers and the additional handlers below. JavaScript snippets using await belong inside an async function such as main().

=name: value assigns a variable. Assignments run in order and may replace previous values. Dots address nested properties; numeric segments index arrays.

=name: Vidar
=message: Hello $name
=name: Storm                 # message is still "Hello Vidar"
=user.name: $name
=people.0.name: Vidar
=first: $people.0.name        # "Vidar"
=a,b: one                    # assigns only a
=,discard: hello |> up       # runs the pipe without storing its result

Start assignments in column one, with no spaces inside =name:. Use simple names or camelCase: names containing _ currently get skipped. Underscores work in object keys, host variables, and pipe names. Write $ when reading a variable, not when naming its assignment.

metadata: ignored            # ordinary root keys do not execute
=data:
  =inner: ordinary data      # nested keys do not create assignments

Root lists do not execute assignments. Forms such as = name: value, =a b: value, and =a@call: value are invalid. Blank lines and document boundaries do not introduce scopes.

Choose the notation that reads best; these styles express ordinary program data. Indentation groups nested values. Comments start with # outside quotes.

# Block and inline forms produce the same value
=block:
  name: Vidar
  roles:
    - admin
    - author
=inline: { name: Vidar, roles: [admin, author] }

=people:
  - name: Vidar
    active: true
  - { name: Storm, active: false }

=values: [42, -2, 1.5, true, false, null]
=empty: { text: '', list: [], object: {}, value: null }
=unset:                      # an omitted value is null
=alsoNull: ~

Quotes preserve text that could otherwise be interpreted as a number, date, comment, or mapping. They do not disable Weblang variable expansion.

=id: '007'
=date: '2026-09-03'
=single: 'It''s text: # included'
=double: "Line one\nLine two\tTabbed"
=explicitText: !!str 42      # "42"

Multiline styles control spaces and newlines. | preserves line breaks; > folds ordinary adjacent lines into spaces. A blank line in folded text creates a paragraph break. Append - to remove final newlines or + to keep them all; without either, one final newline remains.

=literal: |                 # "one\ntwo\n"
  one
  two
=stripped: |-               # "one\ntwo"
  one
  two
=kept: |+                   # "one\ntwo\n\n"
  one
  two

=folded: >                  # "one two\n"
  one
  two
=foldedKeep: >+             # "one two\n\n"
  one
  two

=paragraphs: >-             # "one two\nthree"
  one
  two

  three
=indented: |2-              # "  indented": two spaces belong to the value
    indented

Anchors (&name) and aliases (*name) reuse source data. $name instead reads an executed assignment. << is an ordinary key in the current parser, not a merge operator.

=preset: &preset { theme: dark, retries: 3 }
=copy: *preset
=runtimeCopy: $preset

Document start (---) and end (...) markers can separate source sections.

---
=name: Vidar
...
---
=greeting: Hello $name
...

A whole $name expression reads a value; $user.name reads a nested path. Strings in arrays and objects expand recursively, including their pipes.

=user: { name: Vidar, first_name: Vidar }
=copy: $user                 # same object, by reference
=names: [$user.name, Storm]
=record:
  greeting: Hello $user.first_name!
  loud: $user.name |> up
=count: 42                   # number
=countText: $count           # string "42"
=active: false
=activeCopy: $active         # boolean false
=nothing: null
=nullCopy: $nothing          # null
=missing: $unknown           # undefined: assignment is skipped
=fallback: $unknown |> or guest
=inline: value $count/$active/$unknown  # "value 42/false/"
=x: 1
=y: 2
=adjacent: $x$y              # "12"

Whole lookups preserve objects, arrays, booleans, and null; numbers become strings. Retrieved values are not recursively evaluated as source. An undefined result leaves the target and last assigned result unchanged.

Embedded variables use letters, digits, underscores, and dotted segments. Missing values become empty text; objects stringify as [object Object] and arrays as comma-joined text. Use a pipe to format structured values.

{{ expression }} inserts a string or number result into text. Other result types contribute empty text. Put pipes inside the braces; to pipe the entire result, use a subsequent assignment.

=name: vidar
=count: 2
=greeting: "Hello {{ $name |> up }}!"
=summary: '{{ $name }} has {{ $count }} messages'
=loud: $summary |> up
=flag: false
=empty: '{{ $flag }}'        # empty string
=punctuation: '{{ $name }}!' # braces delimit the variable from punctuation

Use \$ and \|> for literal markers in ordinary strings. Single quotes keep backslashes; double quotes require them to be doubled. Plain string whitespace is preserved; a pipe expression trims its initial text value.

=literal: '\$name and \|> stay literal'
=doubleQuoted: "\\$name and \\|> stay literal"
=spaced: '  hello  '         # keeps both surrounding spaces
=trimmed: '  hello  |> up'   # "HELLO"
=plain: hello|>up            # literal: the operator needs spaces
=old: hello | up             # also literal

Keys can use variables, interpolation, and pipes. Only strings and numbers become key text; other results become ''. Later equal keys replace earlier ones. A key whose original value is an object stays literal.

=field: name
=record:
  $field: Vidar
  '{{ $field |> up }}': Storm
  '$field |> up |> slice 0, 1': Bobby
  'user_{{ $field }}': [$field]
  '\$literal': kept
  $missing: empty key
=nested:
  $field:
    value: $field

record has keys name, NAME, N, user_name, $literal, and ''. nested is { $field: { value: 'name' } }.

value |> name passes a value through a registered function. Chains run left to right, awaiting each result. Use a space on each side of |>.

=short: vidar |> up |> slice 0, 3  # "VID"
=multiline: >-
  vidar
  |> up
  |> slice 0, 3
=names: [Vidar, Storm, Bobby]
=firstTwo: $names |> slice 0, 2
=numberText: 42 |> slice 0, 1            # input is "42", result is "4"

Handlers receive (ctx, value, ...args) and return the next value. Register objects with a handler property; synchronous and async functions both work.

pipes.wrap = {
  handler: function (ctx, value, options) {
    return options.prefix + value + options.suffix
  }
}
pipes.args = {
  handler: function (ctx, value, ...args) {
    return args
  }
}

Commas separate arguments. Space-separated key=value pairs form one object; commas between pairs create separate objects. Quotes preserve spaces and commas.

=user: { name: Vidar }
=suffix: '!'
=wrapped: Vidar |> wrap prefix="Hello " suffix=$suffix
=positional: x |> args 0, 3
=named: x |> args user=$user enabled=true
=separate: x |> args a=1, b=2
=mixed: x |> args 2, enabled=true, $user
=quoted: x |> args "a, b", 'hello world', "$user.name"
=typed: x |> args -2, 1.5, true, false, "2", null

wrapped is Hello Vidar!. separate is [{ a: 1 }, { b: 2 }]. typed is [-2, 1.5, true, false, '2', 'null']: argument literals recognize numbers and booleans; other text stays text. Quoted arguments still expand variables. Handlers may return any value type.

The argument parser is deliberately small:

| Form | Behavior | | --- | --- | | args enabled=true 2 | Error: separate named and positional arguments with a comma | | args prefix=hello world | Error: quote a named value containing spaces | | args "unfinished | Error: unterminated quote | | args "a=b" | Treated as named syntax; quotes do not protect = | | args "a |> b" | Split as a chain; quotes do not protect |> |

Pass complex argument values through variables. More sample handlers are in spec/lib/pipes.

compile(code, { file }) synchronously returns an AST. file labels source metadata; the host reads files. Empty or non-string input produces []. await run(ast, options) resolves to { state }.

| Option or result | Meaning | | --- | --- | | options.vars | Initial variable object, used and mutated directly; defaults to {} | | options.pipes | Registry of pipe handler objects | | Other options | Available to handlers through ctx.opt | | state.vars | Variables after execution | | state.last | Last defined result stored by an assignment | | state.return | Explicit return value, otherwise state.last; undefined for an empty run |

State and completion

Handlers can share data and select a return value. Setting state.return to anything except undefined stops after the current assignment finishes, including its remaining pipes. false and null are valid return values.

pipes.save = {
  handler: function (ctx, value, path) {
    ctx.set(ctx.state.vars, path, value)
    return ctx.get(ctx.state.vars, path)
  }
}
pipes.finish = {
  handler: function (ctx, value) {
    ctx.state.return = value
    return value
  }
}
=first: hello
=,discard: $first |> save snapshot
=first: $unknown                       # keeps "hello"
=result: $first |> finish |> up         # stores "HELLO", returns "hello"
=skipped: never executed

Returning undefined still feeds the next pipe; only the final undefined result skips assignment. Returning { ok: false, error } is ordinary data. Thrown errors and rejected promises reject run() immediately.

Handler context

| Property | Use | | --- | --- | | ctx.state, ctx.opt | Shared state and original run options | | ctx.name | Current pipe name | | ctx.pipe | Parsed { pipe, args } before expansion | | ctx.args | Expanded arguments, also passed after value | | ctx.get(object, path) | Read a nested path | | ctx.set(object, path, value) | Write a nested path | | ctx.expand(state, value, options, expandOptions) | Asynchronously expand another value |

An async handler can explicitly expand retrieved templates. A fourth argument of { pipe: false } expands variables without interpreting pipe chains.

pipes.render = {
  handler: async function (ctx, value) {
    return ctx.expand(ctx.state, value, ctx.opt, { pipe: false })
  }
}
=name: Vidar
=template: 'Hello \$name \|> up'
=rendered: $template |> render          # "Hello Vidar |> up"

Errors

Compilation checks source structure; execution checks pipe arguments and registrations. Assignment syntax errors include one-based line and column, source, and file (default <memory>). Invalid source data can also throw parser errors.

try {
  weblang.compile('= name: Vidar', { file: 'hello.w' })
} catch (error) {
  console.log(error.file, error.line, error.column) // hello.w 1 1
}
try {
  await weblang.run(weblang.compile('=x: hello |> missing'), { pipes })
} catch (error) {
  console.log(error.message) // missing pipe: missing
}

Store executable program data as an ordered array. The current runner needs only operation and value from each compiled node. This preserves repeated assignments without the circular navigation links in the full AST.

var source = '=name: Vidar\n=name: Storm\n=message: Hello $name |> up'
var program = weblang.compile(source).map(function (node) {
  return { operation: node.operation, value: node.value }
})
var stored = JSON.stringify(program)
var result = await weblang.run(JSON.parse(stored), { pipes })
console.log(result.state.return) // HELLO STORM

The stored program is ordinary JSON:

[
  { "operation": { "assign": "name" }, "value": "Vidar" },
  { "operation": { "assign": "name" }, "value": "Storm" },
  { "operation": { "assign": "message" }, "value": "Hello $name |> up" }
]

Keep portable values to JSON types: strings, numbers, booleans, null, arrays, and objects. Functions stay in the host's pipe registry. compile() takes source text; run() can consume the restored records directly.

Development

Describe the workflow, then implement and register its functions:

=result: >-
  $invoice
  |> validate_invoice
  |> enrich_invoice
  |> render_invoice

Conditions, loops, arithmetic, I/O, logging, and error recovery belong in host functions. Weblang has no separate call, return, or control-flow syntax. Optional input/output comments document pipe contracts; the runner does not enforce types.

/*
Pipe typedef:
Input from validate_invoice: { id: string, total: number }
Output for render_invoice: { id: string, total: number, vendor: string }
*/

Use chain start or chain result at the ends of a chain. Run npm test for the tests. Planning material remains in TODO and NOTES.

Created by Vidar Eldøy