Skip to content

Commit c85dbaa

Browse files
authored
Merge pull request #16 from iamgio/renderer-refactor
Refactor: modular architecture for rendering targets
2 parents 5fbf5f1 + b6b9e08 commit c85dbaa

File tree

109 files changed

+608
-386
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+608
-386
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,13 @@ If you would like to familiarize yourself with Quarkdown instead, `quarkdown rep
272272
273273
- **`--pdf`**: produces a PDF file. Learn more in the wiki's [*PDF export*](https://github.com/iamgio/quarkdown/wiki/pdf-export) page.
274274

275-
- `-o <dir>` or `--output <dir>`: sets the directory of the output files. If unset, defaults to `./output`.
275+
- `-o <dir>` or `--output <dir>`: sets the directory of the output files. Defaults to `./output`.
276276

277-
- `-l <dir>` or `--libs <dir>`: sets the directory where external libraries can be loaded from. If unset, defaults to `<install dir>/lib/qmd`. [(?)](https://github.com/iamgio/quarkdown/wiki/importing-external-libraries)
277+
- `-l <dir>` or `--libs <dir>`: sets the directory where external libraries can be loaded from. Defaults to `<install dir>/lib/qmd`. [(?)](https://github.com/iamgio/quarkdown/wiki/importing-external-libraries)
278+
279+
- `-r <renderer>` or `--render <renderer>`: sets the target renderer. Defaults to `html`. Accepted values:
280+
- `html`
281+
- `html-pdf` (equivalent to `-r html --pdf`)
278282

279283
- `--server-port <port>`: optional customization of the local webserver's port. Defaults to `8089`.
280284

quarkdown-cli/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ dependencies {
77
testImplementation("org.jetbrains.kotlin:kotlin-test")
88
testImplementation("org.apache.pdfbox:pdfbox:3.0.4")
99
implementation(project(":quarkdown-core"))
10+
implementation(project(":quarkdown-html"))
1011
implementation(project(":quarkdown-server"))
1112
implementation(project(":quarkdown-interaction"))
12-
implementation(project(":quarkdown-pdf"))
1313
implementation(project(":quarkdown-stdlib"))
1414
implementation("com.github.ajalt.clikt:clikt:5.0.3")
1515
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.quarkdown.cli
22

3+
import com.quarkdown.cli.renderer.RendererRetriever
34
import java.io.File
45

56
/**
@@ -8,6 +9,7 @@ import java.io.File
89
* @param source main source file to process
910
* @param outputDirectory the output directory to save resource in, if set
1011
* @param libraryDirectory the directory to load .qmd library files from
12+
* @param rendererName name of the renderer to use to generate the output for
1113
* @param clean whether to clean the output directory before generating new files
1214
* @param nodePath path to the Node.js executable
1315
* @param npmPath path to the npm executable
@@ -16,7 +18,16 @@ data class CliOptions(
1618
val source: File?,
1719
val outputDirectory: File?,
1820
val libraryDirectory: File?,
21+
val rendererName: String,
1922
val clean: Boolean,
20-
val nodePath: String?,
21-
val npmPath: String?,
22-
)
23+
val nodePath: String,
24+
val npmPath: String,
25+
val noPdfSandbox: Boolean = false,
26+
val generatePdf: Boolean = false,
27+
) {
28+
/**
29+
* The rendering target to generate the output for.
30+
* For instance HTML or PDF.
31+
*/
32+
val renderer by lazy { RendererRetriever(this).getRenderer() }
33+
}

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/PipelineInitialization.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.quarkdown.cli
22

3+
import com.quarkdown.core.context.Context
34
import com.quarkdown.core.context.MutableContext
45
import com.quarkdown.core.flavor.MarkdownFlavor
6+
import com.quarkdown.core.flavor.RendererFactory
57
import com.quarkdown.core.function.library.Library
68
import com.quarkdown.core.function.library.LibraryExporter
79
import com.quarkdown.core.log.DebugFormatter
810
import com.quarkdown.core.log.Log
911
import com.quarkdown.core.pipeline.Pipeline
1012
import com.quarkdown.core.pipeline.PipelineHooks
1113
import com.quarkdown.core.pipeline.PipelineOptions
14+
import com.quarkdown.core.rendering.RenderingComponents
1215
import com.quarkdown.stdlib.Stdlib
1316

1417
/**
@@ -25,6 +28,7 @@ object PipelineInitialization {
2528
flavor: MarkdownFlavor,
2629
loadableLibraryExporters: Set<LibraryExporter>,
2730
options: PipelineOptions,
31+
renderer: (RendererFactory, Context) -> RenderingComponents,
2832
): Pipeline {
2933
// Libraries to load.
3034
val libraries: Set<Library> = LibraryExporter.exportAll(Stdlib)
@@ -52,7 +56,7 @@ object PipelineInitialization {
5256
context = MutableContext(flavor, loadableLibraries = loadableLibraries),
5357
options = options,
5458
libraries = libraries,
55-
renderer = { rendererFactory, context -> rendererFactory.html(context) },
59+
renderer = renderer,
5660
hooks = hooks,
5761
)
5862
}

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/creator/command/CreateProjectCommand.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import com.quarkdown.cli.creator.ProjectCreator
1414
import com.quarkdown.cli.creator.content.DefaultProjectCreatorInitialContentSupplier
1515
import com.quarkdown.cli.creator.content.EmptyProjectCreatorInitialContentSupplier
1616
import com.quarkdown.cli.creator.template.DefaultProjectCreatorTemplateProcessorFactory
17-
import com.quarkdown.cli.util.saveTo
1817
import com.quarkdown.core.document.DocumentAuthor
1918
import com.quarkdown.core.document.DocumentInfo
2019
import com.quarkdown.core.document.DocumentTheme
@@ -23,6 +22,7 @@ import com.quarkdown.core.function.quarkdownName
2322
import com.quarkdown.core.localization.Locale
2423
import com.quarkdown.core.localization.LocaleLoader
2524
import com.quarkdown.core.log.Log
25+
import com.quarkdown.core.pipeline.output.saveTo
2626
import java.io.File
2727

2828
/**

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/CompileCommand.kt

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,15 @@ package com.quarkdown.cli.exec
33
import com.github.ajalt.clikt.parameters.arguments.argument
44
import com.github.ajalt.clikt.parameters.options.flag
55
import com.github.ajalt.clikt.parameters.options.option
6+
import com.github.ajalt.clikt.parameters.options.validate
67
import com.github.ajalt.clikt.parameters.types.file
78
import com.quarkdown.cli.CliOptions
89
import com.quarkdown.cli.exec.strategy.FileExecutionStrategy
910
import com.quarkdown.cli.server.WebServerOptions
10-
import com.quarkdown.core.context.MutableContext
11-
import com.quarkdown.core.flavor.quarkdown.QuarkdownFlavor
1211
import com.quarkdown.core.log.Log
1312
import com.quarkdown.core.pipeline.PipelineOptions
14-
import com.quarkdown.pdf.PdfExportOptions
15-
import com.quarkdown.pdf.PdfExporters
1613
import com.quarkdown.server.browser.DefaultBrowserLauncher
1714
import java.io.File
18-
import kotlin.concurrent.thread
1915

2016
/**
2117
* Command to compile a Quarkdown file into an output.
@@ -42,23 +38,22 @@ class CompileCommand : ExecuteCommand("c") {
4238
private val noPdfSandbox: Boolean by option(
4339
"--pdf-no-sandbox",
4440
help = "(Unsafe) Disable Chrome sandbox for PDF export",
45-
).flag()
46-
47-
override fun finalizeCliOptions(original: CliOptions) = original.copy(source = source)
48-
49-
override fun createExecutionStrategy(cliOptions: CliOptions) = FileExecutionStrategy(source)
50-
51-
/**
52-
* Runs the action in parallel if needed.
53-
*/
54-
private fun runParallelizable(action: () -> Unit) {
55-
val parallelizeCondition = super.preview
41+
).flag().validate {
5642
when {
57-
parallelizeCondition -> thread { action() }
58-
else -> action()
43+
it && !exportPdf -> Log.warn("--pdf-no-sandbox flag is ignored because --pdf flag is not set.")
44+
it -> Log.warn("Disabling Chrome sandbox for PDF export. This is potentially unsafe.")
5945
}
6046
}
6147

48+
override fun finalizeCliOptions(original: CliOptions) =
49+
original.copy(
50+
source = source,
51+
noPdfSandbox = noPdfSandbox,
52+
generatePdf = exportPdf,
53+
)
54+
55+
override fun createExecutionStrategy(cliOptions: CliOptions) = FileExecutionStrategy(source)
56+
6257
override fun postExecute(
6358
outcome: ExecutionOutcome,
6459
cliOptions: CliOptions,
@@ -69,16 +64,6 @@ class CompileCommand : ExecuteCommand("c") {
6964
return
7065
}
7166

72-
if (noPdfSandbox) {
73-
when {
74-
!exportPdf -> Log.warn("--pdf-no-sandbox flag is ignored because --pdf flag is not set.")
75-
else -> Log.warn("Disabling Chrome sandbox for PDF export. This is potentially unsafe.")
76-
}
77-
}
78-
79-
if (exportPdf) {
80-
runParallelizable { exportPdf(outcome.directory) }
81-
}
8267
if (super.preview) {
8368
launchServer(outcome.directory)
8469
}
@@ -97,24 +82,4 @@ class CompileCommand : ExecuteCommand("c") {
9782
),
9883
)
9984
}
100-
101-
private fun exportPdf(
102-
sourceDirectory: File,
103-
outDirectory: File = sourceDirectory.parentFile,
104-
) {
105-
val out = File(outDirectory, "${sourceDirectory.name}.pdf")
106-
107-
// Currently, HTML is hardcoded. In the future, more targets can be chosen
108-
// and the PDF exporter for the corresponding target should be selected.
109-
val html = QuarkdownFlavor.rendererFactory.html(MutableContext(QuarkdownFlavor))
110-
111-
val options = PdfExportOptions(super.nodePath, super.npmPath, this.noPdfSandbox)
112-
val exporter = PdfExporters.getForRenderingTarget(html.postRenderer, options)
113-
114-
try {
115-
exporter.export(sourceDirectory, out)
116-
} catch (e: Exception) {
117-
Log.error("Failed to export PDF: ${e.message}")
118-
}
119-
}
12085
}

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/Execute.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.quarkdown.cli.lib.QmdLibraries
77
import com.quarkdown.cli.server.WebServerOptions
88
import com.quarkdown.cli.server.WebServerStarter
99
import com.quarkdown.cli.util.cleanDirectory
10-
import com.quarkdown.cli.util.saveTo
1110
import com.quarkdown.core.flavor.MarkdownFlavor
1211
import com.quarkdown.core.flavor.quarkdown.QuarkdownFlavor
1312
import com.quarkdown.core.function.error.FunctionRuntimeException
@@ -16,6 +15,7 @@ import com.quarkdown.core.log.Log
1615
import com.quarkdown.core.pipeline.Pipeline
1716
import com.quarkdown.core.pipeline.PipelineOptions
1817
import com.quarkdown.core.pipeline.error.PipelineException
18+
import com.quarkdown.core.pipeline.output.saveTo
1919
import com.quarkdown.server.message.Reload
2020
import com.quarkdown.server.message.ServerMessage
2121
import kotlin.system.exitProcess
@@ -46,7 +46,7 @@ fun runQuarkdown(
4646

4747
// The pipeline that contains all the stages to go through,
4848
// from the source input to the final output.
49-
val pipeline: Pipeline = PipelineInitialization.init(flavor, libraries, pipelineOptions)
49+
val pipeline: Pipeline = PipelineInitialization.init(flavor, libraries, pipelineOptions, cliOptions.renderer)
5050

5151
// Output directory to save the generated resources in.
5252
val outputDirectory = cliOptions.outputDirectory

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/ExecuteCommand.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ abstract class ExecuteCommand(
7878
canBeDir = true,
7979
).default(File(thisExecutableFile?.parentFile, DEFAULT_LIBRARY_DIRECTORY))
8080

81+
/**
82+
* The rendering target to generate output for.
83+
*/
84+
private val renderer: String by option(
85+
"-r",
86+
"--render",
87+
help = "Rendering target to generate output for",
88+
).default("html")
89+
8190
/**
8291
* When enabled, the rendering stage produces pretty output code.
8392
*/
@@ -128,7 +137,7 @@ abstract class ExecuteCommand(
128137
/**
129138
* Path to the Node.js executable, needed for PDF export.
130139
*/
131-
protected val nodePath: String by option("--node-path", help = "Path to the Node.js executable")
140+
private val nodePath: String by option("--node-path", help = "Path to the Node.js executable")
132141
.default(NodeJsWrapper.defaultPath)
133142

134143
/**
@@ -146,6 +155,7 @@ abstract class ExecuteCommand(
146155
source = null,
147156
outputDirectory,
148157
libraryDirectory,
158+
renderer,
149159
clean,
150160
nodePath,
151161
npmPath,

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/strategy/FileExecutionStrategy.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ import java.io.File
1010
class FileExecutionStrategy(
1111
private val file: File,
1212
) : PipelineExecutionStrategy {
13-
override fun execute(pipeline: Pipeline): OutputResource = pipeline.execute(file.readText())
13+
override fun execute(pipeline: Pipeline): OutputResource? = pipeline.execute(file.readText())
1414
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.quarkdown.cli.renderer
2+
3+
import com.quarkdown.cli.CliOptions
4+
import com.quarkdown.core.context.Context
5+
import com.quarkdown.core.flavor.RendererFactory
6+
import com.quarkdown.core.rendering.RenderingComponents
7+
import com.quarkdown.rendering.html.extension.html
8+
import com.quarkdown.rendering.html.extension.htmlPdf
9+
import com.quarkdown.rendering.html.pdf.HtmlPdfExportOptions
10+
11+
private const val HTML = "html"
12+
private const val HTML_PDF = "html-pdf"
13+
14+
/**
15+
* Given a [CliOptions] instance, retrieves the appropriate renderer (e.g. HTML, PDF) for the pipeline
16+
* based on [CliOptions.rendererName] (case-insensitive), [CliOptions.generatePdf] and other options.
17+
*/
18+
class RendererRetriever(
19+
private val options: CliOptions,
20+
) {
21+
private val name
22+
get() = options.rendererName.lowercase()
23+
24+
/**
25+
* Retrieves the rendering target specified by [options].
26+
*
27+
* Note: the current implementation hardcodes renderer names. In the future an extensible retriever will be implemented.
28+
* @return the rendering target for the pipeline, to generate the output for.
29+
*/
30+
fun getRenderer(): (RendererFactory, Context) -> RenderingComponents =
31+
{ factory, context ->
32+
when {
33+
isHtmlPdf() -> factory.htmlPdf(context, createHtmlPdfExportOptions())
34+
isHtml() -> factory.html(context)
35+
else -> throw IllegalArgumentException("Unsupported renderer: '${options.rendererName}'")
36+
}
37+
}
38+
39+
private fun isHtml() = name == HTML
40+
41+
private fun isHtmlPdf() = name == HTML_PDF || (name == HTML && options.generatePdf)
42+
43+
private fun createHtmlPdfExportOptions() =
44+
HtmlPdfExportOptions(
45+
outputDirectory = requireNotNull(options.outputDirectory) { "Output directory must be specified for PDF export." },
46+
nodeJsPath = options.nodePath,
47+
npmPath = options.npmPath,
48+
noSandbox = options.noPdfSandbox,
49+
)
50+
}

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/util/IOUtils.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.quarkdown.cli.util
22

3-
import com.quarkdown.core.pipeline.output.FileResourceExporter
4-
import com.quarkdown.core.pipeline.output.OutputResource
53
import java.io.File
64
import kotlin.io.path.ExperimentalPathApi
75
import kotlin.io.path.deleteRecursively
@@ -26,10 +24,3 @@ val thisExecutableFile: File?
2624
fun File.cleanDirectory() {
2725
listFiles()?.forEach { it.toPath().deleteRecursively() }
2826
}
29-
30-
/**
31-
* Saves [this] resource to file in a [directory].
32-
* @see FileResourceExporter
33-
* @return the saved file
34-
*/
35-
fun OutputResource.saveTo(directory: File): File = accept(FileResourceExporter(location = directory))

0 commit comments

Comments
 (0)