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