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 🙏

© 2024 – Pkg Stats / Ryan Hefner

subwriter

v0.2.0

Published

A tiny (~2kb), writer-first, minimally-invasive DSL for token substitution in long-form text. You can think of it as a presentation layer for writers working with randomly-generated data. **subwriter** strictly separates the text from the content generati

Downloads

4

Readme

subwriter

A tiny (~2kb), writer-first, minimally-invasive DSL for token substitution in long-form text. You can think of it as a presentation layer for writers working with randomly-generated data. subwriter strictly separates the text from the content generation process. By way of constrast, Tracery hybridizes the two.

subwriter is designed to be as minimal as possible, and reserves only 5 characters ([ { | } ]), with 2 additional characters (=, ") reserved within filter declarations. Anything else is fair game as content or as an identifier.

...including emojis, because why not.

Why?

  • unreadable text is hard to write and even harder to proofread
  • because for large amounts of text content, content-as-content is better than content-as-code
  • sometimes sprintf-js just doesn't cut it and pulling in a real templating language is overkill

Requirements

  • an environment with support for Intl.Segmenter,

Example

const data = {
  '🦸‍♀️': { name: 'Sally' }
};

const ctx = {
  // filters are functions that take source text and an optional param.
  PRP: (glyph, caps = true) => {
    const res = [...glyph].includes('♀') ? 'she' : 'he';
    return caps ? res.toLocaleString() : res;
  }
};

sub("Isn't that {🦸‍♀️.name}? Is [🦸‍♀️|PRP=false] a superhero?", data, ctx);
// Isn't that Sally? Is she a superhero?

Features

Property access, filters, &c., are evaluated left to right.

| Feature | Example | Output | | :------------------------- | :---------------------------- | ---------------- | | Interpolation | {name} | World | | Property access | {data.name} | Bob | | Filters | {data.name\|cap} | BOB | | Filters (literal params) | {data.name\|pre=123} | 123Bob | | Filters (reference params) | {🍾} [bottle\|s=🍾] of beer | 1 bottle of beer | | Chained filters | {data.name\|cap\|pre=123} | 123BOB | | Filter expressions | [bobbbbbb!\|cap\|pre=123] | 123BOBBBBBB! | | Nested expressions | [Hello {name}!\|cap] | HELLO WORLD! |

Filters vs. accessors

While most filters a) operate on a scalar value and an optional argument and b) return text, filters can technically operate on arbitrary data and return anything—even content to be passed through additional filters. However, this increases your filters' dependency on data structure and introduces more room for runtime errors. get()-style accessors on your data can be used to the same effect.

const ctx = {
  ageText: person => numberToText(person.age),
  text: num => numberToText(num)
};

const data = {
  name: 'Bob',
  age: 2,
  get ageText() {
    return numberToText(this.age);
  }
};

// Bob is two years old; good solution
sub('{person.name} is {person.age|text} years old.', data, ctx);
// Bob is two years old; passable solution
sub('{person.name} is {person.ageText} years old.', data, ctx);
// Bob is two years old; worst solution
sub('{person.name} is {person|ageText} years old.', data, ctx);

Because subwrite has no understanding of a filter's input, param or return types, it's easy to shoot yourself in the face using a filter that expects an object with a specific structure instead of a scalar value.

So as a general rule: if you need a param or are applying a general-purpose text operation, use a filter. If you don't, an accessor in your data is probably better.

Filter params

When passed as filter args, numbers and booleans are coerced from strings. Double-quoted strings are passed as literal text.

Unquoted strings are passed as their corresponding value in data.

Both null and undefined values given as params are passed as undefined to make default params more useful.

| Example | Argument value | | :----------------- | :------------- | | [foo\|bar=1] | 1 | | [foo\|bar=true] | true | | [foo\|bar] | undefined | | [foo\|bar=null] | undefined | | [foo\|bar="key"] | 'key' | | [foo\|bar=key] | data[key] |

Non-features

  • control flow, conditional logic
  • transclusion
  • RNG
  • anything else

Priorities/Principles

  1. Be readable.
  2. Be writeable.
  3. Minimize conflicts between syntax and real-world prose.
  4. Minimize conflicts between syntax and Markdown.
  5. When in doubt, use a filter.
import sub from 'subwriter';

// properties -> "His name is Bob."
sub(`His name is {name}.`, { name: 'Bob' });

// complex objects
const data = {
  PRP$: 'His',
  name: {
    first: 'Bob',
    last: 'Bobbertson',
    get full(): string {
      return `${this.first} ${this.last}`;
    }
  }
};

// nested properties, accessors -> "his name is Bob Bobbertson."
sub('{PRP$} name is {name.full}.', data);

// filters!
const filters = {
  max: (str, chars) => str.slice(0, chars),
  case: (str, method = 'toUpperCase') => str[method]?.()
};

// filters -> "his name is Bo."
sub(`{PRP$} name is {name.first|max=2}.`, data, filters);

// filters with default params -> "His name is Bob."
sub(`{PRP$|case} name is {name.first}`, data, filters);

// chained filters -> "His name is BO."
sub(`{PRP$} name is {name.first|case|max=2}`, data, filters);
class Person {
  public glyph = '👨‍🎨';
  public first_name = 'Art';
  public last_name = 'Artman';
  public gender = 'M';
  public get name() {
    return `${this.first_name} ${this.last_name}`;
  }
}

// "His name is Art Artman. He's the real deal."
sub(
  `{👨‍🎨|PRP$} name is {👨‍🎨.name}. {👨‍🎨|PRP}'s the real deal.`,
  { '👨‍🎨': new Person() },
  {
    PRP$: person => ({ M: 'His', F: 'Her' }[person.gender] ?? 'Its'),
    PRP: person => ({ M: 'He', F: 'She' }[person.gender] ?? 'It')
  }
);

Inspiration

A good chunk of my inspiration here came from reading the tutorials on creating new content for the game Wildermyth, though it could be more accurately described as "counter-inspiration."

Some sample text using Wildermyth's text interpolation DSL.

<leader> takes a long, appraising look at <hothead>.
<leader.mf:He/She> wipes a fleck of bluish ooze off <leader.mf:his/her> nose.

And the equivalent using subwriter and emojis.

{🫡} takes a long, appraising look at {😡}.
{🫡|He} wipes a fleck of bluish ooze off {🫡|his} nose.

A more complex example using Wildermyth's "splits":

<leader.goofball/bookish:
Surprise everyone! It's fightin' time!
/Ahem. Our foes appear to have arrived.>

Which resolves to:

Ahem. Our foes appear to have arrived.

And the corresponding subwriter source, which resolves to the same text (after trimming).

[Surprise, everyone! It's fightin' time!|leader="🤪"]
[Ahem. Our foes appear to have arrived.|leader="🤓"]