Skip to content

Commit

Permalink
Use shared function for rendering markdown
Browse files Browse the repository at this point in the history
Fixes #1740
  • Loading branch information
felixfbecker committed Jan 9, 2019
1 parent a0e7d0e commit b47b35c
Show file tree
Hide file tree
Showing 19 changed files with 94 additions and 101 deletions.
1 change: 0 additions & 1 deletion client/browser/tslint.json
Expand Up @@ -5,7 +5,6 @@
"deprecation": {
"severity": "warning"
},
"import-blacklist": [true, "highlight.js"],
"jsx-ban-props": false
}
}
2 changes: 1 addition & 1 deletion packages/@sourcegraph/extension-api-types/package.json
Expand Up @@ -19,7 +19,7 @@
],
"sideEffects": false,
"scripts": {
"tslint": "tslint -c tslint.json -p tsconfig.json './src/**/*.{ts,js}'",
"tslint": "tslint -t stylish -c tslint.json -p tsconfig.json './src/**/*.{ts,js}'",
"prepublishOnly": "yarn run tslint"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/sourcegraph-extension-api/package.json
Expand Up @@ -21,7 +21,7 @@
],
"sideEffects": false,
"scripts": {
"tslint": "tslint -c tslint.json -p tsconfig.json './src/**/*.{ts,js}'",
"tslint": "tslint -t stylish -c tslint.json -p tsconfig.json './src/**/*.{ts,js}'",
"docs": "typedoc",
"prepublishOnly": "yarn run tslint && yarn run docs"
}
Expand Down
2 changes: 1 addition & 1 deletion shared/package.json
Expand Up @@ -17,7 +17,7 @@
},
"scripts": {
"tslint:build-rules": "tsc --skipLibCheck --lib es6 --module commonjs dev/tslint/*.ts",
"tslint": "yarn -s run tslint:build-rules && tslint -c tslint.json -p tsconfig.json",
"tslint": "yarn -s run tslint:build-rules && tslint -t stylish -c tslint.json -p tsconfig.json",
"test": "jest",
"graphql": "gulp graphQLTypes",
"schema": "gulp schema",
Expand Down
2 changes: 2 additions & 0 deletions shared/src/highlight/contributions.ts
Expand Up @@ -39,4 +39,6 @@ export function registerHighlightContributions(): void {
registerLanguage('swift', require('highlight.js/lib/languages/swift'))
registerLanguage('markdown', require('highlight.js/lib/languages/markdown'))
registerLanguage('diff', require('highlight.js/lib/languages/diff'))
registerLanguage('json', require('highlight.js/lib/languages/json'))
registerLanguage('yaml', require('highlight.js/lib/languages/yaml'))
}
3 changes: 2 additions & 1 deletion shared/src/hover/HoverOverlay.tsx
Expand Up @@ -10,8 +10,9 @@ import { HoverMerged } from '../api/client/types/hover'
import { TelemetryContext } from '../telemetry/telemetryContext'
import { TelemetryService } from '../telemetry/telemetryService'
import { isErrorLike } from '../util/errors'
import { highlightCodeSafe, renderMarkdown } from '../util/markdown'
import { FileSpec, RepoSpec, ResolvedRevSpec, RevSpec } from '../util/url'
import { highlightCodeSafe, renderMarkdown, toNativeEvent } from './helpers'
import { toNativeEvent } from './helpers'

const LOADING: 'loading' = 'loading'

Expand Down
59 changes: 0 additions & 59 deletions shared/src/hover/helpers.ts
@@ -1,63 +1,4 @@
import { highlight, highlightAuto } from 'highlight.js/lib/highlight'
import marked from 'marked'
import * as React from 'react'
import sanitize from 'sanitize-html'

/**
* Escapes HTML by replacing characters like `<` with their HTML escape sequences like `&lt;`
*/
const escapeHTML = (html: string): string => {
const span = document.createElement('span')
span.textContent = html
return span.innerHTML
}

/**
* Attempts to syntax-highlight the given code.
* If the language is not given, it is auto-detected.
* If an error occurs, the code is returned as plain text with escaped HTML entities
*
* @param code The code to highlight
* @param language The language of the code, if known
* @return Safe HTML
*/
export const highlightCodeSafe = (code: string, language?: string): string => {
try {
if (language === 'plaintext' || language === 'text') {
return escapeHTML(code)
}
if (language) {
return highlight(language, code, true).value
}
return highlightAuto(code).value
} catch (err) {
console.warn('Error syntax-highlighting hover markdown code block', err)
return escapeHTML(code)
}
}

/**
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
* Can throw an exception on parse errors.
*/
export const renderMarkdown = (markdown: string): string => {
const rendered = marked(markdown, {
gfm: true,
breaks: true,
sanitize: false,
highlight: (code, language) => highlightCodeSafe(code, language),
})
return sanitize(rendered, {
// Allow highligh.js styles, e.g.
// <span class="hljs-keyword">
// <code class="language-javascript">
allowedTags: [...sanitize.defaults.allowedTags, 'span'],
allowedAttributes: {
span: ['class'],
code: ['class'],
},
})
}

/**
* Converts a synthetic React event to a persisted, native Event object.
Expand Down
4 changes: 1 addition & 3 deletions shared/src/notifications/NotificationItem.tsx
@@ -1,9 +1,9 @@
import marked from 'marked'
import * as React from 'react'
import { from, Subject, Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, scan, switchMap } from 'rxjs/operators'
import { Progress } from 'sourcegraph'
import { MessageType } from '../api/client/services/notifications'
import { renderMarkdown } from '../util/markdown'
import { Notification } from './notification'

interface Props {
Expand All @@ -16,8 +16,6 @@ interface State {
progress?: Required<Progress>
}

const renderMarkdown = (md: string): string => marked(md, { gfm: true, breaks: true, sanitize: false })

/**
* A notification message displayed in a {@link module:./Notifications.Notifications} component.
*/
Expand Down
4 changes: 2 additions & 2 deletions shared/src/panel/views/PanelView.tsx
@@ -1,5 +1,4 @@
import H from 'history'
import marked from 'marked'
import React from 'react'
import { Observable } from 'rxjs'
import { PanelViewWithComponent, ViewProviderRegistrationOptions } from '../../api/client/services/view'
Expand All @@ -8,6 +7,7 @@ import { Markdown } from '../../components/Markdown'
import { ExtensionsControllerProps } from '../../extensions/controller'
import { SettingsCascadeProps } from '../../settings/settings'
import { createLinkClickHandler } from '../../util/linkClickHandler'
import { renderMarkdown } from '../../util/markdown'
import { EmptyPanelView } from './EmptyPanelView'
import { HierarchicalLocationsView } from './HierarchicalLocationsView'

Expand All @@ -34,7 +34,7 @@ export class PanelView extends React.PureComponent<Props, State> {
>
{this.props.panelView.content && (
<div className="px-2 pt-2">
<Markdown dangerousInnerHTML={marked(this.props.panelView.content)} />
<Markdown dangerousInnerHTML={renderMarkdown(this.props.panelView.content)} />
</div>
)}
{this.props.panelView.reactElement}
Expand Down
60 changes: 60 additions & 0 deletions shared/src/util/markdown.ts
@@ -0,0 +1,60 @@
import { highlight, highlightAuto } from 'highlight.js/lib/highlight'
// tslint:disable-next-line:import-blacklist this is the only file allowed to import this module, all other modules must use renderMarkdown() exported from here
import marked from 'marked'
import sanitize from 'sanitize-html'

/**
* Escapes HTML by replacing characters like `<` with their HTML escape sequences like `&lt;`
*/
const escapeHTML = (html: string): string => {
const span = document.createElement('span')
span.textContent = html
return span.innerHTML
}

/**
* Attempts to syntax-highlight the given code.
* If the language is not given, it is auto-detected.
* If an error occurs, the code is returned as plain text with escaped HTML entities
*
* @param code The code to highlight
* @param language The language of the code, if known
* @return Safe HTML
*/
export const highlightCodeSafe = (code: string, language?: string): string => {
try {
if (language === 'plaintext' || language === 'text') {
return escapeHTML(code)
}
if (language) {
return highlight(language, code, true).value
}
return highlightAuto(code).value
} catch (err) {
console.warn('Error syntax-highlighting hover markdown code block', err)
return escapeHTML(code)
}
}

/**
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
* Can throw an exception on parse errors.
*/
export const renderMarkdown = (markdown: string): string => {
const rendered = marked(markdown, {
gfm: true,
breaks: true,
sanitize: false,
highlight: (code, language) => highlightCodeSafe(code, language),
})
return sanitize(rendered, {
// Allow highligh.js styles, e.g.
// <span class="hljs-keyword">
// <code class="language-javascript">
allowedTags: [...sanitize.defaults.allowedTags, 'span'],
allowedAttributes: {
span: ['class'],
code: ['class'],
},
})
}
1 change: 0 additions & 1 deletion shared/tslint.json
Expand Up @@ -3,7 +3,6 @@
"linterOptions": { "exclude": ["node_modules/**", "coverage/**"] },
"rulesDirectory": "dev/tslint",
"rules": {
"import-blacklist": [true, "highlight.js"],
"ban-imports": [
true,
"^react-router(-dom)?",
Expand Down
2 changes: 1 addition & 1 deletion tslint.json
Expand Up @@ -3,7 +3,7 @@
"linterOptions": { "exclude": ["node_modules/**"] },
"rules": {
"await-promise": false,
"import-blacklist": [true, "highlight.js"],
"import-blacklist": [true, "highlight.js", "marked"],
"ban": [
true,
{ "name": ["assert", "strictEqual"], "message": "Use jest matchers instead." },
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/SearchResult.tsx
@@ -1,9 +1,9 @@
import { decode } from 'he'
import marked from 'marked'
import FileIcon from 'mdi-react/FileIcon'
import React from 'react'
import { ResultContainer } from '../../../shared/src/components/ResultContainer'
import * as GQL from '../../../shared/src/graphql/schema'
import { renderMarkdown } from '../../../shared/src/util/markdown'
import { SearchResultMatch } from './SearchResultMatch'

export interface HighlightRange {
Expand Down Expand Up @@ -37,7 +37,7 @@ export class SearchResult extends React.Component<Props> {
dangerouslySetInnerHTML={{
__html: this.props.result.label.html
? decode(this.props.result.label.html)
: marked(this.props.result.label.text, { gfm: true, breaks: true, sanitize: true }),
: renderMarkdown(this.props.result.label.text),
}}
/>
{this.props.result.detail && (
Expand All @@ -47,7 +47,7 @@ export class SearchResult extends React.Component<Props> {
dangerouslySetInnerHTML={{
__html: this.props.result.detail.html
? decode(this.props.result.detail.html)
: marked(this.props.result.detail.text, { gfm: true, breaks: true, sanitize: true }),
: renderMarkdown(this.props.result.detail.text),
}}
/>
</>
Expand Down
4 changes: 2 additions & 2 deletions web/src/extensions/explore/ExtensionViewsExploreSection.tsx
@@ -1,5 +1,4 @@
import H from 'history'
import marked from 'marked'
import React from 'react'
import { Subscription } from 'rxjs'
import { map } from 'rxjs/operators'
Expand All @@ -8,6 +7,7 @@ import { ContributableViewContainer } from '../../../../shared/src/api/protocol'
import { Markdown } from '../../../../shared/src/components/Markdown'
import { ExtensionsControllerProps } from '../../../../shared/src/extensions/controller'
import { createLinkClickHandler } from '../../../../shared/src/util/linkClickHandler'
import { renderMarkdown } from '../../../../shared/src/util/markdown'

interface Props extends ExtensionsControllerProps {
history: H.History
Expand Down Expand Up @@ -52,7 +52,7 @@ export class ExtensionViewsExploreSection extends React.PureComponent<Props, Sta
<div key={i} className="mt-5">
<h2>{view.title}</h2>
<div onClick={createLinkClickHandler(this.props.history)}>
<Markdown dangerousInnerHTML={marked(view.content)} />
<Markdown dangerousInnerHTML={renderMarkdown(view.content)} />
</div>
</div>
))}
Expand Down
21 changes: 11 additions & 10 deletions web/src/extensions/extension/RegistryExtensionREADME.tsx
@@ -1,9 +1,9 @@
import marked from 'marked'
import * as React from 'react'
import { Link } from 'react-router-dom'
import { Markdown } from '../../../../shared/src/components/Markdown'
import { ConfiguredRegistryExtension } from '../../../../shared/src/extensions/extension'
import { isErrorLike } from '../../../../shared/src/util/errors'
import { renderMarkdown } from '../../../../shared/src/util/markdown'
import { ExtensionNoManifestAlert } from './RegistryExtensionManifestPage'

const PublishNewManifestAlert: React.FunctionComponent<{
Expand All @@ -14,14 +14,15 @@ const PublishNewManifestAlert: React.FunctionComponent<{
}> = ({ extension, text, buttonLabel, alertClass }) => (
<div className={`alert ${alertClass}`}>
{text}
{extension.registryExtension && extension.registryExtension.viewerCanAdminister && (
<>
<br />
<Link className="mt-3 btn btn-primary" to={`${extension.registryExtension.url}/-/releases/new`}>
{buttonLabel}
</Link>
</>
)}
{extension.registryExtension &&
extension.registryExtension.viewerCanAdminister && (
<>
<br />
<Link className="mt-3 btn btn-primary" to={`${extension.registryExtension.url}/-/releases/new`}>
{buttonLabel}
</Link>
</>
)}
</div>
)

Expand Down Expand Up @@ -58,7 +59,7 @@ export const ExtensionREADME: React.FunctionComponent<{
}

try {
const html = marked(manifest.readme, { gfm: true, breaks: true, sanitize: true })
const html = renderMarkdown(manifest.readme)
return <Markdown dangerousInnerHTML={html} />
} catch (err) {
return (
Expand Down
4 changes: 2 additions & 2 deletions web/src/global/GlobalAlert.tsx
@@ -1,10 +1,10 @@
import marked from 'marked'
import ErrorIcon from 'mdi-react/ErrorIcon'
import InformationIcon from 'mdi-react/InformationIcon'
import WarningIcon from 'mdi-react/WarningIcon'
import React from 'react'
import { Markdown } from '../../../shared/src/components/Markdown'
import * as GQL from '../../../shared/src/graphql/schema'
import { renderMarkdown } from '../../../shared/src/util/markdown'
import { DismissibleAlert } from '../components/DismissibleAlert'

/**
Expand All @@ -18,7 +18,7 @@ export const GlobalAlert: React.FunctionComponent<{ alert: GQL.IAlert; className
const content = (
<>
<Icon className="icon-inline mr-2 flex-shrink-0" />
<Markdown dangerousInnerHTML={marked(alert.message, { gfm: true, breaks: true, sanitize: true })} />
<Markdown dangerousInnerHTML={renderMarkdown(alert.message)} />
</>
)
const className = `${commonClassName} alert alert-${alertClassForType(alert.type)} d-flex`
Expand Down
4 changes: 2 additions & 2 deletions web/src/global/GlobalAlerts.tsx
@@ -1,8 +1,8 @@
import marked from 'marked'
import * as React from 'react'
import { Subscription } from 'rxjs'
import { Markdown } from '../../../shared/src/components/Markdown'
import { isSettingsValid, SettingsCascadeProps } from '../../../shared/src/settings/settings'
import { renderMarkdown } from '../../../shared/src/util/markdown'
import { DismissibleAlert } from '../components/DismissibleAlert'
import { Settings } from '../schema/settings.schema'
import { SiteFlags } from '../site'
Expand Down Expand Up @@ -80,7 +80,7 @@ export class GlobalAlerts extends React.PureComponent<Props, State> {
partialStorageKey={`motd.${m}`}
className="alert alert-info global-alerts__alert"
>
<Markdown dangerousInnerHTML={marked(m, { gfm: true, breaks: true, sanitize: true })} />
<Markdown dangerousInnerHTML={renderMarkdown(m)} />
</DismissibleAlert>
))}
</div>
Expand Down

0 comments on commit b47b35c

Please sign in to comment.