Skip to content

Commit 6adaf92

Browse files
brunoborgesCopilot
andcommitted
Add JBang-based Generate.java as Java alternative to generate.py
Uses Jackson for JSON parsing. Produces identical HTML output to the Python script. Can be run with: jbang Generate.java Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 27427a1 commit 6adaf92

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed

Generate.java

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//JAVA 21+
3+
//DEPS com.fasterxml.jackson.core:jackson-databind:2.18.3
4+
5+
import com.fasterxml.jackson.core.type.TypeReference;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.SerializationFeature;
9+
import com.fasterxml.jackson.databind.node.ObjectNode;
10+
11+
import java.io.IOException;
12+
import java.net.URLEncoder;
13+
import java.nio.charset.StandardCharsets;
14+
import java.nio.file.DirectoryStream;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.util.ArrayList;
18+
import java.util.HashMap;
19+
import java.util.LinkedHashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
25+
26+
/**
27+
* Generate HTML detail pages from JSON snippet files and slug-template.html.
28+
* JBang equivalent of generate.py — produces identical output.
29+
*/
30+
public class Generate {
31+
32+
static final String BASE_URL = "https://javaevolved.github.io";
33+
static final String TEMPLATE_FILE = "slug-template.html";
34+
35+
static final Map<String, String> CATEGORY_DISPLAY = Map.ofEntries(
36+
Map.entry("language", "Language"),
37+
Map.entry("collections", "Collections"),
38+
Map.entry("strings", "Strings"),
39+
Map.entry("streams", "Streams"),
40+
Map.entry("concurrency", "Concurrency"),
41+
Map.entry("io", "I/O"),
42+
Map.entry("errors", "Errors"),
43+
Map.entry("datetime", "Date/Time"),
44+
Map.entry("security", "Security"),
45+
Map.entry("tooling", "Tooling")
46+
);
47+
48+
static final List<String> CATEGORIES = List.of(
49+
"language", "collections", "strings", "streams", "concurrency",
50+
"io", "errors", "datetime", "security", "tooling"
51+
);
52+
53+
static final Set<String> EXCLUDED_KEYS = Set.of("_path", "prev", "next", "related");
54+
55+
static final ObjectMapper mapper = new ObjectMapper();
56+
static final Pattern TOKEN_PATTERN = Pattern.compile("\\{\\{(\\w+)}}");
57+
58+
public static void main(String[] args) throws IOException {
59+
String template = Files.readString(Path.of(TEMPLATE_FILE));
60+
Map<String, JsonNode> allSnippets = loadAllSnippets();
61+
System.out.println("Loaded " + allSnippets.size() + " snippets");
62+
63+
// Generate HTML files
64+
for (var entry : allSnippets.entrySet()) {
65+
JsonNode data = entry.getValue();
66+
String htmlContent = generateHtml(template, data, allSnippets).strip();
67+
String category = data.get("category").asText();
68+
String slug = data.get("slug").asText();
69+
Path outPath = Path.of(category, slug + ".html");
70+
Files.writeString(outPath, htmlContent);
71+
}
72+
System.out.println("Generated " + allSnippets.size() + " HTML files");
73+
74+
// Rebuild data/snippets.json
75+
List<Map<String, Object>> snippetsList = new ArrayList<>();
76+
for (var entry : allSnippets.entrySet()) {
77+
Map<String, Object> map = mapper.convertValue(entry.getValue(),
78+
new TypeReference<LinkedHashMap<String, Object>>() {});
79+
EXCLUDED_KEYS.forEach(map::remove);
80+
snippetsList.add(map);
81+
}
82+
83+
Files.createDirectories(Path.of("data"));
84+
ObjectMapper prettyMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
85+
String json = prettyMapper.writeValueAsString(snippetsList) + "\n";
86+
Files.writeString(Path.of("data", "snippets.json"), json);
87+
System.out.println("Rebuilt data/snippets.json with " + snippetsList.size() + " entries");
88+
89+
// Patch index.html with the current snippet count
90+
int count = allSnippets.size();
91+
String indexContent = Files.readString(Path.of("index.html"));
92+
indexContent = indexContent.replace("{{snippetCount}}", String.valueOf(count));
93+
Files.writeString(Path.of("index.html"), indexContent);
94+
System.out.println("Patched index.html with snippet count: " + count);
95+
}
96+
97+
static Map<String, JsonNode> loadAllSnippets() throws IOException {
98+
Map<String, JsonNode> snippets = new LinkedHashMap<>();
99+
for (String cat : CATEGORIES) {
100+
Path catDir = Path.of(cat);
101+
if (!Files.isDirectory(catDir)) continue;
102+
List<Path> jsonFiles = new ArrayList<>();
103+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(catDir, "*.json")) {
104+
stream.forEach(jsonFiles::add);
105+
}
106+
jsonFiles.sort(Path::compareTo);
107+
for (Path path : jsonFiles) {
108+
JsonNode data = mapper.readTree(path.toFile());
109+
String key = data.get("category").asText() + "/" + data.get("slug").asText();
110+
snippets.put(key, data);
111+
}
112+
}
113+
return snippets;
114+
}
115+
116+
static String escape(String text) {
117+
if (text == null) return "";
118+
return text.replace("&", "&amp;")
119+
.replace("<", "&lt;")
120+
.replace(">", "&gt;")
121+
.replace("\"", "&quot;")
122+
.replace("'", "&#x27;");
123+
}
124+
125+
static String jsonEscape(String text) {
126+
// Produce ASCII-only JSON string content (matching Python json.dumps(ensure_ascii=True)[1:-1])
127+
try {
128+
String full = mapper.writeValueAsString(text); // includes surrounding quotes
129+
String inner = full.substring(1, full.length() - 1);
130+
// Jackson doesn't escape non-ASCII by default; do it manually
131+
StringBuilder sb = new StringBuilder();
132+
for (int i = 0; i < inner.length(); i++) {
133+
char c = inner.charAt(i);
134+
if (c > 127) {
135+
sb.append(String.format("\\u%04x", (int) c));
136+
} else {
137+
sb.append(c);
138+
}
139+
}
140+
return sb.toString();
141+
} catch (IOException e) {
142+
throw new RuntimeException(e);
143+
}
144+
}
145+
146+
static String urlEncode(String s) {
147+
return URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20");
148+
}
149+
150+
static String renderNavArrows(JsonNode data) {
151+
List<String> parts = new ArrayList<>();
152+
if (data.has("prev") && !data.get("prev").isNull()) {
153+
parts.add("<a href=\"/" + data.get("prev").asText() + ".html\" aria-label=\"Previous pattern\">←</a>");
154+
} else {
155+
parts.add("<span class=\"nav-arrow-disabled\">←</span>");
156+
}
157+
if (data.has("next") && !data.get("next").isNull()) {
158+
parts.add("<a href=\"/" + data.get("next").asText() + ".html\" aria-label=\"Next pattern\">→</a>");
159+
} else {
160+
parts.add("");
161+
}
162+
return String.join("\n ", parts);
163+
}
164+
165+
static String renderWhyCards(JsonNode whyList) {
166+
List<String> cards = new ArrayList<>();
167+
for (JsonNode w : whyList) {
168+
cards.add(" <div class=\"why-card\">\n"
169+
+ " <div class=\"why-icon\">" + w.get("icon").asText() + "</div>\n"
170+
+ " <h3>" + escape(w.get("title").asText()) + "</h3>\n"
171+
+ " <p>" + escape(w.get("desc").asText()) + "</p>\n"
172+
+ " </div>");
173+
}
174+
return String.join("\n", cards);
175+
}
176+
177+
static String renderRelatedCard(JsonNode relatedData) {
178+
String cat = relatedData.get("category").asText();
179+
String slug = relatedData.get("slug").asText();
180+
String catDisplay = CATEGORY_DISPLAY.get(cat);
181+
String path = cat + "/" + slug;
182+
String difficulty = relatedData.get("difficulty").asText();
183+
184+
return " <a href=\"/" + path + ".html\" class=\"tip-card\">\n"
185+
+ " <div class=\"tip-card-body\">\n"
186+
+ " <div class=\"tip-card-header\">\n"
187+
+ " <div class=\"tip-badges\">\n"
188+
+ " <span class=\"badge " + cat + "\">" + catDisplay + "</span>\n"
189+
+ " <span class=\"badge " + difficulty + "\">" + difficulty + "</span>\n"
190+
+ " </div>\n"
191+
+ " </div>\n"
192+
+ " <h3>" + escape(relatedData.get("title").asText()) + "</h3>\n"
193+
+ " </div>\n"
194+
+ " <div class=\"card-code\">\n"
195+
+ " <div class=\"card-code-layer old-layer\">\n"
196+
+ " <div class=\"mini-label\">" + escape(relatedData.get("oldLabel").asText()) + "</div>\n"
197+
+ " <pre class=\"code-text\">" + escape(relatedData.get("oldCode").asText()) + "</pre>\n"
198+
+ " </div>\n"
199+
+ " <div class=\"card-code-layer modern-layer\">\n"
200+
+ " <div class=\"mini-label\">" + escape(relatedData.get("modernLabel").asText()) + "</div>\n"
201+
+ " <pre class=\"code-text\">" + escape(relatedData.get("modernCode").asText()) + "</pre>\n"
202+
+ " </div>\n"
203+
+ " <span class=\"hover-hint\">Hover to see modern ➜</span>\n"
204+
+ " </div>\n"
205+
+ " <div class=\"tip-card-footer\">\n"
206+
+ " <span class=\"browser-support\"><span class=\"dot\"></span>JDK " + relatedData.get("jdkVersion").asText() + "+</span>\n"
207+
+ " <span class=\"arrow-link\">→</span>\n"
208+
+ " </div>\n"
209+
+ " </a>";
210+
}
211+
212+
static String renderRelatedSection(JsonNode relatedPaths, Map<String, JsonNode> allSnippets) {
213+
List<String> cards = new ArrayList<>();
214+
if (relatedPaths != null) {
215+
for (JsonNode pathNode : relatedPaths) {
216+
String path = pathNode.asText();
217+
if (allSnippets.containsKey(path)) {
218+
cards.add(renderRelatedCard(allSnippets.get(path)));
219+
}
220+
}
221+
}
222+
return String.join("\n", cards);
223+
}
224+
225+
static String renderSocialShare(String slug, String title) {
226+
String pageUrl = BASE_URL + "/" + slug + ".html";
227+
String shareText = title + " \u2013 java.evolved";
228+
String encodedUrl = urlEncode(pageUrl);
229+
String encodedText = urlEncode(shareText);
230+
231+
String xUrl = "https://x.com/intent/tweet?url=" + encodedUrl + "&text=" + encodedText;
232+
String bskyUrl = "https://bsky.app/intent/compose?text=" + encodedText + "%20" + encodedUrl;
233+
String liUrl = "https://www.linkedin.com/sharing/share-offsite/?url=" + encodedUrl;
234+
String redditUrl = "https://www.reddit.com/submit?url=" + encodedUrl + "&title=" + encodedText;
235+
236+
return " <div class=\"social-share\">\n"
237+
+ " <span class=\"share-label\">Share</span>\n"
238+
+ " <a href=\"" + xUrl + "\" target=\"_blank\" rel=\"noopener\" class=\"share-btn share-x\" aria-label=\"Share on X\">𝕏</a>\n"
239+
+ " <a href=\"" + bskyUrl + "\" target=\"_blank\" rel=\"noopener\" class=\"share-btn share-bsky\" aria-label=\"Share on Bluesky\">🦋</a>\n"
240+
+ " <a href=\"" + liUrl + "\" target=\"_blank\" rel=\"noopener\" class=\"share-btn share-li\" aria-label=\"Share on LinkedIn\">in</a>\n"
241+
+ " <a href=\"" + redditUrl + "\" target=\"_blank\" rel=\"noopener\" class=\"share-btn share-reddit\" aria-label=\"Share on Reddit\">⬡</a>\n"
242+
+ " </div>";
243+
}
244+
245+
static String generateHtml(String template, JsonNode data, Map<String, JsonNode> allSnippets) {
246+
String cat = data.get("category").asText();
247+
String slug = data.get("slug").asText();
248+
String catDisplay = CATEGORY_DISPLAY.get(cat);
249+
String title = data.get("title").asText();
250+
251+
Map<String, String> replacements = new HashMap<>();
252+
replacements.put("title", escape(title));
253+
replacements.put("summary", escape(data.get("summary").asText()));
254+
replacements.put("slug", slug);
255+
replacements.put("category", cat);
256+
replacements.put("categoryDisplay", catDisplay);
257+
replacements.put("difficulty", data.get("difficulty").asText());
258+
replacements.put("jdkVersion", data.get("jdkVersion").asText());
259+
replacements.put("oldLabel", escape(data.get("oldLabel").asText()));
260+
replacements.put("modernLabel", escape(data.get("modernLabel").asText()));
261+
replacements.put("oldCode", escape(data.get("oldCode").asText()));
262+
replacements.put("modernCode", escape(data.get("modernCode").asText()));
263+
replacements.put("oldApproach", escape(data.get("oldApproach").asText()));
264+
replacements.put("modernApproach", escape(data.get("modernApproach").asText()));
265+
replacements.put("explanation", escape(data.get("explanation").asText()));
266+
replacements.put("support", escape(data.get("support").asText()));
267+
replacements.put("canonicalUrl", BASE_URL + "/" + cat + "/" + slug + ".html");
268+
replacements.put("flatUrl", BASE_URL + "/" + slug + ".html");
269+
replacements.put("titleJson", jsonEscape(title));
270+
replacements.put("summaryJson", jsonEscape(data.get("summary").asText()));
271+
replacements.put("categoryDisplayJson", jsonEscape(catDisplay));
272+
replacements.put("navArrows", renderNavArrows(data));
273+
replacements.put("whyCards", renderWhyCards(data.get("whyModernWins")));
274+
replacements.put("relatedCards", renderRelatedSection(data.get("related"), allSnippets));
275+
replacements.put("socialShare", renderSocialShare(slug, title));
276+
277+
Matcher m = TOKEN_PATTERN.matcher(template);
278+
StringBuilder sb = new StringBuilder();
279+
while (m.find()) {
280+
String key = m.group(1);
281+
String replacement = replacements.getOrDefault(key, m.group(0));
282+
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
283+
}
284+
m.appendTail(sb);
285+
return sb.toString();
286+
}
287+
}

0 commit comments

Comments
 (0)