From 31a05820c90a9f20a5b3f91945870b4ac331e17f Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Wed, 26 Jun 2024 17:16:53 +0200 Subject: [PATCH] Set pipeline working directory --- .../iamgio/quarkdown/cli/QuarkdownCommand.kt | 1 + .../quarkdown/pipeline/PipelineOptions.kt | 3 ++ .../kotlin/eu/iamgio/quarkdown/stdlib/Data.kt | 43 +++++++++++++++++-- .../eu/iamgio/quarkdown/stdlib/Ecosystem.kt | 8 ++-- .../eu/iamgio/quarkdown/stdlib/DataTest.kt | 15 ++++--- 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/cli/src/main/kotlin/eu/iamgio/quarkdown/cli/QuarkdownCommand.kt b/cli/src/main/kotlin/eu/iamgio/quarkdown/cli/QuarkdownCommand.kt index bbb32394..ed64899e 100644 --- a/cli/src/main/kotlin/eu/iamgio/quarkdown/cli/QuarkdownCommand.kt +++ b/cli/src/main/kotlin/eu/iamgio/quarkdown/cli/QuarkdownCommand.kt @@ -64,6 +64,7 @@ class QuarkdownCommand : CliktCommand() { PipelineOptions( prettyOutput = prettyOutput, wrapOutput = !noWrap, + workingDirectory = source?.parentFile, errorHandler = when { strict -> StrictPipelineErrorHandler() diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/pipeline/PipelineOptions.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/pipeline/PipelineOptions.kt index c730954d..99636c4c 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/pipeline/PipelineOptions.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/pipeline/PipelineOptions.kt @@ -2,6 +2,7 @@ package eu.iamgio.quarkdown.pipeline import eu.iamgio.quarkdown.pipeline.error.BasePipelineErrorHandler import eu.iamgio.quarkdown.pipeline.error.PipelineErrorHandler +import java.io.File /** * Read-only settings that affect different behaviors of a [Pipeline]. @@ -9,11 +10,13 @@ import eu.iamgio.quarkdown.pipeline.error.PipelineErrorHandler * @param wrapOutput whether the rendered code should be wrapped in a template code. * For example, an HTML wrapper may add `......`, * with the actual content injected in `body` + * @param workingDirectory the starting directory to use when resolving relative paths from function calls * @param errorHandler the error handler strategy to use when an error occurs in the pipeline, * during the processing of a Quarkdown file */ data class PipelineOptions( val prettyOutput: Boolean = false, val wrapOutput: Boolean = true, + val workingDirectory: File? = null, val errorHandler: PipelineErrorHandler = BasePipelineErrorHandler(), ) diff --git a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Data.kt b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Data.kt index c0e066a8..e38495a9 100644 --- a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Data.kt +++ b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Data.kt @@ -3,6 +3,9 @@ package eu.iamgio.quarkdown.stdlib import com.github.doyaaaaaken.kotlincsv.dsl.csvReader import eu.iamgio.quarkdown.ast.Table import eu.iamgio.quarkdown.ast.Text +import eu.iamgio.quarkdown.context.Context +import eu.iamgio.quarkdown.function.error.FunctionRuntimeException +import eu.iamgio.quarkdown.function.reflect.Injected import eu.iamgio.quarkdown.function.reflect.Name import eu.iamgio.quarkdown.function.value.NodeValue import eu.iamgio.quarkdown.function.value.StringValue @@ -10,6 +13,7 @@ import eu.iamgio.quarkdown.function.value.data.Range import eu.iamgio.quarkdown.function.value.data.subList import eu.iamgio.quarkdown.function.value.wrappedAsValue import java.io.File +import kotlin.io.path.Path /** * `Data` stdlib module exporter. @@ -21,6 +25,34 @@ val Data: Module = ::csv, ) +/** + * @param path path of the file, relative or absolute (with extension) + * @param requireExistance whether the corresponding file must exist + * @return a [File] instance of the file located in [path]. + * If the path is relative, the location is determined by the working directory of the pipeline. + * @throws FunctionRuntimeException if the file does not exist and [requireExistance] is `true` + */ +internal fun file( + context: Context, + path: String, + requireExistance: Boolean = true, +): File { + val workingDirectory = context.attachedPipeline?.options?.workingDirectory + + val file = + if (workingDirectory != null && !Path(path).isAbsolute) { + File(workingDirectory, path) + } else { + File(path) + } + + if (requireExistance && !file.exists()) { + throw FunctionRuntimeException("File $file does not exist.") + } + + return file +} + /** * @param path path of the file (with extension) * @param lineRange range of lines to extract from the file. @@ -29,10 +61,11 @@ val Data: Module = */ @Name("filecontent") fun fileContent( + @Injected context: Context, path: String, lineRange: Range = Range.INFINITE, ): StringValue { - val file = File(path) + val file = file(context, path) // If the range is infinite on both ends, the whole file is read. if (lineRange.isInfinite) { @@ -49,11 +82,15 @@ fun fileContent( * @param path path of the CSV file (with extension) to show * @return a table whose content is loaded from the file located in [path] */ -fun csv(path: String): NodeValue { +fun csv( + @Injected context: Context, + path: String, +): NodeValue { + val file = file(context, path) val columns = mutableMapOf>() // CSV is read row-by-row, while the Table is built by columns. - csvReader().open(path) { + csvReader().open(file) { readAllWithHeaderAsSequence() .flatMap { it.entries } .forEach { (header, content) -> diff --git a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Ecosystem.kt b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Ecosystem.kt index b10b8e36..c0809ef7 100644 --- a/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Ecosystem.kt +++ b/stdlib/src/main/kotlin/eu/iamgio/quarkdown/stdlib/Ecosystem.kt @@ -6,7 +6,6 @@ import eu.iamgio.quarkdown.function.expression.eval import eu.iamgio.quarkdown.function.reflect.Injected import eu.iamgio.quarkdown.function.value.OutputValue import eu.iamgio.quarkdown.function.value.ValueFactory -import java.io.File /** * `Ecosystem` stdlib module exporter. @@ -29,19 +28,20 @@ fun include( path: String, ): OutputValue<*> { // Read file content - val raw = File(path).readText() + val file = file(context, path) + val raw = file.readText() // Evaluate the Quarkdown source. // This automatically converts the source into a value (e.g. a node, a string, a number, etc.) // and fills the current context with new declarations (e.g. variables, functions, link definitions, etc.) val result = ValueFactory.expression(raw, context)?.eval() - ?: throw FunctionRuntimeException("Cannot include sub-file $path: the Quarkdown source could not be evaluated") + ?: throw FunctionRuntimeException("Cannot include sub-file $file: the Quarkdown source could not be evaluated") // The value must be an output value in order to comply with the function rules. return result as? OutputValue<*> ?: throw FunctionRuntimeException( - "Cannot include sub-file $path: the evaluation of the Quarkdown source is not a suitable output value " + + "Cannot include sub-file $file: the evaluation of the Quarkdown source is not a suitable output value " + "(${result::class.simpleName} found)", ) } diff --git a/stdlib/src/test/kotlin/eu/iamgio/quarkdown/stdlib/DataTest.kt b/stdlib/src/test/kotlin/eu/iamgio/quarkdown/stdlib/DataTest.kt index b3eef9e4..2fefb562 100644 --- a/stdlib/src/test/kotlin/eu/iamgio/quarkdown/stdlib/DataTest.kt +++ b/stdlib/src/test/kotlin/eu/iamgio/quarkdown/stdlib/DataTest.kt @@ -1,7 +1,10 @@ package eu.iamgio.quarkdown.stdlib +import eu.iamgio.quarkdown.ast.MutableAstAttributes import eu.iamgio.quarkdown.ast.Table import eu.iamgio.quarkdown.ast.Text +import eu.iamgio.quarkdown.context.BaseContext +import eu.iamgio.quarkdown.flavor.quarkdown.QuarkdownFlavor import eu.iamgio.quarkdown.function.value.data.Range import kotlin.test.Test import kotlin.test.assertEquals @@ -15,35 +18,37 @@ private val LINE_SEPARATOR = System.lineSeparator() * [Data] module tests. */ class DataTest { + private val context = BaseContext(MutableAstAttributes(), QuarkdownFlavor, emptySet()) + @Test fun `file contents`() { val path = "$DATA_FOLDER/test.txt" assertEquals( "Line 1${LINE_SEPARATOR}Line 2${LINE_SEPARATOR}${LINE_SEPARATOR}Line 4${LINE_SEPARATOR}Line 5", - fileContent(path).unwrappedValue, + fileContent(context, path).unwrappedValue, ) assertEquals( "Line 2${LINE_SEPARATOR}${LINE_SEPARATOR}Line 4", - fileContent(path, Range(1, 3)).unwrappedValue, + fileContent(context, path, Range(1, 3)).unwrappedValue, ) assertEquals( "Line 1${LINE_SEPARATOR}Line 2", - fileContent(path, Range(null, 1)).unwrappedValue, + fileContent(context, path, Range(null, 1)).unwrappedValue, ) assertEquals( "Line 4${LINE_SEPARATOR}Line 5", - fileContent(path, Range(3, null)).unwrappedValue, + fileContent(context, path, Range(3, null)).unwrappedValue, ) } @Test fun `csv table`() { val path = "$DATA_FOLDER/people.csv" - val table = csv(path) + val table = csv(context, path) assertIs(table.unwrappedValue)