feat: add project files

This commit is contained in:
AXOLOTsh
2026-06-04 19:24:07 +03:00
parent 871e21e00c
commit af9e4a2da2
10 changed files with 469 additions and 26 deletions

View File

@@ -0,0 +1,38 @@
plugins {
id("java-library")
id("maven-publish")
}
group = "io.github.axolotsh"
version = "1.0-SNAPSHOT"
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
groupId = project.group.toString()
artifactId = project.name
version = project.version.toString()
}
}
repositories {
maven {
name = "Reposilite"
val releasesUrl = "https://maven.axolotsh.org/releases"
val snapshotsUrl = "https://maven.axolotsh.org/snapshots"
url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl)
credentials {
username = providers.gradleProperty("reposiliteUser").orNull ?: "admin"
password = providers.gradleProperty("reposilitePassword").orNull ?: "password"
}
}
}
}

View File

@@ -0,0 +1,11 @@
package io.github.axolotsh.formatterlib.abstraction;
import java.util.Collection;
public interface IFormatter {
public String content();
public Collection<IFormatterTag> tags();
public String format(String input, IFormatterContext context);
}

View File

@@ -0,0 +1,5 @@
package io.github.axolotsh.formatterlib.abstraction;
public interface IFormatterContext {
}

View File

@@ -0,0 +1,9 @@
package io.github.axolotsh.formatterlib.abstraction;
import java.util.Map;
public interface IFormatterTag {
public String name();
public String apply(String content, Map<String, String> args, IFormatterContext context);
}

View File

@@ -0,0 +1,58 @@
plugins {
id("java-library")
id("maven-publish")
}
group = "io.github.axolotsh"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
compileOnly("org.projectlombok:lombok:1.18.46")
annotationProcessor("org.projectlombok:lombok:1.18.46")
compileOnly("org.jetbrains:annotations:26.1.0")
implementation(project(":formatterlib-abstraction"))
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
groupId = project.group.toString()
artifactId = project.name
version = project.version.toString()
}
}
repositories {
maven {
name = "Reposilite"
val releasesUrl = "https://maven.axolotsh.org/releases"
val snapshotsUrl = "https://maven.axolotsh.org/snapshots"
url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl)
credentials {
username = providers.gradleProperty("reposiliteUser").orNull ?: "admin"
password = providers.gradleProperty("reposilitePassword").orNull ?: "password"
}
}
}
}

View File

@@ -0,0 +1,145 @@
package io.github.axolotsh.formatterlib;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import io.github.axolotsh.formatterlib.abstraction.IFormatter;
import io.github.axolotsh.formatterlib.abstraction.IFormatterContext;
import io.github.axolotsh.formatterlib.abstraction.IFormatterTag;
import lombok.Builder;
import lombok.Getter;
import lombok.experimental.Accessors;
@Getter
@Accessors(chain = true, fluent = true)
public class Formatter implements IFormatter {
private static final Pattern TAG_PATTERN = Pattern.compile(
"<([a-zA-Z0-9]+)([^>]*?)(?:/>|>([\\s\\S]*?)</\\1>|>)");
private static final Pattern ATTR_PATTERN = Pattern.compile(
"([a-zA-Z0-9_-]+)=\"([^\"]*)\"");
private static final String CONTENT_ATTR = "content";
@Nullable
private String content;
private Collection<IFormatterTag> tags;
private final Map<String, IFormatterTag> storage = new HashMap<>();
public Formatter(String content, IFormatterTag... tags) {
this(content, Set.of(tags));
}
@Builder(toBuilder = true)
Formatter(String content, Collection<IFormatterTag> tags) {
this.content = content;
this.tags = tags;
for (var tag : tags)
storage.put(tag.name(), tag);
}
public static class FormatterBuilder {
public FormatterBuilder tag(String tag) {
return tag(new ReplacementTag(tag));
}
public FormatterBuilder tag(String... tags) {
for (var tag : tags)
tag(new ReplacementTag(tag));
return this;
}
public FormatterBuilder tag(String tag, String replacement) {
return tag(new ReplacementTag(tag, replacement));
}
public FormatterBuilder tag(String tag, Function<IFormatterContext, String> replacement) {
return tag(new ReplacementTag(tag, replacement));
}
public FormatterBuilder tag(String tag, IFormatter formatter) {
return tag(tag, ctx -> formatter.format(formatter.content(), ctx));
}
public FormatterBuilder tag(IFormatter formatter) {
return tag(formatter.tags().toArray(ReplacementTag[]::new));
}
public FormatterBuilder tag(IFormatterTag... tags) {
for (var tag : tags)
tag(tag);
return this;
}
public FormatterBuilder tag(IFormatterTag tag) {
if (tags == null)
tags = new HashSet<>();
tags.add(tag);
return this;
}
}
public String format() {
return format(content);
}
public String format(IFormatterContext context) {
return format(content, context);
}
public String format(String input) {
return format(input, null);
}
public String format(String input, IFormatterContext context) {
if (input == null || input.isBlank())
input = content;
if (input == null || input.isBlank())
return "";
return TAG_PATTERN.matcher(input).replaceAll(matchResult -> {
var tagName = matchResult.group(1).toLowerCase();
var rawArgs = matchResult.group(2);
var content = matchResult.group(3);
var tag = storage.get(tagName);
if (tag == null)
return matchResult.group(0);
var args = parseAttributes(rawArgs);
if (content == null && args.containsKey(CONTENT_ATTR)) {
content = args.get(CONTENT_ATTR);
args.remove(CONTENT_ATTR);
}
return tag.apply(content, args, context);
});
}
@Override
public String toString() {
return format();
}
private static Map<String, String> parseAttributes(String rawArgs) {
Map<String, String> argsMap = new HashMap<>();
if (rawArgs == null || rawArgs.isBlank())
return argsMap;
var matcher = ATTR_PATTERN.matcher(rawArgs);
while (matcher.find())
argsMap.put(matcher.group(1), matcher.group(2));
return argsMap;
}
}

View File

@@ -0,0 +1,47 @@
package io.github.axolotsh.formatterlib;
import java.util.Map;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import io.github.axolotsh.formatterlib.abstraction.IFormatter;
import io.github.axolotsh.formatterlib.abstraction.IFormatterContext;
import io.github.axolotsh.formatterlib.abstraction.IFormatterTag;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
@Getter
@Accessors(chain = true, fluent = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class ReplacementTag implements IFormatterTag {
@EqualsAndHashCode.Include
private String name;
@Nullable
private Function<IFormatterContext, String> replacement;
public ReplacementTag(String name) {
this.name = name;
}
public ReplacementTag(String name, String replacement) {
this(name, _ -> replacement);
}
public ReplacementTag(String name, Function<IFormatterContext, String> replacement) {
this.name = name;
this.replacement = replacement;
}
public ReplacementTag(String name, IFormatter formatter) {
this.name = name;
this.replacement = ctx -> {
return formatter.format(formatter.content(), ctx);
};
}
public String apply(String content, Map<String, String> args, IFormatterContext context) {
return replacement.apply(context);
}
}

View File

@@ -0,0 +1,154 @@
package io.github.axolotsh.formatterlib;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.Map;
import org.junit.jupiter.api.Test;
import io.github.axolotsh.formatterlib.abstraction.IFormatterContext;
import io.github.axolotsh.formatterlib.abstraction.IFormatterTag;
public class FormatterLibTest {
private static class TestContext implements IFormatterContext {
private final String value;
public TestContext(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
@Test
public void testSingleTagReplacement() {
Formatter formatter = new Formatter("Hello <name/>!", new ReplacementTag("name", "World"));
assertEquals("Hello World!", formatter.format());
}
@Test
public void testMultipleTagsReplacement() {
Formatter formatter = new Formatter("<greeting/>, <name/>!",
new ReplacementTag("greeting", "Hello"),
new ReplacementTag("name", "John"));
assertEquals("Hello, John!", formatter.format());
}
@Test
public void testUnregisteredTagIsIgnored() {
Formatter formatter = new Formatter("Hello <unknown/>!");
assertEquals("Hello <unknown/>!", formatter.format());
}
@Test
public void testTagWithContentExtraction() {
IFormatterTag echoContentTag = new IFormatterTag() {
@Override
public String name() {
return "echo";
}
@Override
public String apply(String content, Map<String, String> args, IFormatterContext context) {
return "Echo: " + content;
}
};
Formatter formatter = new Formatter("<echo>some inner text</echo>", echoContentTag);
assertEquals("Echo: some inner text", formatter.format());
}
@Test
public void testTagWithAttributesExtraction() {
IFormatterTag attrTag = new IFormatterTag() {
@Override
public String name() {
return "user";
}
@Override
public String apply(String content, Map<String, String> args, IFormatterContext context) {
return args.get("firstname") + " " + args.get("lastname");
}
};
Formatter formatter = new Formatter("User is <user firstname=\"John\" lastname=\"Doe\"/>", attrTag);
assertEquals("User is John Doe", formatter.format());
}
@Test
public void testContentAttributeFallback() {
IFormatterTag contentAttrTag = new IFormatterTag() {
@Override
public String name() {
return "msg";
}
@Override
public String apply(String content, Map<String, String> args, IFormatterContext context) {
return "Message: " + content;
}
};
Formatter formatter = new Formatter("<msg content=\"Hello from attribute\"/>", contentAttrTag);
assertEquals("Message: Hello from attribute", formatter.format());
}
@Test
public void testContextPassing() {
Formatter formatter = Formatter.builder()
.content("Context value: <ctx/>")
.tag("ctx", ctx -> {
if (ctx instanceof TestContext testCtx) {
return testCtx.getValue();
}
return "null";
})
.build();
TestContext context = new TestContext("42");
assertEquals("Context value: 42", formatter.format(context));
}
@Test
public void testNullOrBlankInputHandling() {
Formatter formatter = new Formatter("Default <tag/>", new ReplacementTag("tag", "Content"));
assertEquals("Default Content", formatter.format((String) null));
assertEquals("Default Content", formatter.format(" "));
Formatter emptyFormatter = new Formatter(null);
assertEquals("", emptyFormatter.format((String) null));
}
@Test
public void testFormatterBuilder() {
Formatter formatter = Formatter.builder()
.content("Build: <a/> and <b/>")
.tag("a", "Alpha")
.tag(new ReplacementTag("b", "Beta"))
.build();
assertNotNull(formatter.tags());
assertEquals(2, formatter.tags().size());
assertEquals("Build: Alpha and Beta", formatter.format());
}
@Test
public void testNestedFormatterUsingBuilder() {
Formatter innerFormatter = Formatter.builder()
.content("Inner <x/>")
.tag("x", "Value")
.build();
Formatter outerFormatter = Formatter.builder()
.content("Outer -> <inner/>")
.tag("inner", innerFormatter)
.build();
assertEquals("Outer -> Inner Value", outerFormatter.format());
}
}

View File

@@ -1,25 +0,0 @@
plugins {
id("java-library")
}
repositories {
mavenCentral()
}
dependencies {
compileOnly("org.projectlombok:lombok:1.18.46")
annotationProcessor("org.projectlombok:lombok:1.18.46")
compileOnly("org.jetbrains:annotations:26.1.0")
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
tasks.named<Test>("test") {
useJUnitPlatform()
}

View File

@@ -1,2 +1,3 @@
rootProject.name = "FormatterLib"
include("lib")
include("formatterlib")
include("formatterlib-abstraction")