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 🙏

© 2025 – Pkg Stats / Ryan Hefner

comment-directive

v1.2.1

Published

A text preprocessor that uses comments as conditional directives

Downloads

13

Readme

comment-directive

A brutally effective text preprocessor that uses comments as conditional directives. No need for separate .template files, no overly complicated DSL; just comments that do useful work.

Well-tested, isomorphic, dependency-free, slices and dices lines: ~300/µs, ~3,000/ms, and ~3,000,000/s; But, as with anything RegExp, it's more like a well-oiled ice sculpture chainsaw than a surgical blade

QuickStart

import commentDirective from 'comment-directive';

const input = `
// ###[IF]env=1;rm=comment;un=comment;
// console.log('if env=1, remove me; else, un-comment me');

// ###[IF]env=1;rm=line;sed=/surgical blade/chainsaw/;
const aWellOiled = "surgical blade";`;

commentDirective(input, {env: 1}).trim() === ``;
commentDirective(input, {env: 0}) === `
console.log('if env=1, remove me; else, un-comment me');

const aWellOiled = "chainsaw";`;
/* @INPUT
For example, replacing a string with an ENV variable is easy;
Injecting an object function into an argument with an ENV variable, not so easy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]prod=1;un=comment;rm=comment;
const anExample = (arg = [1, /* {aFn: () => ['aHotArray']}, */ 2, 3]) => {
  // ###[IF]prod=0;sed=/80/3000/;
  // ###[IF]prod=1;sed=/localhost/api.fun/;
  const str = 'https://localhost:80'
  return {str, arg};
};


/* @IF prod=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
const anExample = (arg = [1, {aFn: () => ['aHotArray']}, 2, 3]) => {
  const str = 'https://api.fun:80'
  return {str, arg};
};


/* @IF prod=0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
const anExample = (arg = [1, 2, 3]) => {
  const str = 'https://localhost:3000'
  return {str, arg};
};

▎INSTALL

# pick your poison
npm install --save-dev comment-directive
bun  add --dev comment-directive
pnpm add --save-dev comment-directive
yarn add --dev comment-directive

API

function commentDirective(
  template: string, // template string to process
  flags: Record<string, boolean | number | string>, // condition flags
  options?: Partial<CommentOptions> // comment option/format
): string;

type CommentOptions = {
  // id/match options
  delimiter?: string;       // sed and sequence delimiter (default: '/')
  identifier?: string;      // comment directive identifier (default: '###[IF]')
  // parse options
  escape?: boolean;         // escape regex patterns to match literal strings (default: true)
  loose?: boolean;          // allow directives on lines with other content (default: false)
  nested?: boolean;         // allow nested multi-line comments (default: false)
  disableCache?: boolean,   // if memory is a concern in absurd/extreme use cases (default: false)
  throw?: boolean;          // throw on any error instead of logging and ignoring (default: false)
  // keep/preserve options
  keepDirective?: boolean;  // keep comment directive in output (default: false)
  keepEmpty?: boolean;      // keep/preserve removed empty comments/lines (default: false)
  // keepPad* -> false=none; true=both; 1=single-only; 2=multi-only
  keepPadStart?: boolean | 1 | 2, // start/leading whitespace for un/rm-comment (default: true)
  keepPadIn?: boolean | 1 | 2,    // inside whitespace for un/rm-comment (default: 2)
  keepPadEnd?: boolean | 1 | 2,   // end whitespace for un/rm-comment (default: 2)
  keepPadEmpty?: boolean | 1 | 2, // empty-line whitespace-only for un/rm-comment (default: false) 
  // 'fn' action/comment directive consumer (default: I => I)
  fn?: (input: (string | number)[], id: string, idx: number)=> (string | number)[];
  // regex comment/language support options
  multi?: [start: RegExp | string, end: RegExp | string];
  single?: [start: RegExp | string, end?: null | RegExp | string];
};

[!IMPORTANT] Defaults to C-like // and /* */ comments, but can be customized to suit any language; NOTE: it's not a full-fledged parser and has inherent limitations

Directive Syntax

Defined using the single-line comment format on its own line, unless loose is enabled

<comment_start> ###[IF]condition;action_if_true;[action_if_false;] [<comment_end>]
/* @example one-way directive: 'if' env=prod -> 'then' replace '3000' with '80' */
// ###[IF]env=prod;sed=/3000/80/;
const port = 3000;


/* @example two-way directive: 'if' env=prod -> 'then' uncomment -> 'else' remove */
// ###[IF]env=prod;un=comment;rm=comment;
// console.log('ENV: production');

Directive Actions

Directives are executed top-to-bottom and can be "stacked" on top of one another.

no=op

Also known as nada, nought, squat, zilch, or, if you prefer a more benign term, no operation:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]bingo=7;no=op;rm=line;
log('winner winner');
log('chicken dinner');

/* @IF bingo=7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
log('winner winner');
log('chicken dinner');

/* @IF bingo=0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
log('chicken dinner');

/* @IF bingo=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
log('chicken dinner');

NOTE: in other words, a negated condition: if any value but X, do Y

rm=[<N>L][@<stop>]

Removes the next N lines, or up to @<stop>, such as rm=3L removes the next 3 lines:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]env=apple;rm=2L;
debug('removes the');
debug('de-bug(s)');
warn('but not this warn');
// ###[IF]env=apple;rm=@//STOP_HERE;
debug('a lemon');
debug('is a lemon');
//STOP_HERE
warn('as an apple is an apple');


/* @IF env=apple ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
warn('but not this warn');
warn('as an apple is an apple');


/* @IF env=lemon ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
debug('removes the');
debug('de-bug(s)');
warn('but not this warn');
debug('a lemon');
debug('is a lemon');
//STOP_HERE
warn('as an apple is an apple');

NOTE: the @<stop> marker is removed if matched and keepDirective is false; otherwise, it is kept

(rm|un)=comment

Removes or uncomments the next comment block it finds, whether single-line, multi-line, or inline:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]prod=1;rm=comment;un=comment;
/*
debug('debug all');
debug('the bugs');
*/
// another comment


/* @IF prod=1 (rm) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

// another comment


/* @IF prod=0 (un) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

debug('debug all');
debug('the bugs');
// another comment

fn=<id>/[<N>L][@<stop>]

The fn directive, along with the matching fn option function, offers a way to override the output of the next line, a span of <N> lines, or up to @<stop>. Basically, an escape hatch for custom logic.

const input = `
// ###[IF]dist=1;fn=distTo;fn=distFrom;
import { commentDirective } from './index.ts';
import { type CommentOptions } from './index.ts';`;

type FnAction = (input: (string | number)[], id: string, idx: number)=> (string | number)[];
const fn: FnAction = (input, id, _idx) => input.map(line => {
  if (id === 'distTo') {
    return String(line).replace(`'./`, `'../dist/`).replace('.ts', '.js');
  }
  if (id === 'distFrom') {
    return String(line).replace('../dist/', './').replace('.js', '.ts');
  }
  return line;
});

const options = {keepDirective: true, fn};
const once    = commentDirective(input, {dist: 1}, options);
const twice   = commentDirective(once,  {dist: 1}, options);
const thrice  = commentDirective(twice, {dist: 0}, options);

// thrice undoes twice and matches the original input
let isTrue = input === thrice;
// twice matches once, i.e: it didn't re-change the code
isTrue = once === twice && twice === `
// ###[IF]dist=1;fn=distTo;fn=distFrom;
import { commentDirective } from '../dist/index.js';
import { type CommentOptions } from './index.ts';`;

sed=/pattern/replacement/[flags][<N>L][@<stop>]

Text replacement and/or substitution, it's sed-like, in RegExp's clothing. All patterns are escaped to match literal strings; to use actual RegExp syntax, the escape option must be set.

  • pattern : search pattern; treated as literal string with characters escaped; see escape below
  • replacement: pattern replacement; supports substitution groups like $1, if you live dangerously
  • [flags] : (optional) standard regex flags: g, i, m, u, y, s
  • [<N>L] : (optional) limits the action to the next N lines
  • [@<stop>] : (optional) processes lines until the <stop> marker (e.g: @//#STOP)

<N>L

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]boat=1;sed=/log/debug/2L;
log('test');
log('the');
log('best');


/* @IF boat=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
debug('test');
debug('the');
log('best');

@<stop>

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]goat=1;sed=/log/debug/@//#STOP;
log('test');
log('the');
//#STOP
log('best');


/* @IF goat=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
debug('test');
debug('the');
//#STOP
log('best');

/global/flag/g

The global g flag applies to all content below the directive during the preliminary pass, before any other line-by-line directives. Use it sparingly; better yet, avoid it entirely, or at minimum, use a limit via <N>L:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
LOG('test');
// ###[IF]jolt=1;sed=/log/debug/gi;
LOG('the');
LOG('best');
LOG('log');


/* @IF jolt=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
LOG('test');
debug('the');
debug('best');
debug('debug');

NOTE: @<stop> does not work with g

▎SEQUENCE

Sequence actions add or remove a given <value> to the next line, <N> lines, or until @<stop>; a simpler, saner alternative to sed for commenting out code.

  • prepend|unshift: adds <value> to the beginning of line(s)
  • append|push : adds <value> to the end of line(s)
  • shift : removes <value> from the beginning of line(s)
  • pop : removes <value> from the end of line(s)

(append|prepend|push|unshift)=<value>/[<N>L][@<stop>]

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]bolt=1;prepend=// /4L;
// ###[IF]bolt=1;append=/* app-ended */ /@//#STOP;
const itsExponential = (fac = 2) => {
  return 2 ** fac;
};
//#STOP


/* @IF bolt=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]bolt=1;prepend=// /4L;
// ###[IF]bolt=1;append= /* app-ended *//@//#STOP;
// const itsExponential = (fac = 2) => { /* app-ended */ 
//   return 2 ** fac; /* app-ended */ 
// }; /* app-ended */ 
// //#STOP /* app-ended */ 

To remove the trailing space in /* app-ended */ you could use: /* app-ended *///@//#STOP; Or use a different delimiter like ^ to write: /* app-ended */^@//#STOP;

(shift|pop)=<value>/[<N>L][@<stop>]

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]volt=1;shift=// /4L;
// ###[IF]volt=1;pop= /* app-ended */ /@//#STOP;
// const itsExponential = (fac = 2) => { /* app-ended */ 
//   return 2 ** fac; /* app-ended */ 
// }; /* app-ended */ 
// //#STOP /* app-ended */ 


/* @IF volt=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]volt=1;shift=// /4L;
// ###[IF]volt=1;pop= /* app-ended *//@//#STOP;
const itsExponential = (fac = 2) => {
  return 2 ** fac;
};
//#STOP

⎸NonDestructive

If keepDirective is set, sequence actions are nondestructive and can be restored to their original state:

const input = `
// ###[IF]prod=1;prepend=// /2L;shift=// /2L;
log('nondestructive');
log('reversible');`;
const once   = commentDirective(input, {prod: 1}, {keepDirective: true});
const twice  = commentDirective(once,  {prod: 1}, {keepDirective: true});
const thrice = commentDirective(twice, {prod: 0}, {keepDirective: true});

// thrice undoes twice and matches the original input
let isTrue = input === thrice;
// twice matches once, i.e: it didn't re-comment the code
isTrue = once === twice && twice === `
// ###[IF]prod=1;prepend=// /2L;shift=// /2L;
// log('nondestructive');
// log('reversible');` && twice !== input;

Stacked

Stacked directives are processed in order against the first non-directive content below them:

const input = `
// ###[IF]opt=1;un=comment;
// ###[IF]alt=2;sed=/vegetable/protein/;
// ###[IF]cat=0;sed=/stack/hack/;
// let vegetable = 'stack';`.trim();

commentDirective(input, {opt: 0}) === "// let vegetable = 'stack';";
commentDirective(input, {opt: 1}) === "let vegetable = 'stack';";
commentDirective(input, {opt: 1, alt: 2}) === "let protein = 'stack';";
commentDirective(input, {cat: 0, alt: 2}) === "// let protein = 'hack';";

The first directive result becomes the input for the second, and so on, allowing for any manner of nonsense:

commentDirective(`
// ###[IF]stacked=1;sed=/aaa/bbb/;
// ###[IF]stacked=1;sed=/bbb/ccc/;
aaa
`.trim(), {stacked: 1}) === "ccc";

commentDirective(`
// ###[IF]stacked=1;un=comment;
/*
// ###[IF]stacked=1;un=comment;
// ###[IF]stacked=1;sed=/foolery/foolery works!/;
// let like = 'even this kind of tomfoolery';
*/
`.trim(), {stacked: 1}) === "let like = 'even this kind of tomfoolery works!';";

commentDirective(`
// ###[IF]srsly=1;sed=/the quick/brown fox/;
// ###[IF]srsly=1;sed=/brown fox/jumps over/;
// ###[IF]srsly=1;sed=/jumps over/the lazy dog/;
brown fox jumps over the the quick
`.trim(), {srsly: 1}) === "the lazy dog jumps over the brown fox";

Language Support

If you can RegExp-it, you can comment-directive it!

▎DEFAULT

// ./src/lang.ts -> exports a handful of language RegExp definitions
import {
  css,
  html,
  python,
  make,
  extensions, // all langs by extension, e.g: extensions.nim, *.rb, *.py, *.toml
} from 'comment-directive/lang';


// default c-like comment format (js/ts/c/rust/go/swift/kotlin/scala)
const DEFAULT_OPTIONS: CommentOptions = {
  // regex comment/language support options
  multi: [/\s*\/\*/, /\*\/\s*/],
  single: [/\s*\/\/\s*/, null], // eating the surrounding space simplifies alignment
  // id/match options
  delimiter: '/',        // sed and sequence actions delimiter (default: '/')
  identifier: '###[IF]', // comment directive identifier (default: '###[IF]')
  // parse options
  escape: true,          // escape regex patterns to match literal strings (default: true)
  loose: false,          // allow directives on lines with other content (default: false)
  nested: false,         // allow nested multi-line comments (default: false)
  disableCache: false,   // if memory is a concern in absurd/extreme use cases (default: false)
  throw: false,          // throw on any error instead of logging and ignoring (default: false) 
  // keep/preserve options
  keepDirective: false,  // keep comment directive in output (default: false)
  keepEmpty: false,      // keep/preserve removed empty comments/lines (default: false)
  // keepPad* ->  false=none; true=both; 1=single only; 2=multi only
  keepPadStart: true,    // start/leading whitespace for un/rm-comment (default: true)
  keepPadIn: 2,          // inside whitespace for un/rm-comment (default: 2)
  keepPadEnd: 2,         // end whitespace for un/rm-comment (default: 2)
  keepPadEmpty: false,   // empty-line whitespace-only for un/rm-comment (default: false) 
  fn: (input, _id, _idx) => input, // 'fn' comment directive consumer
};

▎PYTHON

const canPython = commentDirective(`
# ###[IF]python=1;un=comment;
# print('python may be for thee, but not for me')
# ###[IF]python=1;un=comment;
'''
print('python may be for thee, but not for me')
'''
# ###[IF]python=1;un=comment;
"""
print('python may be for thee, but not for me')
"""`, { python: 1 }, {
  single: [/#\s*/, null],
  // explicit anchor to line start as trip quotes aren't true multi-line comments
  multi: [/^\s*('''|""")/, /('''|""")/],
}).split('\n').filter(Boolean).join('\n') === `
print('python may be for thee, but not for me')
print('python may be for thee, but not for me')
print('python may be for thee, but not for me')`.trim();

▎HTML

const canHTML = commentDirective(`
<!-- ###[IF]doyou=html;un=comment; -->
<!-- ###[IF]doyou=html;sed=/yes.js/ido.js/; -->
<!-- <script src="yes.js"></script> -->`, { doyou: 'html' }, {
  multi: [/<!--/, /-->/],
  // no single-line comment syntax; explicit start/end anchors; safety first
  single: [/^\s*<!--\s*/, /\s*-->\s*$/],
}).trim() === '<script src="ido.js"></script>';

Options

delimiter

Changes the default delimiter ('/') for both sed and sequence actions:

commentDirective(`
// ###[IF]reg=1;sed=%s://a.super/long/path%://z.super/dup/er/long/path%;
'https://a.super/long/path';
`.trim(), {reg: 1}, {delimiter: '%'}) === "'http://z.super/dup/er/long/path';";

commentDirective(`
// ###[IF]reg=1;sed=###/###+###;
let maths = 1 / 2 / 3;
`.trim(), {reg: 1}, {delimiter: '###'}) === "let maths = 1 + 2 / 3;";

// but, yo, i want to replace all '/' to '+'
commentDirective(`
// ###[IF]reg=1;sed=###/###+###g1L;
let maths  = 1 / 2 / 3;
let mathss = 1 / 2 / 3;`, {reg: 1}, {delimiter: '###'})
// done
=== `
let maths  = 1 + 2 + 3;
let mathss = 1 / 2 / 3;`;

escape

Set escape: false to use actual RegExp syntax in patterns:

commentDirective(`
// ###[IF]reg=1;sed=/\\d{3,}/456/;
123
`.trim(), {reg: 1}, {escape: false}) === "456";

identifier

Changes the '###[IF]' comment directive identifier:

commentDirective(`
// @@@[VROOOOOOOOOM]too=fast;rm=line;
console.log('web fast');`, {
  too: 'fast'
}, {identifier: '@@@[VROOOOOOOOOM]'}).trim() === "";

loose

Comment directives must be on their own line, but loose disables this restriction:

const result = commentDirective(`
console.log('willy'); // ###[IF]loosey=goosey;rm=comment;
console.log('nilly'); // remove me!
// a directive always targets the line below
console.log('i stay'); // ###[IF]loosey=goosey;rm=line;
console.log('i dont'); // remove me!
// // ###[IF]loosey=goosey;rm=line;
console.log('only removed if loose');`, {loosey: 'goosey'}, {loose: true});

/* @loose=false (default) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
console.log('willy'); // ###[IF]loosey=goosey;rm=comment;
console.log('nilly'); // remove me!
// a directive always targets the line below
console.log('i stay'); // ###[IF]loosey=goosey;rm=line;
console.log('i dont'); // remove me!
// // ###[IF]loosey=goosey;rm=line;
console.log('only removed if loose');


/* @loose=true ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
console.log('willy');
console.log('nilly');
// a directive always targets the line below
console.log('i stay');
//

If you like to play fast and loose, you can also stack directives:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]loose=1;sed=/log/play/;
// ###[IF]loose=1;sed=/super/fast/;
loging('super'); // ###[IF]loose=1;sed=/loging/and/;
loging('loose');

/* @loose=true ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
playing('fast');
and('loose');

keepDirective

Keep comment directives in the output, purged by default:

const result = commentDirective(`
// ###[IF]nap=1;un=comment;
// console.log('what about the comments?');`, {nap: 1}, {keepDirective: false});

/* @keepDirective=false (default) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

console.log('what about the comments?');


/* @keepDirective=true ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]nap=1;un=comment;
console.log('what about the comments?');

keepEmpty

Keep empty lines and directives in the output, vanquished by default:

const result = commentDirective(`
// #######################################
// ###[IF]empty=me;rm=comment;
/*
console.debug('take anything but');
console.debug('my whitespace');
*/
// #######################################`, {empty: 'me'}, {keepEmpty: false});

/* @keepEmpty=false (default) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

// #######################################
// #######################################


/* @keepEmpty=true ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

// #######################################





// #######################################

keepPad*

The keepPad* options control how whitespace \s* is handled by (un|rm)=comment with the values of:

  • false - whitespace is removed
  • true - whitespace is kept for both single and multi
  • 1 - whitespace is kept for single only
  • 2 - whitespace is kept for multi only

⎸SYNTAX DEFINITIONS

The behavior of keepPad* revolves around how whitespace is consumed/defined within the RegExp:

# single: [/\s*\/\/\s*/, null]
  <keepPadStart>  //  <keepPadIn>  text

# single: [/\s*\/\//, null]
  <keepPadStart>  //   text

# multi: [/\s*\/\*\s*/, /\s*\*\/\s*/]
  <keepPadStart>  /*  <keepPadIn>  text  <keepPadIn>  */   <keepPadEnd>

# multi: [/\s*\/\*/, /\*\//]
  <keepPadStart>  /*   text   */

# any (if whitespace-only empty-line)
      <keepEmpty>       

⎸EXAMPLE

const whitespace = ' '.repeat(10);
const input = `
    // ###[IF]space=1;un=comment;
    // let space = 'keep';${whitespace}
    // ###[IF]space=1;un=comment;
  1 /*${whitespace}
    let keep = 'space';${whitespace}
    */${whitespace}`;

commentDirective(input, {space: 1}, {
  keepPadEmpty: true, // keeps last empty line of whitespace
  multi: [/\s*\/\*\s*/, /\s*\*\/\s*/],
}).replaceAll(' ', '⠐') === `
⠐⠐⠐⠐let⠐space⠐=⠐'keep';⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐
⠐⠐1⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐
⠐⠐⠐⠐let⠐keep⠐=⠐'space';⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐
⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐`);

commentDirective(input, {space: 1}, {
  keepPadIn: false,
  keepPadStart: false,
  keepPadEnd: false,
  multi: [/\s*\/\*\s*/, /\s*\*\/\s*/],
}).replaceAll(' ', '⠐') === `
let⠐space⠐=⠐'keep';⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐
⠐⠐1
⠐⠐⠐⠐let⠐keep⠐=⠐'space';⠐⠐⠐⠐⠐⠐⠐⠐⠐⠐\n`);

nested

Nested comments are permitted in kool-kid languages such as Rust, D, Nim, Scala, Kotlin, Haskell, and F#, but hopefully, you will never have to deal with such a desecration of natural law:

commentDirective(`
// ###[IF]nested=1;un=comment;
// ###[IF]nested=1;un=comment;
// ###[IF]nested=1;un=comment;
// ###[IF]nested=1;sed=/details/Nested Comments/i;
/* /* /* /* /* The Devil's in the Details */ */ */ */ */`, {
  nested: 1
}, {nested: true}).trim() === "/* /* The Devil's in the Nested Comments */ */";

Raw Regex Hackin'

Since you can't shoehorn PEG logic into a comment, this library isn't intended for complex operations, but sometimes brandishing a crude RegExp chainsaw is unavoidable:

commentDirective(str, {rexy: 1}, {
  keepDirective: true, // prevents comment directive from being removed
  delimiter: '##',     // changes sed delimiter from '/' to '##'
  escape: false,       // disables string escape (needed to use regex)
});

NOTE: the sequence directive is designed for commenting out code, but this makes for a good example

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]rexy=1;sed=##^(?!\\/\\/\\s)(.*)##// $1##@//#STOP;
// ###[IF]rexy=0;sed=##^(\\/\\/\\s)(.*)##$2##@//#STOP;
const myKoolFunction = (arg = 'logic'): number => {
  const res = 'big ' + arg;

  return res.length;
};
//#STOP

const lesserFunction = (arg = ':('): number => {
  return res.length;
};


/* @IF rexy=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]rexy=1;sed=##^(?!\\/\\/\\s)(.*)##// $1##@//#STOP;
// ###[IF]rexy=0;sed=##^(\\/\\/\\s)(.*)##$2##@//#STOP;
// const myKoolFunction = (arg = 'logic'): number => {
//   const res = 'big ' + arg;
// 
//   return res.length;
// };
// //#STOP

const lesserFunction = (arg = ':('): number => {
  return res.length;
};

Let's take that the output of rexy=1 and reverse it with rexy=0:

/* @INPUT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]rexy=1;sed=##^(?!\\/\\/\\s)(.*)##// $1##@//#STOP;
// ###[IF]rexy=0;sed=##^(\\/\\/\\s)(.*)##$2##@//#STOP;
// const myKoolFunction = (arg = 'logic'): number => {
//   const res = 'big ' + arg;
// 
//   return res.length;
// };
// //#STOP

const lesserFunction = (arg = ':('): number => {
  return res.length;
};

/* @IF rexy=0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// ###[IF]rexy=1;sed=##^(?!\\/\\/\\s)(.*)##// $1##@//#STOP;
// ###[IF]rexy=0;sed=##^(\\/\\/\\s)(.*)##$2##@//#STOP;
const myKoolFunction = (arg = 'logic'): number => {
  const res = 'big ' + arg;

  return res.length;
};
//#STOP

const lesserFunction = (arg = ':('): number => {
  return res.length;
};

Voila! A block of code that you can toggle on and off by flipping a rexy flag with an elegant RegExp chainsaw. Use it sparingly, otherwise you might hack the 'i' out of 'eye'.

# The above comment directive broken down

[IF]rexy=0  (comments out myKoolFunction)
  sed=
    match  : ^(?!\\/\\/\\s)(.*)   - matches any line except those that start with '// '
    replace: // $1                - uses substitution to add comments
    stop-at: //#STOP              - stops matching lines at '//#STOP'

[IF]rexy=1  (uncomments myKoolFunction)
  sed=
    match  : ^(\\/\\/\\s)(.*)     - matches any line that starts with '// '
    replace: $2                   - uses substitution to remove comments
    stop-at: //#STOP              - stops matching lines at '//#STOP'

NOTE: alternatively, you could use the if/else syntax like so:
// ###[IF]rexy=0;sed=##^(?!\\/\\/\\s)(.*)##// $1##@//#STOP;sed=##^(\\/\\/\\s)(.*)##$2##@//#STOP;

Limitations

RegExp.

Development/Contributing

Contributions, pull requests, and suggestions are appreciated. First, make sure you have installed and configured the required build dependencies: Bun and Make.

▎PULL REQUEST STEPS

  1. Commit your changes/code
  2. Run make to clean, setup, build, lint, and test
  3. Assuming everything checks out, push your branch to the repository, and submit a pull request

▎MAKEFILE REFERENCE

# USAGE
   make [flags...] <target>

# TARGET
  -------------------
   all                   clean, setup, build, lint, test, aok (the entire jamboree)
  -------------------
   build                 builds the .{js,d.ts} (skips: lint, test, and .min.* build)
   build_cjs             builds the .cjs export
   build_esm             builds the .js (esm) export
   build_declarations    builds typescript .d.{ts,mts,cts} declarations
  -------------------
   install               installs dependencies via bun
   update                updates dependencies
   update_dry            list dependencies that would be updated via update
  -------------------
   lint                  lints via tsc & eslint
   lint_eslint           lints via eslint
   lint_eslint_fix       lints and auto-fixes via eslint --fix
   lint_tsc              lints via tsc
  -------------------
   test                  runs bun test(s)
   test_watch            runs bun test(s) in watch mode
   test_update           runs bun test --update-snapshots
  -------------------
   help                  displays (this) help screen