# Igniculus
SQL Syntax Highlighter and Logger. Unadorned and customizable.

[![version](https://img.shields.io/npm/v/igniculus.svg)](https://www.npmjs.com/package/igniculus)
[![license](https://img.shields.io/npm/l/igniculus.svg)](https://github.com/Undre4m/igniculus/blob/master/LICENSE)
[![downloads](https://img.shields.io/npm/dt/igniculus.svg?colorB=ffdf00)](https://www.npmjs.com/package/igniculus)
[![build status](https://img.shields.io/travis/Undre4m/igniculus.svg?logo=travis&logoWidth=15)](https://travis-ci.org/Undre4m/igniculus)
[![maintainability](https://api.codeclimate.com/v1/badges/30482d982a79b253aed9/maintainability)](https://codeclimate.com/github/Undre4m/igniculus/maintainability)
[![test Coverage](https://api.codeclimate.com/v1/badges/30482d982a79b253aed9/test_coverage)](https://codeclimate.com/github/Undre4m/igniculus/test_coverage)

## Install

```console
$ npm install igniculus
```

## Usage

```js
const igniculus = require('igniculus')();

igniculus('SELECT [port] AS Printer, \'on fire\' AS Status ' +
          'FROM [Printers] P ' +
          'WHERE P."online" = 1 AND P."check" = 1');
```

![Simple Query Default](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/simple-query.png)

## Table of Contents
- [Logger](#logger)
- [Options](#options)
    - [Rules](#rules)
    - [Styles](#styles)
    - [Custom Rules](#custom-rules)
- [Examples](#examples)
- [Integration](#integration)
    - [Sequelize](#sequelize)

## Logger

A reference to the log function is returned on initialization but can be accessed anywhere through `.log`

```js
// config.js
const igniculus = require('igniculus');
const options = { ... };

igniculus(options);
```

```js
// any.js
const igniculus = require('igniculus');

let query = 'SELECT ...';
igniculus.log(query);
```

## Options

A default color scheme is provided. However, you can define the highlight style for each rule when instantiating:

```js
const igniculus = require('igniculus');
/* White constants over red background using inverse mode.
 * Gray keywords.
 * Prefixed by white '(query)' message.
 */
const options = {
    constants:         { mode: 'inverse', fg: 'red', bg: 'white' },
    standardKeywords:  { mode: 'bold', fg: 'black' },
    lesserKeywords:    { mode: 'bold', fg: 'black' },
    prefix:            { mode: 'bold', fg: 'white', text: '(query) '}
};
const illumine = igniculus(options);

illumine('SELECT * FROM Student s ' +
         'WHERE s.programme = \'IT\' AND EXISTS (' +
             'SELECT * FROM Enrolled e ' +
             'JOIN Class c ON c.code = e.code ' +
             'JOIN Tutor t ON t.tid = c.tid ' +
             'WHERE e.sid = s.sid AND t.name LIKE \'%Hoffman\')');
```

![Subquery](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/subquery.png)

The _options_ argument is optional and each property should be one of the following.

### Rules

- options.**comments** - Single and multi-line comments. _E.g:_ `-- comments` or `/* Author: undre4m */`
- options.**constants** - Values surrounded by single quotes. _E.g:_ `'static'`
- options.**numbers** - Numeric values. _E.g:_ `2.5`
- options.**operators** - Arithmetic, Bitwise and Comparison operators. _E.g:_ `+` or `>=`
- options.**variables** - Local variables and parameters. _E.g:_ `@name` or `@@IDENTITY`
- options.**delimitedIdentifiers** - Text between brackets or double quotes. _E.g:_ `[Employee]` or `"salary"`
- options.**dataTypes** - One of the included data types. _E.g:_ `INTEGER` or `VARCHAR`
  - dataTypes.**types** - Array of custom data types. Replaces the ones by default. _E.g:_ `['SERIAL', 'TIMESTAMP']`
  - dataTypes.**casing** - Either `'lowercase'` or `'uppercase'`. If not defined data types won't be capitalized.
- options.**standardKeywords** - One the included keywords. _E.g:_ `SELECT` or `CONSTRAINT`
  - standardKeywords.**keywords** - Array of custom standard keywords. Replaces the ones by default. _E.g:_ `['CLUSTER', 'NATURAL']`
  - standardKeywords.**casing** - Either `'lowercase'` or `'uppercase'`. If not defined standard keywords won't be capitalized.
- options.**lesserKeywords** - One of the included lesser keywords. _E.g:_ `ANY`, `AVG` or `DESC`
  - lesserKeywords.**keywords** - Array of custom lesser keywords. Replaces the ones by default. _E.g:_ `['VOLATILE', 'ASYMMETRIC']`
  - lesserKeywords.**casing** - Either `'lowercase'` or `'uppercase'`. If not defined lesser keywords won't be capitalized.
- options.**prefix**
  - prefix.**text** - A prefix can be appended to every log through this option. This prefix can be styled like any previous options.
  - prefix.**replace** - Also, a _string_ or _regular expression_ can be provided and it will replace (if a prefix.**text** was given) or remove a prefix that matches such parameter. _E.g:_ [Sequelize](https://www.npmjs.com/package/sequelize) prefixes every _SQL statement_ with `Executing (default|transaction_id):` This is removed by **default** by the option `prefix: { replace: /.*?: / }`
- options.**postfix**
  - postfix.**text** - A postfix can be appended to every log through this option. This postfix can be styled like any previous options.
- options.**output** - Output function for the highlighted statements, `console.log` by default. _E.g:_ `process.stdout`, `st => st`
- options.**own** - Your own custom-built rules can be defined here. See _[(Custom Rules)](#custom-rules)_ below for details.

If defined, the _options_ argument takes precedence over _default_ options. If a rule or it's style is missing it won't be applied. This allows to _"enable"_ or _"disable"_ certain syntax highlighting as you see fit. _[(Examples below)](#examples)_

>#### A word on types and keywords
>Most often, highlighting every reserved keyword can make syntax difficult to read, defeating the purpose altogether. Therefore, three distinct rules are provided: _dataTypes_, _standardKeywords_ and _lesserKeywords_.
Each of these rules can be customized individually and come with a [predefined list](https://github.com/Undre4m/igniculus/blob/master/index.js#L3) of most widely used T-SQL and SQL-92 keywords and data types. Furthermore each of this lists can be customized as described above.
>
>Starting from [v1.1.0](https://github.com/Undre4m/igniculus/blob/master/CHANGELOG.md#110--26-feb-2018) _types_ and _keywords_ are no longer uppercased by default. Custom styles should use the `casing: 'uppercase'` option for this behaviour. Predefined style already provides this option so no changes should be required.

### Styles

All of the previous rule styles can be defined like this:

```js
/* options = {"rule": style, ... } where
 * style = { mode: "modifier", fg: "color", bg: "color"}
 */
const options = {
    constants: {
        mode: 'inverse',
        fg: 'red',
        bg: 'white'
    },
    ...
};
```

Each style having an optional:

- style.**mode** - Modifier. _E.g:_ `'bold'`
- style.**fg** - Foreground text color. _E.g:_ `'red'`
- style.**bg** - Background color. _E.g:_ `'black'`

These can be one of the following.

#### Modifiers

- `reset`
- `bold`
- `dim`
- `italic`
- `underline`
- `blink`
- `inverse`
- `hidden`
- `strikethrough`

#### Colors (Foreground and Background)

- `black`
- `red`
- `green`
- `yellow`
- `blue`
- `magenta`
- `cyan`
- `white`

### Custom Rules
>#### ⚠ Be advised
>This feature is experimental and should be used with discretion. Custom pattern-matching has the potential to disrupt other rules and induce defects in highlighting.

You can define as many rules as needed. Like built-in rules, an optional style can be set for each one. Every **rule** can be named as desired, simple names are encouraged to avoid problems though. Option **transform** is not required, **regexp** is.

- options.**own**
    - own.**rule**
      - rule.**regexp** - A _regular expression_ must be provided for the rule to be applied. _E.g:_ `/(https?|ftp):\/\/[^\s/$.?#].[^\s]*/g`
      - rule.**transform** - Each matched expression can be either replaced by a _string_ or transformed by a _function_. The function takes one argument, the matched expression, and it's return value will be used for replacement. _E.g:_ `'hidden'` or `match => match.trim()`

## Examples

```js
/* Predifined style */
const defaults = {
    comments:               { mode: 'dim', fg: 'white' },
    constants:              { mode: 'dim', fg: 'red' },
    delimitedIdentifiers:   { mode: 'dim', fg: 'yellow' },
    variables:              { mode: 'dim', fg: 'magenta' },
    dataTypes:              { mode: 'dim', fg: 'green', casing: 'uppercase' },
    standardKeywords:       { mode: 'dim', fg: 'cyan', casing: 'uppercase' },
    lesserKeywords:         { mode: 'bold', fg: 'black', casing: 'uppercase' },
    prefix:                 { replace: /.*?: / }
};
```

![Defaults](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/default.png)

```js
const igniculus = require('igniculus')(
    {
        constants:             { mode: 'bold', fg: 'yellow' },
        numbers:               { mode: 'bold', fg: 'magenta' },
        delimitedIdentifiers:  { mode: 'bold', fg: 'red' },
        standardKeywords:      { mode: 'bold', fg: 'blue' }
    }
);

igniculus("INSERT INTO [Printers] ([port], [name], [ready], [online], [check]) " +
          "VALUES ('lp0', 'Bob Marley', 0, 1, 1)");
```

![Custom Insert](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/simple-insert-custom.png)

```js
const igniculus = require('igniculus');

const options = {
    delimitedIdentifiers: {
        fg: 'yellow'
    },
    dataTypes: {
        fg: 'magenta',
        types: ['VARBINARY']
    },
    standardKeywords: {
        fg: 'red',
        keywords: ['CREATE', 'PRIMARY', 'KEY']
    },
    lesserKeywords: {
        mode: 'bold',
        fg: 'black',
        keywords: ['TABLE', 'NOT', 'NULL']
    },
    prefix: {
        text: '\n'
    }
};
igniculus(options);

igniculus.log('CREATE TABLE User (' +
              '[username] VARCHAR(20) NOT NULL, ' +
              '[password] BINARY(64) NOT NULL, ' +
              '[avatar] VARBINARY(MAX), PRIMARY KEY ([username]))');
```

![Custom Create](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/simple-create-custom.png)


```js
const igniculus = require('igniculus');

const log = igniculus({
    constants:                { fg: 'red' },
    delimitedIdentifiers:     { mode: 'bold', fg: 'cyan' },
    standardKeywords:         { fg: 'blue', casing: 'uppercase' },
    own: {
        _: {
            mode: 'bold',
            fg: 'white',
            regexp: /^/,
            transform: '█ '
        },
        comments: {
            regexp: /(-{2}.*)|(\/\*(.|[\r\n])*?\*\/)[\r\n]*/g,
            transform: ''
        },
        UUIDv4s: {
            mode: 'bold',
            fg: 'black',
            regexp: /'[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}'/gi,
            transform: (uuid) => uuid.replace(/\w{1}/g, 'x')
        }
    }
});

log("/* May 13th, 2018 | 06:09:28.262 | http://server.local:8000 */" +
    "select [username], [password] from Users where [_uuid] = '4072FA1B-D9E7-4F0E-9553-5F2CFFE6CC7A'");
```

![Custom Rules](https://raw.githubusercontent.com/Undre4m/igniculus/v1.4.0-rc/media/simple-query-custom-rules.png)

## Integration

Igniculus' logger is a _drop in_ replacement on any tool that passes the log function either a `string` or `Object` paramater. In the latest case the `toString()` method will be called to obtain a `string` primitive.

### Sequelize

Using igniculus with sequelize is straightforward.

```js
const Sequelize = require('sequelize');
const igniculus = require('igniculus')();

const sequelize = new Sequelize('database', 'username', 'password', {
    logging: igniculus
});
```

```js
/* Or add some customizations */
const Sequelize = require('sequelize');
const igniculus = require('igniculus')(
    {
        constants:             { fg: 'red' },
        delimitedIdentifiers:  { fg: 'yellow' },
        dataTypes:             { fg: 'red' },
        standardKeywords:      { fg: 'magenta' },
        lesserKeywords:        { mode: 'bold', fg: 'black' },
        prefix:                {
                                   mode: 'bold',
                                   fg: 'white',
                                   replace: /.*?:/,
                                   text: '(Sequelize)'
                               },
        postfix:               { text: '\r\n' }
    }
);
const sequelize = new Sequelize('database', 'username', 'password', {
    logging: igniculus
});

...

sequelize.sync({ logging: igniculus});
```

#### Before
![Before](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/sequelize-without.png)

#### After
![After](https://raw.githubusercontent.com/Undre4m/igniculus/master/media/sequelize-with.png)

## Notes

### Changes
For a full list of changes please refer to the [changelog](https://github.com/Undre4m/igniculus/blob/master/CHANGELOG.md).

### Future Upgrades

#### [v2.0.0 milestone](https://github.com/Undre4m/igniculus/milestone/1)
- Separation of style-related and option-specific configurations **BC**
- Adding and omitting data types and keywords from the predefined sets
- Basic built-in themes
- Easier to read documentation
- Option validation and friendly error detection

## Maintainers

[Lucas Astrada](https://github.com/undre4m)

## License

[MIT](https://github.com/Undre4m/igniculus/blob/master/LICENSE)