diff --git a/src/asciidocEngine.ts b/src/asciidocEngine.ts index d4fb402a..fb6a8ad4 100644 --- a/src/asciidocEngine.ts +++ b/src/asciidocEngine.ts @@ -24,21 +24,12 @@ export type AsciidoctorBuiltInBackends = 'html5' | 'docbook5' const previewConfigurationManager = new AsciidocPreviewConfigurationManager() export class AsciidocEngine { - private stylesdir: string - constructor ( readonly contributionProvider: AsciidocContributionProvider, readonly asciidoctorConfigProvider: AsciidoctorConfigProvider, readonly asciidoctorExtensionsProvider: AsciidoctorExtensionsProvider, readonly asciidoctorDiagnosticProvider: AsciidoctorDiagnosticProvider ) { - // Asciidoctor.js in the browser environment works with URIs however for desktop clients - // the "stylesdir" attribute is expected to look like a file system path (especially on Windows) - if ('browser' in process && (process as any).browser === true) { - this.stylesdir = vscode.Uri.joinPath(contributionProvider.extensionUri, 'media').toString() - } else { - this.stylesdir = vscode.Uri.joinPath(contributionProvider.extensionUri, 'media').fsPath - } } // Export @@ -59,7 +50,7 @@ export class AsciidocEngine { await this.asciidoctorConfigProvider.activate(registry, textDocumentUri) asciidoctorProcessor.restoreBuiltInSyntaxHighlighter() - const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).getBaseDir() + const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).baseDir const options: { [key: string]: any } = { attributes: { 'env-vscode': '', @@ -92,10 +83,16 @@ export class AsciidocEngine { context: vscode.ExtensionContext, editor: WebviewResourceProvider, line?: number - ): Promise<{html: string, document?: Asciidoctor.Document}> { + ): Promise<{ html: string, document?: Asciidoctor.Document }> { const textDocument = await vscode.workspace.openTextDocument(documentUri) - const { html, document } = await this.convertFromTextDocument(textDocument, context, editor, line) - return { html, document } + const { + html, + document, + } = await this.convertFromTextDocument(textDocument, context, editor, line) + return { + html, + document, + } } public async convertFromTextDocument ( @@ -112,7 +109,10 @@ export class AsciidocEngine { // load the Asciidoc header only to get kroki-server-url attribute const text = textDocument.getText() const attributes = AsciidoctorAttributesConfig.getPreviewAttributes() - const document = processor.load(text, { attributes, header_only: true }) + const document = processor.load(text, { + attributes, + header_only: true, + }) const isRougeSourceHighlighterEnabled = document.isAttribute('source-highlighter', 'rouge') if (isRougeSourceHighlighterEnabled) { // Force the source highlighter to Highlight.js (since Rouge is not supported) @@ -150,8 +150,7 @@ export class AsciidocEngine { cursor, antoraDocumentContext.getContentCatalog(), antoraConfig - ) - )) + ))) } if (context && editor) { highlightjsAdapter.register(asciidoctorProcessor.highlightjsBuiltInSyntaxHighlighter, context, editor) @@ -160,12 +159,26 @@ export class AsciidocEngine { } const antoraSupport = AntoraSupportManager.getInstance(context.workspaceState) const antoraAttributes = await antoraSupport.getAttributes(textDocumentUri) - const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).getBaseDir() + const asciidocTextDocument = AsciidocTextDocument.fromTextDocument(textDocument) + const baseDir = asciidocTextDocument.baseDir + const documentDirectory = asciidocTextDocument.dirName + const documentBasename = asciidocTextDocument.fileName + const documentExtensionName = asciidocTextDocument.extensionName + const documentFilePath = asciidocTextDocument.filePath const templateDirs = this.getTemplateDirs() const options: { [key: string]: any } = { attributes: { ...attributes, ...antoraAttributes, + // The following attributes are "intrinsic attributes" but they are not set when the input is a string + // like we are doing, in that case it is expected that the attributes are set here for the API: + // https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-ref/#intrinsic-attributes + // this can be set since safe mode is 'UNSAFE' + ...(documentDirectory && { docdir: documentDirectory }), + ...(documentFilePath && { docfile: documentFilePath }), + ...(documentBasename && { docname: documentBasename }), + docfilesuffix: documentExtensionName, + filetype: asciidoctorWebViewConverter.outfilesuffix.substring(1), // remove the leading '.' '!data-uri': '', // disable data-uri since Asciidoctor.js is unable to read files from a VS Code workspace. }, backend: 'webview-html5', diff --git a/src/asciidocLoader.ts b/src/asciidocLoader.ts index 8b46442f..76de3284 100644 --- a/src/asciidocLoader.ts +++ b/src/asciidocLoader.ts @@ -29,7 +29,7 @@ export class AsciidocLoader { memoryLogger, registry, } = await this.prepare(textDocument) - const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).getBaseDir() + const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).baseDir const attributes = AsciidoctorAttributesConfig.getPreviewAttributes() const doc = this.processor.load(textDocument.getText(), this.getOptions(attributes, registry, baseDir)) this.asciidoctorDiagnosticProvider.reportErrors(memoryLogger, textDocument) @@ -94,7 +94,7 @@ export class AsciidocIncludeItemsLoader extends AsciidocLoader { registry, } = await this.prepare(textDocument) this.asciidoctorIncludeItemsProvider.activate(registry) - const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).getBaseDir() + const baseDir = AsciidocTextDocument.fromTextDocument(textDocument).baseDir const attributes = AsciidoctorAttributesConfig.getPreviewAttributes() this.asciidoctorIncludeItemsProvider.reset() this.processor.load(textDocument.getText(), this.getOptions(attributes, registry, baseDir)) diff --git a/src/asciidocTextDocument.ts b/src/asciidocTextDocument.ts index e5c967f7..118a879f 100644 --- a/src/asciidocTextDocument.ts +++ b/src/asciidocTextDocument.ts @@ -7,31 +7,77 @@ interface DocumentWithUri { } export class AsciidocTextDocument { - private uri: Uri + public baseDir: string | undefined + public dir: string | undefined + public dirName: string | undefined + public extensionName: string + public fileName: string | undefined + public filePath: string | undefined - private constructor () { + private constructor (private uri: Uri) { + this.baseDir = AsciidocTextDocument.getBaseDir(uri) + this.dirName = AsciidocTextDocument.getDirName(uri) + this.extensionName = AsciidocTextDocument.getExtensionName(uri) + this.fileName = AsciidocTextDocument.getFileName(uri) + this.filePath = AsciidocTextDocument.getFilePath(uri) } public static fromTextDocument (textDocument: DocumentWithUri): AsciidocTextDocument { - const asciidocTextDocument = new AsciidocTextDocument() - asciidocTextDocument.uri = textDocument.uri - return asciidocTextDocument + return new AsciidocTextDocument(textDocument.uri) } /** * Get the base directory. * @private */ - public getBaseDir (): string | undefined { + private static getBaseDir (uri: Uri): string | undefined { const useWorkspaceAsBaseDir = vscode.workspace.getConfiguration('asciidoc', null).get('useWorkspaceRootAsBaseDirectory') if (useWorkspaceAsBaseDir) { - const workspaceFolder = getWorkspaceFolder(this.uri) + const workspaceFolder = getWorkspaceFolder(uri) if (workspaceFolder) { return workspaceFolder.uri.fsPath } } + return AsciidocTextDocument.getDirName(uri) + } + + private static getDirName (uri: Uri): string | undefined { return 'browser' in process && (process as any).browser === true ? undefined - : path.dirname(path.resolve(this.uri.fsPath)) + : path.dirname(path.resolve(uri.fsPath)) + } + + /** + * Return the extension name of the file without the '.'. + * @param uri + * @private + */ + private static getExtensionName (uri: Uri): string { + const textDocumentExt = path.extname(uri.path) + return textDocumentExt.startsWith('.') ? textDocumentExt.substring(1) : '' + } + + /** + * Return the file name without the file extension. + * @param uri + * @private + */ + public static getFileName (uri: Uri): string | undefined { + if ('browser' in process && (process as any).browser === true) { + return undefined + } + return path.parse(uri.fsPath).name + } + + /** + * Return the filesystem path of the URI. + * @param uri + * @private + */ + public static getFilePath (uri: Uri): string | undefined { + if ('browser' in process && (process as any).browser === true) { + return undefined + } + return uri.fsPath } } diff --git a/src/commands/exportAsPDF.ts b/src/commands/exportAsPDF.ts index 7c6e1b23..b51b8321 100644 --- a/src/commands/exportAsPDF.ts +++ b/src/commands/exportAsPDF.ts @@ -6,7 +6,6 @@ import { exec, spawn, SpawnOptions } from 'child_process' import { uuidv4 } from 'uuid' import { AsciidocEngine } from '../asciidocEngine' import { Command } from '../commandManager' -import { Logger } from '../logger' import { Asciidoctor } from '@asciidoctor/core' import { AsciidocTextDocument } from '../asciidocTextDocument' import { getAsciidoctorConfigContent } from '../features/asciidoctorConfig' @@ -16,7 +15,7 @@ export class ExportAsPDF implements Command { public readonly id = 'asciidoc.exportAsPDF' private readonly exportAsPdfStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100) - constructor (private readonly engine: AsciidocEngine, private readonly context: vscode.ExtensionContext, private readonly logger: Logger) { + constructor (private readonly engine: AsciidocEngine, private readonly context: vscode.ExtensionContext) { } public async execute () { @@ -31,11 +30,9 @@ export class ExportAsPDF implements Command { await vscode.window.showWarningMessage('Unable to get the workspace folder, aborting.') return } - const workspacePath = workspaceFolder.uri.fsPath - const docNameWithoutExtension = path.parse(doc.uri.fsPath).name - - const baseDirectory = AsciidocTextDocument.fromTextDocument(doc).getBaseDir() - const pdfFilename = vscode.Uri.file(path.join(baseDirectory, docNameWithoutExtension + '.pdf')) + const asciidocTextDocument = AsciidocTextDocument.fromTextDocument(doc) + const baseDirectory = asciidocTextDocument.baseDir + const pdfFilename = vscode.Uri.file(path.join(baseDirectory, asciidocTextDocument.fileName + '.pdf')) const asciidocPdfConfig = vscode.workspace.getConfiguration('asciidoc.pdf') const pdfOutputUri = await vscode.window.showSaveDialog({ defaultUri: pdfFilename }) @@ -51,9 +48,9 @@ export class ExportAsPDF implements Command { text = `${asciidoctorConfigContent} ${text}` } - - const pdfEnfine = asciidocPdfConfig.get('engine') - if (pdfEnfine === 'asciidoctor-pdf') { + const workspacePath = workspaceFolder.uri.fsPath + const pdfEngine = asciidocPdfConfig.get('engine') + if (pdfEngine === 'asciidoctor-pdf') { const asciidoctorPdfCommand = await this.resolveAsciidoctorPdfCommand(asciidocPdfConfig, workspacePath) if (asciidoctorPdfCommand === undefined) { return @@ -84,7 +81,7 @@ ${text}` } finally { this.exportAsPdfStatusBarItem.hide() } - } else if (pdfEnfine === 'wkhtmltopdf') { + } else if (pdfEngine === 'wkhtmltopdf') { let wkhtmltopdfCommandPath = asciidocPdfConfig.get('wkhtmltopdfCommandPath', '') if (wkhtmltopdfCommandPath === '') { wkhtmltopdfCommandPath = `wkhtmltopdf${process.platform === 'win32' ? '.exe' : ''}` diff --git a/src/extension.ts b/src/extension.ts index 3335120b..23837252 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -95,7 +95,7 @@ export async function activate (context: vscode.ExtensionContext) { commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager)) commandManager.register(new commands.ShowAsciidoctorExtensionsTrustModeSelectorCommand(asciidoctorExtensionsTrustModeSelector)) commandManager.register(new commands.OpenDocumentLinkCommand(asciidocLoader)) - commandManager.register(new commands.ExportAsPDF(asciidocEngine, context, logger)) + commandManager.register(new commands.ExportAsPDF(asciidocEngine, context)) commandManager.register(new commands.PasteImage(asciidocLoader)) commandManager.register(new commands.ToggleLockCommand(previewManager)) commandManager.register(new commands.ShowPreviewCommand(previewManager)) diff --git a/src/test/asciidoctorWebViewConverter.test.ts b/src/test/asciidoctorWebViewConverter.test.ts index da6c07fb..158ef526 100644 --- a/src/test/asciidoctorWebViewConverter.test.ts +++ b/src/test/asciidoctorWebViewConverter.test.ts @@ -1,4 +1,5 @@ import vscode from 'vscode' +import path from 'path' import { AsciidoctorWebViewConverter } from '../asciidoctorWebViewConverter' import { WebviewResourceProvider } from '../util/resources' import { AsciidocPreviewConfigurationManager } from '../features/previewConfig' @@ -36,6 +37,30 @@ function createAntoraDocumentContextStub (resourcePath: string | undefined) { return antoraDocumentContextStub } +function createConverterOptions (converter: AsciidoctorWebViewConverter, fileName: string) { + // treat the file as the source file for conversion to handle xref correctly between documents + // review src/asciidocEngin.ts for more information + const intrinsicAttr = { + docdir: path.dirname(fileName), + docfile: fileName, + docfilesuffix: path.extname(fileName).substring(1), + docname: path.basename(fileName, path.extname(fileName)), + filetype: converter.outfilesuffix.substring(1), + } + + return { + converter, + attributes: { + ...intrinsicAttr, + + // required for navigation between source files in preview + // see: https://docs.asciidoctor.org/asciidoc/latest/macros/inter-document-xref/#navigating-between-source-files + relfilesuffix: '.adoc', + }, + safe: 'unsafe', // needed so that we can actually perform includes, enabling xref tests + } +} + async function testAsciidoctorWebViewConverter ( input: string, antoraDocumentContext: AntoraDocumentContext | undefined, @@ -55,12 +80,7 @@ async function testAsciidoctorWebViewConverter ( undefined ) - const html = processor.convert(input, { - converter: asciidoctorWebViewConverter, - // required for navigation between source files in preview - // see: https://docs.asciidoctor.org/asciidoc/latest/macros/inter-document-xref/#navigating-between-source-files - attributes: { relfilesuffix: '.adoc' }, - }) + const html = processor.convert(input, createConverterOptions(asciidoctorWebViewConverter, file.fileName)) assert.strictEqual(html, expected) } @@ -82,7 +102,10 @@ async function testAsciidoctorWebViewConverterStandalone ( antoraDocumentContext, undefined ) - const html = processor.convert(input, { converter: asciidoctorWebViewConverter, standalone: true }) + const html = processor.convert(input, { + ...createConverterOptions(asciidoctorWebViewConverter, file.fileName), + standalone: true, + }) html.includes(expected) } @@ -102,6 +125,31 @@ link:help.adoc[] createdFiles.push(await createDirectory('docs')) await createFile('', 'docs', 'modules', 'ROOT', 'pages', 'dummy.adoc') // virtual file createdFiles.push(asciidocFile) + + // these help with testing xref cross documents + createdFiles.push(await createFile(`= Parent document + +Some text + +[#anchor] +== Link to here + +Please scroll me into position + +include::docB.adoc[]`, 'docA.adoc')) + createdFiles.push(await createFile(`= Child document + +[#other_anchor] +== Other link to here + +Other text + +I want to link to xref:docA.adoc#anchor[]`, 'docB.adoc')) + createdFiles.push(await createFile(`= Child document + +third text + +I want to link to xref:docB.adoc#other_anchor[]`, 'docC.adoc')) }) suiteTeardown(async () => { await removeFiles(createdFiles) @@ -153,6 +201,42 @@ link:help.adoc[] `, }, // xref + { + title: 'Should resolve "xref:" macro from included document referencing the source document', + filePath: ['docA.adoc'], + input: `= Parent document + +Some text + +[#anchor] +== Link to here + +Please scroll me into position + +include::docB.adoc[]`, + antoraDocumentContext: undefined, // Antora not enabled + expected: 'Link to here', + standalone: true, + }, + { + title: 'Should resolve "xref:" macro from included document referencing a separate included document', + filePath: ['docA.adoc'], + input: `= Parent document + +Some text + +[#anchor] +== Link to here + +Please scroll me into position + +include::docB.adoc[] + +include::docC.adoc[]`, + antoraDocumentContext: undefined, // Antora not enabled + expected: 'Other link to here', + standalone: true, + }, { title: 'Should resolve "xref:" macro to document', filePath: ['asciidoctorWebViewConverterTest.adoc'],