Practical Example


import { LINE_BREAK } from '../../constants/characters';
import { Options, LanguageInfo, Token, Component } from '../../types';
import { EventBus } from '../../event/EventBus';
import { PROJECT_CODE_SHORT } from '../../constants/project';
import { BODY, CODE, CONTAINER, LINE, ROOT, TOKEN } from '../../constants/classes';
import { forOwn, escapeHtml } from '../../utils';


/**
 * Stores all Component functions.
 */
const Components: Record<string, Component> = {};

/**
 * The class for highlighting code via provided tokens.
 *
 * @since 0.0.1
 */
export class Renderer {
  /**
   * Adds components.
   *
   * @param components - An object literal with Component functions.
   */
  static compose( components: Record<string, Component> ): void {
    forOwn( components, ( Component, name ) => {
      Components[ name ] = Component;
    } );
  }

  /**
   * Holds lines with tokens.
   */
  readonly lines = [];

  /**
   * Holds the language info.
   */
  readonly info: LanguageInfo;

  /**
   * Holds the root element if provided.
   */
  readonly root: HTMLElement | undefined;

  /**
   * Holds options.
   */
  readonly options: Options;

  /**
   * Holds the EventBus instance.
   */
  readonly event: EventBus = new EventBus();

  /**
   * The Renderer constructor.
   *
   * @param lines   - Lines with tokens to render.
   * @param info    - The language info object.
   * @param root    - Optional. A root element to highlight.
   * @param options - Options.
   */
  constructor( lines: Token[][], info: LanguageInfo, root?: HTMLElement, options: Options = {} ) {
    this.lines   = lines;
    this.info    = info;
    this.root    = root;
    this.options = options;

    this.init();
  }

  /**
   * Initializes the instance.
   */
  protected init(): void {
    const { lines } = this;

    if ( lines.length ) {
      const tokens = lines[ lines.length - 1 ];

      if ( ! tokens.length || ( tokens.length === 1 && ! tokens[ 0 ][ 1 ].trim() ) ) {
        // Removes the last empty line.
        lines.pop();
      }
    }

    forOwn( Components, Component => {
      Component( this );
    } );

    this.event.emit( 'mounted' );
  }

  /**
   * Renders lines as HTML.
   *
   * @param append - A function to add fragments to the HTML string.
   *
   * @return A rendered HTML string.
   */
  protected renderLines( append: ( fragment: string ) => void ): void {
    const event = this.event;
    const tag   = this.options.span ? 'span' : 'code';

    for ( let i = 0; i < this.lines.length; i++ ) {
      const tokens  = this.lines[ i ];
      const classes = [ LINE ];

      event.emit( 'line:open', append, classes, i );
      append( `<div class="${ classes.join( ' ' ) }">` );

      if ( tokens.length ) {
        for ( let j = 0; j < tokens.length; j++ ) {
          const token   = tokens[ j ];
          const classes = [ `${ TOKEN } ${ PROJECT_CODE_SHORT }__${ token[ 0 ] }` ];

          event.emit( 'token', token, classes );

          append( `<${ tag } class="${ classes.join( ' ' ) }">${ escapeHtml( token[ 1 ] ) }</${ tag }>` );
        }
      } else {
        append( LINE_BREAK );
      }

      append( '</div>' );
      event.emit( 'line:closed', append, i );
    }
  }

  /**
   * Returns all lines and wrapper elements.
   *
   * @param pre - Whether to wrap elements by `pre` or not.
   *
   * @return An HTML string.
   */
  html( pre: boolean ): string {
    const event = this.event;
    let html  = '';

    const append = ( fragment: string ) => { html += fragment };

    if ( pre ) {
      html += `<pre class="${ ROOT } ${ ROOT }--${ this.info.id }">`;
    }

    const containerClasses = [ CONTAINER ];
    event.emit( 'open', append, containerClasses );

    html += `<div class="${ containerClasses.join( ' ' ) }">`;
    event.emit( 'opened', append );

    const bodyClasses = [ `${ BODY }${ this.options.wrap ? ` ${ BODY }--wrap` : '' }` ];
    event.emit( 'body:open', append, bodyClasses );

    html += `<div class="${ bodyClasses.join( ' ' ) }">`;
    event.emit( 'body:opened', append );

    html += `<div class="${ CODE }">`;
    this.renderLines( append );
    html += `</div>`; // code

    event.emit( 'body:close', append );
    html += `</div>`; // body

    event.emit( 'close', append );
    html += `</div>`; // container

    event.emit( 'closed', append );

    if ( pre ) {
      html += `</pre>`;
    }

    return html;
  }

  /**
   * Destroys the instance.
   */
  destroy(): void {
    this.event.emit( 'destroy' );
    this.event.destroy();
  }
}
  

Comments/Strings

/**
 * Multiline comment
 * 'Should not be a string'
 * "Should not be a string"
 */
/* Multiline comment in a single line */

// Single line comment
// 'Should not be a string'
// "Should not be a string"

'Single quote'
'Single \'quote\' with escape'
'Single 'quote' with single quote'
'Single "quote" with double quote'
'Single `quote` with back quote'

"Double quote"
"Double \"quote\" with escape"
"Double "quote" with double quote"
"Double 'quote' with single quote"
"Double `quote` with back quote"

`Back quote`
'Back \`quote\` with escape'
'Back `quote` with back quote'
'Back 'quote' with single quote'
'Back "quote" with double quote'

'/* Should not be a comment */'
'// Should not be a comment'

"/* Should not be a comment */"
"// Should not be a comment"

`/* Should not be a comment */`
`// Should not be a comment`
  

RegExp

/^.*?[\n\s]/gmsi
  

Template Literal

`Multiline
    template
      literal`

`The result will be ${ ( a + b ) * 3 }`

// Nested template literal
`container ${
  isMobile()
  // ${ comment }
  // `
  ? 'is-mobile'
  : `container--${ page.isFront() ? 'front' : 'page' }`
}`;
  

Functions/Generics

// Function
function apply<T extends object>( value: T ) {}

// Anonymous function
const a = function <T extends object>( value: T ) {}

// Arrow function
<T extends object> ( value: T ) => {}

// Method
{
  apply<T extends object>( value: T ) {}
}

type Assign<T, U> = Omit<T, keyof U> & U;

export function assign<T extends object, U extends object[]>( object: T, ...sources: U ): Assign<T, U> {
    const keys: Array<string | symbol> = getKeys( source );

    if ( a < 1 && b > d ) {
      console.log( a );
    }

    for ( let i = 0; i < keys.length; i++ ) {
    }
  }

  return object;
}
  

Typing

declare var process: any;

type Token = [ string, number, ...RegExp[] ];

interface CustomDivElement extends HTMLDivElement {
  selectionStart: number,
  selectionEnd: number,
  setSelection( number, number ): void;
}

namespace Lexer {
  export interface Grammar {
    main: Tokenizers[];
    [ key: string ]: Tokenizers[];
  }
}

function isArray<T>( subject: T[] ): subject is T[] {
  return Array.isArray( subject );
}
  

Keywords

declare, keyof, namespace, readonly, type, string,
number, boolean, bigint, symbol, any, never, unknown
  

Classes

Object.keys( object );

class Component {
  constructor() {
  }
}

const component = new Component();
  

Booleans

true, false
  

Numbers

0 1 1.23 .23 +1.23 -1.23
1e10 1e+10 1e-10 1E10 1E+10 1E-10
1.2e10 1.2e+10 1.2e-10 1.2E10 1.2E+10 1.2E-10
  

Operators

+ - ~ ! / * % ** < > <= >= == != === !==
<< >> >>> & | ^ && || ?? ?
= *= **= /= %= += -= <<= >>= >>>= &= ^= |= &&= ||= ??= :
  

Brackets

{} () []
  

Delimiters

; . ,
;;;; .... ,,,,