Skip to content

Commit 2884ac0

Browse files
committed
Add GraalJS-based djot converter for HTML export
Use embedded djot.js via GraalJS for proper Djot-to-HTML conversion. No external dependencies (PHP/Node.js) required.
1 parent c5b13e9 commit 2884ac0

File tree

3 files changed

+91
-68
lines changed

3 files changed

+91
-68
lines changed

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ repositories {
1111
mavenCentral()
1212
}
1313

14+
dependencies {
15+
implementation("org.graalvm.js:js:23.0.2")
16+
implementation("org.graalvm.js:js-scriptengine:23.0.2")
17+
}
18+
1419
intellij {
1520
version.set("2024.1")
1621
type.set("PS") // PhpStorm
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.phpcollective.djot
2+
3+
import org.graalvm.polyglot.Context
4+
import org.graalvm.polyglot.Source
5+
import java.net.URL
6+
7+
/**
8+
* Converts Djot markup to HTML using djot.js via GraalJS.
9+
*/
10+
object DjotConverter {
11+
12+
private val djotJs: String by lazy {
13+
try {
14+
URL("https://cdn.jsdelivr.net/npm/@djot/[email protected]/dist/djot.js").readText()
15+
} catch (e: Exception) {
16+
// Fallback: return empty, will use fallback converter
17+
""
18+
}
19+
}
20+
21+
private val contextBuilder: Context.Builder by lazy {
22+
Context.newBuilder("js")
23+
.allowAllAccess(false)
24+
.option("engine.WarnInterpreterOnly", "false")
25+
}
26+
27+
fun toHtml(djot: String): String {
28+
if (djotJs.isEmpty()) {
29+
return fallbackConvert(djot)
30+
}
31+
32+
return try {
33+
contextBuilder.build().use { context ->
34+
// Load djot.js
35+
context.eval(Source.newBuilder("js", djotJs, "djot.js").build())
36+
37+
// Convert djot to HTML
38+
val escaped = djot
39+
.replace("\\", "\\\\")
40+
.replace("`", "\\`")
41+
.replace("\$", "\\\$")
42+
.replace("\r\n", "\\n")
43+
.replace("\r", "\\n")
44+
.replace("\n", "\\n")
45+
46+
val result = context.eval("js", """
47+
(function() {
48+
var doc = djot.parse(`$escaped`);
49+
return djot.renderHTML(doc);
50+
})();
51+
""".trimIndent())
52+
53+
result.asString()
54+
}
55+
} catch (e: Exception) {
56+
fallbackConvert(djot)
57+
}
58+
}
59+
60+
private fun fallbackConvert(djot: String): String {
61+
// Basic fallback - headings, emphasis, code only
62+
return djot
63+
.replace(Regex("^###### (.+)$", RegexOption.MULTILINE), "<h6>$1</h6>")
64+
.replace(Regex("^##### (.+)$", RegexOption.MULTILINE), "<h5>$1</h5>")
65+
.replace(Regex("^#### (.+)$", RegexOption.MULTILINE), "<h4>$1</h4>")
66+
.replace(Regex("^### (.+)$", RegexOption.MULTILINE), "<h3>$1</h3>")
67+
.replace(Regex("^## (.+)$", RegexOption.MULTILINE), "<h2>$1</h2>")
68+
.replace(Regex("^# (.+)$", RegexOption.MULTILINE), "<h1>$1</h1>")
69+
.replace(Regex("\\*([^*]+)\\*"), "<strong>$1</strong>")
70+
.replace(Regex("_([^_]+)_"), "<em>$1</em>")
71+
.replace(Regex("`([^`]+)`"), "<code>$1</code>")
72+
.replace(Regex("\\{=([^=]+)=\\}"), "<mark>$1</mark>")
73+
.replace(Regex("^---+$", RegexOption.MULTILINE), "<hr>")
74+
.replace(Regex("^\\*\\*\\*+$", RegexOption.MULTILINE), "<hr>")
75+
.replace("\n\n", "</p>\n<p>")
76+
.let { "<p>$it</p>" }
77+
.replace(Regex("<p>(<h[1-6]>)"), "$1")
78+
.replace(Regex("(</h[1-6]>)</p>"), "$1")
79+
.replace(Regex("<p>(<hr>)</p>"), "$1")
80+
.replace("<p></p>", "")
81+
}
82+
}

src/main/kotlin/org/phpcollective/djot/actions/ExportHtmlAction.kt

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import com.intellij.openapi.application.ApplicationManager
77
import com.intellij.openapi.fileChooser.FileChooserFactory
88
import com.intellij.openapi.fileChooser.FileSaverDescriptor
99
import com.intellij.openapi.fileEditor.FileDocumentManager
10-
import com.intellij.openapi.project.Project
1110
import com.intellij.openapi.ui.Messages
12-
import com.intellij.openapi.vfs.VirtualFile
11+
import org.phpcollective.djot.DjotConverter
1312
import org.phpcollective.djot.DjotFileType
14-
import java.io.File
1513

1614
class ExportHtmlAction : AnAction() {
1715

@@ -35,7 +33,7 @@ class ExportHtmlAction : AnAction() {
3533

3634
// Convert and save
3735
ApplicationManager.getApplication().executeOnPooledThread {
38-
val html = convertToHtml(project, content)
36+
val html = convertToHtml(content)
3937
val fullHtml = wrapFullHtml(file.nameWithoutExtension, html)
4038

4139
ApplicationManager.getApplication().invokeLater {
@@ -57,70 +55,8 @@ class ExportHtmlAction : AnAction() {
5755
}
5856
}
5957

60-
private fun convertToHtml(project: Project, djot: String): String {
61-
// Try djot-php first
62-
try {
63-
val phpCode = "require_once 'vendor/autoload.php'; " +
64-
"\$c = new \\Djot\\DjotConverter(); " +
65-
"echo \$c->convert(file_get_contents('php://stdin'));"
66-
val process = ProcessBuilder("php", "-r", phpCode)
67-
.directory(project.basePath?.let { File(it) })
68-
.redirectErrorStream(true)
69-
.start()
70-
71-
process.outputStream.bufferedWriter().use { it.write(djot) }
72-
process.outputStream.close()
73-
74-
val result = process.inputStream.bufferedReader().readText()
75-
val exitCode = process.waitFor()
76-
77-
if (exitCode == 0 && result.isNotBlank()) return result
78-
} catch (_: Exception) {}
79-
80-
// Try npx djot (Node.js)
81-
try {
82-
val process = ProcessBuilder("npx", "djot")
83-
.directory(project.basePath?.let { File(it) })
84-
.redirectErrorStream(false)
85-
.start()
86-
87-
process.outputStream.bufferedWriter().use { it.write(djot) }
88-
process.outputStream.close()
89-
90-
val result = process.inputStream.bufferedReader().readText()
91-
val exitCode = process.waitFor()
92-
93-
if (exitCode == 0 && result.isNotBlank()) return result
94-
} catch (_: Exception) {}
95-
96-
// Fallback: show warning comment in output
97-
return "<!-- WARNING: Export requires djot-php or Node.js djot package for proper rendering.\n" +
98-
" Install: composer require php-collective/djot-php\n" +
99-
" Or: npm install -g @djot/djot -->\n\n" +
100-
fallbackConvert(djot)
101-
}
102-
103-
private fun fallbackConvert(djot: String): String {
104-
// Basic fallback - headings, emphasis, code only
105-
return djot
106-
.replace(Regex("^###### (.+)$", RegexOption.MULTILINE), "<h6>$1</h6>")
107-
.replace(Regex("^##### (.+)$", RegexOption.MULTILINE), "<h5>$1</h5>")
108-
.replace(Regex("^#### (.+)$", RegexOption.MULTILINE), "<h4>$1</h4>")
109-
.replace(Regex("^### (.+)$", RegexOption.MULTILINE), "<h3>$1</h3>")
110-
.replace(Regex("^## (.+)$", RegexOption.MULTILINE), "<h2>$1</h2>")
111-
.replace(Regex("^# (.+)$", RegexOption.MULTILINE), "<h1>$1</h1>")
112-
.replace(Regex("\\*([^*]+)\\*"), "<strong>$1</strong>")
113-
.replace(Regex("_([^_]+)_"), "<em>$1</em>")
114-
.replace(Regex("`([^`]+)`"), "<code>$1</code>")
115-
.replace(Regex("\\{=([^=]+)=\\}"), "<mark>$1</mark>")
116-
.replace(Regex("^---+$", RegexOption.MULTILINE), "<hr>")
117-
.replace(Regex("^\\*\\*\\*+$", RegexOption.MULTILINE), "<hr>")
118-
.replace("\n\n", "</p>\n<p>")
119-
.let { "<p>$it</p>" }
120-
.replace(Regex("<p>(<h[1-6]>)"), "$1")
121-
.replace(Regex("(</h[1-6]>)</p>"), "$1")
122-
.replace(Regex("<p>(<hr>)</p>"), "$1")
123-
.replace("<p></p>", "")
58+
private fun convertToHtml(djot: String): String {
59+
return DjotConverter.toHtml(djot)
12460
}
12561

12662
private fun wrapFullHtml(title: String, content: String): String {

0 commit comments

Comments
 (0)