@ryusei/light
Version:
<div align="center"> <a href="https://light.ryuseijs.com"> <img alt="RyuseiLight" src="https://light.ryuseijs.com/images/svg/logo.svg" width="70"> </a>
182 lines (148 loc) • 4.5 kB
text/typescript
import { BODY, CODE, CONTAINER, LINE, ROOT, TOKEN } from '../../constants/classes';
import { PROJECT_CODE_SHORT } from '../../constants/project';
import { EventBus } from '../../event/EventBus';
import { Extension, LanguageInfo, Options, Token } from '../../types';
import { escapeHtml, forOwn, tag } from '../../utils';
/**
* Stores all Extensions functions.
*/
const Extensions: Record<string, Extension> = {};
/**
* The class for highlighting code via provided tokens.
*
* @since 0.0.1
*/
export class Renderer {
/**
* Adds extensions.
*
* @param extensions - An object literal with Component functions.
*/
static compose( extensions: Record<string, Extension> ): void {
forOwn( extensions, ( Component, name ) => {
Extensions[ name ] = Component;
} );
}
/**
* Holds lines with tokens.
*/
readonly lines: Token[][] = [];
/**
* 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.
*/
private 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( Extensions, 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.
*/
private renderLines( append: ( fragment: string ) => void ): void {
const event = this.event;
const tagName = 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( tag( classes ) );
for ( let j = 0; j < tokens.length; j++ ) {
const token = tokens[ j ];
const categories = token[ 0 ].split( '.' );
const className = `${ PROJECT_CODE_SHORT }__${ categories[ 0 ] }`;
const modifiers = categories.slice( 1 ).map( sub => `${ className }--${ sub }` );
const classes = [ TOKEN, className ].concat( modifiers );
event.emit( 'token', token, classes );
append( `${ tag( classes, tagName ) }${ escapeHtml( token[ 1 ] ) }</${ tagName }>` );
}
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;
const closeTag = '</div>';
let html = '';
const append = ( fragment: string ) => { html += fragment };
if ( pre ) {
html += tag( [ `${ ROOT } ${ ROOT }--${ this.info.id }` ], 'pre' );
}
const containerClasses = [ CONTAINER ];
event.emit( 'open', append, containerClasses );
html += tag( containerClasses );
const bodyClasses = [ `${ BODY }${ this.options.wrap ? ` ${ BODY }--wrap` : '' }` ];
event.emit( 'body:open', append, bodyClasses );
html += tag( bodyClasses );
event.emit( 'code:open', append );
html += tag( [ CODE ] );
this.renderLines( append );
html += closeTag; // code
event.emit( 'body:close', append );
html += closeTag; // body
event.emit( 'close', append );
html += closeTag; // container
event.emit( 'closed', append );
if ( pre ) {
html += `</pre>`;
}
return html;
}
/**
* Destroys the instance.
*/
destroy(): void {
this.event.emit( 'destroy' );
this.event.destroy();
}
}