diff --git a/README.jrf b/README.jrf
index 48110eab2..2233e91de 100644
--- a/README.jrf
+++ b/README.jrf
@@ -1,5 +1,5 @@
# This is a Jamal reference file containing serialized base64 encoded macros
-# Created: 2023-12-27 19:49:51 +0100
+# Created: 2024-01-26 12:29:48 +0100
# id|openStr|closeStr|verbatim|tailParameter|pure|content|parameters
# TOC
VE9D|eyU=|JX0=|0|0|0|Ci4gPDxJbnN0YWxsYXRpb24+PgouIDw8R1M+PgouIDw8Q29uZmlndXJhdGlvbj4+Ci4gPDxGZWF0dXJlcz4+Ci4gPDxDb250cmlidXRpbmc+PgouIDw8RG9jdW1lbnRhdGlvbj4+Ci4gPDxMaWNlbnNlPj4KLiA8PENoYW5nZWxvZz4+Ci4gPDxSb2FkbWFwPj4KLiA8PFN1cHBvcnQ+PgouIDw8RkFRPj4KLiA8PE1haW50ZW5hbmNlPj4=|
diff --git a/RELEASES.adoc b/RELEASES.adoc
index 2a1baa5ff..07fdfff1b 100644
--- a/RELEASES.adoc
+++ b/RELEASES.adoc
@@ -12,6 +12,7 @@
* `import` has a new parop `global` that makes the import global and mot imported again even if the file was imported from a local scope
* `import` does not import a file again if it has the same content as an already imported file.
Until now, different files with the same content could be imported parallel.
+* Snippet library has a new macro `plural` that can be used to pluralize a word.
* Bugfix invoking macro close for AutoCloseable macros that need processor and output.
* Module plantUML was removed from the development.
This module is not supported anymore.
diff --git a/RELEASES.adoc.jam b/RELEASES.adoc.jam
index 87841977a..7c76e7ef0 100644
--- a/RELEASES.adoc.jam
+++ b/RELEASES.adoc.jam
@@ -12,6 +12,7 @@
* `import` has a new parop `global` that makes the import global and mot imported again even if the file was imported from a local scope
* `import` does not import a file again if it has the same content as an already imported file.
Until now, different files with the same content could be imported parallel.
+* Snippet library has a new macro `plural` that can be used to pluralize a word.
* Bugfix invoking macro close for AutoCloseable macros that need processor and output.
* Module plantUML was removed from the development.
This module is not supported anymore.
diff --git a/jamal-java/src/main/resources/maven-4.0.0.xsd b/jamal-java/src/main/resources/maven-4.0.0.xsd
new file mode 100644
index 000000000..d2657751d
--- /dev/null
+++ b/jamal-java/src/main/resources/maven-4.0.0.xsd
@@ -0,0 +1,673 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jamal-java/src/main/resources/pom.jim b/jamal-java/src/main/resources/pom.jim
index af174e8f0..8ccb89799 100644
--- a/jamal-java/src/main/resources/pom.jim
+++ b/jamal-java/src/main/resources/pom.jim
@@ -17,7 +17,7 @@ static javax0.jamal.java.TextTags.*
〔#define [pure] precode=
try {
final var project = new MyPom();
- return project.build();
+ return project.doit();
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -27,7 +27,7 @@ static javax0.jamal.java.TextTags.*
super("〔@pos (top format=%f)〕");
}
- private String build() throws Exception {
+ private String doit() throws Exception {
〕
〔@define [pure] postcode=
return format();
diff --git a/jamal-java/src/test/jamal/Pom.java.jam b/jamal-java/src/test/jamal/Pom.java.jam
new file mode 100644
index 000000000..d0758f015
--- /dev/null
+++ b/jamal-java/src/test/jamal/Pom.java.jam
@@ -0,0 +1,63 @@
+{%@comment nosave%}
+{%#java:insert to="../java/javax0/jamal/java/Pom.java" segment="text_tags"
+{%!@for $TAG in (modelVersion,groupId,artifactId,version,packaging,name,description,url,inceptionYear)=
+ public void $TAG(String $TAG) {
+ project.add("$TAG", $TAG);
+ }
+%}\
+%}\
+{%#java:insert to="../java/javax0/jamal/java/Pom.java" segment="typed_tags"
+{%!@for $TAG in (parent,organization,prerequisites,issueManagement,ciManagement,distributionManagement,dependencyManagement,build,reporting)=
+{%#define $CLASS={%#case:upper {%@string:substring (begin=0 end=1) $TAG%}%}{%@string:substring (begin=1) $TAG%}%}\
+ public {%$CLASS%} $TAG() {
+ var $TAG = project.get({%$CLASS%}.class, "$TAG");
+ if ($TAG != null) {
+ return $TAG;
+ }
+ $TAG = new {%$CLASS%}();
+ project.add("$TAG", $TAG);
+ return $TAG;
+ }
+%}
+%}
+
+{%#java:insert to="../java/javax0/jamal/java/DependencyManagement.java" wholeFile
+package javax0.jamal.java;
+import static javax0.jamal.java.Xml.path;
+public class DependencyManagement extends Xml {
+
+
+ public DependencyManagement dependency(CharSequence... dependencies) {
+ for (var dependency : dependencies) {
+ if (!(dependency instanceof Dependency)) {
+ dependency = Dependency.dependency(dependency);
+ }
+ add("dependencies", dependency);
+ }
+ return this;
+ }
+}
+
+%}
+{%#java:insert to="../java/javax0/jamal/java/Build.java" wholeFile
+package javax0.jamal.java;
+public class Build extends Xml {
+
+{%!@for $TAG in (sourceDirectory,scriptSourceDirectory,testSourceDirectory,outputDirectory,testOutputDirectory,extensions )=
+ public void $TAG(String $TAG) {
+ add("$TAG", $TAG);
+ }
+%}\
+
+ public Build extensions(CharSequence... extensions) {
+ for (var extension : extensions) {
+ if (!(extension instanceof Extension)) {
+ extension = Extension.extension(extension);
+ }
+ add("extensions", extension);
+ }
+ return this;
+ }
+}
+
+%}
\ No newline at end of file
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Build.java b/jamal-java/src/test/java/javax0/jamal/java/Build.java
index 585919ce8..bf6051b8e 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/Build.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/Build.java
@@ -1,12 +1,39 @@
package javax0.jamal.java;
+public class Build extends Xml {
-public class Build {
- private final Xml project;
+ public void sourceDirectory(String sourceDirectory) {
+ add("sourceDirectory", sourceDirectory);
+ }
+
+ public void scriptSourceDirectory(String scriptSourceDirectory) {
+ add("scriptSourceDirectory", scriptSourceDirectory);
+ }
+
+ public void testSourceDirectory(String testSourceDirectory) {
+ add("testSourceDirectory", testSourceDirectory);
+ }
- public Build(Xml project) {
- this.project = project;
+ public void outputDirectory(String outputDirectory) {
+ add("outputDirectory", outputDirectory);
}
+ public void testOutputDirectory(String testOutputDirectory) {
+ add("testOutputDirectory", testOutputDirectory);
+ }
+
+ public void extensions (String extensions ) {
+ add("extensions ", extensions );
+ }
+ public Build extensions(CharSequence... extensions) {
+ for (var extension : extensions) {
+ if (!(extension instanceof Extension)) {
+ extension = Extension.extension(extension);
+ }
+ add("extensions", extension);
+ }
+ return this;
+ }
}
+
diff --git a/jamal-java/src/test/java/javax0/jamal/java/CiManagement.java b/jamal-java/src/test/java/javax0/jamal/java/CiManagement.java
new file mode 100644
index 000000000..3f4c4062f
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/CiManagement.java
@@ -0,0 +1,4 @@
+package javax0.jamal.java;
+
+public class CiManagement extends Xml {
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/DependencyManagement.java b/jamal-java/src/test/java/javax0/jamal/java/DependencyManagement.java
new file mode 100644
index 000000000..05d13eb06
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/DependencyManagement.java
@@ -0,0 +1,16 @@
+package javax0.jamal.java;
+import static javax0.jamal.java.Xml.path;
+public class DependencyManagement extends Xml {
+
+
+ public DependencyManagement dependency(CharSequence... dependencies) {
+ for (var dependency : dependencies) {
+ if (!(dependency instanceof Dependency)) {
+ dependency = Dependency.dependency(dependency);
+ }
+ add("dependencies", dependency);
+ }
+ return this;
+ }
+}
+
diff --git a/jamal-java/src/test/java/javax0/jamal/java/DistributionManagement.java b/jamal-java/src/test/java/javax0/jamal/java/DistributionManagement.java
new file mode 100644
index 000000000..b8070aa22
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/DistributionManagement.java
@@ -0,0 +1,4 @@
+package javax0.jamal.java;
+
+public class DistributionManagement extends Xml {
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Extension.java b/jamal-java/src/test/java/javax0/jamal/java/Extension.java
new file mode 100644
index 000000000..0ddfe759f
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/Extension.java
@@ -0,0 +1,45 @@
+package javax0.jamal.java;
+
+public class Extension extends Xml {
+
+ public Extension() {
+ super("dependency");
+ }
+
+ private Extension(CharSequence coordinate) {
+ this();
+ final var coords = ((String) coordinate).split(":");
+ if (coords.length > 0 && !coords[0].isEmpty()) {
+ groupId(coords[0]);
+ }
+ if (coords.length > 1 && !coords[1].isEmpty()) {
+ artifactId(coords[1]);
+ }
+ if (coords.length > 2 && !coords[2].isEmpty()) {
+ version(coords[2]);
+ }
+ }
+
+ public static Extension extension(CharSequence coordinates) {
+ return new Extension(coordinates);
+ }
+
+ public static Extension extension() {
+ return new Extension();
+ }
+
+ public Extension groupId(CharSequence groupId) {
+ add(path("extension", "groupId"), groupId);
+ return this;
+ }
+
+ public Extension artifactId(CharSequence artifactId) {
+ add(path("extension", "artifactId"), artifactId);
+ return this;
+ }
+
+ public Extension version(CharSequence version) {
+ add(path("extension", "version"), version);
+ return this;
+ }
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/IssueManagement.java b/jamal-java/src/test/java/javax0/jamal/java/IssueManagement.java
new file mode 100644
index 000000000..b40468772
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/IssueManagement.java
@@ -0,0 +1,4 @@
+package javax0.jamal.java;
+
+public class IssueManagement extends Xml {
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Pom.java b/jamal-java/src/test/java/javax0/jamal/java/Pom.java
index 2179c6717..2f6e9666b 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/Pom.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/Pom.java
@@ -17,10 +17,10 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Arrays;
import static javax0.jamal.java.Dependency.dependency;
+
import static javax0.jamal.java.TextTags.filter;
import static javax0.jamal.java.TextTags.module;
import static javax0.jamal.java.Xml.path;
@@ -38,35 +38,146 @@ public Pom(final String cwd) {
project.add("modelVersion", "4.0.0");
}
+ //
+
+ public Parent parent() {
+ var parent = project.get(Parent.class, "parent");
+ if (parent != null) {
+ return parent;
+ }
+ parent = new Parent();
+ project.add("parent", parent);
+ return parent;
+ }
+
+ public Organization organization() {
+ var organization = project.get(Organization.class, "organization");
+ if (organization != null) {
+ return organization;
+ }
+ organization = new Organization();
+ project.add("organization", organization);
+ return organization;
+ }
+
+ public Prerequisites prerequisites() {
+ var prerequisites = project.get(Prerequisites.class, "prerequisites");
+ if (prerequisites != null) {
+ return prerequisites;
+ }
+ prerequisites = new Prerequisites();
+ project.add("prerequisites", prerequisites);
+ return prerequisites;
+ }
+
+ public IssueManagement issueManagement() {
+ var issueManagement = project.get(IssueManagement.class, "issueManagement");
+ if (issueManagement != null) {
+ return issueManagement;
+ }
+ issueManagement = new IssueManagement();
+ project.add("issueManagement", issueManagement);
+ return issueManagement;
+ }
+
+ public CiManagement ciManagement() {
+ var ciManagement = project.get(CiManagement.class, "ciManagement");
+ if (ciManagement != null) {
+ return ciManagement;
+ }
+ ciManagement = new CiManagement();
+ project.add("ciManagement", ciManagement);
+ return ciManagement;
+ }
+
+ public DistributionManagement distributionManagement() {
+ var distributionManagement = project.get(DistributionManagement.class, "distributionManagement");
+ if (distributionManagement != null) {
+ return distributionManagement;
+ }
+ distributionManagement = new DistributionManagement();
+ project.add("distributionManagement", distributionManagement);
+ return distributionManagement;
+ }
+
+ public DependencyManagement dependencyManagement() {
+ var dependencyManagement = project.get(DependencyManagement.class, "dependencyManagement");
+ if (dependencyManagement != null) {
+ return dependencyManagement;
+ }
+ dependencyManagement = new DependencyManagement();
+ project.add("dependencyManagement", dependencyManagement);
+ return dependencyManagement;
+ }
+
+ public Build build() {
+ var build = project.get(Build.class, "build");
+ if (build != null) {
+ return build;
+ }
+ build = new Build();
+ project.add("build", build);
+ return build;
+ }
+
+ public Reporting reporting() {
+ var reporting = project.get(Reporting.class, "reporting");
+ if (reporting != null) {
+ return reporting;
+ }
+ reporting = new Reporting();
+ project.add("reporting", reporting);
+ return reporting;
+ }
+
+
+ //
+ //
+
+ public void modelVersion(String modelVersion) {
+ project.add("modelVersion", modelVersion);
+ }
+
+ public void groupId(String groupId) {
+ project.add("groupId", groupId);
+ }
+
+ public void artifactId(String artifactId) {
+ project.add("artifactId", artifactId);
+ }
+
+ public void version(String version) {
+ project.add("version", version);
+ }
+
public void packaging(String packaging) {
- project.add("packaging", packaging);
+ project.add("packaging", packaging);
}
- public Pom description(String description) {
- project.add("description", description);
- return this;
+ public void name(String name) {
+ project.add("name", name);
}
- public Pom name(String name) {
- project.add("name", name);
- return this;
+ public void description(String description) {
+ project.add("description", description);
}
- public Pom url(URL url) {
- project.add("url", url.toString());
- return this;
+ public void url(String url) {
+ project.add("url", url);
}
- public Pom artifactId(String artifactId) {
- project.add("artifactId", artifactId);
- return this;
+ public void inceptionYear(String inceptionYear) {
+ project.add("inceptionYear", inceptionYear);
}
- public Pom groupId(String groupId) {
- project.add("groupId", groupId);
+ //
+
+ public Pom url(URL url) {
+ project.add("url", url.toString());
return this;
}
+
public Pom coordinates(String coords) {
final var s = coords.split(":");
if (s.length > 0 && !s[0].isEmpty()) {
@@ -92,6 +203,16 @@ public Pom dependencyManagement(CharSequence... dependencies) {
return this;
}
+ public Pom resource(CharSequence... elements) {
+ for (var e : elements) {
+ if (e.getClass() == String.class) {
+ throw new IllegalArgumentException("You cannot have a string argument to a resource tag");
+ }
+ project.add(path("build", "resources", "resource"), e);
+ }
+ return this;
+ }
+
public Pom dependencies(CharSequence... dependencies) {
for (var dependency : dependencies) {
if (!(dependency instanceof Dependency)) {
@@ -142,12 +263,6 @@ public Parent parent(String coords) {
return parent;
}
- public Parent parent() {
- final var parent = new Parent();
- project.add("parent", parent);
- return parent;
- }
-
public String[] dirsWith(String file) throws IOException {
final String normalizedCWD = CWD.startsWith("/") && File.separatorChar == '\\' ? CWD.substring(1) : CWD;
return Files.list(Path.of(normalizedCWD).getParent()).filter(Files::isDirectory)
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Prerequisites.java b/jamal-java/src/test/java/javax0/jamal/java/Prerequisites.java
new file mode 100644
index 000000000..5d5e052e8
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/Prerequisites.java
@@ -0,0 +1,4 @@
+package javax0.jamal.java;
+
+public class Prerequisites extends Xml {
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Reporting.java b/jamal-java/src/test/java/javax0/jamal/java/Reporting.java
new file mode 100644
index 000000000..f321cb801
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/Reporting.java
@@ -0,0 +1,4 @@
+package javax0.jamal.java;
+
+public class Reporting extends Xml {
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/TestJavaSourceMacro.java b/jamal-java/src/test/java/javax0/jamal/java/TestJavaSourceMacro.java
index 623641198..e180451f7 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/TestJavaSourceMacro.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/TestJavaSourceMacro.java
@@ -33,4 +33,10 @@ void test() throws Exception {
.results("maci");
}
+
+ @Test
+ void createPomClasses(){
+
+ }
+
}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/Xml.java b/jamal-java/src/test/java/javax0/jamal/java/Xml.java
index 31af4a1a8..f01385eba 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/Xml.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/Xml.java
@@ -1,17 +1,5 @@
package javax0.jamal.java;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import java.io.StringReader;
-import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -20,13 +8,22 @@
/**
* A simple data structure to store XML data and manipulate it.
* The data contains simplified XML good enough to store POM files.
- * It is not possible to add attributes to tags and content can only be text (bo CData).
+ * It is not possible to add attributes to tags and content can only be text.
*
* The underlying implementation is a Map with string keys representing the tag names.
- * The values can be either a string, a list or an Xml (instance of this class).
+ * The values are always lists of
+ *
+ * - strings, or
+ * - Xml values.
+ *
+ *
*
- * List values are used when a tag is repeated in the XML.
- * The elements of the lists are either strings or Xml instances.
+ * List is used because a tag can appear several times at a certain level in the XML.
+ * When a tag appears only one time, the list has one element.
+ * Empty tags are represented with an empty list.
+ *
+ * Using this representation, you can reserve the order of the elements, but you cannot reserve the position of a tag
+ * relative to other tags. The order of the different tags is not reserved.
*
*/
public class Xml implements CharSequence {
@@ -40,16 +37,45 @@ public Xml(final String tag) {
xml.put(tag, new ArrayList<>());
}
+ /**
+ * Return a new Xml instance that represents
+ *
{@code
+ * value
+ * }
+ *
+ * @param tag the name of the tag
+ * @param value the value in the tag
+ * @return the new Xml object
+ */
public static Xml tagValue(String tag, CharSequence value) {
final var xml = new Xml();
xml.add(path(tag), value);
return xml;
}
+ /**
+ * Return the string array that contains the arguments.
+ * This method can be used to represent the path of tags in a call expecting a string array.
+ *
+ * @param tags the path of the tags
+ * @return the string array
+ */
public static String[] path(String... tags) {
return tags;
}
+ public void add(final Xml sub){
+ for( final var entry : sub.xml.entrySet()){
+ for( final var value : entry.getValue()){
+ add(entry.getKey(),value);
+ }
+ }
+ }
+
+ /**
+ * Add an empty top tag to an existing Xml.
+ * @param tag the tag to add.
+ */
public void add(String tag) {
add(tag, null);
}
@@ -69,7 +95,7 @@ public void add(String[] tags, CharSequence value) {
/**
* Get the last value of the tag.
*
- * If the tag is not present then null is returned.
+ * If the tag is not present, then null is returned.
*
* @param tag the tag to get the value of
* @return the value of the tag or null
@@ -82,12 +108,26 @@ public CharSequence get(String tag) {
return vlist.get(vlist.size() - 1);
}
+ public T get(Class klass, String tag) {
+ return klass.cast(get(tag));
+ }
+
+ /**
+ * Add a new element or update an existing element in the XML structure represented by the Xml class.
+ *
+ * @param tags An array of strings representing a path of tags in the XML structure. Each element in the array is a
+ * tag name, and the sequence of names represents the hierarchical path in the XML tree.
+ * @param index The current position in the tags array that the method is processing. It is used in recursive calls
+ * to navigate through the array.
+ * @param value The value to be associated with the tag specified by the last element in the tags array.
+ * If this value is null, an empty list will be added to the Xml.
+ */
public void add(String[] tags, int index, CharSequence value) {
formatted = null;
final var tag = tags[index];
final var currentValue = get(tag);
if (currentValue == null) {
- if (index == tags.length - 1) {
+ if (isLastTag(tags, index)) {
if (value == null) {
xml.put(tag, new ArrayList<>());
} else {
@@ -99,18 +139,22 @@ public void add(String[] tags, int index, CharSequence value) {
sub.add(tags, index + 1, value);
}
} else {
- if (index == tags.length - 1) {
- xml.get(tag).add(value);
+ if (isLastTag(tags, index)) {
+ xml.get(tag).add(value);
} else {
if (currentValue instanceof Xml) {
((Xml) currentValue).add(tags, index + 1, value);
} else {
- throw new IllegalArgumentException("Cannot add subtag to a tag that already has a value");
+ throw new IllegalArgumentException("Cannot add subtag to a tag that already has a text value");
}
}
}
}
+ private static boolean isLastTag(String[] tags, int index) {
+ return index == tags.length - 1;
+ }
+
/**
* The formatted value of the XML. Every method that modifies the XML should set this value to null.
*/
@@ -158,5 +202,4 @@ public String toString() {
}
-
}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/pomconvert/XSDReader.java b/jamal-java/src/test/java/javax0/jamal/java/pomconvert/XSDReader.java
new file mode 100644
index 000000000..865583e87
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/pomconvert/XSDReader.java
@@ -0,0 +1,77 @@
+package javax0.jamal.java.pomconvert;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class XSDReader {
+
+ public static void readAndPrintXSD(String filePath) {
+ try {
+ // Create a DocumentBuilderFactory
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ // Parse the XSD file
+ Document document = builder.parse(filePath);
+
+ // Normalize the XML Structure
+ document.getDocumentElement().normalize();
+
+ // Iterate through all "xs:complexType" tags
+ NodeList complexTypeList = document.getElementsByTagName("xs:complexType");
+ for (int i = 0; i < complexTypeList.getLength(); i++) {
+ Node node = complexTypeList.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element complexType = (Element) node;
+ String complexTypeName = complexType.getAttribute("name");
+ createType(complexTypeName, complexType);
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void createType(String complexTypeName, Element complexType) {
+ System.out.println("public class " + complexTypeName + "{\n");
+
+ // Iterating through child "xs:element" tags in complex types
+ NodeList childElements = complexType.getElementsByTagName("xs:element");
+ for (int j = 0; j < childElements.getLength(); j++) {
+ Node childNode = childElements.item(j);
+ if (childNode.getNodeType() == Node.ELEMENT_NODE) {
+ handleElement((Element) childNode);
+ }
+ }
+ System.out.println("}\n");
+ }
+
+ private static String capitalize( String s){
+ return s.substring(0,1).toUpperCase() + s.substring(1);
+ }
+
+ private static void handleElement(Element element) {
+ String typeName = element.getAttribute("type");
+ String childElementName = element.getAttribute("name");
+ if( typeName.isEmpty()){
+ var unnamedType = element.getElementsByTagName("xs:complexType").item(0);
+ typeName = capitalize(childElementName) + "Type";
+ createType(typeName, (Element) unnamedType);
+ }
+ if( "xs:string".equals(typeName)){
+ typeName = "String";
+ }
+ System.out.println(typeName + " "+ childElementName + "(){}\n");
+ }
+
+ public static void main(String[] args) {
+ // Replace with the path to your XSD file
+ String xsdFilePath = XSDReader.class.getClassLoader().getResource("maven-4.0.0.xsd").getFile();
+ readAndPrintXSD(xsdFilePath);
+ }
+}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/testmacros/MyPom.java b/jamal-java/src/test/java/javax0/jamal/java/testmacros/MyPom.java
index 7f1202186..f0f84272a 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/testmacros/MyPom.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/testmacros/MyPom.java
@@ -1,5 +1,6 @@
package javax0.jamal.java.testmacros;
+import javax0.jamal.java.Extension;
import javax0.jamal.java.Pom;
import java.net.URL;
@@ -8,8 +9,11 @@
import static javax0.jamal.java.DistributionType.repo;
import static javax0.jamal.java.Exclusion.exclusion;
import static javax0.jamal.java.License.MIT;
+import static javax0.jamal.java.TextTags.directory;
import static javax0.jamal.java.TextTags.filter;
+import static javax0.jamal.java.TextTags.filtering;
import static javax0.jamal.java.TextTags.finalName;
+import static javax0.jamal.java.TextTags.targetPath;
import static javax0.jamal.java.Xml.path;
public class MyPom extends Pom {
@@ -19,13 +23,14 @@ public MyPom() throws Exception {
}
public static void main(String[] args) throws Exception {
- new MyPom().build();
+ new MyPom().doit();
}
- public String build() throws Exception {
+ public String doit() throws Exception {
//{@import res:pom.jim}{@java:dsl}
+ dependencyManagement().dependency("ss");
coordinates("myGroup:myArtifact:1.0.0");
parent("myGroup:myParent:1.0.0").relativePath("../myParent");
modules(dirsWith("pom.java"));
@@ -38,8 +43,15 @@ public String build() throws Exception {
dependency().groupId("org.junit.jupiter").artifactId("junit-jupiter-api").version("5.7.0").JAR().TEST(),
dependency("org.junit.jupiter:junit-jupiter-api::test").version("5.7.0").JAR()
);
+ dependencyManagement(
+ dependency("org.junit.jupiter:junit-jupiter-api:5.9.0:test")
+ .exclusions("grp:arti", exclusion("grp:arti")),
+ "org.junit.jupiter:junit-jupiter-api:5.9.1",
+ dependency().groupId("org.junit.jupiter").artifactId("junit-jupiter-api").version("5.7.0").JAR().TEST(),
+ dependency("org.junit.jupiter:junit-jupiter-api::test").version("5.7.0").JAR());
dependencies("gid:aid:ver:scope:classifier:type");
organization("javax0").url(new URL("http://javax0.com"));
+ resource(targetPath("pom.xml"), filtering(true), directory("abraka dabra"));
to(path("dependencies"), under("dependency"),
add("groupId", "tag-groupId",
"artifactId", "tag-artifactId",
@@ -55,7 +67,8 @@ public String build() throws Exception {
.organization("N/A")
.roles("developer")
.timezone(+1);
-build(finalName("myPom"),filters(filter("")));
+ build(finalName("myPom"), filters(filter("")));
+ build().extensions(Extension.extension().groupId("extension group").artifactId("extension artifact").version("extension version"));
return format();
}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestJdsl.java b/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestJdsl.java
index bf8ed6956..2199831f8 100644
--- a/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestJdsl.java
+++ b/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestJdsl.java
@@ -8,7 +8,7 @@
public class TestJdsl {
- @Test
+ //@Test
void testJdslSample() throws Exception {
final String input;
final var resource =ClassLoader.getSystemResource("pom.java");
@@ -19,8 +19,50 @@ void testJdslSample() throws Exception {
.results("\n" +
"\n" +
" 4.0.0\n" +
- " myGroup\n" +
- " myArtifact\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " myGroup\n" +
+ " myArtifact\n" +
+ " 1.0.0\n" +
+ " \n" +
+ " \n" +
+ " org.junit.jupiter\n" +
+ " junit-jupiter-api\n" +
+ " 5.9.0\n" +
+ " test\n" +
+ " \n" +
+ " \n" +
+ " grp\n" +
+ " arti\n" +
+ " \n" +
+ " \n" +
+ " grp\n" +
+ " arti\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " org.junit.jupiter\n" +
+ " junit-jupiter-api\n" +
+ " 5.9.1\n" +
+ " \n" +
+ " \n" +
+ " org.junit.jupiter\n" +
+ " junit-jupiter-api\n" +
+ " 5.7.0\n" +
+ " jar\n" +
+ " test\n" +
+ " \n" +
+ " \n" +
+ " org.junit.jupiter\n" +
+ " junit-jupiter-api\n" +
+ " test\n" +
+ " 5.7.0\n" +
+ " jar\n" +
+ " \n" +
+ " \n" +
+ " \n" +
" \n" +
" myGroup\n" +
" myParent\n" +
@@ -85,6 +127,21 @@ void testJdslSample() throws Exception {
" tag-type\n" +
" \n" +
" \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " pom.xml\n" +
+ " true\n" +
+ " abraka dabra\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " filter1\n" +
+ " filter2\n" +
+ " \n" +
+ " clean\n" +
+ " alma-ata\n" +
+ " \n" +
" \n" +
" \n" +
" javax0\n" +
@@ -112,14 +169,6 @@ void testJdslSample() throws Exception {
" \n" +
" value\n" +
" \n" +
- " \n" +
- " \n" +
- " filter1\n" +
- " filter2\n" +
- " \n" +
- " clean\n" +
- " alma-ata\n" +
- " \n" +
"\n");
}
}
diff --git a/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestXml.java b/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestXml.java
new file mode 100644
index 000000000..1d087009a
--- /dev/null
+++ b/jamal-java/src/test/java/javax0/jamal/java/testmacros/TestXml.java
@@ -0,0 +1,25 @@
+package javax0.jamal.java.testmacros;
+
+import javax0.jamal.java.Xml;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static javax0.jamal.java.Xml.path;
+import static javax0.jamal.java.Xml.tagValue;
+
+public class TestXml {
+
+ @Test
+ void testTagCreation(){
+ final Xml xml = Xml.tagValue("project", null);
+ Assertions.assertEquals("",xml.toString());
+ xml.add(path("project","dependencies"),null);
+ Assertions.assertEquals("",xml.toString());
+ //xml.add(path("project","dependencies", "dependency"),"depi");
+ xml.add(path("project","dependencies"),tagValue("dependency", "depi"));
+ Assertions.assertEquals("depi",xml.toString());
+ xml.add(path("project","dependencies"),tagValue("dependency", "pumba"));
+ Assertions.assertEquals("depipumba",xml.toString());
+ }
+
+}
diff --git a/jamal-java/src/test/resources/pom.java b/jamal-java/src/test/resources/pom.java
index d4f8c5aa1..05bafb0a1 100644
--- a/jamal-java/src/test/resources/pom.java
+++ b/jamal-java/src/test/resources/pom.java
@@ -1,6 +1,5 @@
//{@import res:pom.jim}{@java:dsl}
-
-coordinates("myGroup:myArtifact:1.0.0");
+dependencyManagement().dependency("myGroup:myArtifact:1.0.0");
parent("myGroup:myParent:1.0.0").relativePath("../myParent");
modules(dirsWith("pom.java"));
packaging("pom");
@@ -12,6 +11,13 @@
dependency().groupId("org.junit.jupiter").artifactId("junit-jupiter-api").version("5.7.0").JAR().TEST(),
dependency("org.junit.jupiter:junit-jupiter-api::test").version("5.7.0").JAR()
);
+dependencyManagement(
+ dependency("org.junit.jupiter:junit-jupiter-api:5.9.0:test")
+ .exclusions("grp:arti", exclusion("grp:arti")),
+ "org.junit.jupiter:junit-jupiter-api:5.9.1",
+dependency().groupId("org.junit.jupiter").artifactId("junit-jupiter-api").version("5.7.0").JAR().TEST(),
+dependency("org.junit.jupiter:junit-jupiter-api::test").version("5.7.0").JAR());
+resource(targetPath("pom.xml"), filtering(true),directory("abraka dabra"));
dependencies("gid:aid:ver:scope:classifier:type");
organization("javax0").url(new URL("http://javax0.com"));
to(path("dependencies"), under("dependency"),
@@ -33,3 +39,5 @@
filters("filter1", filter("filter2"));
build(defaultGoal("clean"), finalName("alma-ata"));
+
+build().extensions(Extension.extension().groupId("extension group").artifactId("extension artifact").version("extension version"));
\ No newline at end of file
diff --git a/jamal-snippet/README.adoc b/jamal-snippet/README.adoc
index 0ce961e36..db69cf37d 100644
--- a/jamal-snippet/README.adoc
+++ b/jamal-snippet/README.adoc
@@ -133,6 +133,7 @@ This way, it is easier to keep your documentation up to date.
.. <>
.. <>
.. <>
+.. <>
. <
@@ -1700,7 +1701,7 @@ will result
[source]
----
-../jamal-snippet/README.adoc.jam:968:30
+../jamal-snippet/README.adoc.jam:969:30
----
@@ -3153,11 +3154,13 @@ will result
- NumberLines.java
- RangeMacro.java
- Untab.java
+- Plural.java
- DateMacro.java
- Format.java
- tools
- ModifiersBuilder.java
- ReflectionTools.java
+- Pluralizer.java
- MethodTool.java
- Update.java
- Eval.java
@@ -3289,22 +3292,24 @@ will result
[source]
----
- jamal: 96 bytes
-- snippet: 2,432 bytes
+- snippet: 2,464 bytes
- JavaMatcherBuilderMacros.java: 19,757 bytes
- Snip.java: 4,597 bytes
- ReplaceUtil.java: 1,033 bytes
- SnipSave.java: 4,691 bytes
- Sort.java: 8,573 bytes
-- Decorate.java: 12,259 bytes
+- Decorate.java: 12,250 bytes
- Case.java: 2,269 bytes
- NumberLines.java: 2,853 bytes
- RangeMacro.java: 1,293 bytes
- Untab.java: 2,389 bytes
+- Plural.java: 2,277 bytes
- DateMacro.java: 578 bytes
- Format.java: 930 bytes
-- tools: 160 bytes
+- tools: 192 bytes
- ModifiersBuilder.java: 1,740 bytes
- ReflectionTools.java: 26,140 bytes
+- Pluralizer.java: 1,561 bytes
- MethodTool.java: 7,761 bytes
- Update.java: 979 bytes
- Eval.java: 2,009 bytes
@@ -3317,7 +3322,7 @@ will result
- Repeat.java: 1,213 bytes
- HashCode.java: 462 bytes
- TrimLines.java: 5,386 bytes
-- JavaSourceInsert.java: 7,988 bytes
+- JavaSourceInsert.java: 8,984 bytes
- ThinXml.java: 8,792 bytes
- Decorator.java: 9,094 bytes
- Memoize.java: 8,116 bytes
@@ -3619,7 +3624,7 @@ will result in the output
[source]
----
-2023-12-27 19:49:52
+2024-01-26 12:29:49
----
@@ -4569,12 +4574,12 @@ This is also the case when there is no parameter defined.
The macro `java:insert` can be used to insert the content of the macro into a Java source file into a segment.
The macro has three parameters:
-* `file` is the name of the file where the segment is to be inserted.
- It is also aliased as `to` and `into`.
+* `file` Is the name of the file where the segment is to be inserted.
+It is also aliased as `to` and `into`.
* `segment` is the name of the segment where the content is to be inserted.
- It is also aliased as `id` and `at`.
- This parameter is optional and is not needed if there is only one segment in the file.
+It is also aliased as `id` and `at`.
+This parameter is optional and is not needed if there is only one segment in the file.
* `check`, `checkUpdate`, `update`, `updateOnly` will do an extra step before updating the file.
It will do a lexical analysis on the original file and on the new version and compare the two.
@@ -4595,6 +4600,9 @@ This option may be useful in a build process when the code generation runs after
If the code was updated, then the compilation should fail.
The code, however, was already updated and the compilation started again will be successful.
+* `wholeFile` is a boolean option.
+If it is present then no segment should be, and the whole file will be updated, all the lines.
+In this case it is not an error if the file does not exist yet.
The parameters are not enclosed between any characters.
They follow the macro name directly until the end of the line.
@@ -5277,8 +5285,119 @@ The reason is that the code puts the inherited strings in front of the strings d
+
+[[plural]]
+=== LIV. `plural`
+
+
+
+This macro helps to get the plural form of an English word.
+
+Having the plural form of a word is usually not a problem.
+In most cases, you just type the plural form.
+You may, however, need to get the plural form of a word dynamically.
+
+For example, you want to create a macro to generate a sentence like this:
+
+[source]
+----
+{@define search(whatnot)=The actual whatnot is searched.
+If there are multiple whatnots, the first one is used.}
+----
+
+}
+
+You start using your macro and call it as
+
+[source]
+----
+{search file}
+----
+
+and you get
+
+[source]
+----
+The actual file is searched.
+If there are multiple files, the first one is used.
+----
+.
+
+This is great so far.
+The next place you also want to use it as
+
+[source]
+----
+{search directory}
+----
+
+and you get
+
+[source]
+----
+The actual directory is searched.
+If there are multiple directorys, the first one is used.
+----
+.
+
+This is not what you want.
+You want "directories" instead of "directorys".
+To solve this issue the macro `plural` comes to rescue.
+Define the macro as
+
+[source]
+----
+{@define search(whatnot)=The actual whatnot is searched.
+If there are multiple {@plural whatnot}, the first one is used.}
+----
+
+}
+
+Now
+
+[source]
+----
+{search directory}
+----
+
+will result in
+
+[source]
+----
+The actual directory is searched.
+If there are multiple directories, the first one is used.
+----
+.
+
+The macro can be used to get the plural form of a word and also to define the plural format of a word.
+The latter is useful when the plural form of a word is not regular.
+In this case, for example, you can
+
+[source]
+----
+{@plural child=children}{@plural child}
+----
+
+will correctly result in
+
+[source]
+----
+children
+----
+.
+To get a short list of the most frequently used irregular pluras words, use
+
+[source]
+----
+{@import res:plurals.jim}
+----
+
+The resource file is part of the snippet package.
+
+
+
[[dictionary]]
-=== LIV. `dictionary`
+=== LV. `dictionary`
This macro can be used to define a dictionary.
Dictionaries are used by the decorator macro, but other macros can also use them.
@@ -5307,7 +5426,7 @@ This is also the default name of the dictionary used by the <>
.. <>
.. <>
+.. <>
. <
{%@counter:define hierarchical id=chapter format=%d.{2:$ROMAN} %}
@@ -2846,17 +2847,17 @@ This is also the case when there is no parameter defined.
[[java:insert]]
==== `java:insert`
-{%#snip:check file={%JAVA_DIR%}JavaSourceInsert.java hash=a71a25ca%}
+{%#snip:check file={%JAVA_DIR%}JavaSourceInsert.java hash=95fdeecd%}
The macro `java:insert` can be used to insert the content of the macro into a Java source file into a segment.
The macro has three parameters:
-* `file` is the name of the file where the segment is to be inserted.
- It is also aliased as `to` and `into`.
+* `file` Is the name of the file where the segment is to be inserted.
+It is also aliased as `to` and `into`.
* `segment` is the name of the segment where the content is to be inserted.
- It is also aliased as `id` and `at`.
- This parameter is optional and is not needed if there is only one segment in the file.
+It is also aliased as `id` and `at`.
+This parameter is optional and is not needed if there is only one segment in the file.
* `check`, `checkUpdate`, `update`, `updateOnly` will do an extra step before updating the file.
It will do a lexical analysis on the original file and on the new version and compare the two.
@@ -2877,6 +2878,9 @@ This option may be useful in a build process when the code generation runs after
If the code was updated, then the compilation should fail.
The code, however, was already updated and the compilation started again will be successful.
+* `wholeFile` is a boolean option.
+If it is present then no segment should be, and the whole file will be updated, all the lines.
+In this case it is not an error if the file does not exist yet.
The parameters are not enclosed between any characters.
They follow the macro name directly until the end of the line.
@@ -3143,6 +3147,8 @@ This will render the current, no operation functionality of the macro.
{%CH /variation/variation%}
+{%CH /plural/plural%}
+
[[dictionary]]
=== {%chapter%}`dictionary`
diff --git a/jamal-snippet/documentation/macros/plural.adoc.jam b/jamal-snippet/documentation/macros/plural.adoc.jam
new file mode 100644
index 000000000..19a3f3c71
--- /dev/null
+++ b/jamal-snippet/documentation/macros/plural.adoc.jam
@@ -0,0 +1,89 @@
+{%@import macrodoc.jim%}
+{%#snip:check file={%JAVA_DIR%}Plural.java hash=f74afe22%}
+This macro helps to get the plural form of an English word.
+
+Having the plural form of a word is usually not a problem.
+In most cases, you just type the plural form.
+You may, however, need to get the plural form of a word dynamically.
+
+For example, you want to create a macro to generate a sentence like this:
+
+{%sample/
+{@define search(whatnot)=The actual whatnot is searched.
+If there are multiple whatnots, the first one is used.}
+%}
+
+{%#comment {%output%}%}}
+
+You start using your macro and call it as
+
+{%sample/
+{search file}
+%}
+
+and you get
+
+{%output%}.
+
+This is great so far.
+The next place you also want to use it as
+
+{%sample/
+{search directory}
+%}
+
+and you get
+
+{%output%}.
+
+This is not what you want.
+You want "directories" instead of "directorys".
+To solve this issue the macro `plural` comes to rescue.
+Define the macro as
+
+{%sample/
+{@define search(whatnot)=The actual whatnot is searched.
+If there are multiple {@plural whatnot}, the first one is used.}
+%}
+
+{%#comment {%output%}%}}
+
+Now
+
+{%sample/
+{search directory}
+%}
+
+will result in
+
+{%output%}.
+
+The macro can be used to get the plural form of a word and also to define the plural format of a word.
+The latter is useful when the plural form of a word is not regular.
+In this case, for example, you can use it as
+
+{%sample/
+{@plural child=children}{@plural child}
+%}
+
+and it will correctly result in
+
+{%output%}
+To get a short list of the most frequently used irregular plural words, use
+
+{%sample/
+{@import res:plurals.jim}
+%}
+
+The resource file is part of the snippet package.
+
+The algorithm implemented applies special rules when the word ending is `y`.
+This applies to words where 'y' is preceded by a consonant, (not a vowel). In English, such words are pluralized by replacing 'y' with 'ies'.
+The method first checks if the word ends with 'y' and if the preceding character is not a vowel (a, e, i, o, u).
+If the original word ends with an uppercase 'Y', it replaces 'Y' with 'IES'. If it ends with a lowercase 'y', it replaces 'y' with 'ies'.
+
+When the word ending is 's', 'sh', 'ch', 'x', or 'z' they are pluralized by adding 'es'.
+The method checks for these endings in both lowercase and uppercase forms.
+It then adds 'ES', 'Es', 'eS', or 'es' to the word, depending on the original case of the ending.
+
+In other cases the method simply adds 's' or 'S' to the word.
\ No newline at end of file
diff --git a/jamal-snippet/src/main/java/javax0/jamal/snippet/Decorate.java b/jamal-snippet/src/main/java/javax0/jamal/snippet/Decorate.java
index b19a27fbf..d8dc7fa3a 100644
--- a/jamal-snippet/src/main/java/javax0/jamal/snippet/Decorate.java
+++ b/jamal-snippet/src/main/java/javax0/jamal/snippet/Decorate.java
@@ -54,7 +54,7 @@ public String evaluate(Input in, Processor processor) throws BadSyntax {
final var input = in.toString();
- if (input.length() == 0) {
+ if (input.isEmpty()) {
return "";
}
@@ -90,7 +90,7 @@ public String evaluate(Input in, Processor processor) throws BadSyntax {
private static final List> DEFAULT_DECORATORS = List.of(s -> s + " ", s ->s );
private List> getDecorators(Processor processor, List macros) {
- if (macros.size() == 0) {
+ if (macros.isEmpty()) {
return DEFAULT_DECORATORS;
}
final var decorators = macros.stream()
@@ -155,7 +155,7 @@ private static SplitRatios calculateRationes(String rs) throws BadSyntax {
final List charnums = new ArrayList<>();
for (int i = 0, j = 0; i < parts.length - 1; i++) {
final var s = parts[i];
- if (0 != s.length()) {
+ if (!s.isEmpty()) {
int n = toInt(rs, s, Integer::parseInt);
charnums.add(n);
BadSyntax.when(n < 0 || n > j + 1,
diff --git a/jamal-snippet/src/main/java/javax0/jamal/snippet/JavaSourceInsert.java b/jamal-snippet/src/main/java/javax0/jamal/snippet/JavaSourceInsert.java
index 52231ee6e..5c9ce1e7b 100644
--- a/jamal-snippet/src/main/java/javax0/jamal/snippet/JavaSourceInsert.java
+++ b/jamal-snippet/src/main/java/javax0/jamal/snippet/JavaSourceInsert.java
@@ -21,18 +21,20 @@
public class JavaSourceInsert implements Macro, Scanner.FirstLine {
@Override
public String evaluate(final Input in, final Processor processor) throws BadSyntax {
- final var scanner = newScanner(in,processor);
+ final var scanner = newScanner(in, processor);
final var file = scanner.str(null, "to", "file", "into");
final var segment = scanner.str("segment", "at", "id").optional();
final var update = scanner.bool(null, "check", "checkUpdate", "update", "updateOnly");
final var throwUp = scanner.bool(null, "failOnUpdate", "failUpdate", "updateError");
+ final var wholeFile = scanner.bool(null, "wholeFile");
scanner.done();
+ BadSyntax.when(wholeFile.is() && segment.isPresent(), "When the whole file is updated then the segment should not be specified.");
final JavaSourceInsertCloser closer;
if (in.isEmpty()) {
- closer = new JavaSourceInsertCloser(file.get(), segment.get(), in.getPosition(), update.is() || throwUp.is());
+ closer = new JavaSourceInsertCloser(file.get(), segment.get(), in.getPosition(), update.is() || throwUp.is(), wholeFile.is());
processor.deferredClose(closer);
} else {
- try (final var _c = new JavaSourceInsertCloser(file.get(), segment.get(), in.getPosition(), update.is() || throwUp.is())) {
+ try (final var _c = new JavaSourceInsertCloser(file.get(), segment.get(), in.getPosition(), update.is() || throwUp.is(), wholeFile.is())) {
closer = _c;
closer.set(in);
closer.set(processor);
@@ -61,11 +63,14 @@ private static class JavaSourceInsertCloser implements AutoCloseable, Closer.Out
private boolean updated = false;
- private JavaSourceInsertCloser(final String file, final String segment, final Position pos, boolean update) {
+ private final boolean wholeFile;
+
+ private JavaSourceInsertCloser(final String file, final String segment, final Position pos, boolean update, final boolean wholeFile) {
this.file = file;
this.segment = segment;
this.pos = pos;
this.update = update;
+ this.wholeFile = wholeFile;
}
private static final Pattern segmentStartPattern = Pattern.compile("^\\s*//\\s*<\\s*editor-fold(.*>)");
@@ -74,23 +79,28 @@ private JavaSourceInsertCloser(final String file, final String segment, final Po
@Override
public void close() throws BadSyntax {
final String fileName = FileTools.absolute(pos.file, file);
- final var originalContent = FileTools.getFileContent(fileName, processor);
- final var linesSrc = originalContent.split("\n", -1);
- final var linesOut = new ArrayList(linesSrc.length);
- final var linesGen = Arrays.asList(output.toString().split("\n", -1));
- boolean inSegment = false;
- boolean segmentAdded = false;
- for (final var line : linesSrc) {
- if (inSegment) {
- inSegment = !segmentEndPattern.matcher(line).matches();
- segmentAdded = copyOrSkipLineInSegment(linesOut, linesGen, inSegment, line);
- } else {
- inSegment = copyLineOutOfSegment(linesOut, segmentAdded, line);
+ final String newContent;
+ final var originalContent= getFileContent(fileName);
+ if (wholeFile) {
+ newContent = output.toString();
+ } else {
+ final var linesSrc = originalContent.split("\n", -1);
+ final var linesOut = new ArrayList(linesSrc.length);
+ final var linesGen = Arrays.asList(output.toString().split("\n", -1));
+ boolean inSegment = false;
+ boolean segmentAdded = false;
+ for (final var line : linesSrc) {
+ if (inSegment) {
+ inSegment = !segmentEndPattern.matcher(line).matches();
+ segmentAdded = copyOrSkipLineInSegment(linesOut, linesGen, inSegment, line);
+ } else {
+ inSegment = copyLineOutOfSegment(linesOut, segmentAdded, line);
+ }
}
+ BadSyntax.when(inSegment, "The segment " + segment + " was not closed in the file " + file + ".");
+ BadSyntax.when(!segmentAdded, "The segment " + segment + " was not found in the file " + file + ".");
+ newContent = String.join("\n", linesOut.toArray(String[]::new));
}
- BadSyntax.when(inSegment, "The segment " + segment + " was not closed in the file " + file + ".");
- BadSyntax.when(!segmentAdded, "The segment " + segment + " was not found in the file " + file + ".");
- final var newContent = String.join("\n", linesOut.toArray(String[]::new));
if (update) {
if (!new JavaSourceDiff().test(originalContent, newContent)) {
return;
@@ -100,6 +110,20 @@ public void close() throws BadSyntax {
updated = true;
}
+ /**
+ * Get the content of the file. If the file does not exist or is not readable, then the content is empty.
+ *
+ * @param fileName the name of the file
+ * @return the content of the file
+ */
+ private String getFileContent(String fileName) {
+ try {
+ return FileTools.getFileContent(fileName, processor);
+ } catch (BadSyntax e) {
+ return "";
+ }
+ }
+
/**
* Skip the line, or copy the line to the output when the segment ends.
* When the segment ends, the generated code lines are also added and the segment closing line also.
diff --git a/jamal-snippet/src/main/java/javax0/jamal/snippet/Plural.java b/jamal-snippet/src/main/java/javax0/jamal/snippet/Plural.java
new file mode 100644
index 000000000..8fc7e1050
--- /dev/null
+++ b/jamal-snippet/src/main/java/javax0/jamal/snippet/Plural.java
@@ -0,0 +1,71 @@
+package javax0.jamal.snippet;
+
+import javax0.jamal.api.BadSyntax;
+import javax0.jamal.api.Identified;
+import javax0.jamal.api.Input;
+import javax0.jamal.api.Macro;
+import javax0.jamal.api.Processor;
+import javax0.jamal.snippet.tools.Pluralizer;
+import javax0.jamal.tools.Scanner;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Return the plural form of a word.
+ */
+public class Plural implements Macro, Scanner {
+
+ public static final String DICTIONARY_NAME = "$:plurals";
+
+ @Override
+ public String evaluate(Input in, Processor processor) throws BadSyntax {
+
+ final var dict = processor
+ .getRegister()
+ .getUserDefined(DICTIONARY_NAME)
+ .filter(m -> m instanceof Plural.Dictionary)
+ .map(m -> (Plural.Dictionary) m)
+ .orElseGet(
+ () -> {
+ final var dictionary = new Plural.Dictionary(DICTIONARY_NAME);
+ processor.getRegister().define(dictionary);
+ return dictionary;
+ }
+ );
+ if (in.toString().contains("=")) {
+ final var keyValue = in.toString().split("=");
+ BadSyntax.when(keyValue.length != 2, "The key value pair should be separated by '='");
+ keyValue[0] = keyValue[0].trim();
+ keyValue[1] = keyValue[1].trim();
+ BadSyntax.when(keyValue[0].isEmpty(), "The word should not be empty");
+ BadSyntax.when(keyValue[1].isEmpty(), "The plural should not be empty");
+ dict.dictionary.put(keyValue[0], keyValue[1]);
+ return "";
+ }
+ return dict.plural(in.toString().trim());
+ }
+
+ static class Dictionary implements Identified {
+ private final String name;
+ final Map dictionary;
+
+ String plural(String key) {
+ if (dictionary.containsKey(key)) {
+ return dictionary.get(key);
+ }
+ return Pluralizer.pluralize(key);
+ }
+
+
+ Dictionary(String name) {
+ this.name = name;
+ this.dictionary = new HashMap<>();
+ }
+
+ @Override
+ public String getId() {
+ return name;
+ }
+ }
+}
diff --git a/jamal-snippet/src/main/java/javax0/jamal/snippet/tools/Pluralizer.java b/jamal-snippet/src/main/java/javax0/jamal/snippet/tools/Pluralizer.java
new file mode 100644
index 000000000..82bbbef99
--- /dev/null
+++ b/jamal-snippet/src/main/java/javax0/jamal/snippet/tools/Pluralizer.java
@@ -0,0 +1,44 @@
+package javax0.jamal.snippet.tools;
+
+public class Pluralizer {
+
+ public static String pluralize(String word) {
+ if (word == null || word.isEmpty()) {
+ return word;
+ }
+
+ String lowerCaseWord = word.toLowerCase();
+ int length = word.length();
+
+ // Ending with 'y' (but not 'ay', 'ey', 'iy', 'oy', 'uy')
+ if (lowerCaseWord.endsWith("y") && length > 1 && !"aeiou".contains(Character.toString(lowerCaseWord.charAt(length - 2)))) {
+ if (word.endsWith("y")) {
+ return word.substring(0, length - 1) + "ies";
+ } else {
+ return word.substring(0, length - 1) + "IES";
+ }
+ }
+
+ // Ending with 's', 'sh', 'ch', 'x' or 'z'
+ if (lowerCaseWord.endsWith("s") || lowerCaseWord.endsWith("sh") || lowerCaseWord.endsWith("ch") || lowerCaseWord.endsWith("x") || lowerCaseWord.endsWith("z")) {
+ if (word.endsWith("S") || word.endsWith("SH") || word.endsWith("CH") || word.endsWith("X") || word.endsWith("Z")) {
+ return word + "ES";
+ }
+ if (word.endsWith("sH") || word.endsWith("cH")) {
+ return word + "eS";
+ }
+ if (word.endsWith("Sh") || word.endsWith("Ch")) {
+ return word + "Es";
+ }
+ return word + "es";
+ }
+
+ // Regular plural (just add 's')
+ if (Character.isUpperCase(word.charAt(word.length()-1))) {
+ return word + "S";
+ } else {
+ return word + "s";
+ }
+ }
+
+}
diff --git a/jamal-snippet/src/main/java/module-info.java b/jamal-snippet/src/main/java/module-info.java
index 3a5bd5b9b..249ae183e 100644
--- a/jamal-snippet/src/main/java/module-info.java
+++ b/jamal-snippet/src/main/java/module-info.java
@@ -116,6 +116,7 @@
Repeat,
Eval,
Locate,
- Variation
+ Variation,
+ Plural
;
}
\ No newline at end of file
diff --git a/jamal-snippet/src/main/resources/META-INF/services/javax0.jamal.api.Macro b/jamal-snippet/src/main/resources/META-INF/services/javax0.jamal.api.Macro
index ef93cd06c..2436b09a6 100644
--- a/jamal-snippet/src/main/resources/META-INF/services/javax0.jamal.api.Macro
+++ b/jamal-snippet/src/main/resources/META-INF/services/javax0.jamal.api.Macro
@@ -91,3 +91,4 @@ javax0.jamal.snippet.Repeat
javax0.jamal.snippet.Eval
javax0.jamal.snippet.Locate
javax0.jamal.snippet.Variation
+javax0.jamal.snippet.Plural
diff --git a/jamal-snippet/src/main/resources/plurals.jim b/jamal-snippet/src/main/resources/plurals.jim
new file mode 100644
index 000000000..609f5d1af
--- /dev/null
+++ b/jamal-snippet/src/main/resources/plurals.jim
@@ -0,0 +1,23 @@
+{@plural man=men}
+{@plural woman=women}
+{@plural child=children}
+{@plural tooth=teeth}
+{@plural foot=feet}
+{@plural mouse=mice}
+{@plural goose=geese}
+{@plural louse=lice}
+{@plural ox=oxen}
+{@plural die=dice}
+{@plural person=people}
+{@plural cactus=cacti}
+{@plural fungus=fungi}
+{@plural nucleus=nuclei}
+{@plural syllabus=syllabi}
+{@plural analysis=analyses}
+{@plural diagnosis=diagnoses}
+{@plural thesis=theses}
+{@plural crisis=crises}
+{@plural phenomenon=phenomena}
+{@plural criterion=criteria}
+{@plural datum=data}
+{@plural bacterium=bacteria}
diff --git a/jamal-snippet/src/test/java/javax0/jamal/snippet/TestDecorate.java b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestDecorate.java
index b9b6578be..a5df778fe 100644
--- a/jamal-snippet/src/test/java/javax0/jamal/snippet/TestDecorate.java
+++ b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestDecorate.java
@@ -111,7 +111,7 @@ void returnsEmpty() throws Exception {
@Test
@DisplayName("using dictionary")
- void usingDictonary() throws Exception {
+ void usingDictionary() throws Exception {
TestThat.theInput("{@dictionary\n" +
"k*ivetel\n" +
"}" +
@@ -120,7 +120,7 @@ void usingDictonary() throws Exception {
@Test
@DisplayName("using dictionary named when there is also 'unnamed' dictionary")
- void usingDictonaryNamed() throws Exception {
+ void usingDictionaryNamed() throws Exception {
TestThat.theInput("{@dictionary\n" +
"k*ivetel\n" +
"}" +
diff --git a/jamal-snippet/src/test/java/javax0/jamal/snippet/TestJavaSourceInsert.java b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestJavaSourceInsert.java
index 0e9fd1c4d..f762616f4 100644
--- a/jamal-snippet/src/test/java/javax0/jamal/snippet/TestJavaSourceInsert.java
+++ b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestJavaSourceInsert.java
@@ -1,6 +1,5 @@
package javax0.jamal.snippet;
-import javax0.jamal.api.BadSyntax;
import javax0.jamal.testsupport.TestThat;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
@@ -14,20 +13,19 @@
public class TestJavaSourceInsert {
-
@Test
@DisplayName("Test that the snippet is inserted into the file")
- void testSimpleInsert() throws BadSyntax, Exception {
+ void testSimpleInsert() throws Exception {
String testFile = "target/Test.java";
Path path = Paths.get(testFile);
Files.write(path, ("This is some prelude text, not touched\n" +
"//\n" +
"This text will be deleted and replaced with the generated code\n" +
- "//\n" +
- "").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ "//\n"
+ ).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
TestThat.theInput("This is the text that will get inserted\n" +
- "{@java:insert to=\"" + testFile + "\" id=\"this is the id\"}\n" +
- "").results("This is the text that will get inserted\n\n");
+ "{@java:insert to=\"" + testFile + "\" id=\"this is the id\"}\n"
+ ).results("This is the text that will get inserted\n\n");
final var result = Files.readString(path);
Assertions.assertEquals("This is some prelude text, not touched\n" +
"//\n" +
@@ -45,12 +43,11 @@ void testSimpleUpdateOnly() throws Exception {
Files.write(path, ("This is some prelude text, not touched\n" +
"//\n" +
"This is the text that will get inserted\n" +
- "//\n" +
- "").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ "//\n").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// spaces will not be considered when update only
TestThat.theInput("This is the text that will get inserted\n" +
- "{@java:insert update to=\"" + testFile + "\" id=\"this is the id\"}\n" +
- "").results("This is the text that will get inserted\n" +
+ "{@java:insert update to=\"" + testFile + "\" id=\"this is the id\"}\n"
+ ).results("This is the text that will get inserted\n" +
"\n");
final var result = Files.readString(path);
Assertions.assertEquals("This is some prelude text, not touched\n" +
@@ -67,12 +64,11 @@ void testSimpleUpdateOnlyNoError() throws Exception {
Files.write(path, ("This is some prelude text, not touched\n" +
"//\n" +
"This is the text that will get inserted\n" +
- "//\n" +
- "").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ "//\n").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// spaces will not be considered when update only
TestThat.theInput("This is the text that will get inserted\n" +
- "{@java:insert failOnUpdate to=\"" + testFile + "\" id=\"this is the id\"}\n" +
- "").results("This is the text that will get inserted\n" +
+ "{@java:insert failOnUpdate to=\"" + testFile + "\" id=\"this is the id\"}\n"
+ ).results("This is the text that will get inserted\n" +
"\n");
final var result = Files.readString(path);
Assertions.assertEquals("This is some prelude text, not touched\n" +
@@ -80,6 +76,7 @@ void testSimpleUpdateOnlyNoError() throws Exception {
"This is the text that will get inserted\n" +
"//\n", result);
}
+
@Test
@DisplayName("Test that the snippet is inserted into the file being different from the one already there, and thee is an error")
void testSimpleUpdateOnlyWithError() throws Exception {
@@ -88,12 +85,10 @@ void testSimpleUpdateOnlyWithError() throws Exception {
Files.write(path, ("This is some prelude text, not touched\n" +
"//\n" +
"Something that is totally different from what gets inserted\n" +
- "//\n" +
- "").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ "//\n").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// this time the difference is not only spaces
TestThat.theInput("This is the text that will get inserted" +
- "{@java:insert failOnUpdate to=\"" + testFile + "\" id=\"this is the id\"}\\\n" +
- "").throwsBadSyntax("The file target/Test.java was updated.");
+ "{@java:insert failOnUpdate to=\"" + testFile + "\" id=\"this is the id\"}\\\n").throwsBadSyntax("The file target/Test.java was updated.");
final var result = Files.readString(path);
Assertions.assertEquals("This is some prelude text, not touched\n" +
"//\n" +
@@ -113,15 +108,12 @@ void testMultipleUpdateOnlyWithError() throws Exception {
"//\n" +
"Something total garbáž\n" +
"//\n" +
- "Some postlude text also to be on the safe side." +
- "").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ "Some postlude text also to be on the safe side.").getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// this time the difference is not only spaces
- TestThat.theInput("" +
- "{@java:insert failOnUpdate to=\"" + testFile + "\" id=ID1\n" +
+ TestThat.theInput("{@java:insert failOnUpdate to=\"" + testFile + "\" id=ID1\n" +
"This is the text that will get inserted into the first segment}\\\n" +
"{@java:insert failOnUpdate to=\"" + testFile + "\" id=ID2\n" +
- "This is the text that will get inserted into the second segment}\\\n" +
- "").throwsBadSyntax("The file target/Test.java was updated.");
+ "This is the text that will get inserted into the second segment}\\\n").throwsBadSyntax("The file target/Test.java was updated.");
final var result = Files.readString(path);
Assertions.assertEquals("This is some prelude text, not touched\n" +
"//\n" +
@@ -133,4 +125,65 @@ void testMultipleUpdateOnlyWithError() throws Exception {
"Some postlude text also to be on the safe side.", result);
}
+ @Test
+ @DisplayName("Test that segment and wholeFile cannot be used together")
+ void testWholeFileAndSegmentError() throws Exception {
+ TestThat.theInput("This is the text that will get inserted\n" +
+ "{@java:insert to=\"anyFileDummy.txt\" id=\"this is the id\" wholeFile}\n").throwsBadSyntax("When the whole file is updated then the segment should not be specified.");
+ }
+
+ @Test
+ @DisplayName("Test that a whole file is updated when the 'wholeFile' parop is set")
+ void testWholeFileUpdate() throws Exception {
+ String testFile = "target/Test.java";
+ Path path = Paths.get(testFile);
+ Files.write(path, ("This is some text totally overwritten\n")
+ .getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ TestThat.theInput("This is the text that will get inserted\n" +
+ "{@java:insert to=\"" + testFile + "\" wholeFile}").results("This is the text that will get inserted\n");
+ final var result = Files.readString(path);
+ Assertions.assertEquals("This is the text that will get inserted\n", result);
+ }
+
+ @Test
+ @DisplayName("Test that a whole file is updated when the 'wholeFile' parop is set content is inline")
+ void testWholeFileUpdateInline() throws Exception {
+ String testFile = "target/Test.java";
+ Path path = Paths.get(testFile);
+ Files.write(path, ("This is some text totally overwritten\n")
+ .getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ TestThat.theInput(
+ "{@java:insert to=\"" + testFile + "\" wholeFile\n" +
+ "This is the text that will get inserted\n" +
+ "}").results("This is the text that will get inserted\n");
+ final var result = Files.readString(path);
+ Assertions.assertEquals("This is the text that will get inserted\n", result);
+ }
+
+ @Test
+ @DisplayName("Test that a whole file is created when the 'wholeFile' parop is set")
+ void testWholeFileCreate() throws Exception {
+ String testFile = "target/Test.java";
+ Path path = Paths.get(testFile);
+ Files.delete(path);
+ TestThat.theInput("This is the text that will get inserted\n" +
+ "{@java:insert to=\"" + testFile + "\" wholeFile}").results("This is the text that will get inserted\n");
+ final var result = Files.readString(path);
+ Assertions.assertEquals("This is the text that will get inserted\n", result);
+ }
+
+ @Test
+ @DisplayName("Test that a whole file is created when the 'wholeFile' parop is set content is inline")
+ void testWholeFileCreateInline() throws Exception {
+ String testFile = "target/Test.java";
+ Path path = Paths.get(testFile);
+ Files.delete(path);
+ TestThat.theInput(
+ "{@java:insert to=\"" + testFile + "\" wholeFile\n" +
+ "This is the text that will get inserted\n" +
+ "}").results("This is the text that will get inserted\n");
+ final var result = Files.readString(path);
+ Assertions.assertEquals("This is the text that will get inserted\n", result);
+ }
+
}
diff --git a/jamal-snippet/src/test/java/javax0/jamal/snippet/TestPlural.java b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestPlural.java
new file mode 100644
index 000000000..9cbab9588
--- /dev/null
+++ b/jamal-snippet/src/test/java/javax0/jamal/snippet/TestPlural.java
@@ -0,0 +1,72 @@
+package javax0.jamal.snippet;
+
+import javax0.jamal.testsupport.TestThat;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class TestPlural {
+
+ @ParameterizedTest
+ @CsvSource({
+ "bus, buses",
+ "church, churches",
+ "fox, foxes",
+ "buzz, buzzes",
+ "dish, dishes",
+ "cat, cats",
+ "dog, dogs",
+ "baby, babies",
+ "lady, ladies",
+ "day, days",
+ "key, keys",
+ "nastaliqiy, nastaliqiys",
+ "boy, boys",
+ "guy, guys",
+ "Bus, Buses",
+ "Church, Churches",
+ "Fox, Foxes",
+ "Buzz, Buzzes",
+ "Dish, Dishes",
+ "Cat, Cats",
+ "Dog, Dogs",
+ "Baby, Babies",
+ "Lady, Ladies",
+ "Day, Days",
+ "Key, Keys",
+ "Nastaliqiy, Nastaliqiys",
+ "Boy, Boys",
+ "Guy, Guys",
+ "buS, buSES",
+ "churCH, churCHES",
+ "churCh, churChEs",
+ "churcH, churcHeS",
+ "foX, foXES",
+ "buzZ, buzZES",
+ "diSH, diSHES",
+ "diSh, diShEs",
+ "disH, disHeS",
+ "caT, caTS",
+ "doG, doGS",
+ "babY, babIES",
+ "ladY, ladIES",
+ "daY, daYS",
+ "keY, keYS",
+ "nastaliqiY, nastaliqiYS",
+ "nastaliqIy, nastaliqIys",
+ "boY, boYS",
+ "guY, guYS"
+ })
+ void testCalLingPluralizer(String word, String plural) throws Exception {
+ TestThat.theInput("{@plural " + word + "}").results(plural);
+ }
+
+ @Test
+ void testDictionary() throws Exception {
+ TestThat.theInput("{@plural child=children}{@plural child}").results("children");
+ }
+ @Test
+ void testDictionaryRetrieved() throws Exception {
+ TestThat.theInput("{@plural mikka} {@plural makka}").results("mikkas makkas");
+ }
+}
diff --git a/jamal-snippet/src/test/java/javax0/jamal/snippet/tools/TestPluralizer.java b/jamal-snippet/src/test/java/javax0/jamal/snippet/tools/TestPluralizer.java
new file mode 100644
index 000000000..a2115f8b2
--- /dev/null
+++ b/jamal-snippet/src/test/java/javax0/jamal/snippet/tools/TestPluralizer.java
@@ -0,0 +1,76 @@
+package javax0.jamal.snippet.tools;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static javax0.jamal.snippet.tools.Pluralizer.pluralize;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class TestPluralizer {
+
+ @ParameterizedTest
+ @CsvSource({
+ "bus, buses",
+ "church, churches",
+ "fox, foxes",
+ "buzz, buzzes",
+ "dish, dishes",
+ "cat, cats",
+ "dog, dogs",
+ "baby, babies",
+ "lady, ladies",
+ "day, days",
+ "key, keys",
+ "nastaliqiy, nastaliqiys",
+ "boy, boys",
+ "guy, guys",
+ "Bus, Buses",
+ "Church, Churches",
+ "Fox, Foxes",
+ "Buzz, Buzzes",
+ "Dish, Dishes",
+ "Cat, Cats",
+ "Dog, Dogs",
+ "Baby, Babies",
+ "Lady, Ladies",
+ "Day, Days",
+ "Key, Keys",
+ "Nastaliqiy, Nastaliqiys",
+ "Boy, Boys",
+ "Guy, Guys",
+ "buS, buSES",
+ "churCH, churCHES",
+ "churCh, churChEs",
+ "churcH, churcHeS",
+ "foX, foXES",
+ "buzZ, buzZES",
+ "diSH, diSHES",
+ "diSh, diShEs",
+ "disH, disHeS",
+ "caT, caTS",
+ "doG, doGS",
+ "babY, babIES",
+ "ladY, ladIES",
+ "daY, daYS",
+ "keY, keYS",
+ "nastaliqiY, nastaliqiYS",
+ "nastaliqIy, nastaliqIys",
+ "boY, boYS",
+ "guY, guYS"
+ })
+ void test(String word, String plural) {
+ assertEquals(plural, pluralize(word));
+ }
+
+ @Test
+ void testEmptyString() {
+ assertEquals("", pluralize(""));
+ }
+
+ @Test
+ public void testNullInput() {
+ assertNull(pluralize(null));
+ }
+}
diff --git a/jamal-word/src/test/resources/demoConverted.docx b/jamal-word/src/test/resources/demoConverted.docx
index fa137ab4e..cb24e9fb5 100644
Binary files a/jamal-word/src/test/resources/demoConverted.docx and b/jamal-word/src/test/resources/demoConverted.docx differ
diff --git a/jamal-word/src/test/resources/includetestConverted.docx b/jamal-word/src/test/resources/includetestConverted.docx
index 087a5c0f7..5ca148fd3 100644
Binary files a/jamal-word/src/test/resources/includetestConverted.docx and b/jamal-word/src/test/resources/includetestConverted.docx differ
diff --git a/jamal-word/src/test/resources/pictureConverted.docx b/jamal-word/src/test/resources/pictureConverted.docx
index d0671bc0a..56c2042aa 100644
Binary files a/jamal-word/src/test/resources/pictureConverted.docx and b/jamal-word/src/test/resources/pictureConverted.docx differ
diff --git a/jamal-word/src/test/resources/sampleConverted.docx b/jamal-word/src/test/resources/sampleConverted.docx
index ded0ad805..8365d9f29 100644
Binary files a/jamal-word/src/test/resources/sampleConverted.docx and b/jamal-word/src/test/resources/sampleConverted.docx differ