import { cleanAndSplitDocString } from './docStringUtils' ; export function convertDocStringToMarkdown ( docString ) { return new DocStringConverter ( docString ) . convert ( ) ; } export function convertDocStringToPlainText ( docString ) { const lines = cleanAndSplitDocString ( docString ) ; const output = [ ] ; for ( const line of lines ) { const last = output . length > 0 ? output [ output . length - 1 ] : undefined ; if ( _isUndefinedOrWhitespace ( line ) && _isUndefinedOrWhitespace ( last ) ) { continue ; } output . push ( line ) ; } return output . join ( '\n' ) . trimEnd ( ) ; } interface RegExpReplacement { exp : RegExp ; replacement : string ; } const LeadingSpaceCountRegExp = /\S|$/ ; const NonWhitespaceRegExp = /\S/ ; const TildaHeaderRegExp = /^\s*~~~+$/ ; const PlusHeaderRegExp = /^\s*\+\+\++$/ ; const EqualHeaderRegExp = /^\s*===+\s+===+$/ ; const DashHeaderRegExp = /^\s*---+\s+---+$/ ; const LeadingDashListRegExp = /^(\s*)-\s/ ; const LeadingAsteriskListRegExp = /^(\s*)\*\s/ ; const LeadingNumberListRegExp = /^(\s*)\d+\.\s/ ; const LeadingAsteriskRegExp = /^(\s+\* )(.*)$/ ; const SpaceDotDotRegExp = /^\s*\.\. / ; const DirectiveLikeRegExp = /^\s*\.\.\s+(\w+)::\s*(.*)$/ ; const DoctestRegExp = / *>>> / ; const DirectivesExtraNewlineRegExp = /^\s*:(param|arg|type|return|rtype|raise|except|var|ivar|cvar|copyright|license)/ ; const epyDocFieldTokensRegExp = /^[.\s\t]+(@\w+)/ ; const epyDocCv2FixRegExp = /^(\.\s{3})|^(\.)/ ; const PotentialHeaders = [ { exp : /^\s*=+(\s+=+)+$/ , replacement : '=' } , { exp : /^\s*-+(\s+-+)+$/ , replacement : '-' } , { exp : /^\s*~+(\s+-+)+$/ , replacement : '~' } , { exp : /^\s*\++(\s+\++)+$/ , replacement : '+' } , ] ; const WhitespaceRegExp = /\s/g ; const DoubleTickRegExp = /``/g ; const TildeRegExp = /~/g ; const PlusRegExp = /\+/g ; const UnescapedMarkdownCharsRegExp = /(?<!\\)([_*~[\]])/g ; const linkRegExp = /(\[.*\]\(.*\))/g ; const HtmlEscapes = [ { exp : /</g , replacement : '&lt;' } , { exp : />/g , replacement : '&gt;' } , ] ; const LiteralBlockEmptyRegExp = /^\s*::$/ ; const LiteralBlockReplacements = [ { exp : /\s+::$/g , replacement : '' } , { exp : /(\S)\s*::$/g , replacement : '$1:' } , { exp : /:[\w_\-+:.]+:`/g , replacement : '`' } , { exp : /`:[\w_\-+:.]+:/g , replacement : '`' } , ] ; type State = ( ) => void ; interface RestTableState { header : string ; inHeader : boolean ; } class DocStringConverter { private _builder = '' ; private _skipAppendEmptyLine = true ; private _insideInlineCode = false ; private _appendDirectiveBlock = false ; private _state : State ; private _stateStack : State [ ] = [ ] ; private _lines : string [ ] ; private _lineNum = 0 ; private _blockIndent = 0 ; private _tableState : RestTableState | undefined ; constructor ( input ) { this . _state = this . _parseText ; this . _lines = cleanAndSplitDocString ( input ) ; } convert ( ) { const isEpyDoc = this . _lines . some ( ( v ) => epyDocFieldTokensRegExp . exec ( v ) ) ; if ( isEpyDoc ) { this . _lines = this . _lines . map ( ( v ) => v . replace ( epyDocCv2FixRegExp , '' ) ) ; } while ( this . _currentLineOrUndefined ( ) !== undefined ) { const before = this . _state ; const beforeLine = this . _lineNum ; this . _state ( ) ; if ( this . _state === before && this . _lineNum === beforeLine ) { break ; } } if ( this . _state === this . _parseBacktickBlock || this . _state === this . _parseDocTest || this . _state === this . _parseLiteralBlock ) { this . _trimOutputAndAppendLine ( '```' ) ; } else if ( this . _insideInlineCode ) { this . _trimOutputAndAppendLine ( '`' , true ) ; } return this . _builder . trim ( ) ; } private _eatLine ( ) { this . _lineNum ++ ; } private _currentLineOrUndefined ( ) { return this . _lineNum < this . _lines . length ? this . _lines [ this . _lineNum ] : undefined ; } private _currentLine ( ) { return this . _currentLineOrUndefined ( ) || '' ; } private _currentIndent ( ) { return _countLeadingSpaces ( this . _currentLine ( ) ) ; } private _prevIndent ( ) { return _countLeadingSpaces ( this . _lineAt ( this . _lineNum - 1 ) ?? '' ) ; } private _lineAt ( i ) { return i < this . _lines . length ? this . _lines [ i ] : undefined ; } private _nextBlockIndent ( ) { return _countLeadingSpaces ( this . _lines . slice ( this . _lineNum + 1 ) . find ( ( v ) => ! _isUndefinedOrWhitespace ( v ) ) || '' ) ; } private _currentLineIsOutsideBlock ( ) { return this . _currentIndent ( ) < this . _blockIndent ; } private _currentLineWithinBlock ( ) { return this . _currentLine ( ) . substr ( this . _blockIndent ) ; } private _pushAndSetState ( next ) { if ( this . _state === this . _parseText ) { this . _insideInlineCode = false ; } this . _stateStack . push ( this . _state ) ; this . _state = next ; } private _popState ( ) { this . _state = this . _stateStack . splice ( 0 , 1 ) [ 0 ] ; if ( this . _state === this . _parseText ) { this . _insideInlineCode = false ; } } private _parseText ( ) { if ( _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) ) { this . _state = this . _parseEmpty ; return ; } if ( this . _beginBacktickBlock ( ) ) { return ; } if ( this . _beginLiteralBlock ( ) ) { return ; } if ( this . _beginDocTest ( ) ) { return ; } if ( this . _beginDirective ( ) ) { return ; } if ( this . _beginList ( ) ) { return ; } if ( this . _beginFieldList ( ) ) { return ; } if ( this . _beginTableBlock ( ) ) { return ; } const line = this . _formatPlainTextIndent ( this . _currentLine ( ) ) ; this . _appendTextLine ( line ) ; this . _eatLine ( ) ; } private _formatPlainTextIndent ( line ) { const prev = this . _lineAt ( this . _lineNum - 1 ) ; const prevIndent = this . _prevIndent ( ) ; const currIndent = this . _currentIndent ( ) ; if ( currIndent > prevIndent && ! _isUndefinedOrWhitespace ( prev ) && ! this . _builder . endsWith ( '\\\n' ) && ! this . _builder . endsWith ( '\n\n' ) && ! _isHeader ( prev ) ) { this . _builder = this . _builder . slice ( 0 , - 1 ) + '\\\n' ; } if ( prevIndent > currIndent && ! _isUndefinedOrWhitespace ( prev ) && ! this . _builder . endsWith ( '\\\n' ) && ! this . _builder . endsWith ( '\n\n' ) ) { this . _builder = this . _builder . slice ( 0 , - 1 ) + '\\\n' ; } if ( prevIndent === 0 || this . _builder . endsWith ( '\\\n' ) || this . _builder . endsWith ( '\n\n' ) ) { line = this . _convertIndent ( line ) ; } else { line = line . trimStart ( ) ; } return line ; } private _convertIndent ( line ) { line = line . replace ( /^([ \t]+)(.+)$/g , ( _match , g1 , g2 ) => '&nbsp;' . repeat ( g1 . length ) + g2 ) ; return line ; } private _escapeHtml ( line ) { HtmlEscapes . forEach ( ( escape ) => { line = line . replace ( escape . exp , escape . replacement ) ; } ) ; return line ; } private _appendTextLine ( line ) { line = this . _preprocessTextLine ( line ) ; const parts = line . split ( '`' ) ; for ( let i = 0 ; i < parts . length ; i ++ ) { let part = parts [ i ] ; if ( i > 0 ) { this . _insideInlineCode = ! this . _insideInlineCode ; this . _append ( '`' ) ; } if ( this . _insideInlineCode ) { this . _append ( part ) ; continue ; } part = this . _escapeHtml ( part ) ; if ( i === 0 ) { if ( parts . length === 1 ) { for ( const expReplacement of PotentialHeaders ) { if ( expReplacement . exp . test ( part ) ) { part = part . replace ( WhitespaceRegExp , expReplacement . replacement ) ; break ; } } if ( TildaHeaderRegExp . test ( part ) ) { this . _append ( part . replace ( TildeRegExp , '-' ) ) ; continue ; } if ( PlusHeaderRegExp . test ( part ) ) { this . _append ( part . replace ( PlusRegExp , '-' ) ) ; continue ; } } const match = LeadingAsteriskRegExp . exec ( part ) ; if ( match !== null && match . length === 3 ) { this . _append ( match [ 1 ] ) ; part = match [ 2 ] ; } } const subparts = part . split ( linkRegExp ) ; subparts . forEach ( ( item ) => { if ( linkRegExp . test ( item ) ) { this . _append ( item ) ; } else { this . _append ( item . replace ( UnescapedMarkdownCharsRegExp , '\\$1' ) ) ; } } ) ; } this . _builder += '\n' ; } private _preprocessTextLine ( line ) { if ( LiteralBlockEmptyRegExp . test ( line ) ) { return '' ; } LiteralBlockReplacements . forEach ( ( item ) => ( line = line . replace ( item . exp , item . replacement ) ) ) ; line = line . replace ( DoubleTickRegExp , '`' ) ; return line ; } private _parseEmpty ( ) { if ( _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) ) { this . _appendLine ( ) ; this . _eatLine ( ) ; return ; } this . _state = this . _parseText ; } private _beginMinIndentCodeBlock ( state ) { this . _appendLine ( '```' ) ; this . _pushAndSetState ( state ) ; this . _blockIndent = this . _currentIndent ( ) ; } private _beginBacktickBlock ( ) { if ( this . _currentLine ( ) . startsWith ( '```' ) ) { this . _appendLine ( this . _currentLine ( ) ) ; this . _pushAndSetState ( this . _parseBacktickBlock ) ; this . _eatLine ( ) ; return true ; } return false ; } private _parseBacktickBlock ( ) { if ( this . _currentLine ( ) . startsWith ( '```' ) ) { this . _appendLine ( '```' ) ; this . _appendLine ( ) ; this . _popState ( ) ; } else { this . _appendLine ( this . _currentLine ( ) ) ; } this . _eatLine ( ) ; } private _beginDocTest ( ) { if ( ! DoctestRegExp . test ( this . _currentLine ( ) ) ) { return false ; } this . _beginMinIndentCodeBlock ( this . _parseDocTest ) ; this . _appendLine ( this . _currentLineWithinBlock ( ) ) ; this . _eatLine ( ) ; return true ; } private _parseDocTest ( ) { if ( this . _currentLineIsOutsideBlock ( ) || _isUndefinedOrWhitespace ( this . _currentLine ( ) ) ) { this . _trimOutputAndAppendLine ( '```' ) ; this . _appendLine ( ) ; this . _popState ( ) ; return ; } this . _appendLine ( this . _currentLineWithinBlock ( ) ) ; this . _eatLine ( ) ; } private _beginLiteralBlock ( ) { const prev = this . _lineAt ( this . _lineNum - 1 ) ; if ( prev === undefined ) { return false ; } else if ( ! _isUndefinedOrWhitespace ( prev ) ) { return false ; } let i = this . _lineNum - 2 ; for ( ; i >= 0 ; i -- ) { const line = this . _lineAt ( i ) ; if ( _isUndefinedOrWhitespace ( line ) ) { continue ; } if ( line ! . endsWith ( '::' ) ) { break ; } return false ; } if ( i < 0 ) { return false ; } if ( this . _currentIndent ( ) === 0 ) { this . _appendLine ( '```' ) ; this . _pushAndSetState ( this . _parseLiteralBlockSingleLine ) ; return true ; } this . _beginMinIndentCodeBlock ( this . _parseLiteralBlock ) ; return true ; } private _parseLiteralBlock ( ) { if ( _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) ) { this . _appendLine ( ) ; this . _eatLine ( ) ; return ; } if ( this . _currentLineIsOutsideBlock ( ) ) { this . _trimOutputAndAppendLine ( '```' ) ; this . _appendLine ( ) ; this . _popState ( ) ; return ; } this . _appendLine ( this . _currentLineWithinBlock ( ) ) ; this . _eatLine ( ) ; } private _parseLiteralBlockSingleLine ( ) { this . _appendLine ( this . _currentLine ( ) ) ; this . _appendLine ( '```' ) ; this . _appendLine ( ) ; this . _popState ( ) ; this . _eatLine ( ) ; } private _beginDirective ( ) { if ( ! SpaceDotDotRegExp . test ( this . _currentLine ( ) ) ) { return false ; } this . _pushAndSetState ( this . _parseDirective ) ; this . _blockIndent = this . _nextBlockIndent ( ) ; this . _appendDirectiveBlock = false ; return true ; } private _beginFieldList ( ) { if ( this . _insideInlineCode ) { return false ; } let line = this . _currentLine ( ) ; if ( line . startsWith ( '@' ) ) { this . _appendLine ( ) ; this . _appendTextLine ( line ) ; this . _eatLine ( ) ; return true ; } const hasOddNumColons = ! line ?. endsWith ( ':' ) && ! line ?. endsWith ( '::' ) && ( line . match ( /:/g ) ?. length ?? 0 ) % 2 === 1 ; const restDirective = DirectivesExtraNewlineRegExp . test ( line ) ; if ( hasOddNumColons || restDirective ) { const prev = this . _lineAt ( this . _lineNum - 1 ) ; if ( ! this . _builder . endsWith ( `\\\n` ) && ! this . _builder . endsWith ( `\n\n` ) && ! _isHeader ( prev ) ) { this . _builder = this . _builder . slice ( 0 , - 1 ) + '\\\n' ; } line = this . _convertIndent ( line ) ; this . _appendTextLine ( line ) ; this . _eatLine ( ) ; return true ; } return false ; } private _beginTableBlock ( ) { if ( this . _insideInlineCode ) { return false ; } const line = this . _currentLine ( ) ; if ( EqualHeaderRegExp . test ( line ) ) { this . _tableState = { header : line . trimStart ( ) , inHeader : true } ; this . _eatLine ( ) ; this . _pushAndSetState ( this . _parseTableBlock ) ; return true ; } return false ; } private _parseTableBlock ( ) { if ( _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) || ! this . _tableState ) { this . _tableState = undefined ; this . _popState ( ) ; return ; } let line = this . _currentLine ( ) ; if ( EqualHeaderRegExp . test ( line ) ) { this . _eatLine ( ) ; this . _appendLine ( '\n<br/>\n' ) ; this . _popState ( ) ; this . _tableState = undefined ; return ; } else { let formattedLine = '|' ; const columnParts = this . _tableState . header . split ( ' ' ) ; const headerStrings = [ ] ; if ( this . _tableState . inHeader ) { do { let colStart = 0 ; for ( let i = 0 ; i < columnParts . length ; i ++ ) { const equalStr = columnParts [ i ] ; const len = equalStr . length + 1 ; const columnStr = line . slice ( colStart , colStart + len ) ; if ( headerStrings [ i ] === undefined ) { headerStrings [ i ] = `${ columnStr } ` ; } else { headerStrings [ i ] = headerStrings [ i ] . concat ( `<br>${ columnStr } ` ) ; } colStart += len ; } this . _eatLine ( ) ; line = this . _currentLine ( ) ; } while ( ! _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) && ! DashHeaderRegExp . test ( line ) && ! EqualHeaderRegExp . test ( line ) ) ; this . _tableState . inHeader = false ; headerStrings . forEach ( ( h ) => { formattedLine += `${ h }|` ; } ) ; this . _appendLine ( formattedLine ) ; const endHeaderStr = line . trimStart ( ) . replace ( /=/g , '-' ) . replace ( ' ' , '|' ) ; this . _appendLine ( `|${ endHeaderStr }|` ) ; this . _eatLine ( ) ; } else { let colStart = 0 ; columnParts . forEach ( ( column ) => { const len = column . length + 1 ; const columnStr = line . slice ( colStart , colStart + len ) ; formattedLine += `${ columnStr }|` ; colStart += len ; } ) ; this . _appendLine ( formattedLine ) ; this . _eatLine ( ) ; } } } private _beginList ( ) { if ( this . _insideInlineCode ) { return false ; } let line = this . _currentLine ( ) ; const dashMatch = LeadingDashListRegExp . exec ( line ) ; if ( dashMatch ?. length === 2 ) { if ( dashMatch [ 1 ] . length >= 4 ) { line = ' ' . repeat ( dashMatch [ 1 ] . length / 2 ) + line . trimLeft ( ) ; } this . _appendTextLine ( line ) ; this . _eatLine ( ) ; if ( this . _state !== this . _parseList ) { this . _pushAndSetState ( this . _parseList ) ; } return true ; } const asteriskMatch = LeadingAsteriskListRegExp . exec ( line ) ; if ( asteriskMatch ?. length === 2 ) { if ( asteriskMatch [ 1 ] . length === 0 ) { line = line = ' ' + line ; } else if ( asteriskMatch [ 1 ] . length >= 4 ) { line = ' ' . repeat ( asteriskMatch [ 1 ] . length / 2 ) + line . trimLeft ( ) ; } this . _appendTextLine ( line ) ; this . _eatLine ( ) ; if ( this . _state !== this . _parseList ) { this . _pushAndSetState ( this . _parseList ) ; } return true ; } const leadingNumberList = LeadingNumberListRegExp . exec ( line ) ; if ( leadingNumberList ?. length === 2 ) { this . _appendTextLine ( line ) ; this . _eatLine ( ) ; return true ; } return false ; } private _parseList ( ) { if ( _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) || this . _currentLineIsOutsideBlock ( ) ) { this . _popState ( ) ; return ; } const isMultiLineItem = ! this . _beginList ( ) ; if ( isMultiLineItem ) { const line = this . _currentLine ( ) . trimStart ( ) ; this . _appendTextLine ( line ) ; this . _eatLine ( ) ; } } private _parseDirective ( ) { const match = DirectiveLikeRegExp . exec ( this . _currentLine ( ) ) ; if ( match !== null && match . length === 3 ) { const directiveType = match [ 1 ] ; const directive = match [ 2 ] ; if ( directiveType === 'class' ) { this . _appendDirectiveBlock = true ; this . _appendLine ( ) ; this . _appendLine ( '```' ) ; this . _appendLine ( directive ) ; this . _appendLine ( '```' ) ; this . _appendLine ( ) ; } } if ( this . _blockIndent === 0 ) { this . _popState ( ) ; } else { this . _state = this . _parseDirectiveBlock ; } this . _eatLine ( ) ; } private _parseDirectiveBlock ( ) { if ( ! _isUndefinedOrWhitespace ( this . _currentLineOrUndefined ( ) ) && this . _currentLineIsOutsideBlock ( ) ) { this . _popState ( ) ; return ; } if ( this . _appendDirectiveBlock ) { this . _appendTextLine ( this . _currentLine ( ) . trimLeft ( ) ) ; } this . _eatLine ( ) ; } private _appendLine ( line ? ) { if ( ! _isUndefinedOrWhitespace ( line ) ) { this . _builder += line + '\n' ; this . _skipAppendEmptyLine = false ; } else if ( ! this . _skipAppendEmptyLine ) { this . _builder += '\n' ; this . _skipAppendEmptyLine = true ; } } private _append ( text ) { this . _builder += text ; this . _skipAppendEmptyLine = false ; } private _trimOutputAndAppendLine ( line , noNewLine = false ) { this . _builder = this . _builder . trimRight ( ) ; this . _skipAppendEmptyLine = false ; if ( ! noNewLine ) { this . _appendLine ( ) ; } this . _appendLine ( line ) ; } } function _countLeadingSpaces ( s ) { return s . search ( LeadingSpaceCountRegExp ) ; } function _isUndefinedOrWhitespace ( s ) { return s === undefined || ! NonWhitespaceRegExp . test ( s ) ; } function _isHeader ( line ) { return line !== undefined && ( line . match ( /^\s*[#`~=-]{3,}/ ) ?. length ?? 0 ) > 0 ; }