summaryrefslogtreecommitdiff
path: root/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2020-03-05 03:00:59 +0000
committerNorbert Preining <norbert@preining.info>2020-03-05 03:00:59 +0000
commit898048513951b471a492afa23e46112d14bcb236 (patch)
tree8596afc705f55d2d07b324a756f7283ac0e2d21b /support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
parent19d25b8009801aa98ea2f46b45c37c257f990491 (diff)
CTAN sync 202003050300
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt')
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt312
1 files changed, 312 insertions, 0 deletions
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
new file mode 100644
index 0000000000..e901e25fcf
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
+import java.io.File
+import java.util.regex.Pattern
+import org.islandoftex.arara.Arara
+import org.islandoftex.arara.configuration.AraraSpec
+import org.islandoftex.arara.filehandling.FileHandlingUtils
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.model.AraraException
+import org.islandoftex.arara.utils.DisplayUtils
+import org.slf4j.LoggerFactory
+
+/**
+ * Implements directive utilitary methods.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@UseExperimental(kotlinx.serialization.ImplicitReflectionSerializer::class)
+object DirectiveUtils {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ // get the logger context from a factory
+ private val logger = LoggerFactory.getLogger(DirectiveUtils::class.java)
+
+ /**
+ * This function filters the lines of a file to identify the potential
+ * directives.
+ *
+ * @param lines The lines of the file.
+ * @return A map containing the line number and the line's content.
+ */
+ private fun getPotentialDirectiveLines(lines: List<String>):
+ Map<Int, String> {
+ val header = Arara.config[AraraSpec.Execution.onlyHeader]
+ val validLineRegex = Arara.config[AraraSpec.Execution.filePattern]
+ val validLinePattern = validLineRegex.toPattern()
+ val validLineStartPattern = (validLineRegex + Arara.config[AraraSpec
+ .Application.namePattern]).toPattern()
+ val map = mutableMapOf<Int, String>()
+ for ((i, text) in lines.withIndex()) {
+ val validLineMatcher = validLineStartPattern.matcher(text)
+ if (validLineMatcher.find()) {
+ val line = text.substring(validLineMatcher.end())
+ map[i + 1] = line
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_POTENTIAL_PATTERN_FOUND,
+ i + 1, line.trim()))
+ } else if (header && !checkLinePattern(validLinePattern, text)) {
+ // if we should only look within the file's header and reached
+ // a point where the line pattern does not match anymore, we
+ // assume we have left the header and break
+ break
+ }
+ }
+ return map
+ }
+
+ /**
+ * Extracts a list of directives from a list of strings.
+ *
+ * @param lines List of strings.
+ * @return A list of directives.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("MagicNumber")
+ fun extractDirectives(lines: List<String>): List<Directive> {
+ val pairs = getPotentialDirectiveLines(lines)
+ .takeIf { it.isNotEmpty() }
+ ?: throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_NO_DIRECTIVES_FOUND))
+
+ val assemblers = mutableListOf<DirectiveAssembler>()
+ var assembler = DirectiveAssembler()
+ val linebreakPattern = Arara.config[AraraSpec.Directive
+ .linebreakPattern].toPattern()
+ for ((lineno, content) in pairs) {
+ val linebreakMatcher = linebreakPattern.matcher(content)
+ if (linebreakMatcher.find()) {
+ if (!assembler.isAppendAllowed) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_ORPHAN_LINEBREAK,
+ lineno
+ )
+ )
+ } else {
+ assembler.addLineNumber(lineno)
+ assembler.appendLine(linebreakMatcher.group(1))
+ }
+ } else {
+ if (assembler.isAppendAllowed) {
+ assemblers.add(assembler)
+ }
+ assembler = DirectiveAssembler()
+ assembler.addLineNumber(lineno)
+ assembler.appendLine(content)
+ }
+ }
+ if (assembler.isAppendAllowed) {
+ assemblers.add(assembler)
+ }
+
+ return assemblers.map { generateDirective(it) }
+ }
+
+ /**
+ * Generates a directive from a directive assembler.
+ *
+ * @param assembler The directive assembler.
+ * @return The corresponding directive.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("MagicNumber")
+ fun generateDirective(assembler: DirectiveAssembler): Directive {
+ val matcher = Arara.config[AraraSpec.Directive.directivePattern]
+ .toPattern().matcher(assembler.getText())
+ if (matcher.find()) {
+ val directive = Directive(
+ identifier = matcher.group(1)!!,
+ parameters = getParameters(matcher.group(3),
+ assembler.getLineNumbers()),
+ conditional = Conditional(
+ type = getType(matcher.group(5)),
+ condition = matcher.group(6) ?: ""
+ ),
+ lineNumbers = assembler.getLineNumbers()
+ )
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_POTENTIAL_DIRECTIVE_FOUND, directive))
+
+ return directive
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_INVALID_DIRECTIVE_FORMAT,
+ "(" + assembler.getLineNumbers()
+ .joinToString(", ") + ")"
+ )
+ )
+ }
+ }
+
+ /**
+ * Gets the conditional type based on the input string.
+ *
+ * @param text The input string.
+ * @return The conditional type.
+ */
+ private fun getType(text: String?): Conditional.ConditionalType {
+ return when (text) {
+ null -> Conditional.ConditionalType.NONE
+ "if" -> Conditional.ConditionalType.IF
+ "while" -> Conditional.ConditionalType.WHILE
+ "until" -> Conditional.ConditionalType.UNTIL
+ else -> Conditional.ConditionalType.UNLESS
+ }
+ }
+
+ /**
+ * Gets the parameters from the input string.
+ *
+ * @param text The input string.
+ * @param numbers The list of line numbers.
+ * @return A map containing the directive parameters.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ private fun getParameters(
+ text: String?,
+ numbers: List<Int>
+ ): Map<String, Any> {
+ if (text == null)
+ return mapOf()
+
+ /* Before using kotlinx.serialization, there has been a dedicated
+ * directive resolver which instructed SnakeYAML to do the following:
+ *
+ * addImplicitResolver(Tag.MERGE, MERGE, "<")
+ * addImplicitResolver(Tag.NULL, NULL, "~nN\u0000")
+ * addImplicitResolver(Tag.NULL, EMPTY, null)
+ *
+ * This has been removed.
+ */
+ return ObjectMapper(YAMLFactory()).registerKotlinModule().runCatching {
+ readValue<Map<String, Any>>(text)
+ }.getOrElse {
+ throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_YAML_EXCEPTION,
+ "(" + numbers.joinToString(", ") + ")"),
+ it)
+ }
+ }
+
+ /**
+ * Replicate a directive for given files.
+ *
+ * @param holder The list of files.
+ * @param parameters The parameters for the directive.
+ * @param directive The directive to clone.
+ * @return List of cloned directives.
+ * @throws AraraException If there is an error validating the [holder]
+ * object.
+ */
+ @Throws(AraraException::class)
+ private fun replicateDirective(
+ holder: Any,
+ parameters: Map<String, Any>,
+ directive: Directive
+ ): List<Directive> {
+ return if (holder is List<*>) {
+ // we received a file list, so we map that list to files
+ holder.filterIsInstance<Any>()
+ .asSequence()
+ .map { File(it.toString()) }
+ .map(FileHandlingUtils::getCanonicalFile)
+ // and because we want directives, we replicate our
+ // directive to be applied to that file
+ .map { reference ->
+ directive.copy(parameters = parameters
+ .plus("reference" to reference))
+ }
+ .toList()
+ // we take the result if and only if we have at least one
+ // file and we did not filter out any invalid argument
+ .takeIf { it.isNotEmpty() && holder.size == it.size }
+ // TODO: check exception according to condition
+ ?: throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_EMPTY_FILES_LIST,
+ "(" + directive.lineNumbers
+ .joinToString(", ") + ")"
+ )
+ )
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_FILES_IS_NOT_A_LIST,
+ "(" + directive.lineNumbers.joinToString(", ") + ")"
+ )
+ )
+ }
+ }
+
+ /**
+ * Validates the list of directives, returning a new list.
+ *
+ * @param directives The list of directives.
+ * @return A new list of directives.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun process(directives: List<Directive>): List<Directive> {
+ val result = mutableListOf<Directive>()
+ directives.forEach { directive ->
+ val parameters = directive.parameters
+
+ if (parameters.containsKey("reference"))
+ throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_REFERENCE_IS_RESERVED,
+ "(" + directive.lineNumbers.joinToString(", ") + ")"))
+
+ if (parameters.containsKey("files")) {
+ result.addAll(replicateDirective(parameters.getValue("files"),
+ parameters.minus("files"), directive))
+ } else {
+ result.add(directive.copy(parameters = parameters
+ .plus("reference" to
+ Arara.config[AraraSpec.Execution.reference])))
+ }
+ }
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_VALIDATED_DIRECTIVES))
+ logger.info(DisplayUtils.displayOutputSeparator(
+ messages.getMessage(Messages.LOG_INFO_DIRECTIVES_BLOCK)))
+ result.forEach { logger.info(it.toString()) }
+ logger.info(DisplayUtils.displaySeparator())
+
+ return result
+ }
+
+ /**
+ * Checks if the provided line contains the corresponding pattern, based on
+ * the file type, or an empty line.
+ *
+ * @param pattern Pattern to be matched, based on the file type.
+ * @param line Provided line.
+ * @return Logical value indicating if the provided line contains the
+ * corresponding pattern, based on the file type, or an empty line.
+ */
+ private fun checkLinePattern(pattern: Pattern, line: String): Boolean {
+ return line.isBlank() || pattern.matcher(line).find()
+ }
+}