Skip to content

Commit

Permalink
support AsciidoctorJ extensions in the preview (#532)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahus1 committed Aug 11, 2020
1 parent 8ac75b5 commit 2c54bcc
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This document provides a high-level view of the changes introduced by release.
[[releasenotes]]
== Release notes

=== 0.31.20 (preview, available from GitHub releases)

- support AsciidoctorJ extensions in the preview (#532)

=== 0.31.19 (preview, available from GitHub releases)

- pasting image from the clipboard remembers previous selection for file type and target folder (#477)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
= Asciidoctor Extensions
:description: Asciidoctor Extensions can provide additional macros using Ruby code. These are executed when rendering the preview.
:description: Asciidoctor Extensions can provide additional macros using Ruby or Java code. These are executed when rendering the preview.

Asciidoctor Extensions can provide additional macros.
To see the rendered result in the preview, the plugin can use extensions during rendering.
Asciidoctor Extensions can provide for example additional custom macros or post-processing of the AsciiDoc AST.
To see the rendered result in the preview, the plugin can use extensions while rendering it.

See https://github.com/asciidoctor/asciidoctor-intellij-plugin/wiki/Support-for-Asciidoctor-Extensions[Wiki page] for details.
[WARNING]
====
This is an experimental feature starting with version 0.23.0. While it is experimental names, conventions and functionality may change.
Support for AsciidoctorJ extensions is available from 0.31.19.
====

== Situation

When there are extensions for Asciidoctor used in a command line build, users want to see their effect on the preview rendered within your JetBrains IDE.

To find out more about extensions, have a look at the https://github.com/asciidoctor/asciidoctor-extensions-lab[Asciidoctor Extensions Lab] or the https://asciidoctor.org/docs/extensions/[Asciidoctor Extensions List].

== Solution

The plugin will search the directory _.asciidoctor/lib_ in the root of the project and load all files with the extension "`rb`" as Asciidoctor extensions and all files with the extension "`jar`" as AsciidoctorJ extensions.

AsciidoctorJ extensions must use the https://github.com/asciidoctor/asciidoctorj/blob/master/docs/integrator-guide.adoc#automatically-loading-extensions[service loader mechanism of AsciidoctorJ] to register themselves.

If a user doesn't want to store ruby or JAR files in their code repository, a user may choose to add a download or build script that populates this folder.
Using a configuration file like `.gitignore` can ensure that the script is checked in to the repository, while the downloaded files are not checked in.

== Behavior

The user working with the IDE needs to trust the Ruby or Java code of the extensions, as the code will run with the privileges of the user inside the IDE.
When a developer runs for example a build script in a repository, executing some else's code is expected behavior.
Running someone else's code when viewing an Asciidoctor document is unexpected.
Therefore, the user needs to confirm to enable Asciidoctor extensions once per project and IDE restart.

== Caveats

Extensions run in the same JVM as the IDE.
Errors in extentions might consume lots of memory, CPU and might crash the IDE.
Changing extensions at runtime re-instantiates the Asciidoctor and JRuby runtime, which will lead to memory leaks.

== Example

Please see the Arquillian Smart Testing project for an example: https://github.com/arquillian/smart-testing/tree/master/.asciidoctor/lib

== Ecosystem

Users can use Asciidoctor command line to render the output with the plugins from the _.asciidoctor/lib_ directory.
72 changes: 58 additions & 14 deletions src/main/java/org/asciidoc/intellij/AsciiDoc.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -286,7 +287,7 @@ private Asciidoctor initWithExtensions(List<String> extensions, boolean springRe
}
}
try {
asciidoctor = createInstance();
asciidoctor = createInstance(extensionsEnabled ? extensions : Collections.emptyList());
asciidoctor.registerLogHandler(logHandler);
// require openssl library here to enable download content via https
// requiring it later after other libraries have been loaded results in "undefined method `set_params' for #<OpenSSL::SSL::SSLContext"
Expand Down Expand Up @@ -364,7 +365,9 @@ private Asciidoctor initWithExtensions(List<String> extensions, boolean springRe

if (extensionsEnabled) {
for (String extension : extensions) {
asciidoctor.rubyExtensionRegistry().requireLibrary(extension);
if (extension.toLowerCase().endsWith(".rb")) {
asciidoctor.rubyExtensionRegistry().requireLibrary(extension);
}
}
}
INSTANCES.put(md, asciidoctor);
Expand Down Expand Up @@ -419,7 +422,7 @@ private boolean isPdfPresent() {
/**
* Create an instance of Asciidoctor.
*/
private Asciidoctor createInstance() {
private Asciidoctor createInstance(List<String> extensions) {
ClassLoader cl = AsciiDocAction.class.getClassLoader();
List<URL> urls = new ArrayList<>();
try {
Expand All @@ -432,15 +435,46 @@ private Asciidoctor createInstance() {
urls.add(file2.toURI().toURL());
}
} catch (MalformedURLException e) {
throw new RuntimeException("unable to add AsciidoctorJ to class path");
throw new RuntimeException("unable to add JAR AsciidoctorJ to class path", e);
}
File tempDirectory = null;
for (String extension : extensions) {
if (extension.toLowerCase().endsWith(".jar")) {
File jar = new File(extension);
try {
if (jar.exists()) {
// copy JAR to temporary folder to avoid locking the original file on Windows
if (tempDirectory == null) {
tempDirectory = Files.createTempDirectory("asciidoctor-intellij").toFile();
}
File target = new File(tempDirectory, jar.getName());
FileUtils.copyFile(jar, target);
urls.add(target.toURI().toURL());
}
} catch (MalformedURLException e) {
throw new RuntimeException("unable to add JAR '" + extension + "' AsciidoctorJ to class path", e);
} catch (IOException e) {
throw new RuntimeException("unable to create temporary folder");
}
}
}

if (urls.size() > 0) {
cl = new URLClassLoader(urls.toArray(new URL[]{}), cl);
} else if (cl instanceof URLClassLoader) {
// Wrap an existing URLClassLoader with an empty list to prevent scanning of JARs by Ruby Runtime during Unit Tests.
cl = new URLClassLoader(new URL[]{}, cl);
}
return AsciidoctorJRuby.Factory.create(cl);

ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
try {
// set classloader for current thread as otherwise JRubyAsciidoctor#processRegistrations() will not register extensions
Thread.currentThread().setContextClassLoader(cl);
return AsciidoctorJRuby.Factory.create(cl);
} finally {
Thread.currentThread().setContextClassLoader(oldCl);
}

}

/**
Expand Down Expand Up @@ -502,7 +536,8 @@ private void notify(ByteArrayOutputStream boasOut, ByteArrayOutputStream boasErr
!AsciiDocApplicationSettings.getInstance().getAsciiDocPreviewSettings().isShowAsciiDocWarningsAndErrorsInEditor());
}

public void notifyAlways(ByteArrayOutputStream boasOut, ByteArrayOutputStream boasErr, List<LogRecord> logRecords) {
public void notifyAlways(ByteArrayOutputStream boasOut, ByteArrayOutputStream
boasErr, List<LogRecord> logRecords) {
notify(boasOut, boasErr, logRecords, true);
}

Expand Down Expand Up @@ -614,7 +649,10 @@ public static List<String> getExtensions(Project project) {
List<String> extensions = new ArrayList<>();
if (lib != null) {
for (VirtualFile vf : lib.getChildren()) {
if ("rb".equals(vf.getExtension())) {
if ("rb".toLowerCase().equals(vf.getExtension())) {
extensions.add(vf.getCanonicalPath());
}
if ("jar".toLowerCase().equals(vf.getExtension())) {
extensions.add(vf.getCanonicalPath());
}
}
Expand All @@ -635,11 +673,13 @@ public String render(@Language("asciidoc") String text, String config, List<Stri
return render(text, config, extensions, this::notify);
}

public String render(@Language("asciidoc") String text, String config, List<String> extensions, Notifier notifier) {
public String render(@Language("asciidoc") String text, String config, List<String> extensions, Notifier
notifier) {
return render(text, config, extensions, notifier, FileType.JAVAFX);
}

public String render(@Language("asciidoc") String text, String config, List<String> extensions, Notifier notifier, FileType format) {
public String render(@Language("asciidoc") String text, String config, List<String> extensions, Notifier
notifier, FileType format) {
VirtualFile springRestDocsSnippets = findSpringRestDocSnippets(
LocalFileSystem.getInstance().findFileByIoFile(new File(projectBasePath)),
LocalFileSystem.getInstance().findFileByIoFile(fileBaseDir)
Expand Down Expand Up @@ -735,7 +775,7 @@ private static int validateAccess() {
// the AsciiDocJavaDocInfoGenerator will get here with an existing ReadLock, use a timeout here to avoid a deadlock.
Set<StackTraceElement> nonblocking = Arrays.stream(Thread.currentThread().getStackTrace()).filter(stackTraceElement ->
stackTraceElement.getClassName().endsWith("AsciiDocJavaDocInfoGenerator") ||
stackTraceElement.getClassName().endsWith("AsciidocletJavaDocInfoGenerator")
stackTraceElement.getClassName().endsWith("AsciidocletJavaDocInfoGenerator")
).collect(Collectors.toSet());
if (nonblocking.size() > 0) {
return 20;
Expand Down Expand Up @@ -836,7 +876,8 @@ private static void unlock() {
LOCK.unlock();
}

public static Map<String, String> populateAntoraAttributes(String projectBasePath, File fileBaseDir, VirtualFile antoraModuleDir) {
public static Map<String, String> populateAntoraAttributes(String projectBasePath, File fileBaseDir, VirtualFile
antoraModuleDir) {
Map<String, String> result = new HashMap<>();
if (antoraModuleDir != null) {
result.putAll(collectAntoraAttributes(antoraModuleDir));
Expand Down Expand Up @@ -977,7 +1018,8 @@ public Map<String, Object> getExportOptions(Map<String, Object> options, FileTyp
}

@SuppressWarnings("checkstyle:ParameterNumber")
private Map<String, Object> getDefaultOptions(FileType fileType, VirtualFile springRestDocsSnippets, Map<String, String> attributes) {
private Map<String, Object> getDefaultOptions(FileType fileType, VirtualFile
springRestDocsSnippets, Map<String, String> attributes) {
AttributesBuilder builder = AttributesBuilder.attributes()
.showTitle(true)
.backend(fileType.backend)
Expand Down Expand Up @@ -1057,15 +1099,17 @@ public String toString() {

}

private static void mapAttribute(Map<String, String> result, Map<String, Object> antora, String nameSource, String nameTarget) {
private static void mapAttribute(Map<String, String> result, Map<String, Object> antora, String
nameSource, String nameTarget) {
Object value = antora.get(nameSource);
if (value != null) {
result.put(nameTarget, value.toString());
}
}

@NotNull
public static String enrichPage(@NotNull String html, String standardCss, @NotNull Map<String, String> attributes) {
public static String enrichPage(@NotNull String html, String
standardCss, @NotNull Map<String, String> attributes) {
/* Add CSS line */
String stylesheet = attributes.get("stylesheet");
if (stylesheet != null && stylesheet.length() != 0) {
Expand Down

0 comments on commit 2c54bcc

Please sign in to comment.