Skip to content

Commit

Permalink
create PDF from IDE using an editor action and asciidoctorj-pdf (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahus1 committed Sep 1, 2019
1 parent 7183cfd commit e0580de
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 15 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.30.0 (preview, available from Github releases)

- support creating a PDF from the IDE based on asciidoctorj-pdf:1.5.0-beta.2 (#325)

=== 0.29.11 (preview, available from Github releases)

- No end of sentence after a digit
Expand Down
7 changes: 7 additions & 0 deletions FEATURES.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ When using mathematical formulas in the preview, the preview can flicker.
There is an experimental feature that you can enable in the settings of the plugin to activate a new update mechanism that avoids the flickering.
Activate the option "`Replace preview contents without flicker (JavaFX preview only)`" for this.
EXPERIMENTAL:
The user can choose _Create PDF from current file_ to convert the file in the current AsciiDoc editor to a PDF using AsciiDoctor PDF version 1.5.0-beta.2.
For now contents in _.asciidoctorconfig_ are not automatically included when creating a PDF.
You'll have to include this file manually.
If creating the PDF succeeds, the PDF is opened in the system's PDF viewer.
To find out more how to configure the output and formatting, please visit https://asciidoctor.org/docs/asciidoctor-pdf/.
=== Advanced
==== Configuration File
Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ publishPlugin {

dependencies {
compile 'org.asciidoctor:asciidoctorj-diagram:1.5.18'
compile 'org.asciidoctor:asciidoctorj-pdf:1.5.0-beta.2'
/* snakeyaml is s used by asciidoctorj-pdf, but is actually provided within jruby-stdlib
* a snakeyaml version in the classpath takes precedence, but IntelliJ includes a version that is too old
* therefore this plugin includes the same version of snakeyaml that is already included in jruby-stdlib
* to prevent loading the older version from IntelliJ.
* When a different version than jruby-stdlib 9.2.7.0 is used after upgrading asciidoctorj,
* double check the snakeyaml version.
* https://github.com/asciidoctor/asciidoctorj-pdf/issues/25
*/
compile 'org.yaml:snakeyaml:1.23'
compile 'org.asciidoctor:asciidoctorj:2.1.0'
compile 'commons-io:commons-io:2.4'
compile 'nl.jworks.markdown_to_asciidoc:markdown_to_asciidoc:1.1'
Expand Down
88 changes: 73 additions & 15 deletions src/main/java/org/asciidoc/intellij/AsciiDoc.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ private void initWithExtensions(List<String> extensions) {
asciidoctor.shutdown();
asciidoctor = null;
}
ClassLoader old = Thread.currentThread().getContextClassLoader();
ByteArrayOutputStream boasOut = new ByteArrayOutputStream();
ByteArrayOutputStream boasErr = new ByteArrayOutputStream();
SystemOutputHijacker.register(new PrintStream(boasOut), new PrintStream(boasErr));
Expand All @@ -143,7 +142,6 @@ private void initWithExtensions(List<String> extensions) {
}
}
try {
Thread.currentThread().setContextClassLoader(AsciiDocAction.class.getClassLoader());
asciidoctor = Asciidoctor.Factory.create();
asciidoctor.registerLogHandler(logHandler);
// disable JUL logging of captured messages
Expand Down Expand Up @@ -175,7 +173,6 @@ private void initWithExtensions(List<String> extensions) {
}
SystemOutputHijacker.deregister();
notify(boasOut, boasErr, Collections.emptyList());
Thread.currentThread().setContextClassLoader(old);
}
}
}
Expand Down Expand Up @@ -238,9 +235,19 @@ private void scanForRubyFiles(Path path, MessageDigest md) throws IOException {
}

private void notify(ByteArrayOutputStream boasOut, ByteArrayOutputStream boasErr, List<LogRecord> logRecords) {
notify(boasOut, boasErr, logRecords,
!AsciiDocApplicationSettings.getInstance().getAsciiDocPreviewSettings().isShowAsciiDocWarningsAndErrorsInEditor());
}

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

private void notify(ByteArrayOutputStream boasOut, ByteArrayOutputStream boasErr, List<LogRecord> logRecords,
boolean logAll) {
String out = boasOut.toString();
String err = boasErr.toString();
if (!AsciiDocApplicationSettings.getInstance().getAsciiDocPreviewSettings().isShowAsciiDocWarningsAndErrorsInEditor()) {
if (logAll) {
// logRecords will not be handled in the org.asciidoc.intellij.annotator.ExternalAnnotator
for (LogRecord logRecord : logRecords) {
if (logRecord.getSeverity() == Severity.DEBUG) {
Expand Down Expand Up @@ -361,21 +368,18 @@ public String render(String text, List<String> extensions) {
public String render(String text, List<String> extensions, Notifier notifier) {
synchronized (AsciiDoc.class) {
CollectingLogHandler logHandler = new CollectingLogHandler();
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(AsciiDocAction.class.getClassLoader());
ByteArrayOutputStream boasOut = new ByteArrayOutputStream();
ByteArrayOutputStream boasErr = new ByteArrayOutputStream();
SystemOutputHijacker.register(new PrintStream(boasOut), new PrintStream(boasErr));
try {
initWithExtensions(extensions);
ClassLoader old = Thread.currentThread().getContextClassLoader();
ByteArrayOutputStream boasOut = new ByteArrayOutputStream();
ByteArrayOutputStream boasErr = new ByteArrayOutputStream();
SystemOutputHijacker.register(new PrintStream(boasOut), new PrintStream(boasErr));
asciidoctor.registerLogHandler(logHandler);
try {
Thread.currentThread().setContextClassLoader(AsciiDocAction.class.getClassLoader());
return "<div id=\"content\">\n" + asciidoctor.convert(text, getDefaultOptions()) + "\n</div>";
return "<div id=\"content\">\n" + asciidoctor.convert(text, getDefaultOptions("html5")) + "\n</div>";
} finally {
asciidoctor.unregisterLogHandler(logHandler);
SystemOutputHijacker.deregister();
notifier.notify(boasOut, boasErr, logHandler.getLogRecords());
Thread.currentThread().setContextClassLoader(old);
}
} catch (Exception | ServiceConfigurationError ex) {
log.warn("unable to render AsciiDoc document", ex);
Expand All @@ -396,13 +400,67 @@ public String render(String text, List<String> extensions, Notifier notifier) {
} while (t != null);
response.append("<p>(the full exception stack trace is available in the IDE's log file. Visit menu item 'Help | Show Log in Explorer' to see the log)");
return response.toString();
} finally {
SystemOutputHijacker.deregister();
notifier.notify(boasOut, boasErr, logHandler.getLogRecords());
Thread.currentThread().setContextClassLoader(old);
}
}
}

public void renderPdf(File file, List<String> extensions) {
Notifier notifier = this::notifyAlways;
synchronized (AsciiDoc.class) {
CollectingLogHandler logHandler = new CollectingLogHandler();
ByteArrayOutputStream boasOut = new ByteArrayOutputStream();
ByteArrayOutputStream boasErr = new ByteArrayOutputStream();
SystemOutputHijacker.register(new PrintStream(boasOut), new PrintStream(boasErr));
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(AsciiDocAction.class.getClassLoader());
try {
extensions.add("asciidoctor-pdf");
initWithExtensions(extensions);
asciidoctor.registerLogHandler(logHandler);
try {
asciidoctor.convertFile(file, getDefaultOptions("pdf"));
} finally {
asciidoctor.unregisterLogHandler(logHandler);
}
} catch (Exception | ServiceConfigurationError ex) {
log.warn("unable to render AsciiDoc document", ex);
logHandler.log(new LogRecord(Severity.FATAL, ex.getMessage()));
StringBuilder response = new StringBuilder();
response.append("unable to render AsciiDoc document");
Throwable t = ex;
do {
response.append("<p>").append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage());
if (t instanceof MainExitException && t.getMessage().startsWith("unknown encoding name")) {
response.append("<p>Either your local encoding is not supported by JRuby, or you passed an unrecognized value to the Java property 'file.encoding' either in the IntelliJ options file or via the JAVA_TOOL_OPTION environment variable.");
String property = SafePropertyAccessor.getProperty("file.encoding", null);
response.append("<p>encoding passed by system property 'file.encoding': ").append(property);
response.append("<p>available encodings (excuding aliases): ");
EncodingDB.getEncodings().forEach(entry -> response.append(entry.getEncoding().getCharsetName()).append(" "));
}
t = t.getCause();
} while (t != null);
response.append("<p>(the full exception stack trace is available in the IDE's log file. Visit menu item 'Help | Show Log in Explorer' to see the log)");
try {
boasErr.write(response.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException("Unable to write bytes");
}
} finally {
SystemOutputHijacker.deregister();
notifier.notify(boasOut, boasErr, logHandler.getLogRecords());
Thread.currentThread().setContextClassLoader(old);
}
}
}

private Map<String, Object> getDefaultOptions() {
private Map<String, Object> getDefaultOptions(String backend) {
AttributesBuilder builder = AttributesBuilder.attributes()
.showTitle(true)
.backend(backend)
.sourceHighlighter("coderay")
.attribute("coderay-css", "style")
.attribute("env", "idea")
Expand All @@ -424,7 +482,7 @@ private Map<String, Object> getDefaultOptions() {

settings.getAsciiDocPreviewSettings().getAttributes().forEach(attrs::setAttribute);

OptionsBuilder opts = OptionsBuilder.options().safe(SafeMode.UNSAFE).backend("html5").headerFooter(false)
OptionsBuilder opts = OptionsBuilder.options().safe(SafeMode.UNSAFE).backend(backend).headerFooter(false)
.attributes(attrs)
.option("sourcemap", "true")
.baseDir(fileBaseDir);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.asciidoc.intellij.actions.asciidoc;

import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.ProjectViewPane;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import org.apache.commons.io.FileUtils;
import org.asciidoc.intellij.AsciiDoc;
import org.asciidoc.intellij.editor.AsciiDocPreviewEditor;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

public class CreatePdfAction extends AsciiDocAction {
public static final String ID = "org.asciidoc.intellij.actions.asciidoc.CreatePdfAction";

private Project project;

@Override
public boolean displayTextInToolbar() {
// this doesn't have an icon, therefore show text
return true;
}

@Override
public void actionPerformed(AnActionEvent event) {
project = event.getProject();
if (project == null) {
return;
}
Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
if (editor == null) {
return;
}

VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
if (file == null) {
return;
}
if (file.getCanonicalPath() == null) {
return;
}

ApplicationManager.getApplication().runWriteAction(() ->
ProgressManager.getInstance().executeProcessUnderProgress(() -> {
ApplicationManager.getApplication().saveAll();
File fileBaseDir = new File("");
VirtualFile parent = file.getParent();
if (parent != null && parent.getCanonicalPath() != null) {
// parent will be null if we use Language Injection and Fragment Editor
fileBaseDir = new File(parent.getCanonicalPath());
}
Path tempImagesPath = AsciiDoc.tempImagesPath();
try {
AsciiDoc asciiDoc = new AsciiDoc(project.getBasePath(), fileBaseDir,
tempImagesPath, file.getName());
/* TODO: content from .asciidoctorconfig not included, needing more experiments and ideas
how to include this into a file on disk. Maybe convert it to an extension? */
List<String> extensions = AsciiDoc.getExtensions(project);
asciiDoc.renderPdf(new File(file.getCanonicalPath()), extensions);
VirtualFile virtualFile = VirtualFileManager.getInstance()
.refreshAndFindFileByUrl(file.getUrl().replaceAll("\\.(adoc|asciidoc|ad)$", ".pdf"));
updateProjectView(virtualFile != null ? virtualFile : parent);
if (virtualFile != null) {
new OpenFileDescriptor(project, virtualFile).navigate(true);
}
} finally {
if (tempImagesPath != null) {
try {
FileUtils.deleteDirectory(tempImagesPath.toFile());
} catch (IOException _ex) {
Logger.getInstance(AsciiDocPreviewEditor.class).warn("could not remove temp folder", _ex);
}
}
}
}, new ProgressIndicatorBase()));

}

private void updateProjectView(VirtualFile virtualFile) {
//update project view
ProjectView projectView = ProjectView.getInstance(project);
projectView.changeView(ProjectViewPane.ID);
projectView.select(null, virtualFile, true);
}

}
5 changes: 5 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
description="Toggle using soft wraps in current editor"
icon="AllIcons.Actions.ToggleSoftWrap">
</action>
<action class="org.asciidoc.intellij.actions.asciidoc.CreatePdfAction"
id="org.asciidoc.intellij.actions.asciidoc.CreatePdfAction"
text="PDF"
description="Create PDF from current file">
</action>

<!--
<action id="asciidoc.convert.document6" class="org.asciidoc.intellij.actions.ConvertToAsciiDocAction"
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/org/asciidoc/intellij/AsciiDocTest.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package org.asciidoc.intellij;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase;
import org.asciidoc.intellij.settings.AsciiDocApplicationSettings;
import org.junit.Assert;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;

/**
* @author Alexander Schwartz 2018
*/
public class AsciiDocTest extends LightPlatformCodeInsightFixtureTestCase {
private Logger log = Logger.getInstance(AsciiDocTest.class);

private AsciiDoc asciidoc;

Expand All @@ -33,6 +38,40 @@ public void testShouldRenderPlainAsciidoc() {
Assert.assertTrue(html.contains("<strong>bold</strong>"));
}

public void testShouldRenderPdf() throws IOException {
// given...
File asciidoc = File.createTempFile("asciidocforapdf", ".adoc");
File pdf = new File(asciidoc.getAbsoluteFile().getAbsolutePath().replaceAll("\\.adoc$", ".pdf"));
try {
Assert.assertTrue("replacemante should have worked", pdf.getName().endsWith(".pdf"));
if (pdf.exists()) {
fail("PDF already exists, but shouldn't before runnning AsciiDoc");
}
FileWriter fw = new FileWriter(asciidoc);
fw.write("Hello world.");
fw.close();

// when...
this.asciidoc.renderPdf(asciidoc, new ArrayList<>());

// then...
Assert.assertTrue(pdf.exists());

} finally {
// cleanup...
if (asciidoc.exists()) {
if (!asciidoc.delete()) {
log.warn("unable to delete source file");
}
}
if (pdf.exists()) {
if (!pdf.delete()) {
log.warn("unable to delete destination file");
}
}
}
}

public void testShouldRenderAttributesAsciidoc() {
String expectedContent = "should replace attribute placeholder with value";
AsciiDocApplicationSettings.getInstance().getAsciiDocPreviewSettings().getAttributes().put("attr", expectedContent);
Expand Down

0 comments on commit e0580de

Please sign in to comment.