diff --git a/platform/lang-impl/src/com/intellij/codeInsight/folding/impl/DocumentFoldingInfo.java b/platform/lang-impl/src/com/intellij/codeInsight/folding/impl/DocumentFoldingInfo.java index 2f91efed0e73..0acf48f30609 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/folding/impl/DocumentFoldingInfo.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/folding/impl/DocumentFoldingInfo.java @@ -33,6 +33,7 @@ import com.intellij.openapi.fileEditor.impl.text.CodeFoldingState; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.util.containers.ContainerUtil; @@ -67,7 +68,8 @@ class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState { @NonNls private static final String EXPANDED_ATT = "expanded"; @NonNls private static final String MARKER_TAG = "marker"; @NonNls private static final String DATE_ATT = "date"; - @NonNls private static final String PLACEHOLDER_ATT = "placeholder"; + @NonNls private static final String PLACEHOLDER_OLD_ATT = "placeholder"; + @NonNls private static final String PLACEHOLDER_ATT = "ph"; DocumentFoldingInfo(@NotNull Project project, @NotNull Document document) { myProject = project; @@ -274,7 +276,7 @@ class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState { String signature = Integer.valueOf(marker.getStartOffset()) + ":" + Integer.valueOf(marker.getEndOffset()); e.setAttribute(SIGNATURE_ATT, signature); String placeHolderText = fi == null ? DEFAULT_PLACEHOLDER : fi.placeHolder; - e.setAttribute(PLACEHOLDER_ATT, placeHolderText); + e.setAttribute(PLACEHOLDER_ATT, StringUtil.escapeIllegalXmlChars(placeHolderText)); element.addContent(e); } } @@ -331,7 +333,8 @@ class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState { if (start < 0 || end >= document.getTextLength() || start > end) continue; RangeMarker marker = document.createRangeMarker(start, end); myRangeMarkers.add(marker); - String placeHolderText = e.getAttributeValue(PLACEHOLDER_ATT); + String placeHolderText = StringUtil.unescapeIllegalXmlChars(e.getAttributeValue(PLACEHOLDER_ATT)); + if (placeHolderText == null) e.getAttributeValue(PLACEHOLDER_OLD_ATT); if (placeHolderText == null) placeHolderText = DEFAULT_PLACEHOLDER; FoldingInfo fi = new FoldingInfo(placeHolderText, expanded); marker.putUserData(FOLDING_INFO_KEY, fi); diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index d7c129700dc5..b6f5ab65a0e3 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -24,6 +24,7 @@ import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.CharArrayUtil; import com.intellij.util.text.CharSequenceSubSequence; import com.intellij.util.text.StringFactory; +import org.jdom.Verifier; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -2194,6 +2195,64 @@ public class StringUtil extends StringUtilRt { } } + /** + * Some characters are illegal in XML even as numerical character references. This method performs escaping of them + * in a custom format, which is supposed to be unescaped on retrieving from XML using {@link #unescapeIllegalXmlChars(String)}. + * Resulting text can be part of XML version 1.0 document. + * + * @see https://www.w3.org/International/questions/qa-controls + * @see Verifier#isXMLCharacter(int) + */ + public static String escapeIllegalXmlChars(@NotNull String text) { + StringBuilder b = null; + int lastPos = 0; + for (int i = 0; i < text.length(); i++) { + int c = text.codePointAt(i); + if (Character.isSupplementaryCodePoint(c)) { + //noinspection AssignmentToForLoopParameter + i++; + } + if (c == '#' || !Verifier.isXMLCharacter(c)) { + if (b == null) b = new StringBuilder(text.length() + 5); // assuming there's one 'large' char (e.g. 0xFFFF) to escape numerically + b.append(text, lastPos, i).append('#'); + if (c != '#') b.append(Integer.toHexString(c)); + b.append('#'); + lastPos = i + 1; + } + } + return b == null ? text : b.append(text, lastPos, text.length()).toString(); + } + + /** + * @see #escapeIllegalXmlChars(String) + */ + public static String unescapeIllegalXmlChars(@NotNull String text) { + StringBuilder b = null; + int lastPos = 0; + for (int i = 0; i < text.length(); i++) { + int c = text.charAt(i); + if (c == '#') { + int numberEnd = text.indexOf('#', i + 1); + if (numberEnd > 0) { + int charCode; + try { + charCode = numberEnd == (i + 1) ? '#' : Integer.parseInt(text.substring(i + 1, numberEnd), 16); + } + catch (NumberFormatException e) { + continue; + } + if (b == null) b = new StringBuilder(text.length()); + b.append(text, lastPos, i); + b.append((char) charCode); + //noinspection AssignmentToForLoopParameter + i = numberEnd; + lastPos = i + 1; + } + } + } + return b == null ? text : b.append(text, lastPos, text.length()).toString(); + } + public static void quote(@NotNull final StringBuilder builder) { quote(builder, '\"'); } diff --git a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java index f8136aebdde0..0d37f66d1db2 100644 --- a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java +++ b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java @@ -19,6 +19,7 @@ import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.LineSeparator; import com.intellij.xml.util.XmlStringUtil; +import org.jdom.Verifier; import org.junit.Test; import java.nio.CharBuffer; @@ -492,4 +493,13 @@ public class StringUtilTest { assertEquals(3, StringUtil.lastIndexOf("axaxa", 'x', 0, 5)); assertEquals(2, StringUtil.lastIndexOf("abcd", 'c', -42, 99)); // #IDEA-144968 } + + @Test + public void testEscapingIllegalXmlChars() { + for (String s : new String[]{"ab\n\0\r\tde", "\\abc\1\2\3\uFFFFdef"}) { + String escapedText = StringUtil.escapeIllegalXmlChars(s); + assertNull(Verifier.checkCharacterData(escapedText)); + assertEquals(s, StringUtil.unescapeIllegalXmlChars(escapedText)); + } + } }