KnodesCommunity/typedoc-plugins

View on GitHub
packages/plugin-code-blocks/src/code-sample-file.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import assert from 'assert';
import { readFileSync } from 'fs';

import { textUtils } from '@knodes/typedoc-pluginutils';

export const DEFAULT_BLOCK_NAME = '__DEFAULT__';
const REGION_REGEX = /^[\t ]*\/\/[\t ]*#((?:end)?region)(?:[\t ]+(.*?))?$/gm;

export interface ICodeSample {
    region: string;
    file: string;
    code: string;
    startLine: number;
    endLine: number;
}

interface IRegionMarkerBase{
    fullMatch: string;
    line: number;
    type: 'start' | 'end';
    name?: string;
}
interface IStartRegionMarker extends IRegionMarkerBase {
    type: 'start';
    name: string;
}
interface IEndRegionMarker extends IRegionMarkerBase{
    type: 'end';
}
type RegionMarker = IStartRegionMarker | IEndRegionMarker

const parseRegionMarker = ( fileContent: string ) => ( match: RegExpMatchArray ): RegionMarker => {
    assert( typeof match.index === 'number', new Error( 'Missing index' ) );
    const type = match[1].toLocaleLowerCase() === 'region' ? 'start' : 'end';
    const name = match[2];
    assert( type !== 'start' || name, new Error( 'Missing name of start `#region`' ) );
    const location = textUtils.getCoordinates( fileContent, match.index );
    return { ...location, type, name, fullMatch: match[0] };
};

const assembleStartEndMarkers = ( prevMarkers: Array<{open?: IStartRegionMarker; close?: IEndRegionMarker; name: string}>, marker: RegionMarker ) => {
    if( marker.type === 'start' ){
        assert( !prevMarkers.find( r => r.name === marker.name ), new Error( `Region ${marker.name} already exists` ) );
        prevMarkers.push( {
            open: marker,
            name: marker.name,
        } );
    } else { // End marker
        if( marker.name ){
            const openRegion = prevMarkers.find( r => r.name === marker.name );
            assert( openRegion, new Error( `Missing region ${marker.name} explicitly closed` ) );
            assert( !openRegion.close, new Error( `Region ${marker.name} already closed` ) );
            openRegion.close = marker;
        } else {
            const lastNotClosed = prevMarkers.concat().reverse().find( r => !r.close );
            assert( lastNotClosed, new Error( 'Missing implicitly closed region' ) );
            assert( !lastNotClosed.close, new Error( `Region ${lastNotClosed.name} already closed` ) );
            lastNotClosed.close = marker;
        }
    }
    return prevMarkers;
};

interface IRegion {
    open: IStartRegionMarker;
    close: IEndRegionMarker;
    name: string;
}

const addRegionInSet = ( file: string, contentLines: string[] ) => ( regionsMap: Map<string, ICodeSample>, { open, close, name }: IRegion ) => {
    const code = contentLines.slice( open.line, close.line ).filter( l => !l.match( REGION_REGEX ) ).join( '\n' );
    regionsMap.set( name, {
        file,
        region: name,
        endLine: close.line,
        startLine: open.line,
        code,
    } );
    return regionsMap;
};

export const readCodeSample = ( file: string ): Map<string, ICodeSample> => {
    const content = readFileSync( file, 'utf-8' );
    const lines = content.split( '\n' );
    const regionMarkers = [ ...content.matchAll( REGION_REGEX ) ];

    if( regionMarkers.length === 0 ){
        return new Map( [
            [ DEFAULT_BLOCK_NAME, {
                file,
                region: DEFAULT_BLOCK_NAME,
                code: content,
                endLine: lines.length,
                startLine: 1,
            } ],
        ] );
    }

    return regionMarkers
        .map( parseRegionMarker( content ) )
        .reduce( assembleStartEndMarkers, [] )
        // Check validity of regions
        .map<IRegion>( r => {
            assert( r.open && r.close, new SyntaxError( `Region ${r.name} is not properly opened & closed` ) );
            assert( r.open.line < r.close.line, new SyntaxError( `Region ${r.name} is closed before being opened. Opened at line ${r.open.line}, closed at line ${r.close.line}` ) );
            return r as IRegion;
        } )
        .reduce( addRegionInSet( file, lines ), new Map<string, ICodeSample>() );
};