1 package com.guinetik.terminaljavadocs.plugin;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.nio.charset.StandardCharsets;
7 import java.nio.file.FileVisitResult;
8 import java.nio.file.Files;
9 import java.nio.file.Path;
10 import java.nio.file.SimpleFileVisitor;
11 import java.nio.file.StandardCopyOption;
12 import java.nio.file.attribute.BasicFileAttributes;
13 import java.util.List;
14 import java.util.regex.Matcher;
15 import java.util.regex.Pattern;
16 import org.apache.maven.execution.MavenSession;
17 import org.apache.maven.plugin.AbstractMojo;
18 import org.apache.maven.plugin.MojoExecutionException;
19 import org.apache.maven.plugins.annotations.Mojo;
20 import org.apache.maven.plugins.annotations.Parameter;
21 import org.apache.maven.project.MavenProject;
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 @Mojo(name = "inject-styles", defaultPhase = org.apache.maven.plugins.annotations.LifecyclePhase.POST_SITE)
42 public class InjectSiteStylesMojo extends AbstractMojo {
43
44
45
46
47
48
49
50
51 public enum PageType {
52
53 LANDING("landing", "terminaljavadocs-landing.min.css"),
54
55 COVERAGE("coverage", "terminaljavadocs-coverage.min.css"),
56
57 JXR("jxr", "terminaljavadocs-jxr.min.css"),
58
59 JAVADOC("javadoc", "terminaljavadocs-javadoc.min.css"),
60
61 SITE("site", "terminaljavadocs-site.min.css");
62
63
64 private final String name;
65
66
67 private final String cssFile;
68
69
70
71
72
73
74
75 PageType(String name, String cssFile) {
76 this.name = name;
77 this.cssFile = cssFile;
78 }
79
80
81
82
83
84
85 public String getName() {
86 return name;
87 }
88
89
90
91
92
93
94 public String getCssFile() {
95 return cssFile;
96 }
97 }
98
99
100 private static final String STYLES_RESOURCE_PATH = "styles/";
101
102
103 private static final String JS_FILE = "terminaljavadocs.min.js";
104
105
106 private static final String JACOCO_RESOURCES_PATH = "jacoco-resources/";
107
108
109
110
111
112 private static final String[] JACOCO_RESOURCE_FILES = {
113 "branchfc.gif", "branchnc.gif", "branchpc.gif",
114 "greenbar.gif", "redbar.gif",
115 "down.gif", "up.gif", "sort.gif"
116 };
117
118
119
120
121
122 private static final String INJECTION_MARKER = "<!-- terminal-javadocs-injected";
123
124
125
126
127 @Parameter(defaultValue = "${session}", readonly = true, required = true)
128 private MavenSession session;
129
130
131
132
133 @Parameter(defaultValue = "${project}", readonly = true, required = true)
134 private MavenProject project;
135
136
137
138
139
140 @Parameter(property = "terminaljavadocs.skip", defaultValue = "false")
141 private boolean skip;
142
143
144
145
146 @Parameter(defaultValue = "${project.build.directory}", readonly = true)
147 private File buildDirectory;
148
149
150
151
152 @Parameter(property = "terminaljavadocs.stylesDir", defaultValue = "terminal-styles")
153 private String stylesDir;
154
155
156
157
158 @Parameter(property = "terminaljavadocs.processNestedSites", defaultValue = "true")
159 private boolean processNestedSites;
160
161
162
163
164
165 @Parameter(property = "terminaljavadocs.project.name", defaultValue = "${project.name}")
166 private String projectName;
167
168
169
170
171
172
173 @Parameter(property = "terminaljavadocs.project.logo", defaultValue = "")
174 private String projectLogo;
175
176
177 private int processedFiles = 0;
178
179
180 private int landingFiles = 0;
181
182
183 private int coverageFiles = 0;
184
185
186 private int jxrFiles = 0;
187
188
189 private int javadocFiles = 0;
190
191
192 private int siteFiles = 0;
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209 @Override
210 public void execute() throws MojoExecutionException {
211 if (skip) {
212 getLog().info("Skipping style injection");
213 return;
214 }
215
216 try {
217
218 File siteDir = new File(buildDirectory, "staging");
219 if (!siteDir.exists()) {
220 siteDir = new File(buildDirectory, "site");
221 }
222
223 if (!siteDir.exists()) {
224 getLog().info("No site directory found, skipping style injection");
225 return;
226 }
227
228 getLog().info("Injecting Terminal Javadocs styles into site: " + siteDir.getAbsolutePath());
229
230
231 File stylesTargetDir = new File(siteDir, stylesDir);
232 copyStyleResources(stylesTargetDir);
233
234
235 themeJacocoResources(siteDir);
236
237
238 processHtmlFiles(siteDir, siteDir);
239
240
241 if (processNestedSites) {
242 List<MavenProject> projects = session.getProjects();
243
244
245
246
247 if (siteDir.getName().equals("staging")) {
248 for (MavenProject reactorProject : projects) {
249 if (reactorProject.equals(project)) {
250 continue;
251 }
252
253 String artifactId = reactorProject.getArtifactId();
254 File moduleStagedDir = new File(siteDir, artifactId);
255
256 if (moduleStagedDir.exists() && moduleStagedDir.isDirectory()) {
257
258 File moduleStylesDir = new File(moduleStagedDir, stylesDir);
259 copyStyleResources(moduleStylesDir);
260
261
262 themeJacocoResources(moduleStagedDir);
263
264 getLog().info("Processing staged module site: " + artifactId);
265 processHtmlFiles(moduleStagedDir, moduleStagedDir);
266 }
267 }
268 }
269
270
271
272 for (MavenProject reactorProject : projects) {
273 if (reactorProject.equals(project)) {
274 continue;
275 }
276
277 String buildDir = reactorProject.getBuild().getDirectory();
278
279
280 File moduleSiteDir = new File(buildDir, "staging");
281 if (!moduleSiteDir.exists()) {
282 moduleSiteDir = new File(buildDir, "site");
283 }
284
285 if (moduleSiteDir.exists()) {
286 String artifactId = reactorProject.getArtifactId();
287
288
289 File moduleStylesDir = new File(moduleSiteDir, stylesDir);
290 copyStyleResources(moduleStylesDir);
291
292
293 themeJacocoResources(moduleSiteDir);
294
295 getLog().info("Processing individual module site: " + artifactId);
296 processHtmlFiles(moduleSiteDir, moduleSiteDir);
297 }
298 }
299 }
300
301
302 getLog().info("Style injection complete:");
303 getLog().info(" Total HTML files processed: " + processedFiles);
304 getLog().info(" Landing pages: " + landingFiles);
305 getLog().info(" Coverage pages: " + coverageFiles);
306 getLog().info(" JXR pages: " + jxrFiles);
307 getLog().info(" Javadoc pages: " + javadocFiles);
308 getLog().info(" Site pages: " + siteFiles);
309
310 } catch (IOException e) {
311 throw new MojoExecutionException("Failed to inject styles", e);
312 }
313 }
314
315
316
317
318
319
320
321
322 private void copyStyleResources(File targetDir) throws IOException {
323 targetDir.mkdirs();
324
325
326 for (PageType pageType : PageType.values()) {
327 copyResource(
328 STYLES_RESOURCE_PATH + pageType.getCssFile(),
329 new File(targetDir, pageType.getCssFile()));
330 }
331
332
333 copyJsWithTokenReplacement(STYLES_RESOURCE_PATH + JS_FILE, new File(targetDir, JS_FILE));
334 }
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350 private void copyJsWithTokenReplacement(String resourcePath, File targetFile) throws IOException {
351 try (InputStream is = getResourceStream(resourcePath)) {
352 if (is != null) {
353
354 java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
355 byte[] data = new byte[4096];
356 int bytesRead;
357 while ((bytesRead = is.read(data, 0, data.length)) != -1) {
358 buffer.write(data, 0, bytesRead);
359 }
360 String content = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
361
362
363 content = content.replace("%%PROJECT_NAME%%", projectName != null ? projectName : "");
364 content = content.replace("%%PROJECT_LOGO%%", projectLogo != null ? projectLogo : "");
365
366 Files.write(targetFile.toPath(), content.getBytes(StandardCharsets.UTF_8));
367 getLog().debug("Copied JS with token replacement: " + resourcePath + " -> " + targetFile);
368 getLog().debug(" PROJECT_NAME: " + projectName);
369 getLog().debug(" PROJECT_LOGO: " + projectLogo);
370 } else {
371 getLog().warn("Resource not found: " + resourcePath +
372 ". Run 'npm run build' in css-zen-garden to generate it.");
373 }
374 }
375 }
376
377
378
379
380
381
382
383
384 private void copyResource(String resourcePath, File targetFile) throws IOException {
385 try (InputStream is = getResourceStream(resourcePath)) {
386 if (is != null) {
387 Files.copy(is, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
388 getLog().debug("Copied resource: " + resourcePath + " -> " + targetFile);
389 } else {
390 getLog().warn("Resource not found: " + resourcePath +
391 ". Run 'npm run build' in css-zen-garden to generate it.");
392 }
393 }
394 }
395
396
397
398
399
400
401
402 private void copyJacocoResources(File jacocoResourcesDir) throws IOException {
403 if (!jacocoResourcesDir.exists()) {
404 return;
405 }
406
407 for (String fileName : JACOCO_RESOURCE_FILES) {
408 copyResource(JACOCO_RESOURCES_PATH + fileName, new File(jacocoResourcesDir, fileName));
409 }
410 getLog().debug("Copied themed JaCoCo resources to: " + jacocoResourcesDir);
411 }
412
413
414
415
416
417
418
419
420 private void themeJacocoResources(File siteDir) throws IOException {
421 Files.walkFileTree(siteDir.toPath(), new SimpleFileVisitor<Path>() {
422 @Override
423 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
424 if (dir.getFileName() != null && dir.getFileName().toString().equals("jacoco-resources")) {
425 copyJacocoResources(dir.toFile());
426 }
427 return FileVisitResult.CONTINUE;
428 }
429 });
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446 private InputStream getResourceStream(String resourcePath) {
447 InputStream is = Thread.currentThread()
448 .getContextClassLoader()
449 .getResourceAsStream(resourcePath);
450
451 if (is == null) {
452 is = InjectSiteStylesMojo.class.getClassLoader().getResourceAsStream(resourcePath);
453 }
454
455 if (is == null) {
456 is = InjectSiteStylesMojo.class.getResourceAsStream("/" + resourcePath);
457 }
458
459 return is;
460 }
461
462
463
464
465
466
467
468
469 private void processHtmlFiles(File directory, File siteRoot) throws IOException {
470 Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() {
471 @Override
472 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
473 if (file.toString().endsWith(".html")) {
474 processHtmlFile(file.toFile(), siteRoot);
475 }
476 return FileVisitResult.CONTINUE;
477 }
478
479 @Override
480 public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
481 getLog().warn("Failed to visit file: " + file + " - " + exc.getMessage());
482 return FileVisitResult.CONTINUE;
483 }
484 });
485 }
486
487
488
489
490
491
492
493
494 private void processHtmlFile(File htmlFile, File siteRoot) throws IOException {
495 String content = new String(Files.readAllBytes(htmlFile.toPath()), StandardCharsets.UTF_8);
496
497
498 if (content.contains(INJECTION_MARKER)) {
499 getLog().debug("Skipping already injected file: " + htmlFile);
500 return;
501 }
502
503
504 PageType pageType = detectPageType(htmlFile, content);
505
506
507 String relativePath = calculateRelativePath(htmlFile, siteRoot);
508
509
510 String styleSnippet = generateStyleSnippet(pageType, relativePath);
511
512
513 String modifiedContent = injectStyles(content, styleSnippet);
514
515 if (modifiedContent != null) {
516 Files.write(htmlFile.toPath(), modifiedContent.getBytes(StandardCharsets.UTF_8));
517 processedFiles++;
518
519
520 switch (pageType) {
521 case LANDING:
522 landingFiles++;
523 break;
524 case COVERAGE:
525 coverageFiles++;
526 break;
527 case JXR:
528 jxrFiles++;
529 break;
530 case JAVADOC:
531 javadocFiles++;
532 break;
533 case SITE:
534 siteFiles++;
535 break;
536 }
537
538 getLog().debug("Injected " + pageType.getName() + " styles into: " + htmlFile);
539 }
540 }
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558 private PageType detectPageType(File htmlFile, String content) {
559 String path = htmlFile.getAbsolutePath().replace('\\', '/').toLowerCase();
560 String fileName = htmlFile.getName().toLowerCase();
561
562
563 if (fileName.equals("coverage.html") || fileName.equals("source-xref.html")) {
564 return PageType.LANDING;
565 }
566
567
568 if (path.contains("/jacoco/") || path.contains("/coverage/")) {
569 return PageType.COVERAGE;
570 }
571 if (path.contains("/xref/") || path.contains("/xref-test/")) {
572 return PageType.JXR;
573 }
574 if (path.contains("/apidocs/") || path.contains("/testapidocs/") || path.contains("/javadoc/")) {
575 return PageType.JAVADOC;
576 }
577
578
579 if (isLandingContent(content)) {
580 return PageType.LANDING;
581 }
582 if (isJacocoContent(content)) {
583 return PageType.COVERAGE;
584 }
585 if (isJxrContent(content)) {
586 return PageType.JXR;
587 }
588 if (isJavadocContent(content)) {
589 return PageType.JAVADOC;
590 }
591
592
593 return PageType.SITE;
594 }
595
596
597
598
599
600
601
602 private boolean isLandingContent(String content) {
603 return content.contains("class=\"terminal-header\"") ||
604 content.contains("class=\"terminal-brand\"") ||
605 content.contains("class=\"module-list\"");
606 }
607
608
609
610
611
612
613
614 private boolean isJacocoContent(String content) {
615 return content.contains("jacoco") ||
616 content.contains("Coverage Report") ||
617 content.contains("class=\"el_package\"") ||
618 content.contains("class=\"el_class\"") ||
619 content.contains("class=\"ctr2\"");
620 }
621
622
623
624
625
626
627
628 private boolean isJxrContent(String content) {
629 return content.contains("jxr") ||
630 content.contains("Cross-Reference") ||
631 content.contains("class=\"jxr_") ||
632 content.contains("id=\"jxr_");
633 }
634
635
636
637
638
639
640
641 private boolean isJavadocContent(String content) {
642 return content.contains("Generated by javadoc") ||
643 content.contains("<!-- Generated by javadoc") ||
644 content.contains("class=\"summary-table\"") ||
645 content.contains("class=\"member-signature\"") ||
646 content.contains("class=\"description\"");
647 }
648
649
650
651
652
653
654
655
656 private String calculateRelativePath(File htmlFile, File siteRoot) {
657 Path htmlPath = htmlFile.toPath().getParent();
658 Path rootPath = siteRoot.toPath();
659
660 try {
661 Path relativePath = htmlPath.relativize(rootPath);
662 String result = relativePath.toString().replace('\\', '/');
663 if (result.isEmpty()) {
664 return "./";
665 }
666 if (!result.endsWith("/")) {
667 result += "/";
668 }
669 return result;
670 } catch (IllegalArgumentException e) {
671
672 return "./";
673 }
674 }
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691 private String generateStyleSnippet(PageType pageType, String relativePath) {
692 String stylesPath = relativePath + stylesDir + "/";
693
694 return "\n<!-- " + INJECTION_MARKER + " [" + pageType.getName() + "] -->\n" +
695 "<link rel=\"stylesheet\" href=\"" + stylesPath + pageType.getCssFile() + "\">\n" +
696 "<script src=\"" + stylesPath + JS_FILE + "\" defer></script>\n";
697 }
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714 private String injectStyles(String content, String styleSnippet) {
715
716 Pattern headPattern = Pattern.compile("(</head>)", Pattern.CASE_INSENSITIVE);
717 Matcher matcher = headPattern.matcher(content);
718
719 if (matcher.find()) {
720 return content.substring(0, matcher.start()) +
721 styleSnippet +
722 content.substring(matcher.start());
723 }
724
725
726 Pattern headOpenPattern = Pattern.compile("(<head[^>]*>)", Pattern.CASE_INSENSITIVE);
727 Matcher openMatcher = headOpenPattern.matcher(content);
728
729 if (openMatcher.find()) {
730 return content.substring(0, openMatcher.end()) +
731 styleSnippet +
732 content.substring(openMatcher.end());
733 }
734
735
736 getLog().warn("No <head> section found in HTML, cannot inject styles");
737 return null;
738 }
739 }