Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
.idea
out
out
/build/
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,36 @@ Use your IDE. Preferences/Plugins/Browse repositories and search for "camelcase"

## Build
Just clone this repo and open the project it in IntelliJ IDEA.

### Local WordUtils hotfix (IDEA 2026.2)

The conversion code now capitalizes words without Apache Commons Lang, fixing
[upstream #36](https://github.com/netnexus/camelcaseplugin/issues/36).

On macOS, build and test with the installed IDEA JDK and original CamelCase 3.0.12.
Download the original Marketplace artifact once (or pass its path with `--base-jar`):

```sh
mkdir -p build/base
curl -fL -o build/base/CamelCasePlugin-3.0.12.jar https://plugins.jetbrains.com/files/7160/153616/CamelCasePlugin.jar
python3 scripts/build-local.py
```

Optional arguments: `--ide /path/to/IDE.app/Contents` and `--base-jar /path/to/CamelCasePlugin.jar`.
Keep a copy of the original 3.0.12 JAR for rebuilding and rollback.

This is a hotfix package, not a full source build: the script replaces only
`Conversion.class` and updates the version/platform metadata in the original JAR.
For packaging it uses `scripts/compat-3.0.12/.../Conversion.java`, taken from
upstream commit `dfb9512` with only the WordUtils replacement. The current upstream
source uses a different method signature and cannot replace the released class.
It preserves the original compiled settings UI because the checked-in generated
UI source is stale. Tests run without Commons Lang, and all other JAR entries
are checked for identical content. A separate test is compiled against the original
JAR and executed against the finished package to verify binary compatibility.
The installed plugin is not modified.

Output: `build/distributions/CamelCasePlugin-3.0.12.2-local.jar` (IDE build 262+).
Do not install `3.0.12.1-local`: that earlier package has an incompatible method signature.
Install via **Settings → Plugins → ⚙ → Install Plugin from Disk…**, then restart.
The plugin ID is unchanged, so this replaces CamelCase rather than adding a second action.
71 changes: 71 additions & 0 deletions scripts/build-local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Build a local hotfix using the binary-compatible 3.0.12 conversion source."""
import argparse
from copy import copy
from pathlib import Path
import re
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from zipfile import ZipFile

root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--ide', type=Path, default=Path('/Applications/IntelliJ IDEA.app/Contents'))
parser.add_argument('--base-jar', type=Path, default=root / 'build/base/CamelCasePlugin-3.0.12.jar')
args = parser.parse_args()
jdk = args.ide / 'jbr/Contents/Home/bin'
annotations = args.ide / 'lib/annotations.jar'
class_path = 'de/netnexus/CamelCasePlugin/Conversion.class'
descriptor_path = 'META-INF/plugin.xml'
output = root / 'build/distributions/CamelCasePlugin-3.0.12.2-local.jar'
if args.base_jar.resolve() == output.resolve():
parser.error('The output must not overwrite the base JAR.')

with ZipFile(args.base_jar) as original, tempfile.TemporaryDirectory() as temp:
descriptor = original.read(descriptor_path).decode('utf-8')
metadata = ET.fromstring(descriptor)
if metadata.findtext('id') != 'de.netnexus.camelcaseplugin' or metadata.findtext('version') != '3.0.12':
parser.error('Expected the original CamelCase 3.0.12 JAR.')
if any(n.upper().endswith(('.SF', '.RSA', '.DSA', '.EC')) for n in original.namelist()):
parser.error('Signed base JARs are not supported.')
subprocess.run([str(jdk / 'javac'), '--release', '17', '-encoding', 'UTF-8',
'-cp', str(annotations), '-d', temp,
str(root / 'src/de/netnexus/CamelCasePlugin/Conversion.java'),
str(root / 'tests/de/netnexus/CamelCasePlugin/ConversionTest.java')], check=True)
# Run without the IDE or Commons Lang on the runtime classpath.
subprocess.run([str(jdk / 'java'), '-cp', temp,
'de.netnexus.CamelCasePlugin.ConversionTest'], check=True)
# Compile the caller against the ORIGINAL binary, not our replacement source.
# Running this caller against the output catches NoSuchMethodError regressions.
probe = Path(temp) / 'probe'
subprocess.run([str(jdk / 'javac'), '--release', '11', '-encoding', 'UTF-8',
'-cp', str(args.base_jar), '-d', str(probe),
str(root / 'tests/de/netnexus/CamelCasePlugin/LegacyBinaryTest.java')], check=True)
legacy = Path(temp) / 'legacy'
subprocess.run([str(jdk / 'javac'), '--release', '11', '-encoding', 'UTF-8',
'-cp', str(annotations), '-d', str(legacy),
str(root / 'scripts/compat-3.0.12/de/netnexus/CamelCasePlugin/Conversion.java')], check=True)
replacement = (legacy / class_path).read_bytes()
assert b'org/apache/commons/lang/WordUtils' not in replacement
descriptor = descriptor.replace('<version>3.0.12</version>', '<version>3.0.12.2-local</version>')
# This local artifact targets the affected 2026.2 platform, not older IDEs.
descriptor = re.sub(r'<idea-version\b[^>]*/>', '<idea-version since-build="262"/>', descriptor)
output.parent.mkdir(parents=True, exist_ok=True)
with ZipFile(output, 'w') as patched:
for entry in original.infolist():
data = original.read(entry.filename)
if entry.filename == class_path:
data = replacement
elif entry.filename == descriptor_path:
data = descriptor.encode('utf-8')
patched.writestr(copy(entry), data)
with ZipFile(output) as patched:
assert patched.testzip() is None
assert patched.namelist() == original.namelist()
for name in original.namelist():
if name not in (class_path, descriptor_path):
assert patched.read(name) == original.read(name), name
subprocess.run([str(jdk / 'java'), '-cp', str(probe) + ':' + str(output),
'de.netnexus.CamelCasePlugin.LegacyBinaryTest'], check=True)
print(output)
217 changes: 217 additions & 0 deletions scripts/compat-3.0.12/de/netnexus/CamelCasePlugin/Conversion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package de.netnexus.CamelCasePlugin;

import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Character.isLowerCase;
import static java.lang.Character.isUpperCase;

class Conversion {

private static final String CONVERSION_SPACE_CASE = "space case";
private static final String CONVERSION_KEBAB_CASE = "kebab-case";
private static final String CONVERSION_UPPER_SNAKE_CASE = "SNAKE_CASE";
private static final String CONVERSION_PASCAL_CASE = "CamelCase";
private static final String CONVERSION_CAMEL_CASE = "camelCase";
private static final String CONVERSION_PASCAL_CASE_SPACE = "Camel Case";
private static final String CONVERSION_LOWER_SNAKE_CASE = "snake_case";

@NotNull
static String transform(String text,
boolean usePascalCaseWithSpace,
boolean useSpaceCase,
boolean useKebabCase,
boolean useUpperSnakeCase,
boolean usePascalCase,
boolean useCamelCase,
boolean useLowerSnakeCase,
String[] conversionList) {
String newText, appendText = "";
boolean repeat = true;
int iterations = 0;
String next = null;

Pattern p = Pattern.compile("^\\W+");
Matcher m = p.matcher(text);
if (m.find()) {
appendText = m.group(0);
}
//remove all special chars
text = text.replaceAll("^\\W+", "");

do {
newText = text;
boolean isLowerCase = text.equals(text.toLowerCase());
boolean isUpperCase = text.equals(text.toUpperCase());

if (isLowerCase && text.contains("_")) {
// snake_case to space case
if (next == null) {
next = getNext(CONVERSION_LOWER_SNAKE_CASE, conversionList);
} else {
if (next.equals(CONVERSION_SPACE_CASE)) {
repeat = !useSpaceCase;
next = getNext(CONVERSION_SPACE_CASE, conversionList);
}
}
newText = text.replace('_', ' ');

} else if (isLowerCase && text.contains(" ")) {
// space case to Camel Case
if (next == null) {
next = getNext(CONVERSION_SPACE_CASE, conversionList);
} else {
newText = capitalize(text);
if (next.equals(CONVERSION_PASCAL_CASE_SPACE)) {
repeat = !usePascalCaseWithSpace;
next = getNext(CONVERSION_PASCAL_CASE_SPACE, conversionList);
}
}

} else if (isUpperCase(text.charAt(0)) && isLowerCase(text.charAt(1)) && text.contains(" ")) {
// Camel Case to kebab-case
if (next == null) {
next = getNext(CONVERSION_PASCAL_CASE_SPACE, conversionList);
} else {
newText = text.toLowerCase().replace(' ', '-');
if (next.equals(CONVERSION_KEBAB_CASE)) {
repeat = !useKebabCase;
next = getNext(CONVERSION_KEBAB_CASE, conversionList);
}
}

} else if (isLowerCase && text.contains("-") || (isLowerCase && !text.contains(" "))) {
// kebab-case to SNAKE_CASE
if (next == null) {
next = getNext(CONVERSION_KEBAB_CASE, conversionList);
} else {
newText = text.replace('-', '_').toUpperCase();
if (next.equals(CONVERSION_UPPER_SNAKE_CASE)) {
repeat = !useUpperSnakeCase;
next = getNext(CONVERSION_UPPER_SNAKE_CASE, conversionList);
}
}

} else if ((isUpperCase && text.contains("_")) || (isLowerCase && !text.contains("_") && !text.contains(" ")) || (isUpperCase && !text.contains(" "))) {
// SNAKE_CASE to PascalCase
if (next == null) {
next = getNext(CONVERSION_UPPER_SNAKE_CASE, conversionList);
} else {
newText = Conversion.toCamelCase(text.toLowerCase());
if (next.equals(CONVERSION_PASCAL_CASE)) {
repeat = !usePascalCase;
next = getNext(CONVERSION_PASCAL_CASE, conversionList);
}
}

} else if (!isUpperCase && text.substring(0, 1).equals(text.substring(0, 1).toUpperCase()) && !text.contains("_")) {
// PascalCase to camelCase
if (next == null) {
next = getNext(CONVERSION_PASCAL_CASE, conversionList);
} else {
newText = text.substring(0, 1).toLowerCase() + text.substring(1);
if (next.equals(CONVERSION_CAMEL_CASE)) {
repeat = !useCamelCase;
next = getNext(CONVERSION_CAMEL_CASE, conversionList);
}
}
} else {
// camelCase to snake_case
if (next == null) {
next = getNext(CONVERSION_CAMEL_CASE, conversionList);
} else {
newText = Conversion.toSnakeCase(text);
if (next.equals(CONVERSION_LOWER_SNAKE_CASE)) {
repeat = !useLowerSnakeCase;
next = getNext(CONVERSION_LOWER_SNAKE_CASE, conversionList);
}
}
}
if (iterations++ > 20) {
repeat = false;
}
text = newText;
} while (repeat);

return appendText + newText;
}

/**
* Return next conversion (or wrap to first)
*
* @param conversion String
* @param conversions Array of strings
* @return next conversion
*/
private static String getNext(String conversion, String[] conversions) {
int index;
index = Arrays.asList(conversions).indexOf(conversion) + 1;
if (index < conversions.length) {
return conversions[index];
} else {
return conversions[0];
}
}

/**
* Convert a string (CamelCase) to snake_case
*
* @param in CamelCase string
* @return snake_case String
*/
private static String toSnakeCase(String in) {
in = in.replaceAll(" +", "");
StringBuilder result = new StringBuilder("" + Character.toLowerCase(in.charAt(0)));
for (int i = 1; i < in.length(); i++) {
char c = in.charAt(i);
if (isUpperCase(c)) {
result.append("_").append(Character.toLowerCase(c));
} else {
result.append(c);
}
}
return result.toString();
}

/**
* Convert a string (snake_case) to CamelCase
*
* @param in snake_case String
* @return CamelCase string
*/
private static String toCamelCase(String in) {
StringBuilder camelCased = new StringBuilder();
String[] tokens = in.split("_");
for (String token : tokens) {
if (token.length() >= 1) {
camelCased.append(token.substring(0, 1).toUpperCase()).append(token.substring(1));
} else {
camelCased.append("_");
}
}
return camelCased.toString();
}
// Match WordUtils.capitalize: title-case each whitespace-delimited word,
// preserving whitespace and all remaining characters (upstream issue #36).
private static String capitalize(String text) {
StringBuilder result = new StringBuilder(text.length());
boolean capitalizeNext = true;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (Character.isWhitespace(ch)) {
result.append(ch);
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toTitleCase(ch));
capitalizeNext = false;
} else {
result.append(ch);
}
}
return result.toString();
}

}
23 changes: 21 additions & 2 deletions src/de/netnexus/CamelCasePlugin/Conversion.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.netnexus.CamelCasePlugin;

import org.apache.commons.lang.WordUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
Expand Down Expand Up @@ -46,7 +45,7 @@ static String transform(String text, String target) {
// snake_case to space case
case CONVERSION_LOWER_SNAKE_CASE -> text = text.replace('_', ' ');
// space case to Camel Case
case CONVERSION_SPACE_CASE -> text = WordUtils.capitalize(text);
case CONVERSION_SPACE_CASE -> text = capitalize(text);
// Camel Case to kebab-case
case CONVERSION_PASCAL_CASE_SPACE -> text = text.toLowerCase().replace(' ', '-');
// kebab-case to SNAKE_CASE
Expand Down Expand Up @@ -123,6 +122,26 @@ private static String toCamelCase(String in) {
return camelCased.toString();
}

// Match WordUtils.capitalize: title-case each whitespace-delimited word,
// preserving whitespace and all remaining characters (upstream issue #36).
private static String capitalize(String text) {
StringBuilder result = new StringBuilder(text.length());
boolean capitalizeNext = true;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (Character.isWhitespace(ch)) {
result.append(ch);
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toTitleCase(ch));
capitalizeNext = false;
} else {
result.append(ch);
}
}
return result.toString();
}

/**
* Get a string case type
*
Expand Down
Loading