diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyValue.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyValue.java
index c3c1d6ca1f6..131250dad05 100644
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyValue.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyValue.java
@@ -22,6 +22,7 @@
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.NameCaseConvention;
import org.openrewrite.internal.StringUtils;
+import org.openrewrite.yaml.trait.BlockScalar;
import org.openrewrite.yaml.tree.Yaml;
import java.util.Iterator;
@@ -96,7 +97,7 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
String prop = getProperty(getCursor());
if (keyMatcher.matchesGlob(prop) && matchesOldValue(e.getValue())) {
- Yaml.Block updatedValue = updateValue(e.getValue());
+ Yaml.Block updatedValue = updateValue(e.getValue(), getCursor());
if (updatedValue != null) {
e = e.withValue(updatedValue);
}
@@ -107,19 +108,24 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
}
// returns null if value should not change
- private Yaml.@Nullable Block updateValue(Yaml.Block value) {
+ private Yaml.@Nullable Block updateValue(Yaml.Block value, Cursor parent) {
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
- Yaml.Scalar newScalar = scalar.withValue(Boolean.TRUE.equals(regex) ?
- scalar.getValue().replaceAll(Objects.requireNonNull(oldValue), newValue) :
- newValue);
- return scalar.getValue().equals(newScalar.getValue()) ? null : newScalar;
+ BlockScalar block = new BlockScalar.Matcher().get(scalar, parent).orElse(null);
+ String body = block != null ? block.getBody() : scalar.getValue();
+ String updatedBody = Boolean.TRUE.equals(regex) ?
+ body.replaceAll(Objects.requireNonNull(oldValue), newValue) :
+ newValue;
+ if (body.equals(updatedBody)) {
+ return null;
+ }
+ return block != null ? block.withBody(updatedBody) : scalar.withValue(updatedBody);
}
if (value instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) value;
return sequence.withEntries(ListUtils.map(sequence.getEntries(), entry -> {
if (matchesOldValue(entry.getBlock())) {
- Yaml.Block updatedValue = updateValue(entry.getBlock());
+ Yaml.Block updatedValue = updateValue(entry.getBlock(), parent);
if (updatedValue != null) {
return entry.withBlock(updatedValue);
}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangeValue.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangeValue.java
index 18aab31be58..68a38d9c396 100644
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangeValue.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangeValue.java
@@ -20,6 +20,7 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.marker.Markers;
+import org.openrewrite.yaml.trait.BlockScalar;
import org.openrewrite.yaml.tree.Yaml;
import static org.openrewrite.Tree.randomId;
@@ -66,7 +67,15 @@ public TreeVisitor, ExecutionContext> getVisitor() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
- if (matcher.matches(getCursor()) && (!(e.getValue() instanceof Yaml.Scalar) || !((Yaml.Scalar) e.getValue()).getValue().equals(value))) {
+ if (!matcher.matches(getCursor())) {
+ return e;
+ }
+ BlockScalar block = new BlockScalar.Matcher().get(new Cursor(getCursor(), e.getValue())).orElse(null);
+ if (block != null) {
+ if (!block.getBody().equals(value)) {
+ e = e.withValue(block.withBody(value));
+ }
+ } else if (!(e.getValue() instanceof Yaml.Scalar) || !((Yaml.Scalar) e.getValue()).getValue().equals(value)) {
Yaml.Anchor anchor = (e.getValue() instanceof Yaml.Scalar) ? ((Yaml.Scalar) e.getValue()).getAnchor() : null;
Yaml.Tag tag = (e.getValue() instanceof Yaml.Scalar) ? ((Yaml.Scalar) e.getValue()).getTag() : null;
String prefix = e.getValue() instanceof Yaml.Sequence ? ((Yaml.Sequence) e.getValue()).getOpeningBracketPrefix() : e.getValue().getPrefix();
@@ -82,7 +91,14 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
Yaml.Scalar s = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
- s = s.withValue(value);
+ BlockScalar block = new BlockScalar.Matcher().get(getCursor()).orElse(null);
+ if (block != null) {
+ if (!block.getBody().equals(value)) {
+ s = block.withBody(value);
+ }
+ } else if (!s.getValue().equals(value)) {
+ s = s.withValue(value);
+ }
}
return s;
}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/DeleteProperty.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/DeleteProperty.java
index 58664fcaa76..3c21273af79 100755
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/DeleteProperty.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/DeleteProperty.java
@@ -184,6 +184,11 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
entry = entry.withPrefix(firstDeletedPrefix);
} else if (previousWasDeleted && !entries.isEmpty() && !containsNewline(entry.getPrefix())) {
entry = entry.withPrefix("\n" + entry.getPrefix());
+ } else if (previousWasDeleted && !entries.isEmpty()
+ && endsWithBlockScalar(entries.get(entries.size() - 1))
+ && containsNewline(entry.getPrefix())) {
+ // Block-scalar predecessor already owns the boundary newline; strip the duplicate.
+ entry = entry.withPrefix(stripLeadingLineBreak(entry.getPrefix()));
}
entries.add(entry);
previousWasDeleted = false;
@@ -257,6 +262,32 @@ private static boolean containsOnlyWhitespace(@Nullable String str) {
return true;
}
+ private static String stripLeadingLineBreak(String s) {
+ if (s.startsWith("\r\n")) {
+ return s.substring(2);
+ }
+ if (s.startsWith("\n") || s.startsWith("\r")) {
+ return s.substring(1);
+ }
+ // Line break not at the start — strip the first one found.
+ int idx = -1;
+ int idxN = s.indexOf('\n');
+ int idxR = s.indexOf('\r');
+ if (idxN >= 0 && (idxR < 0 || idxN < idxR)) {
+ idx = idxN;
+ } else if (idxR >= 0) {
+ idx = idxR;
+ }
+ if (idx < 0) {
+ return s;
+ }
+ int after = idx + 1;
+ if (s.charAt(idx) == '\r' && after < s.length() && s.charAt(after) == '\n') {
+ after++;
+ }
+ return s.substring(0, idx) + s.substring(after);
+ }
+
private static boolean containsNewline(@Nullable String str) {
return str != null && str.indexOf('\n') >= 0;
}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/BlockScalar.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/BlockScalar.java
new file mode 100644
index 00000000000..12f60da1719
--- /dev/null
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/BlockScalar.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.yaml.trait;
+
+import lombok.Value;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.Cursor;
+import org.openrewrite.internal.StringUtils;
+import org.openrewrite.trait.SimpleTraitMatcher;
+import org.openrewrite.trait.Trait;
+import org.openrewrite.yaml.tree.Yaml;
+
+/**
+ * Body-aware access to a FOLDED / LITERAL {@link Yaml.Scalar}: the raw {@link Yaml.Scalar#value}
+ * carries the block envelope (chomp indicator, header newline, indented body, trailing whitespace
+ * bounding the next sibling), so rewriting it directly via the Lombok-generated {@code withValue}
+ * clobbers the envelope.
+ */
+@Value
+public class BlockScalar implements Trait {
+ Cursor cursor;
+
+ public String getBody() {
+ String value = getTree().getValue();
+ int headerEnd = value.indexOf('\n');
+ if (headerEnd < 0) {
+ return "";
+ }
+ int bodyEnd = value.length();
+ while (bodyEnd > headerEnd + 1 && Character.isWhitespace(value.charAt(bodyEnd - 1))) {
+ bodyEnd--;
+ }
+ if (bodyEnd <= headerEnd + 1) {
+ return "";
+ }
+ String bodyRegion = value.substring(headerEnd + 1, bodyEnd);
+ int indent = 0;
+ while (indent < bodyRegion.length() && bodyRegion.charAt(indent) == ' ') {
+ indent++;
+ }
+ String indentStr = bodyRegion.substring(0, indent);
+ String[] lines = bodyRegion.split("\r\n|\r|\n", -1);
+ StringBuilder out = new StringBuilder(bodyRegion.length());
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ if (indent > 0 && line.startsWith(indentStr)) {
+ line = line.substring(indent);
+ }
+ if (i > 0) {
+ out.append('\n');
+ }
+ out.append(line);
+ }
+ return out.toString();
+ }
+
+ public Yaml.Scalar withBody(String newBody) {
+ return withBody(newBody, 2);
+ }
+
+ public Yaml.Scalar withBody(String newBody, int defaultIndentSpaces) {
+ Yaml.Scalar scalar = getTree();
+ String value = scalar.getValue();
+ int headerEnd = value.indexOf('\n');
+ String header = headerEnd < 0 ? value : value.substring(0, headerEnd + 1);
+ String newLine = (headerEnd > 0 && value.charAt(headerEnd - 1) == '\r') ? "\r\n" : "\n";
+ int bodyEnd = value.length();
+ while (bodyEnd > 0 && Character.isWhitespace(value.charAt(bodyEnd - 1))) {
+ bodyEnd--;
+ }
+ String indent;
+ if (headerEnd >= 0 && headerEnd + 1 < bodyEnd) {
+ int indentEnd = headerEnd + 1;
+ while (indentEnd < bodyEnd && value.charAt(indentEnd) == ' ') {
+ indentEnd++;
+ }
+ indent = value.substring(headerEnd + 1, indentEnd);
+ if (indent.isEmpty()) {
+ indent = StringUtils.repeat(" ", defaultIndentSpaces);
+ }
+ } else {
+ indent = StringUtils.repeat(" ", defaultIndentSpaces);
+ }
+ String trailing = value.substring(bodyEnd);
+ String[] lines = newBody.split("\r\n|\r|\n", -1);
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < lines.length; i++) {
+ if (i > 0) {
+ body.append(newLine);
+ }
+ if (!lines[i].isEmpty()) {
+ body.append(indent).append(lines[i]);
+ }
+ }
+ return scalar.withValue(header + body + trailing);
+ }
+
+ public static class Matcher extends SimpleTraitMatcher {
+ @Override
+ protected @Nullable BlockScalar test(Cursor cursor) {
+ if (cursor.getValue() instanceof Yaml.Scalar) {
+ Yaml.Scalar.Style style = ((Yaml.Scalar) cursor.getValue()).getStyle();
+ if (style == Yaml.Scalar.Style.FOLDED || style == Yaml.Scalar.Style.LITERAL) {
+ return new BlockScalar(cursor);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlReference.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlReference.java
index 18ced348a74..afcd36e7dd9 100644
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlReference.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlReference.java
@@ -40,7 +40,12 @@ public boolean supportsRename() {
public Tree rename(Renamer renamer, Cursor cursor, ExecutionContext ctx) {
Tree tree = cursor.getValue();
if (tree instanceof Yaml.Scalar) {
- return ((Yaml.Scalar) tree).withValue(renamer.rename(this));
+ Yaml.Scalar scalar = (Yaml.Scalar) tree;
+ String newName = renamer.rename(this);
+ return new BlockScalar.Matcher()
+ .get(cursor)
+ .map(b -> (Yaml.Scalar) b.withBody(newName))
+ .orElseGet(() -> scalar.withValue(newName));
}
throw new IllegalArgumentException("cursor.getValue() must be an Yaml.Scalar but is: " + tree.getClass());
}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlValue.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlValue.java
index 98fabf683d0..a4c07260c68 100644
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlValue.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/YamlValue.java
@@ -52,8 +52,12 @@ public Yaml.Scalar getValueAsScalar() {
}
public YamlValue withValue(String newValue) {
- Yaml.Scalar value = getValueAsScalar().withValue(newValue);
- cursor = new Cursor(cursor.getParent(), getTree().withValue(value));
+ Yaml.Scalar scalar = getValueAsScalar();
+ Yaml.Scalar updated = new BlockScalar.Matcher()
+ .get(scalar, cursor)
+ .map(b -> b.withBody(newValue))
+ .orElseGet(() -> scalar.withValue(newValue));
+ cursor = new Cursor(cursor.getParent(), getTree().withValue(updated));
return this;
}
diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/Yaml.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/Yaml.java
index ec40f247451..f0e7a2b800d 100755
--- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/Yaml.java
+++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/Yaml.java
@@ -307,6 +307,13 @@ class Scalar implements Block, YamlKey {
@Nullable
Tag tag;
+ /**
+ * For FOLDED/LITERAL scalars this includes the chomp indicator, header newline,
+ * indented body, and trailing whitespace bounding the next sibling; the Lombok-generated
+ * {@code withValue} cannot safely rewrite a block scalar's body. Use the
+ * {@code org.openrewrite.yaml.trait.BlockScalar} trait to mutate the body without
+ * clobbering the block envelope.
+ */
String value;
public enum Style {
diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangePropertyValueTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangePropertyValueTest.java
index 35bbad20208..cd5805b3899 100644
--- a/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangePropertyValueTest.java
+++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangePropertyValueTest.java
@@ -233,6 +233,207 @@ void supportYamlListValuesWithRegex() {
);
}
+ @Test
+ void preservesFoldedClipBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: >
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: >
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesFoldedStripBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: >-
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: >-
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesFoldedKeepBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: >+
+ line one
+ line two
+
+ after: tail
+ """,
+ """
+ key: >+
+ replaced
+
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesLiteralClipBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: |
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: |
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesLiteralStripBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: |-
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: |-
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesLiteralKeepBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ """
+ key: |+
+ line one
+ line two
+
+ after: tail
+ """,
+ """
+ key: |+
+ replaced
+
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void multiLineNewValueReindentsAcrossBlockBody() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "first\nsecond", null, null, null, null)),
+ yaml(
+ """
+ key: |
+ old line
+ after: tail
+ """,
+ """
+ key: |
+ first
+ second
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void regexReplacementOnBlockScalarOperatesOnBodyOnly() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "B", "A", true, null, null)),
+ yaml(
+ """
+ key: |-
+ line A one
+ line A two
+ after: tail
+ """,
+ """
+ key: |-
+ line B one
+ line B two
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesCrlfLiteralBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "replaced", null, null, null, null)),
+ yaml(
+ "key: |\r\n" +
+ " line one\r\n" +
+ " line two\r\n" +
+ "after: tail\r\n",
+ "key: |\r\n" +
+ " replaced\r\n" +
+ "after: tail\r\n"
+ )
+ );
+ }
+
+ @Test
+ void multilineReplacementOnCrlfBlockScalarKeepsCrlf() {
+ // The new value introduces its own interior line break (a bare '\n' from the recipe
+ // argument). On a CRLF file that break must be emitted as CRLF, not glued in as a lone
+ // LF that would leave the block with mixed line endings.
+ rewriteRun(
+ spec -> spec.recipe(new ChangePropertyValue("key", "new one\nnew two", null, null, null, null)),
+ yaml(
+ "key: |\r\n" +
+ " line one\r\n" +
+ " line two\r\n" +
+ "after: tail\r\n",
+ "key: |\r\n" +
+ " new one\r\n" +
+ " new two\r\n" +
+ "after: tail\r\n"
+ )
+ );
+ }
+
@Test
void validatesThatOldValueIsRequiredIfRegexEnabled() {
assertTrue(new ChangePropertyValue("my.prop", "bar", null, true, null, null).validate().isInvalid());
diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangeValueTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangeValueTest.java
index dd8321aad44..3456b86d854 100644
--- a/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangeValueTest.java
+++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/ChangeValueTest.java
@@ -179,6 +179,84 @@ void changeSequenceKeyByWildcard() {
);
}
+ @Test
+ void preservesFoldedStripBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangeValue("$.key", "replaced", null)),
+ yaml(
+ """
+ key: >-
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: >-
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesLiteralKeepBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangeValue("$.key", "replaced", null)),
+ yaml(
+ """
+ key: |+
+ line one
+ line two
+
+ after: tail
+ """,
+ """
+ key: |+
+ replaced
+
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesFoldedClipBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangeValue("$.key", "replaced", null)),
+ yaml(
+ """
+ key: >
+ line one
+ line two
+ after: tail
+ """,
+ """
+ key: >
+ replaced
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void preservesCrlfLiteralBlockEnvelope() {
+ rewriteRun(
+ spec -> spec.recipe(new ChangeValue("$.key", "replaced", null)),
+ yaml(
+ "key: |\r\n" +
+ " line one\r\n" +
+ " line two\r\n" +
+ "after: tail\r\n",
+ "key: |\r\n" +
+ " replaced\r\n" +
+ "after: tail\r\n"
+ )
+ );
+ }
+
@Test
void changeSequenceKeyByExactMatch() {
rewriteRun(
diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/DeletePropertyKeyTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/DeletePropertyKeyTest.java
index aea7551bba2..646e578ff5c 100644
--- a/rewrite-yaml/src/test/java/org/openrewrite/yaml/DeletePropertyKeyTest.java
+++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/DeletePropertyKeyTest.java
@@ -666,4 +666,90 @@ void deleteLastEntryPreservesInlineCommentOnPreviousEntry() {
)
);
}
+
+ @Test
+ void deleteEntryAfterFoldedBlockScalar() {
+ rewriteRun(
+ spec -> spec.recipe(new DeleteProperty("doomed", null, null, null)),
+ yaml(
+ """
+ keep: >-
+ line one
+ line two
+ doomed: value
+ after: tail
+ """,
+ """
+ keep: >-
+ line one
+ line two
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void deleteEntryAfterLiteralKeepBlockScalar() {
+ rewriteRun(
+ spec -> spec.recipe(new DeleteProperty("doomed", null, null, null)),
+ yaml(
+ """
+ keep: |+
+ line one
+ line two
+
+ doomed: value
+ after: tail
+ """,
+ """
+ keep: |+
+ line one
+ line two
+
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void deleteEntryBeforeBlockScalarIsUnchanged() {
+ rewriteRun(
+ spec -> spec.recipe(new DeleteProperty("doomed", null, null, null)),
+ yaml(
+ """
+ doomed: value
+ keep: >-
+ line one
+ line two
+ after: tail
+ """,
+ """
+ keep: >-
+ line one
+ line two
+ after: tail
+ """
+ )
+ );
+ }
+
+ @Test
+ void deleteEntryAfterCrlfBlockScalar() {
+ rewriteRun(
+ spec -> spec.recipe(new DeleteProperty("doomed", null, null, null)),
+ yaml(
+ "keep: >-\r\n" +
+ " line one\r\n" +
+ " line two\r\n" +
+ "doomed: value\r\n" +
+ "after: tail\r\n",
+ "keep: >-\r\n" +
+ " line one\r\n" +
+ " line two\r\n" +
+ "after: tail\r\n"
+ )
+ );
+ }
}
diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/trait/BlockScalarTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/trait/BlockScalarTest.java
new file mode 100644
index 00000000000..196d9438b71
--- /dev/null
+++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/trait/BlockScalarTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.yaml.trait;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.Cursor;
+import org.openrewrite.marker.Markers;
+import org.openrewrite.yaml.tree.Yaml;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.openrewrite.Tree.randomId;
+
+class BlockScalarTest {
+
+ private static BlockScalar of(String value) {
+ Yaml.Scalar scalar = new Yaml.Scalar(randomId(), "", Markers.EMPTY, Yaml.Scalar.Style.LITERAL, null, null, value);
+ return new BlockScalar.Matcher().get(new Cursor(null, scalar)).orElseThrow();
+ }
+
+ @Test
+ void matcherRejectsPlainScalar() {
+ Yaml.Scalar plain = new Yaml.Scalar(randomId(), "", Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, null, "hello");
+ assertThat(new BlockScalar.Matcher().get(new Cursor(null, plain))).isEmpty();
+ }
+
+ @Test
+ void getBodyStripsCrFromLfBody() {
+ assertThat(of("\n line one\n line two\n").getBody()).isEqualTo("line one\nline two");
+ }
+
+ @Test
+ void getBodyStripsCrFromCrlfBody() {
+ assertThat(of("\r\n line one\r\n line two\r\n").getBody()).isEqualTo("line one\nline two");
+ }
+
+ @Test
+ void withBodyKeepsLfForLfScalar() {
+ assertThat(of("\n line one\n line two\n").withBody("new one\nnew two").getValue())
+ .isEqualTo("\n new one\n new two\n");
+ }
+
+ @Test
+ void withBodyEmitsCrlfForCrlfScalar() {
+ assertThat(of("\r\n line one\r\n line two\r\n").withBody("new one\nnew two").getValue())
+ .isEqualTo("\r\n new one\r\n new two\r\n");
+ }
+}