diff --git a/pom.xml b/pom.xml index 159aead..ed3caec 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 net.ftlines wicket-css-scope - 2.1.0-SNAPSHOT + 2.1.1-SNAPSHOT maven-plugin 42 Lines - Wicket Css Scoping diff --git a/src/main/java/net/ftlines/css/scoper/maven/CompileIconSelection.java b/src/main/java/net/ftlines/css/scoper/maven/CompileIconSelection.java new file mode 100644 index 0000000..2d42b1e --- /dev/null +++ b/src/main/java/net/ftlines/css/scoper/maven/CompileIconSelection.java @@ -0,0 +1,166 @@ +package net.ftlines.css.scoper.maven; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +@Mojo(name = "compile-icons", defaultPhase = LifecyclePhase.GENERATE_RESOURCES) +public class CompileIconSelection extends AbstractMojo { + + @Parameter(required = true) + public File outputMixinsFile; + + @Parameter(required = true) + public File outputIconsFile; + + @Parameter(required = true) + public File inputFile; + + @Parameter(required = true) + public File inputMixinsTemplateFilePath; + + @Parameter(required = true) + public File inputIconsTemplateFilePath; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + try(FileWriter writer = new FileWriter(outputMixinsFile)) { + IOUtils.write(renderMixinsTemplate(), writer); + writer.flush(); + } catch (Exception e) { + throw new MojoExecutionException("Error compiling icons", e); + } + + try(FileWriter writer = new FileWriter(outputIconsFile)) { + IOUtils.write(renderIconsTemplate(), writer); + writer.flush(); + } catch (Exception e) { + throw new MojoExecutionException("Error compiling icons", e); + } + } + + private List> getIcons() throws Exception { + return getIcons(inputFile); + } + + protected static List> getIcons(File inputFile) throws Exception { + try (FileReader reader = new FileReader(inputFile)) { + JsonElement elem = new Gson().fromJson(reader, JsonElement.class); + JsonArray iconArray = elem.getAsJsonObject().get("icons").getAsJsonArray(); + List> list = new ArrayList>(); + + Consumer sink = (z) -> { + convertIcon(z, list::add); + }; + + iconArray.asList().stream().forEach(sink); + return list; + } + } + + private static void convertIcon(JsonElement elem, Consumer> sink) { + String name = elem.getAsJsonObject().get("properties").getAsJsonObject().get("name").getAsString(); + int code = elem.getAsJsonObject().get("properties").getAsJsonObject().get("code").getAsInt(); + for(String n: name.split(",")) { + sink.accept(Pair.of(n.strip(), code)); + } + } + + private static String renderSassMixins(List> list) { + StringBuffer buffer = new StringBuffer(); + + list.forEach(icon -> { + buffer.append("@mixin icon-" + icon.getLeft() + "-content {content:\"\\" + Integer.toHexString(icon.getRight()) + "\";}\n"); + }); + + return buffer.toString(); + } + + private static String renderSassIcons(List> list) { + StringBuffer buffer = new StringBuffer(); + + list.forEach(icon -> { + buffer.append(".icon-" + icon.getLeft() + ":before{@include icon-"+icon.getLeft()+"-content;}\n\n"); + }); + + return buffer.toString(); + } + + private String renderMixinsTemplate() throws Exception { + try (FileReader reader = new FileReader(inputMixinsTemplateFilePath)) { + return IOUtils.toString(reader).replace("{{$HASH$}}", md5ToNumeric(getHash(inputFile))).replace("{{$ICONS$}}", renderSassMixins(getIcons())); + } + } + + private String renderIconsTemplate() throws Exception { + try (FileReader reader = new FileReader(inputIconsTemplateFilePath)) { + return IOUtils.toString(reader).replace("{{$HASH$}}", md5ToNumeric(getHash(inputFile))).replace("{{$ICONS$}}", renderSassIcons(getIcons())); + } + } + + private static String md5ToNumeric(String md5Hash) { + return new BigInteger(md5Hash, 16).toString(); + } + + private static String getHash(File file) throws Exception { + MessageDigest digest = MessageDigest.getInstance("MD5"); + FileInputStream fis = new FileInputStream(file); + byte[] byteArray = new byte[1024]; + int bytesCount = 0; + + // Read the file data and update it to the message digest + while ((bytesCount = fis.read(byteArray)) != -1) { + digest.update(byteArray, 0, bytesCount); + }; + + fis.close(); + + // Get the hash's bytes + byte[] bytes = digest.digest(); + + // Convert it to hexadecimal format + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + + // Return the complete hash + return sb.toString(); + } + +// private void replaceAllIconsInFile(File file) throws Exception { +// String filePath = file.getAbsolutePath(); +// try(Stream lines = Files.lines(Paths.get(filePath))){ +// String content = String.join("\n", (Iterable)lines::iterator); +// for(Pair ico: getIcons()) { +// String from = "content:\"\\" + Integer.toHexString(ico.getRight()) + "\";"; +// String to = "@include icon-" + ico.getLeft().strip() + "-content;"; +// content = content.replace(from, to); +// } +// +// Files.write(Paths.get(filePath), content.getBytes(), StandardOpenOption.WRITE); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } + +} diff --git a/src/main/java/net/ftlines/css/scoper/maven/FindMissingIconMojo.java b/src/main/java/net/ftlines/css/scoper/maven/FindMissingIconMojo.java new file mode 100644 index 0000000..6e2b15d --- /dev/null +++ b/src/main/java/net/ftlines/css/scoper/maven/FindMissingIconMojo.java @@ -0,0 +1,117 @@ +package net.ftlines.css.scoper.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.shared.model.fileset.FileSet; +import org.apache.maven.shared.model.fileset.util.FileSetManager; +import org.jsoup.Jsoup; +import org.jsoup.select.Elements; + + +@Mojo(name = "find-icon-violations", defaultPhase = LifecyclePhase.VERIFY) +public class FindMissingIconMojo extends AbstractMojo { + + @Parameter(required = true) + public File inputFile; + + @Parameter(required = true, defaultValue = "") + public FileSet fileset; + + @Parameter(required = true, defaultValue = "false") + public boolean failOnError = false; + + private FileSetManager fileSetManager = new FileSetManager(); + + private List> getIcons() throws Exception { + return CompileIconSelection.getIcons(inputFile); + } + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + try { + + List iconClasses = getIcons().stream().map(z-> "icon-" + z.getLeft()).collect(Collectors.toList()); + findIconViolations(iconClasses, fileset); + + List iconCharacterCodes = getIcons().stream().map(z-> ("\\" + Integer.toHexString(z.getRight()))).collect(Collectors.toList()); + findIconContentViolations(iconCharacterCodes, fileset); + + } catch(Exception ex) { + if(ex instanceof MojoExecutionException) { + throw (MojoExecutionException)ex; + } + + if(ex instanceof MojoFailureException) { + throw (MojoFailureException)ex; + } + + getLog().error(ex); + } + } + + private void findIconContentViolations(List iconCharacterCodes, FileSet fileSet) throws MojoExecutionException { + for (String f : fileSetManager.getIncludedFiles(fileset)) { + try { + List lines = Files.readAllLines(Path.of(fileset.getDirectory(), f)); + + for (String line : lines) { + if (line.contains("content")) { + String contentCode = extractContentCode(line); + if (contentCode != null && !iconCharacterCodes.contains(contentCode)) { + getLog().warn("Found violation in " + f + ": " + line.strip()); + if (failOnError) { + throw new MojoExecutionException("Icon violation found in " + f); + } + } + } + } + } catch (IOException e) { + getLog().error("Error reading file: " + f, e); + } + } + } + + private String extractContentCode(String line) { + Pattern pattern = Pattern.compile("content:\\s*\"\\\\([0-9a-fA-F]+)\";"); + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return "\\" + matcher.group(1); + } + return null; + } + + + private void findIconViolations(List iconClasses, FileSet fileSet) throws IOException, MojoFailureException { + for (String f : fileSetManager.getIncludedFiles(fileset)) { + + org.jsoup.nodes.Document doc = Jsoup.parse(Files.readString(Path.of(fileset.getDirectory(), f)), "UTF-8"); + Elements elementsWithIconClass = doc.select("[class^=icon-]"); + + for (org.jsoup.nodes.Element element : elementsWithIconClass) { + for(String className: element.className().split(" ")) { + if (className.startsWith("icon-") && !iconClasses.contains(className)) { + getLog().warn("Icon violation found in " + f + ": Class " + className + " is not in the allowed list."); + if (failOnError) { + throw new MojoFailureException("Icon violation found: Class " + className + " is not in the allowed list."); + } + } + } + } + } + } + +}