import * as vscode from 'vscode' ; import * as path from 'path' ; import * as nls from 'vscode-nls' ; const localize = nls . loadMessageBundle ( ) ; import { Logger } from '../util/logger' ; import { ContentSecurityPolicyArbiter , RSTPreviewSecurityLevel } from '../util/security' ; import { RSTPreviewConfigurationManager , RSTPreviewConfiguration } from './previewConfig' ; import { RSTEngine } from './rstEngine' ; /**
 * Strings used inside the html preview.
 *
 * Stored here and then injected in the preview so that they
 * can be localized using our normal localization process.
 */ const previewStrings = { cspAlertMessageText : localize ( 'preview.securityMessage.text' , 'Some content has been disabled in this document' ) , cspAlertMessageTitle : localize ( 'preview.securityMessage.title' , 'Potentially unsafe or insecure content has been disabled in the html preview. Change the HTML preview security setting to allow insecure content or enable scripts' ) , cspAlertMessageLabel : localize ( 'preview.securityMessage.label' , 'Content Disabled Security Warning' ) } ; export class RSTContentProvider { constructor ( private readonly context , private readonly cspArbiter , private readonly engine , private readonly logger ) { } private readonly TAG_RegEx = /^\s*?\<(p|h[1-6]|img|code|blockquote|li)((\s+.*?)(class="(.*?)")(.*?\>)|\>|\>|\/\>|\s+.*?\>)/ ; public async provideTextDocumentContent ( rstDocument , previewConfigurations , webview , initialLine = undefined , state ? ) { const sourceUri = rstDocument . uri ; const config = previewConfigurations . loadAndCacheConfiguration ( sourceUri ) ; const initialData = { source : sourceUri . toString ( ) , line : initialLine , lineCount : rstDocument . lineCount , scrollPreviewWithEditor : config . scrollPreviewWithEditor , scrollEditorWithPreview : config . scrollEditorWithPreview , doubleClickToSwitchToEditor : config . doubleClickToSwitchToEditor , disableSecurityWarnings : this . cspArbiter . shouldDisableSecurityWarnings ( ) } ; const body = await this . engine . preview ( rstDocument , webview ) ; const useSphinx = body . search ( '</head>' ) > - 1 ; const nonce = new Date ( ) . getTime ( ) + '' + new Date ( ) . getMilliseconds ( ) ; const csp = this . getCspForResource ( sourceUri , nonce , useSphinx , webview ) ; if ( useSphinx ) { let elementCount = 0 ; let canStart = false ; const parsedDoc = body . split ( /\r?\n/ ) . map ( ( l ) => { if ( l . search ( '<div itemprop="articleBody">' ) > - 1 ) { canStart = true ; } if ( ! canStart ) { return l ; } return l . replace ( this . TAG_RegEx , ( match , p1 , p2 , p3 , p4 , p5 , p6 , offset ) => { elementCount ++ ; return typeof p5 !== 'string' ? `<${ p1 } class="code-line" data-line="${ elementCount }" ${ p2 }` : `<${ p1 } ${ p3 } class="${ p5 } code-line" data-line="${ elementCount }" ${ p6 }` ; } ) ; } ) . join ( '\n' ) ; const newHead = parsedDoc . replace ( '</head>' , `
			<meta id="vscode-rst-preview-data"
			data-settings="${ JSON . stringify ( initialData ) . replace ( /"/g , '&quot;' ) }"
			data-strings="${ JSON . stringify ( previewStrings ) . replace ( /"/g , '&quot;' ) }"
			data-state="${ JSON . stringify ( state || { } ) . replace ( /"/g , '&quot;' ) }">
		<script src="${ this . extensionResourcePath ( 'pre.js' , webview ) }" nonce="${ nonce }"></script>
		<script src="${ this . extensionResourcePath ( 'index.js' , webview ) }" nonce="${ nonce }"></script>
		<base href="${ webview . asWebviewUri ( rstDocument . uri ) }">
		</head>
			` ) ; const newBody = newHead . replace ( '<body class="' , `<body class="vscode-body ${ config . scrollBeyondLastLine ? 'scrollBeyondLastLine' : '' } ${ config . wordWrap ? 'wordWrap' : '' } ${ config . rstEditorSelection ? 'showEditorSelection' : '' } ` ) ; const newAll = newBody . replace ( '</body>' , `
			    <div class="code-line" data-line="${ rstDocument . lineCount }"></div>
			</body>
			` ) ; this . logger . log ( "[preview] Document line count: " + rstDocument . lineCount + "; element count: " + elementCount ) ; if ( rstDocument . lineCount < elementCount ) { this . logger . log ( "[preview] WARN: documentl line count is less than element count." ) ; } return newAll ; } else { const parsedDoc = body . split ( /\r?\n/ ) . map ( ( l , i ) => l . replace ( this . TAG_RegEx , ( match , p1 , p2 , p3 , p4 , p5 , p6 , offset ) => typeof p5 !== 'string' ? `<${ p1 } class="code-line" data-line="${ i + 1 }" ${ p2 }` : `<${ p1 } ${ p3 } class="${ p5 } code-line" data-line="${ i + 1 }" ${ p6 }` ) ) . join ( '\n' ) ; return `<!DOCTYPE html>
				<html>
				<head>
					<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
					${ csp }
					<meta id="vscode-rst-preview-data"
						data-settings="${ JSON . stringify ( initialData ) . replace ( /"/g , '&quot;' ) }"
						data-strings="${ JSON . stringify ( previewStrings ) . replace ( /"/g , '&quot;' ) }"
						data-state="${ JSON . stringify ( state || { } ) . replace ( /"/g , '&quot;' ) }">
					<script src="${ this . extensionResourcePath ( 'pre.js' , webview ) }" nonce="${ nonce }"></script>
					<script src="${ this . extensionResourcePath ( 'index.js' , webview ) }" nonce="${ nonce }"></script>
					${ this . getStyles ( sourceUri , nonce , config , webview ) }
					<base href="${ webview . asWebviewUri ( rstDocument . uri ) }">
				</head>
				<body class="vscode-body ${ config . scrollBeyondLastLine ? 'scrollBeyondLastLine' : '' } ${ config . wordWrap ? 'wordWrap' : '' } ${ config . rstEditorSelection ? 'showEditorSelection' : '' }">
					${ parsedDoc }
					<div class="code-line" data-line="${ rstDocument . lineCount }"></div>
				</body>
				</html>` ; } } private extensionResourcePath ( mediaFile , webview ) { return webview . asWebviewUri ( vscode . Uri . file ( this . context . asAbsolutePath ( path . join ( 'media' , mediaFile ) ) ) ) . toString ( ) ; } private fixHref ( resource , href , webview ) { if ( ! href ) { return href ; } const hrefUri = vscode . Uri . parse ( href ) ; if ( [ 'http' , 'https' ] . indexOf ( hrefUri . scheme ) >= 0 ) { return hrefUri . toString ( ) ; } if ( path . isAbsolute ( href ) || hrefUri . scheme === 'file' ) { return webview . asWebviewUri ( vscode . Uri . file ( href ) ) . toString ( ) ; } const root = vscode . workspace . getWorkspaceFolder ( resource ) ; if ( root ) { return webview . asWebviewUri ( vscode . Uri . file ( path . join ( root . uri . fsPath , href ) ) ) . toString ( ) ; } return webview . asWebviewUri ( vscode . Uri . file ( path . join ( path . dirname ( resource . fsPath ) , href ) ) ) . toString ( ) ; } private computeCustomStyleSheetIncludes ( resource , config , webview ) { if ( Array . isArray ( config . styles ) ) { return config . styles . map ( ( style ) => { return `<link rel="stylesheet" class="code-user-style" data-source="${ style . replace ( /"/g , '&quot;' ) }" href="${ this . fixHref ( resource , style , webview ) }" type="text/css" media="screen">` ; } ) . join ( '\n' ) ; } return '' ; } private getSettingsOverrideStyles ( nonce , config ) { return `<style nonce="${ nonce }">
			body {
				${ config . fontFamily ? `font-family: ${ config . fontFamily };` : '' }
				${ isNaN ( config . fontSize ) ? '' : `font-size: ${ config . fontSize }px;` }
				${ isNaN ( config . lineHeight ) ? '' : `line-height: ${ config . lineHeight };` }
			}
		</style>` ; } private getStyles ( resource , nonce , config , webview ) { const fix = ( href ) => webview . asWebviewUri ( vscode . Uri . file ( href ) ) . toString ( ) ; const baseStyles = config . baseStyles . map ( ( href ) => `<link rel="stylesheet" type="text/css" href="${ fix ( href ) }">` ) . join ( '\n' ) ; return `${ baseStyles }
			${ this . getSettingsOverrideStyles ( nonce , config ) }
			${ this . computeCustomStyleSheetIncludes ( resource , config , webview ) }` ; } private getCspForResource ( resource , nonce , useSphinx , webview ) { let securityLevel = this . cspArbiter . getSecurityLevelForResource ( resource ) ; if ( useSphinx ) { securityLevel = RSTPreviewSecurityLevel . AllowScriptsAndAllContent ; } switch ( securityLevel ) { case RSTPreviewSecurityLevel . AllowInsecureContent : return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${ webview . cspSource } http: https: data:; media-src ${ webview . cspSource } http: https: data:; script-src 'nonce-${ nonce }'; style-src ${ webview . cspSource } 'unsafe-inline' http: https: data:; font-src ${ webview . cspSource } http: https: data:;">` ; case RSTPreviewSecurityLevel . AllowInsecureLocalContent : return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${ webview . cspSource } https: data: http://localhost:* http://127.0.0.1:*; media-src ${ webview . cspSource } https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${ nonce }'; style-src ${ webview . cspSource } 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src ${ webview . cspSource } https: data: http://localhost:* http://127.0.0.1:*;">` ; case RSTPreviewSecurityLevel . AllowScriptsAndAllContent : return '' ; case RSTPreviewSecurityLevel . Strict : default : return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${ webview . cspSource } https: data:; media-src ${ webview . cspSource } https: data:; script-src 'nonce-${ nonce }'; style-src ${ webview . cspSource } 'unsafe-inline' https: data:; font-src ${ webview . cspSource } https: data:;">` ; } } }