Skip to content

Commit fe3c331

Browse files
committed
feat(build): load html libs from install layout
1 parent c6b6188 commit fe3c331

25 files changed

+200
-155
lines changed

build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ distributions.main {
129129
include("*.qd")
130130
}
131131
}
132+
// Third-party HTML library files (KaTeX, fonts, etc.) are bundled by the quarkdown-html module
133+
// and placed in lib/html for filesystem-based loading at runtime.
134+
val htmlModule = project(":quarkdown-html")
135+
into("lib/html") {
136+
from(htmlModule.layout.buildDirectory.dir("thirdparty"))
137+
}
132138
// Include the generated Dokka documentation, generated by quarkdocGenerate,
133139
// in the 'docs' directory.
134140
val dokkaOutputDir = layout.buildDirectory.file("docs")
@@ -140,6 +146,7 @@ distributions.main {
140146

141147
tasks.installDist {
142148
dependsOn(quarkdocGenerate)
149+
dependsOn(":quarkdown-html:bundleThirdParty")
143150
}
144151

145152
tasks.distZip {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.io.File
99
* @param source main source file to process
1010
* @param outputDirectory the output directory to save resource in, if set
1111
* @param libraryDirectory the directory to load .qd library files from
12+
* @param htmlLibraryDirectory the directory containing third-party HTML library files
1213
* @param rendererName name of the renderer to use to generate the output for
1314
* @param clean whether to clean the output directory before generating new files
1415
* @param pipe whether to output the rendered result to standard output, suitable for piping
@@ -21,6 +22,7 @@ data class CliOptions(
2122
val source: File?,
2223
val outputDirectory: File?,
2324
val libraryDirectory: File?,
25+
val htmlLibraryDirectory: File?,
2426
val rendererName: String,
2527
val clean: Boolean,
2628
val pipe: Boolean,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import java.io.File
3232
const val DEFAULT_OUTPUT_DIRECTORY = "output"
3333

3434
/**
35-
* Name of the default directory to load libraries from.
36-
* The default value is relative to the executable JAR file location, and points to the `lib/qd` directory of the distribution archive.
35+
* Default directory to load Quarkdown libraries from.
36+
* The default value is relative to the executable JAR file location, and points to the `lib/qd` directory of the distribution layout.
3737
* It can be overridden by the user.
3838
*/
3939
val DEFAULT_LIBRARY_DIRECTORY = ".." + File.separator + "lib" + File.separator + "qd"
@@ -202,6 +202,7 @@ abstract class ExecuteCommand(
202202
source = null,
203203
outputDirectory,
204204
libraryDirectory,
205+
htmlLibraryDirectory = com.quarkdown.rendering.html.post.resources.ThirdPartyLibraryDirectory.path,
205206
renderer,
206207
clean,
207208
pipe = false,

quarkdown-cli/src/main/kotlin/com/quarkdown/cli/renderer/RendererRetriever.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@ class RendererRetriever(
3232
fun getRenderer(): (RendererFactory, Context) -> RenderingComponents =
3333
{ factory, context ->
3434
when {
35-
isHtmlPdf() -> factory.htmlPdf(context, createHtmlPdfExportOptions())
36-
isHtml() -> factory.html(context)
35+
isHtmlPdf() ->
36+
factory.htmlPdf(
37+
context,
38+
createHtmlPdfExportOptions(),
39+
libraryDirectory = options.htmlLibraryDirectory,
40+
)
41+
isHtml() -> factory.html(context, libraryDirectory = options.htmlLibraryDirectory)
3742
isPlainText() -> factory.plainText(context)
3843
else -> throw IllegalArgumentException("Unsupported renderer: '${options.rendererName}'")
3944
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.quarkdown.core.pipeline.output
2+
3+
import java.io.File
4+
5+
/**
6+
* An [OutputResource] backed by a file on the filesystem.
7+
* Instead of holding content in memory, it references a [file] that is copied to the output location on save.
8+
*
9+
* This is efficient for bundling large pre-existing files (e.g. third-party libraries)
10+
* where loading bytes into memory would be unnecessary overhead.
11+
*
12+
* @param name the output file name (with extension, since the original file name is used as-is)
13+
* @param file the source file to copy
14+
*/
15+
data class FileReferenceOutputResource(
16+
override val name: String,
17+
val file: File,
18+
) : OutputResource {
19+
override fun <T> accept(visitor: OutputResourceVisitor<T>): T = visitor.visit(this)
20+
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/pipeline/output/OutputResourceVisitor.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ interface OutputResourceVisitor<T> {
1010
fun visit(artifact: BinaryOutputArtifact): T
1111

1212
fun visit(group: OutputResourceGroup): T
13+
14+
fun visit(resource: FileReferenceOutputResource): T
1315
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/pipeline/output/visitor/CopyOutputResourceVisitor.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.quarkdown.core.pipeline.output.visitor
22

33
import com.quarkdown.core.pipeline.output.BinaryOutputArtifact
4+
import com.quarkdown.core.pipeline.output.FileReferenceOutputResource
45
import com.quarkdown.core.pipeline.output.OutputResource
56
import com.quarkdown.core.pipeline.output.OutputResourceGroup
67
import com.quarkdown.core.pipeline.output.OutputResourceVisitor
@@ -17,6 +18,8 @@ class CopyOutputResourceVisitor(
1718

1819
override fun visit(artifact: BinaryOutputArtifact): OutputResource = artifact.copy(name = name)
1920

21+
override fun visit(resource: FileReferenceOutputResource): OutputResource = resource.copy(name = name)
22+
2023
override fun visit(group: OutputResourceGroup): OutputResource = group.copy(name = name)
2124
}
2225

quarkdown-core/src/main/kotlin/com/quarkdown/core/pipeline/output/visitor/FileResourceExporter.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.quarkdown.core.pipeline.output.visitor
22

33
import com.quarkdown.core.pipeline.output.ArtifactType
44
import com.quarkdown.core.pipeline.output.BinaryOutputArtifact
5+
import com.quarkdown.core.pipeline.output.FileReferenceOutputResource
56
import com.quarkdown.core.pipeline.output.OutputArtifact
67
import com.quarkdown.core.pipeline.output.OutputResource
78
import com.quarkdown.core.pipeline.output.OutputResourceGroup
@@ -79,6 +80,18 @@ class FileResourceExporter(
7980
}
8081
}
8182

83+
/**
84+
* Copies a [FileReferenceOutputResource] to the output location.
85+
* @return the copied file
86+
*/
87+
override fun visit(resource: FileReferenceOutputResource) =
88+
File(location, resource.name).also {
89+
if (write) {
90+
it.parentFile?.mkdirs()
91+
resource.file.copyTo(it, overwrite = true)
92+
}
93+
}
94+
8295
/**
8396
* Saves an [OutputResourceGroup] to a directory which contains its nested files.
8497
* @return the directory file itself

quarkdown-html/build.gradle.kts

Lines changed: 35 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
import com.github.gradle.node.npm.task.NpmTask
33
import com.github.gradle.node.npm.task.NpxTask
4-
import groovy.json.JsonOutput
54

65
plugins {
76
kotlin("jvm")
@@ -25,28 +24,30 @@ dependencies {
2524
/*
2625
Third-party library bundling
2726
28-
Libraries from node_modules are copied into src/main/resources/render/lib/
29-
at Gradle build time, so they are bundled in the JAR and copied to the
30-
Quarkdown output directory at compilation time.
31-
32-
A nested third-party-manifest.json is generated at the end so that the
33-
Kotlin runtime can enumerate JAR resources (JAR directories are not listable).
27+
Libraries from node_modules are copied into build/thirdparty/
28+
at Gradle build time, so they are included in the distribution archive
29+
and loaded from the filesystem at runtime.
3430
3531
To add a new library:
3632
1. Add the npm package to package.json
3733
2. Add a LibrarySpec entry to `librariesToBundle` (or to `fontsourcePackages` for fonts)
3834
3. For non-font libraries, add a ThirdPartyLibrary subclass in ThirdPartyLibrary.kt
3935
*/
4036

41-
val libDir = projectDir.resolve("src/main/resources/render/lib")
37+
val thirdPartyDir =
38+
layout.buildDirectory
39+
.dir("thirdparty")
40+
.get()
41+
.asFile
4242
val nodeModules = projectDir.resolve("node_modules")
4343
val scssThirdParty = projectDir.resolve("src/main/scss/thirdparty")
44+
val staticFontsDir = projectDir.resolve("src/main/fonts")
4445

4546
/**
46-
* Declarative specification for copying a library from `node_modules` into `lib/`.
47+
* Declarative specification for copying a library from `node_modules` into `build/thirdparty/`.
4748
* Multiple specs may share the same [target] (their files are merged into one directory).
4849
*
49-
* @param target output directory name under `lib/`
50+
* @param target output directory name under `build/thirdparty/`
5051
* @param source path relative to `node_modules/`
5152
* @param includes glob patterns to select files (empty = everything)
5253
*/
@@ -57,7 +58,7 @@ data class LibrarySpec(
5758
)
5859

5960
/**
60-
* All libraries to copy from `node_modules` into JAR resources.
61+
* All libraries to copy from `node_modules` into the distribution.
6162
* To add a new library, append a `LibrarySpec` entry here.
6263
*/
6364
val librariesToBundle =
@@ -72,7 +73,7 @@ val librariesToBundle =
7273
)
7374

7475
/**
75-
* @fontsource packages to bundle. Each is placed in its own `lib/fonts/{name}/` directory
76+
* @fontsource packages to bundle. Each is placed in its own `build/thirdparty/fonts/{name}/` directory
7677
* with auto-discovered latin woff2 variants and a generated `fonts.css`.
7778
* SCSS layout themes select which fonts they need via `@import url(...)`.
7879
*/
@@ -109,19 +110,19 @@ val bundleHighlightJs =
109110
"--format=iife",
110111
"--global-name=hljs",
111112
"--minify",
112-
"--outfile=${libDir.resolve("highlight.js/highlight.min.js")}",
113+
"--outfile=${thirdPartyDir.resolve("highlight.js/highlight.min.js")}",
113114
),
114115
)
115116
}
116117

117118
/**
118119
* Main bundling task: copies libraries, bundles fontsource fonts,
119-
* copies hljs SCSS partials, and generates the nested manifest JSON.
120+
* and copies hljs SCSS partials.
120121
*/
121122
val bundleThirdParty =
122123
tasks.register<DefaultTask>("bundleThirdParty") {
123124
group = "build"
124-
description = "Bundles third-party libraries from node_modules into JAR resources"
125+
description = "Bundles third-party libraries from node_modules into the distribution"
125126
dependsOn(tasks.npmInstall)
126127
dependsOn(bundleHighlightJs)
127128

@@ -131,28 +132,32 @@ val bundleThirdParty =
131132
from(nodeModules.resolve(source)) {
132133
if (includes.isNotEmpty()) include(includes)
133134
}
134-
into(libDir.resolve(target))
135+
into(thirdPartyDir.resolve(target))
135136
}
136137
}
137138

138139
fontsourcePackages.forEach { fontName ->
139140
bundleFontsource("fonts/$fontName", listOf(fontName))
140141
}
141142

143+
// Copy static (non-npm) font files from the source tree.
144+
copy {
145+
from(staticFontsDir)
146+
into(thirdPartyDir.resolve("fonts"))
147+
}
148+
142149
scssThirdParty.mkdirs()
143150
hljsScssPartials.forEach { (source, partialName) ->
144151
nodeModules
145152
.resolve("highlight.js/styles/$source")
146153
.copyTo(scssThirdParty.resolve("_$partialName.scss"), overwrite = true)
147154
}
148-
149-
generateNestedManifest()
150155
}
151156
}
152157

153158
/**
154159
* Auto-discovers latin-subset woff2 files from the given @fontsource packages,
155-
* copies them into the given [targetSubdir] under `lib/`, and generates a `fonts.css`
160+
* copies them into the given [targetSubdir] under `build/thirdparty/`, and generates a `fonts.css`
156161
* with `@font-face` declarations derived from the file naming convention.
157162
*
158163
* @fontsource files follow the pattern `{font}-latin-{weight}-{style}.woff2`,
@@ -162,7 +167,7 @@ fun bundleFontsource(
162167
targetSubdir: String,
163168
fontNames: List<String>,
164169
) {
165-
val targetDir = libDir.resolve(targetSubdir)
170+
val targetDir = thirdPartyDir.resolve(targetSubdir)
166171
val cssBuilder = StringBuilder()
167172

168173
// Matches e.g. "lato-latin-400-normal.woff2" -> (lato, 400, normal)
@@ -201,45 +206,6 @@ fun bundleFontsource(
201206
targetDir.resolve("fonts.css").writeText(cssBuilder.toString())
202207
}
203208

204-
/**
205-
* Generates `third-party-manifest.json` as a nested JSON tree mirroring the `lib/` directory structure.
206-
* Files at each level are listed under `_files`; subdirectories become nested objects.
207-
*
208-
* ```json
209-
* {
210-
* "katex": {
211-
* "_files": ["katex.min.css", "katex.min.js"],
212-
* "fonts": { "_files": ["KaTeX_Main-Regular.woff2"] }
213-
* },
214-
* "fonts": {
215-
* "latex": { "_files": ["fonts.css", "ComputerModern-Serif-Regular.woff"] }
216-
* }
217-
* }
218-
* ```
219-
*/
220-
fun generateNestedManifest() {
221-
fun dirToJson(dir: File): Map<String, Any> =
222-
buildMap {
223-
dir
224-
.listFiles()
225-
?.filter { it.isFile && it.name != "third-party-manifest.json" }
226-
?.map { it.name }
227-
?.sorted()
228-
?.takeIf { it.isNotEmpty() }
229-
?.let { put("_files", it) }
230-
231-
dir
232-
.listFiles()
233-
?.filter { it.isDirectory }
234-
?.sortedBy { it.name }
235-
?.forEach { put(it.name, dirToJson(it)) }
236-
}
237-
238-
libDir
239-
.resolve("third-party-manifest.json")
240-
.writeText(JsonOutput.prettyPrint(JsonOutput.toJson(dirToJson(libDir))))
241-
}
242-
243209
// SCSS and TypeScript bundling
244210

245211
val scssDir = projectDir.resolve("src/main/scss")
@@ -287,7 +253,16 @@ val bundleTypeScript =
287253
tasks.processResources {
288254
dependsOn(tasks.compileSass)
289255
dependsOn(bundleTypeScript)
290-
dependsOn(bundleThirdParty)
256+
// bundleThirdParty no longer needed for processResources since libraries are not in the JAR.
257+
// It is still needed for compileSass (hljs SCSS partials).
258+
259+
// Write the absolute path of the third-party directory into a resource file
260+
// so the CLI can locate it at runtime regardless of how it was launched.
261+
doLast {
262+
val propsDir = destinationDir.resolve("render")
263+
propsDir.mkdirs()
264+
propsDir.resolve("thirdparty.path").writeText(thirdPartyDir.absolutePath)
265+
}
291266
}
292267

293268
// Tests

quarkdown-html/src/main/resources/render/lib/fonts/latex/ComputerModern-Serif-Bold.woff renamed to quarkdown-html/src/main/fonts/latex/ComputerModern-Serif-Bold.woff

File renamed without changes.

0 commit comments

Comments
 (0)