Skip to content

Commit

Permalink
Merge pull request asciidoctor#451 from Mogztter/local-highlightjs
Browse files Browse the repository at this point in the history
Load Highlight.js from the extension, closes asciidoctor#459
  • Loading branch information
danyill committed Oct 3, 2021
2 parents 283b538 + 405dffc commit d603d9e
Show file tree
Hide file tree
Showing 13 changed files with 83 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ jobs:
node-version: '14'
- run: npm ci
- run: npm run lint
- run: npm run compile
- run: npm run build

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ test/samples/playground.*
dist/
.DS_Store
.vscode-test/
media/highlightjs/*
Empty file added media/highlightjs/.gitkeep
Empty file.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,20 @@
]
},
"scripts": {
"dev": "webpack --config webpack.config.preview.js --mode development && npm run compile",
"compile": "tsc -p ./",
"package": "webpack --config webpack.config.preview.js --mode production && npm run compile && vsce package",
"copy-assets": "cp node_modules/@highlightjs/cdn-assets/highlight.min.js media/highlightjs && cp -r node_modules/@highlightjs/cdn-assets/languages media/highlightjs && cp -r node_modules/@highlightjs/cdn-assets/styles media/highlightjs",
"dev": "npm run build",
"build": "npm run copy-assets && npm run build-ext && npm run build-preview",
"build-preview": "webpack --config webpack.config.preview.js --mode production",
"build-ext": "tsc -p ./",
"package": "npm run build && vsce package",
"deploy": "vsce publish -p",
"lint": "eslint 'src/**/*.ts' 'preview-src/**/*.ts' --format unix",
"lint:fix": "npm run lint -- --fix",
"pretest": "npm run compile",
"pretest": "npm run build",
"test": "node ./dist/src/test/runTest.js"
},
"devDependencies": {
"@highlightjs/cdn-assets": "11.2.0",
"@types/highlight.js": "9.1.10",
"@types/lodash.throttle": "^4.1.3",
"@types/mocha": "^9.0.0",
Expand Down
13 changes: 8 additions & 5 deletions src/asciidocEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ export class AsciidocEngine {
return { text, offset }
}

public async render (document: vscode.Uri, stripFrontmatter: boolean, text: string,
forHTML: boolean = false, backend: string = 'html5'): Promise<string> {
public async render (document: vscode.Uri,
stripFrontmatter: boolean,
text: string, forHTML: boolean = false,
backend: string = 'html5',
context?: vscode.ExtensionContext,
editor?: vscode.WebviewPanel): Promise<string> {
let offset = 0
if (stripFrontmatter) {
const asciidocContent = this.stripFrontmatter(text)
Expand All @@ -58,11 +62,10 @@ export class AsciidocEngine {
this.firstLine = offset
const engine = await this.getEngine(document)
const doc = await vscode.workspace.openTextDocument(document)
const asciiDoc = engine.parseText(text, doc, forHTML, backend)
return asciiDoc
return await engine.parseText(text, doc, forHTML, backend, context, editor)
}

public async parse (document: vscode.Uri, source: string): Promise<any> {
public async load (document: vscode.Uri, source: string): Promise<any> {
this.currentDocument = document
const engine = await this.getEngine(document)
const doc = await vscode.workspace.openTextDocument(document)
Expand Down
4 changes: 2 additions & 2 deletions src/features/foldingProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class AsciidocFoldingProvider implements vscode.FoldingRangeProvi
const isRegionMarker = (token: any) => token.type === 'html_block' &&
(isStartRegion(token.content) || isEndRegion(token.content))

const tokens = await this.engine.parse(document.uri, document.getText())
const tokens = await this.engine.load(document.uri, document.getText())
const regionMarkers = tokens.filter(isRegionMarker)
.map((token) => ({ line: token.map[0], isStart: isStartRegion(token.content) }))

Expand Down Expand Up @@ -80,7 +80,7 @@ export default class AsciidocFoldingProvider implements vscode.FoldingRangeProvi
}
}

const tokens = await this.engine.parse(document.uri, document.getText())
const tokens = await this.engine.load(document.uri, document.getText())
const multiLineListItems = tokens.filter(isFoldableToken)
return multiLineListItems.map((listItem) => {
const start = listItem.map[0]
Expand Down
9 changes: 5 additions & 4 deletions src/features/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,13 +356,14 @@ export class AsciidocPreview {

this.currentVersion = { resource, version: document.version }

const content = await this._contentProvider.providePreviewHTML(document, this._previewConfigurations, this.line, this.state)
// add webView
if (this._resource === resource) {
this.editor.title = AsciidocPreview.getPreviewTitle(this._resource, this._locked)
this.editor.iconPath = this.iconPath
this.editor.webview.options = AsciidocPreview.getWebviewOptions(resource, this._contributions)
this.editor.webview.html = content
}
this.editor.iconPath = this.iconPath
this.editor.webview.options = AsciidocPreview.getWebviewOptions(resource, this._contributions)
const content = await this._contentProvider.providePreviewHTML(document, this._previewConfigurations, this.line, this.state, this.editor)
this.editor.webview.html = content
}

private static getWebviewOptions (
Expand Down
17 changes: 10 additions & 7 deletions src/features/previewContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { AsciidocEngine } from '../asciidocEngine'
import * as nls from 'vscode-nls'

import { Logger } from '../logger'
import { ContentSecurityPolicyArbiter, AsciidocPreviewSecurityLevel } from '../security'
import { AsciidocPreviewConfigurationManager, AsciidocPreviewConfiguration } from './previewConfig'
import { AsciidocPreviewSecurityLevel, ContentSecurityPolicyArbiter } from '../security'
import { AsciidocPreviewConfiguration, AsciidocPreviewConfigurationManager } from './previewConfig'
import { AsciidocContributions } from '../asciidocExtensions'

const localize = nls.loadMessageBundle()

/**
Expand Down Expand Up @@ -52,7 +53,8 @@ export class AsciidocContentProvider {
asciidocDocument: vscode.TextDocument,
previewConfigurations: AsciidocPreviewConfigurationManager,
initialLine: number | undefined = undefined,
state?: any
state?: any,
editor?: vscode.WebviewPanel
): Promise<string> {
const sourceUri = asciidocDocument.uri
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri)
Expand All @@ -69,7 +71,7 @@ export class AsciidocContentProvider {
// Content Security Policy
const nonce = new Date().getTime() + '' + new Date().getMilliseconds()
const csp = this.getCspForResource(sourceUri, nonce)
const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', asciidocDocument.getText())
const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', asciidocDocument.getText(), false, 'html5', this.context, editor)
const bodyClassesRegex = /<body(?:(?:\s+(?:id=".*"\s*)?class(?:\s*=\s*(?:"(.+?)"|'(.+?)')))+\s*)>/
const bodyClasses = body.match(bodyClassesRegex)
const bodyClassesVal = bodyClasses === null ? '' : bodyClasses[1]
Expand Down Expand Up @@ -188,19 +190,20 @@ export class AsciidocContentProvider {
}

private getCspForResource (resource: vscode.Uri, nonce: string): string {
const highlightjsInlineScriptHash = 'sha256-ZrDBcrmObbqhVV/Mag2fT/y08UJGejdW7UWyEsi4DXw='
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
case AsciidocPreviewSecurityLevel.AllowInsecureContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: http: https: data:; media-src vscode-resource: http: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:; font-src vscode-resource: http: https: data:;">`
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: http: https: data:; media-src vscode-resource: http: https: data:; script-src vscode-resource: 'nonce-${nonce}' '${highlightjsInlineScriptHash}'; style-src vscode-resource: 'unsafe-inline' http: https: data:; font-src vscode-resource: http: https: data:;">`

case AsciidocPreviewSecurityLevel.AllowInsecureLocalContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; media-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*;">`
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; media-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; script-src vscode-resource: 'nonce-${nonce}' '${highlightjsInlineScriptHash}'; style-src vscode-resource: 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*;">`

case AsciidocPreviewSecurityLevel.AllowScriptsAndAllContent:
return ''

case AsciidocPreviewSecurityLevel.Strict:
default:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data:; media-src vscode-resource: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' https: data:; font-src vscode-resource: https: data:;">`
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data:; media-src vscode-resource: https: data:; script-src vscode-resource: 'nonce-${nonce}' '${highlightjsInlineScriptHash}'; style-src vscode-resource: 'unsafe-inline' https: data:; font-src vscode-resource: https: data:;">`
}
}
}
25 changes: 12 additions & 13 deletions src/highlightjs-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
// from https://github.com/cdnjs/SRIs/blob/master/highlight.js/9.18.3.json
import * as sri from './highlightjs-sri-9.18.3.json'
import * as vscode from 'vscode'
import * as path from 'path'
const { Opal } = require('asciidoctor-opal-runtime')

module.exports.register = (asciidoctor) => {
const HighlightjsSyntaxHighlighter = asciidoctor.SyntaxHighlighter.for('highlight.js')
const customHighlightJsAdapter = Opal.klass(Opal.nil, HighlightjsSyntaxHighlighter, 'CustomHighlightJsAdapter')
module.exports.register = (highlightjsBuiltInSyntaxHighlighter, context: vscode.ExtensionContext, webviewPanel: vscode.WebviewPanel) => {
const customHighlightJsAdapter = Opal.klass(Opal.nil, highlightjsBuiltInSyntaxHighlighter, 'CustomHighlightJsAdapter')
customHighlightJsAdapter.$register_for('highlight.js', 'highlightjs')

let $docinfo
const baseUrl = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3'
Opal.def(customHighlightJsAdapter, '$docinfo', $docinfo = function $$docinfo (location, doc, _opts) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const self = this
if (location === 'head') {
const theme = doc.$attr('highlightjs-theme', 'github')
const integrityValue = sri[`styles/${theme}.min.css`]
return `<link rel="stylesheet" href="${baseUrl}/styles/${theme}.min.css" ${integrityValue ? `integrity="${integrityValue}" ` : ''}crossorigin="anonymous" referrerpolicy="no-referrer">`
const themeStyleSheetResource = vscode.Uri.file(path.join(context.extensionPath, 'media', 'highlightjs', 'styles', `${theme}.min.css`))
return `<link rel="stylesheet" href="${webviewPanel.webview.asWebviewUri(themeStyleSheetResource)}">`
}
// footer
let languageScripts = ''
if (doc['$attr?']('highlightjs-languages')) {
languageScripts = doc.$attr('highlightjs-languages').split(',').map((lang) => {
const key = `languages/${lang.trim()}.min.js`
const integrityValue = sri[key]
return `<script src="${baseUrl}/${key}" ${integrityValue ? `integrity="${integrityValue}" ` : ''}crossorigin="anonymous" referrerpolicy="no-referrer"></script>`
const languageScriptResource = vscode.Uri.file(path.join(context.extensionPath, 'media', 'highlightjs', 'languages', `${lang.trim()}.min.js`))
return `<script src="${webviewPanel.webview.asWebviewUri(languageScriptResource)}"></script>`
}).join('\n')
}
return `<script src="${baseUrl}/highlight.min.js" integrity="${sri['highlight.min.js']}" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
const highlightjsScriptResource = vscode.Uri.file(path.join(context.extensionPath, 'media', 'highlightjs', 'highlight.min.js'))
return `<script src="${webviewPanel.webview.asWebviewUri(highlightjsScriptResource)}"></script>
${languageScripts}
<script>
if (!hljs.initHighlighting.called) {
hljs.initHighlighting.called = true
;[].slice.call(document.querySelectorAll("pre.highlight > code")).forEach(function (el) { hljs.highlightBlock(el) })
;[].slice.call(document.querySelectorAll("pre.highlight > code")).forEach(function (el) { hljs.highlightElement(el) })
}
</script>`
}, $docinfo.$$arity = 3)
Expand Down
1 change: 0 additions & 1 deletion src/highlightjs-sri-9.18.3.json

This file was deleted.

6 changes: 4 additions & 2 deletions src/tableOfContentsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as vscode from 'vscode'
import { AsciidocEngine } from './asciidocEngine'
import { Slug, githubSlugifier } from './slugify'
import { githubSlugifier, Slug } from './slugify'

export interface TocEntry {
readonly slug: Slug;
Expand All @@ -17,7 +17,9 @@ export interface TocEntry {
export interface SkinnyTextDocument {
readonly uri: vscode.Uri;
readonly lineCount: number;

getText(): string;

lineAt(line: number): vscode.TextLine;
}

Expand Down Expand Up @@ -48,7 +50,7 @@ export class TableOfContentsProvider {

private async buildToc (document: SkinnyTextDocument): Promise<TocEntry[]> {
const toc: TocEntry[] = []
const adoc = await this.engine.parse(document.uri, document.getText())
const adoc = await this.engine.load(document.uri, document.getText())

adoc.findBy({ context: 'section' }, function (section) {
toc.push({
Expand Down
35 changes: 26 additions & 9 deletions src/text-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { spawn } from 'child_process'
const asciidoctor = require('@asciidoctor/core')()
const docbook = require('@asciidoctor/docbook-converter')
const kroki = require('asciidoctor-kroki')
const highlightjsBuiltInSyntaxHighlighter = asciidoctor.SyntaxHighlighter.for('highlight.js')
const highlightjsAdapter = require('./highlightjs-adapter')
highlightjsAdapter.register(asciidoctor)

const useKroki = vscode.workspace.getConfiguration('asciidoc', null).get('use_kroki')
if (useKroki) {
Expand All @@ -33,8 +33,13 @@ export class AsciidocParser {
return match
}

private async convertUsingJavascript (text: string, doc: vscode.TextDocument, forHTMLSave: boolean, backend: string) {
return new Promise<string>((resolve) => {
private async convertUsingJavascript (text: string,
doc: vscode.TextDocument,
forHTMLSave: boolean,
backend: string,
context?: vscode.ExtensionContext,
editor?: vscode.WebviewPanel) {
return new Promise<string>((resolve, reject) => {
const documentPath = path.dirname(path.resolve(doc.fileName))
const workspacePath = vscode.workspace.workspaceFolders
const containsStyle = !(text.match(/'^\\s*:(stylesheet|stylesdir)/img) == null)
Expand All @@ -55,6 +60,12 @@ export class AsciidocParser {
const memoryLogger = asciidoctor.MemoryLogger.create()
asciidoctor.LoggerManager.setLogger(memoryLogger)

if (context && editor) {
highlightjsAdapter.register(highlightjsBuiltInSyntaxHighlighter, context, editor)
} else {
highlightjsBuiltInSyntaxHighlighter.$register_for('highlight.js', 'highlightjs')
}

let attributes = {}

if (containsStyle) {
Expand Down Expand Up @@ -183,6 +194,7 @@ export class AsciidocParser {
resolve(resultHTML)
} catch (e) {
vscode.window.showErrorMessage(e.toString())
reject(e)
}
})
}
Expand Down Expand Up @@ -299,12 +311,17 @@ export class AsciidocParser {
})
}

public async parseText (text: string, doc: vscode.TextDocument, forHTMLSave: boolean = false, backend: string = 'html'): Promise<string> {
const useAsciidoctorJS = vscode.workspace.getConfiguration('asciidoc', null).get('use_asciidoctor_js')
if (useAsciidoctorJS) {
return this.convertUsingJavascript(text, doc, forHTMLSave, backend)
} else {
return this.convertUsingApplication(text, doc, forHTMLSave, backend)
public async parseText (text: string,
doc: vscode.TextDocument,
forHTMLSave: boolean = false,
backend: string = 'html',
context?: vscode.ExtensionContext,
editor?: vscode.WebviewPanel): Promise<string> {
const useAsciidoctorJs = vscode.workspace.getConfiguration('asciidoc', null).get('use_asciidoctor_js')
if (useAsciidoctorJs) {
return this.convertUsingJavascript(text, doc, forHTMLSave, backend, context, editor)
}

return this.convertUsingApplication(text, doc, forHTMLSave, backend)
}
}

0 comments on commit d603d9e

Please sign in to comment.