diff --git a/CHANGELOG.md b/CHANGELOG.md index fc49613..cc70c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Introduce changelog `summary` and changelog property [#127](../../issues/127) - Introduce changelog `preTitle` and `title` changelog properties - Ensure patched changelog ends with a newline [#126](../../issues/126) +- Added the `changelog.lineSeparator` property to allow for customizing the line separator used in the changelog. [#104](../../issues/104) - Added the `--version=...` CLI parameter for the `getChangelog` task [#83](../../issues/83) - Throw an exception when `initializeChangelog` task works on non-empty file [#82](../../issues/82) diff --git a/src/main/kotlin/org/jetbrains/changelog/Changelog.kt b/src/main/kotlin/org/jetbrains/changelog/Changelog.kt index 681ca62..5594fc4 100644 --- a/src/main/kotlin/org/jetbrains/changelog/Changelog.kt +++ b/src/main/kotlin/org/jetbrains/changelog/Changelog.kt @@ -9,7 +9,6 @@ import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.ast.getTextInNode import org.intellij.markdown.parser.MarkdownParser import org.jetbrains.changelog.ChangelogPluginConstants.ATX_3 -import org.jetbrains.changelog.ChangelogPluginConstants.NEW_LINE import org.jetbrains.changelog.exceptions.HeaderParseException import org.jetbrains.changelog.exceptions.MissingFileException import org.jetbrains.changelog.exceptions.MissingVersionException @@ -24,6 +23,7 @@ data class Changelog( val unreleasedTerm: String, val headerParserRegex: Regex, val itemPrefix: String, + val lineSeparator: String, ) { private val flavour = ChangelogFlavourDescriptor() @@ -40,14 +40,14 @@ data class Changelog( private val preTitleNodes = tree.children .takeWhile { it.type != MarkdownElementTypes.ATX_1 } val preTitleValue = preTitle ?: preTitleNodes - .joinToString(NEW_LINE) { it.text() } + .joinToString(lineSeparator) { it.text() } .trim() private val titleNodes = tree.children .dropWhile { it.type != MarkdownElementTypes.ATX_1 } .takeWhile { it.type == MarkdownElementTypes.ATX_1 } val titleValue = title ?: titleNodes - .joinToString(NEW_LINE) { it.text() } + .joinToString(lineSeparator) { it.text() } .trim() private val introductionNodes = tree.children @@ -55,8 +55,8 @@ data class Changelog( .dropWhile { it.type == MarkdownElementTypes.ATX_1 } .takeWhile { it.type != MarkdownElementTypes.ATX_2 } val introductionValue = introduction ?: introductionNodes - .joinToString(NEW_LINE) { it.text() } - .reformat() + .joinToString(lineSeparator) { it.text() } + .reformat(lineSeparator) private val itemsNodes = tree.children .dropWhile { it.type != MarkdownElementTypes.ATX_2 } @@ -93,8 +93,8 @@ data class Changelog( node.type != MarkdownElementTypes.ATX_3 && !node.text().startsWith(itemPrefix) } val summary = summaryNodes - .joinToString(NEW_LINE) { it.text() } - .reformat() + .joinToString(lineSeparator) { it.text() } + .reformat(lineSeparator) val items = nodes .drop(summaryNodes.size) @@ -105,8 +105,8 @@ data class Changelog( section.value .map { it.text().trim() } .filterNot { it.startsWith(ATX_3) || it.isEmpty() } - .joinToString(NEW_LINE) - .split("""(^|$NEW_LINE)${Regex.escape(itemPrefix)}\s*""".toRegex()) + .joinToString(lineSeparator) + .split("""(^|$lineSeparator)${Regex.escape(itemPrefix)}\s*""".toRegex()) .mapNotNull { "$itemPrefix $it".takeIf { _ -> it.isNotEmpty() @@ -115,7 +115,7 @@ data class Changelog( } - Item(key, header, summary, items, isUnreleased) + Item(key, header, summary, items, isUnreleased, lineSeparator) } fun has(version: String) = items.containsKey(version) @@ -132,6 +132,7 @@ data class Changelog( val summary: String, private val items: Map>, private val isUnreleased: Boolean = false, + private val lineSeparator: String, ) { private var withHeader = true @@ -161,7 +162,7 @@ data class Changelog( if (withSummary && summary.isNotEmpty()) { yield(summary) - yield(NEW_LINE) + yield(lineSeparator) } getSections() @@ -183,17 +184,17 @@ data class Changelog( } if (hasNext()) { - yield(NEW_LINE) + yield(lineSeparator) } } } } - .joinToString(NEW_LINE) - .reformat() + .joinToString(lineSeparator) + .reformat(lineSeparator) fun toHTML() = markdownToHTML(toText()) - fun toPlainText() = markdownToPlainText(toText()) + fun toPlainText() = markdownToPlainText(toText(), lineSeparator) override fun toString() = toText() } diff --git a/src/main/kotlin/org/jetbrains/changelog/ChangelogPlugin.kt b/src/main/kotlin/org/jetbrains/changelog/ChangelogPlugin.kt index 92080be..f4ba7d4 100644 --- a/src/main/kotlin/org/jetbrains/changelog/ChangelogPlugin.kt +++ b/src/main/kotlin/org/jetbrains/changelog/ChangelogPlugin.kt @@ -19,11 +19,11 @@ import org.jetbrains.changelog.tasks.GetChangelogTask import org.jetbrains.changelog.tasks.InitializeChangelogTask import org.jetbrains.changelog.tasks.PatchChangelogTask import java.io.File +import java.nio.file.Files +import java.nio.file.Path class ChangelogPlugin : Plugin { - private val field: Long? = null //End-Of-Line Comments - override fun apply(project: Project) { checkGradleVersion(project) @@ -46,6 +46,23 @@ class ChangelogPlugin : Plugin { } ) title.convention(ChangelogPluginConstants.DEFAULT_TITLE) + lineSeparator.convention(path.map { path -> + val content = Path.of(path) + .takeIf { Files.exists(it) } + ?.let { Files.readString(it) } + ?: return@map "\n" + val rnless = content.replace("\r\n", "") + + val rn = (content.length - rnless.length) / 2 + val r = rnless.count { it == '\r' } + val n = rnless.count { it == '\n' } + + when { + rn > r && rn > n -> "\r\n" + r > n -> "\r" + else -> "\n" + } + }) } val pathProvider = project.layout.file(extension.path.map { File(it) }) @@ -58,6 +75,7 @@ class ChangelogPlugin : Plugin { itemPrefix.convention(extension.itemPrefix) unreleasedTerm.set(extension.unreleasedTerm) version.set(extension.version) + lineSeparator.convention(extension.lineSeparator) outputs.upToDateWhen { false } } @@ -78,6 +96,7 @@ class ChangelogPlugin : Plugin { patchEmpty.convention(extension.patchEmpty) unreleasedTerm.convention(extension.unreleasedTerm) version.convention(extension.version) + lineSeparator.convention(extension.lineSeparator) } project.tasks.register(INITIALIZE_CHANGELOG_TASK_NAME) { @@ -91,6 +110,7 @@ class ChangelogPlugin : Plugin { outputFile.convention(pathProvider) itemPrefix.set(extension.itemPrefix) unreleasedTerm.set(extension.unreleasedTerm) + lineSeparator.convention(extension.lineSeparator) } } diff --git a/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginConstants.kt b/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginConstants.kt index fddbe71..6b44455 100644 --- a/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginConstants.kt +++ b/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginConstants.kt @@ -17,7 +17,6 @@ object ChangelogPluginConstants { const val ITEM_PREFIX = "-" const val DEFAULT_TITLE = "Changelog" const val UNRELEASED_TERM = "[Unreleased]" - const val NEW_LINE = "\n" const val ATX_1 = "#" const val ATX_2 = "##" const val ATX_3 = "###" diff --git a/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginExtension.kt b/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginExtension.kt index e0ddb5f..36a7bbf 100644 --- a/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginExtension.kt +++ b/src/main/kotlin/org/jetbrains/changelog/ChangelogPluginExtension.kt @@ -58,15 +58,19 @@ abstract class ChangelogPluginExtension { @get:Optional abstract val version: Property + @get:Optional + abstract val lineSeparator: Property + val instance get() = Changelog( - File(path.get()), - preTitle.orNull, - title.orNull, - introduction.orNull, - unreleasedTerm.get(), - getHeaderParserRegex.get(), - itemPrefix.get(), + file = File(path.get()), + preTitle = preTitle.orNull, + title = title.orNull, + introduction = introduction.orNull, + unreleasedTerm = unreleasedTerm.get(), + headerParserRegex = getHeaderParserRegex.get(), + itemPrefix = itemPrefix.get(), + lineSeparator = lineSeparator.get(), ) fun get(version: String) = instance.get(version) diff --git a/src/main/kotlin/org/jetbrains/changelog/extensions.kt b/src/main/kotlin/org/jetbrains/changelog/extensions.kt index 7bdcb92..16dd792 100644 --- a/src/main/kotlin/org/jetbrains/changelog/extensions.kt +++ b/src/main/kotlin/org/jetbrains/changelog/extensions.kt @@ -7,7 +7,6 @@ import org.intellij.markdown.parser.MarkdownParser import org.jetbrains.changelog.ChangelogPluginConstants.ATX_1 import org.jetbrains.changelog.ChangelogPluginConstants.ATX_2 import org.jetbrains.changelog.ChangelogPluginConstants.ATX_3 -import org.jetbrains.changelog.ChangelogPluginConstants.NEW_LINE import org.jetbrains.changelog.flavours.ChangelogFlavourDescriptor import org.jetbrains.changelog.flavours.PlainTextFlavourDescriptor import java.text.SimpleDateFormat @@ -20,25 +19,24 @@ fun markdownToHTML(input: String) = ChangelogFlavourDescriptor().run { .generateHtml() } -fun markdownToPlainText(input: String) = PlainTextFlavourDescriptor().run { +fun markdownToPlainText(input: String, lineSeparator: String) = PlainTextFlavourDescriptor(lineSeparator).run { HtmlGenerator(input, MarkdownParser(this).buildMarkdownTreeFromString(input), this, false) .generateHtml(PlainTextTagRenderer()) } -fun String.reformat(): String { +fun String.reformat(lineSeparator: String): String { val result = listOf( - """(?:^|\n)+(#+ [^\n]*)\n*""".toRegex() to "\n\n$1\n", - """((?:^|\n)#+ .*?)\n(#+ )""".toRegex() to "$1\n\n$2", - """\n{3,}""".toRegex() to "\n\n", + """(?:^|$lineSeparator)+(#+ [^$lineSeparator]*)(?:$lineSeparator)*""".toRegex() to "$lineSeparator$lineSeparator$1$lineSeparator", + """((?:^|$lineSeparator)#+ .*?)$lineSeparator(#+ )""".toRegex() to "$1$lineSeparator$lineSeparator$2", + """($lineSeparator){3,}""".toRegex() to "$lineSeparator$lineSeparator", ).fold(this) { acc, (pattern, replacement) -> acc.replace(pattern, replacement) - }.trim() + NEW_LINE + }.trim() + lineSeparator return when (result) { this -> result - else -> result.reformat() + else -> result.reformat(lineSeparator) } - } internal fun compose( @@ -47,19 +45,20 @@ internal fun compose( introduction: String?, unreleasedTerm: String?, groups: List, + lineSeparator: String, function: suspend SequenceScope.() -> Unit = {}, ) = sequence { if (!preTitle.isNullOrBlank()) { yield(preTitle) - yield(NEW_LINE) + yield(lineSeparator) } if (!title.isNullOrBlank()) { yield("$ATX_1 ${title.trim()}") - yield(NEW_LINE) + yield(lineSeparator) } if (!introduction.isNullOrBlank()) { yield(introduction) - yield(NEW_LINE) + yield(lineSeparator) } if (!unreleasedTerm.isNullOrBlank()) { @@ -72,5 +71,5 @@ internal fun compose( function() } - .joinToString(NEW_LINE) - .reformat() + .joinToString(lineSeparator) + .reformat(lineSeparator) diff --git a/src/main/kotlin/org/jetbrains/changelog/flavours/PlainTextFlavourDescriptor.kt b/src/main/kotlin/org/jetbrains/changelog/flavours/PlainTextFlavourDescriptor.kt index 97d6876..e85a2e7 100644 --- a/src/main/kotlin/org/jetbrains/changelog/flavours/PlainTextFlavourDescriptor.kt +++ b/src/main/kotlin/org/jetbrains/changelog/flavours/PlainTextFlavourDescriptor.kt @@ -9,15 +9,14 @@ import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.html.OpenCloseGeneratingProvider import org.intellij.markdown.parser.LinkMap -import org.jetbrains.changelog.ChangelogPluginConstants.NEW_LINE import java.net.URI -class PlainTextFlavourDescriptor : GFMFlavourDescriptor() { +class PlainTextFlavourDescriptor(private val lineSeparator: String) : GFMFlavourDescriptor() { override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?) = super.createHtmlGeneratingProviders(linkMap, baseURI) + hashMapOf( MarkdownElementTypes.LIST_ITEM to CustomProvider("- "), - MarkdownTokenTypes.EOL to CustomProvider("", NEW_LINE) + MarkdownTokenTypes.EOL to CustomProvider("", lineSeparator) ) private class CustomProvider(private val openTagName: String = "", private val closeTagName: String = "") : diff --git a/src/main/kotlin/org/jetbrains/changelog/tasks/GetChangelogTask.kt b/src/main/kotlin/org/jetbrains/changelog/tasks/GetChangelogTask.kt index b25a2b2..7ceb50f 100644 --- a/src/main/kotlin/org/jetbrains/changelog/tasks/GetChangelogTask.kt +++ b/src/main/kotlin/org/jetbrains/changelog/tasks/GetChangelogTask.kt @@ -5,10 +5,7 @@ package org.jetbrains.changelog.tasks import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* import org.gradle.api.tasks.options.Option import org.jetbrains.changelog.Changelog @@ -51,16 +48,20 @@ abstract class GetChangelogTask : DefaultTask() { @get:Optional abstract val version: Property + @get:Internal + abstract val lineSeparator: Property + @TaskAction fun run() = logger.quiet( Changelog( - inputFile.map { it.asFile }.get(), - null, - null, - null, - unreleasedTerm.get(), - headerParserRegex.get(), - itemPrefix.get(), + file = inputFile.map { it.asFile }.get(), + preTitle = null, + title = null, + introduction = null, + unreleasedTerm = unreleasedTerm.get(), + headerParserRegex = headerParserRegex.get(), + itemPrefix = itemPrefix.get(), + lineSeparator = lineSeparator.get(), ).let { val version = cliVersion ?: when (unreleased) { true -> unreleasedTerm diff --git a/src/main/kotlin/org/jetbrains/changelog/tasks/InitializeChangelogTask.kt b/src/main/kotlin/org/jetbrains/changelog/tasks/InitializeChangelogTask.kt index 28e4be0..b3649ac 100644 --- a/src/main/kotlin/org/jetbrains/changelog/tasks/InitializeChangelogTask.kt +++ b/src/main/kotlin/org/jetbrains/changelog/tasks/InitializeChangelogTask.kt @@ -7,10 +7,7 @@ import org.gradle.api.GradleException import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* import org.jetbrains.changelog.compose abstract class InitializeChangelogTask : DefaultTask() { @@ -43,6 +40,9 @@ abstract class InitializeChangelogTask : DefaultTask() { @get:Optional abstract val unreleasedTerm: Property + @get:Internal + abstract val lineSeparator: Property + @TaskAction fun run() { val file = outputFile.get() @@ -58,11 +58,12 @@ abstract class InitializeChangelogTask : DefaultTask() { } val content = compose( - preTitle.orNull, - title.orNull, - introduction.orNull, - unreleasedTerm.get(), - groups.get(), + preTitle = preTitle.orNull, + title = title.orNull, + introduction = introduction.orNull, + unreleasedTerm = unreleasedTerm.get(), + groups = groups.get(), + lineSeparator = lineSeparator.get(), ) file.writeText(content) } diff --git a/src/main/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTask.kt b/src/main/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTask.kt index 63024d6..6be3d0c 100644 --- a/src/main/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTask.kt +++ b/src/main/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTask.kt @@ -72,18 +72,22 @@ abstract class PatchChangelogTask : DefaultTask() { @get:Optional abstract val version: Property + @get:Internal + abstract val lineSeparator: Property + @TaskAction fun run() { val unreleasedTermValue = unreleasedTerm.get() val changelog = Changelog( - inputFile.get().asFile, - preTitle.orNull, - title.orNull, - introduction.orNull, - unreleasedTerm.get(), - headerParserRegex.get(), - itemPrefix.get(), + file = inputFile.get().asFile, + preTitle = preTitle.orNull, + title = title.orNull, + introduction = introduction.orNull, + unreleasedTerm = unreleasedTerm.get(), + headerParserRegex = headerParserRegex.get(), + itemPrefix = itemPrefix.get(), + lineSeparator = lineSeparator.get(), ) val preTitleValue = preTitle.orNull ?: changelog.preTitleValue @@ -111,11 +115,12 @@ abstract class PatchChangelogTask : DefaultTask() { } val patchedContent = compose( - preTitleValue, - titleValue, - introductionValue, - unreleasedTermValue.takeIf { keepUnreleasedSection.get() }, - groups.get(), + preTitle = preTitleValue, + title = titleValue, + introduction = introductionValue, + unreleasedTerm = unreleasedTermValue.takeIf { keepUnreleasedSection.get() }, + groups = groups.get(), + lineSeparator = lineSeparator.get(), ) { if (item != null) { yield("$ATX_2 $headerValue") diff --git a/src/test/kotlin/org/jetbrains/changelog/BaseTest.kt b/src/test/kotlin/org/jetbrains/changelog/BaseTest.kt index 2339cad..46b35a9 100644 --- a/src/test/kotlin/org/jetbrains/changelog/BaseTest.kt +++ b/src/test/kotlin/org/jetbrains/changelog/BaseTest.kt @@ -15,6 +15,8 @@ open class BaseTest { protected lateinit var project: DefaultProject protected lateinit var extension: ChangelogPluginExtension + protected val lineSeparator + get() = extension.lineSeparator.get() private val gradleDefault = System.getProperty("test.gradle.default") private val gradleHome = System.getProperty("test.gradle.home") diff --git a/src/test/kotlin/org/jetbrains/changelog/ExtensionsTest.kt b/src/test/kotlin/org/jetbrains/changelog/ExtensionsTest.kt index bb11258..fa53c88 100644 --- a/src/test/kotlin/org/jetbrains/changelog/ExtensionsTest.kt +++ b/src/test/kotlin/org/jetbrains/changelog/ExtensionsTest.kt @@ -9,6 +9,8 @@ import kotlin.test.assertEquals class ExtensionsTest { + private val lineSeparator = "\n" + @Test fun dateTest() { assertEquals(SimpleDateFormat("yyyy-MM-dd").format(Date()), date()) @@ -56,7 +58,7 @@ class ExtensionsTest { - buz - biz """.trimIndent(), - markdownToPlainText(content) + markdownToPlainText(content, lineSeparator) ) } @@ -90,7 +92,7 @@ class ExtensionsTest { ## [0.1.0] ### Added - Buz - """.trimIndent().reformat() + """.trimIndent().reformat(lineSeparator) ) assertEquals( @@ -114,7 +116,7 @@ class ExtensionsTest { ## Upcoming version ### Added ### Removed - """.trimIndent().reformat() + """.trimIndent().reformat(lineSeparator) ) } } diff --git a/src/test/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTaskTest.kt b/src/test/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTaskTest.kt index 666cfc9..876e643 100644 --- a/src/test/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTaskTest.kt +++ b/src/test/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTaskTest.kt @@ -3,7 +3,6 @@ package org.jetbrains.changelog.tasks import org.jetbrains.changelog.BaseTest -import org.jetbrains.changelog.ChangelogPluginConstants.NEW_LINE import org.jetbrains.changelog.ChangelogPluginConstants.PATCH_CHANGELOG_TASK_NAME import org.jetbrains.changelog.exceptions.MissingVersionException import java.text.SimpleDateFormat @@ -484,15 +483,15 @@ class PatchChangelogTaskTest : BaseTest() { changelog, ) - assertFalse(changelog.endsWith(NEW_LINE + NEW_LINE)) - assertTrue(changelog.endsWith(NEW_LINE)) + assertFalse(changelog.endsWith(lineSeparator + lineSeparator)) + assertTrue(changelog.endsWith(lineSeparator)) } @Test fun `patched changelog contains an empty line at the end`() { runTask(PATCH_CHANGELOG_TASK_NAME) - assertTrue(changelog.endsWith(NEW_LINE)) + assertTrue(changelog.endsWith(lineSeparator)) } @Test