refactor git provider and site structures
This commit is contained in:
@@ -23,12 +23,21 @@ public class GitConfig {
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
if (reposPath == null) return;
|
||||
Path path = Paths.get(reposPath);
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthorName(String authorName) {
|
||||
this.authorName = authorName;
|
||||
}
|
||||
|
||||
public void setAuthorEmail(String authorEmail) {
|
||||
this.authorEmail = authorEmail;
|
||||
}
|
||||
|
||||
public Path getReposPath() {
|
||||
return Paths.get(reposPath);
|
||||
}
|
||||
|
||||
@@ -34,17 +34,6 @@ public class SiteStructure {
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private int orderIndex;
|
||||
private List<ComponentStructure> components;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class ComponentStructure {
|
||||
private String id;
|
||||
private String type;
|
||||
private int orderIndex;
|
||||
private Map<String, Object> config;
|
||||
private Map<String, Object> styles;
|
||||
private String rawHtml;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class GitHubPagesProvider implements PagesProvider {
|
||||
|
||||
@Override
|
||||
public String branchName() {
|
||||
return "gh-pages";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path contentRoot(Path tempDir) {
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeProviderFiles(Path tempDir) throws IOException {
|
||||
Files.writeString(tempDir.resolve(".nojekyll"), "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildPagesUrl(GitRemote remote) {
|
||||
String url = GitService.normalizeHttpsUrl(remote.getRemoteUrl());
|
||||
String path = url.substring(url.indexOf("github.com") + "github.com".length() + 1);
|
||||
if (path.endsWith(".git")) path = path.substring(0, path.length() - 4);
|
||||
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
|
||||
String[] parts = path.split("/", 2);
|
||||
if (parts.length == 2) {
|
||||
return "https://" + parts[0] + ".github.io/" + parts[1] + "/";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class GitLabPagesProvider implements PagesProvider {
|
||||
|
||||
@Override
|
||||
public String branchName() {
|
||||
return "gl-pages";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path contentRoot(Path tempDir) {
|
||||
return tempDir.resolve("public");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeProviderFiles(Path tempDir) throws IOException {
|
||||
Files.writeString(tempDir.resolve(".gitlab-ci.yml"),
|
||||
"pages:\n" +
|
||||
" stage: deploy\n" +
|
||||
" script:\n" +
|
||||
" - echo \"Deploying GitLab Pages\"\n" +
|
||||
" artifacts:\n" +
|
||||
" paths:\n" +
|
||||
" - public\n" +
|
||||
" rules:\n" +
|
||||
" - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildPagesUrl(GitRemote remote) {
|
||||
String url = GitService.normalizeHttpsUrl(remote.getRemoteUrl());
|
||||
String path = url.substring(url.indexOf("gitlab.com") + "gitlab.com".length() + 1);
|
||||
if (path.endsWith(".git")) path = path.substring(0, path.length() - 4);
|
||||
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
|
||||
String[] parts = path.split("/", 2);
|
||||
if (parts.length == 2) {
|
||||
return "https://" + parts[0] + ".gitlab.io/" + parts[1] + "/";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -194,7 +195,7 @@ public class GitService {
|
||||
|
||||
public record CommitInfo(String hash, String message, String timestamp, String author) {}
|
||||
|
||||
private String normalizeHttpsUrl(String url) {
|
||||
static String normalizeHttpsUrl(String url) {
|
||||
if (url.startsWith("https://")) {
|
||||
return url;
|
||||
}
|
||||
@@ -218,38 +219,15 @@ public class GitService {
|
||||
String authUrl = httpsUrl.replace("https://",
|
||||
"https://git:" + remote.getEncryptedToken() + "@");
|
||||
|
||||
boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com");
|
||||
String branch = isGitLab ? "gl-pages" : "gh-pages";
|
||||
|
||||
PagesProvider provider = PagesProvider.forRemote(remote);
|
||||
Path tempDir = Files.createTempDirectory("indie-pages-" + siteId);
|
||||
try {
|
||||
Path contentRoot = isGitLab ? tempDir.resolve("public") : tempDir;
|
||||
Path contentRoot = provider.contentRoot(tempDir);
|
||||
Files.createDirectories(contentRoot);
|
||||
pushPagesBranch(tempDir, authUrl, provider.branchName(), contentRoot, provider, files, "Deploy site");
|
||||
|
||||
for (Map.Entry<String, byte[]> entry : files.entrySet()) {
|
||||
Path filePath = contentRoot.resolve(entry.getKey());
|
||||
Files.createDirectories(filePath.getParent());
|
||||
Files.write(filePath, entry.getValue());
|
||||
}
|
||||
|
||||
if (isGitLab) {
|
||||
Files.writeString(tempDir.resolve(".gitlab-ci.yml"),
|
||||
"pages:\n" +
|
||||
" stage: deploy\n" +
|
||||
" script:\n" +
|
||||
" - echo \"Deploying GitLab Pages\"\n" +
|
||||
" artifacts:\n" +
|
||||
" paths:\n" +
|
||||
" - public\n" +
|
||||
" rules:\n" +
|
||||
" - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n");
|
||||
} else {
|
||||
Files.writeString(tempDir.resolve(".nojekyll"), "");
|
||||
}
|
||||
pushOrphanBranch(tempDir, authUrl, branch);
|
||||
|
||||
log.info("Pushed site {} to {} branch {}", siteId, remote.getRemoteUrl(), branch);
|
||||
return buildPagesUrl(remote);
|
||||
log.info("Pushed site {} to {} branch {}", siteId, remote.getRemoteUrl(), provider.branchName());
|
||||
return provider.buildPagesUrl(remote);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
@@ -258,19 +236,82 @@ public class GitService {
|
||||
}
|
||||
}
|
||||
|
||||
void pushPagesBranch(Path tempDir, String authUrl, String branch,
|
||||
Path contentRoot, PagesProvider provider, Map<String, byte[]> files,
|
||||
String commitMessage) throws Exception {
|
||||
String authorConfig = "-c user.name='" + gitConfig.getAuthorName()
|
||||
+ "' -c user.email='" + gitConfig.getAuthorEmail() + "'";
|
||||
|
||||
runGit(tempDir, "git init", "git remote add origin " + authUrl);
|
||||
|
||||
private void pushOrphanBranch(Path tempDir, String authUrl, String branch) throws Exception {
|
||||
String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'";
|
||||
runGit(tempDir,
|
||||
"git init",
|
||||
"git " + authorConfig + " commit --allow-empty -m 'Initial commit'",
|
||||
"git push --force " + authUrl + " HEAD:main",
|
||||
"git checkout --orphan " + branch,
|
||||
"git add -A",
|
||||
"git " + authorConfig + " commit -m 'Deploy site'",
|
||||
"git push --force " + authUrl + " HEAD:" + branch
|
||||
);
|
||||
if (branchExistsOnRemote(authUrl, branch)) {
|
||||
runGit(tempDir,
|
||||
"git fetch origin " + branch,
|
||||
"git checkout -b " + branch + " origin/" + branch,
|
||||
"git rm -rf ."
|
||||
);
|
||||
writeSiteFilesTo(contentRoot, files);
|
||||
provider.writeProviderFiles(tempDir);
|
||||
runGit(tempDir,
|
||||
"git add -A",
|
||||
"git " + authorConfig + " commit -m '" + commitMessage + "'",
|
||||
"git fetch origin " + branch,
|
||||
"git merge origin/" + branch + " -X ours",
|
||||
"git push origin " + branch
|
||||
);
|
||||
} else {
|
||||
runGit(tempDir, "git checkout --orphan " + branch);
|
||||
writeSiteFilesTo(contentRoot, files);
|
||||
provider.writeProviderFiles(tempDir);
|
||||
runGit(tempDir,
|
||||
"git add -A",
|
||||
"git " + authorConfig + " commit -m '" + commitMessage + "'",
|
||||
"git push origin " + branch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSiteFilesTo(Path contentRoot, Map<String, byte[]> files) throws IOException {
|
||||
for (Map.Entry<String, byte[]> entry : files.entrySet()) {
|
||||
Path filePath = contentRoot.resolve(entry.getKey());
|
||||
Files.createDirectories(filePath.getParent());
|
||||
Files.write(filePath, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean branchExistsOnRemote(String authUrl, String branch) throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder("sh", "-c",
|
||||
"git ls-remote --exit-code " + authUrl + " " + branch);
|
||||
Process process = pb.start();
|
||||
return process.waitFor() == 0;
|
||||
}
|
||||
|
||||
private Map<String, byte[]> createRemovalPage() {
|
||||
String html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Site Removed</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
min-height: 100vh; margin: 0; background: #f5f5f5; color: #333;
|
||||
text-align: center; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
p { color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>This site has been unpublished</h1>
|
||||
<p>The owner has removed this site.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
return Map.of("index.html", html.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void runGit(Path workDir, String... commands) throws Exception {
|
||||
@@ -287,36 +328,6 @@ public class GitService {
|
||||
}
|
||||
}
|
||||
|
||||
private String buildPagesUrl(GitRemote remote) {
|
||||
String url = normalizeHttpsUrl(remote.getRemoteUrl());
|
||||
String owner = "";
|
||||
String repo = "";
|
||||
|
||||
if (url.contains("github.com")) {
|
||||
String path = url.substring(url.indexOf("github.com") + "github.com".length() + 1);
|
||||
if (path.endsWith(".git")) path = path.substring(0, path.length() - 4);
|
||||
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
|
||||
String[] parts = path.split("/", 2);
|
||||
if (parts.length == 2) {
|
||||
owner = parts[0];
|
||||
repo = parts[1];
|
||||
}
|
||||
return "https://" + owner + ".github.io/" + repo + "/";
|
||||
} else if (url.contains("gitlab.com")) {
|
||||
String path = url.substring(url.indexOf("gitlab.com") + "gitlab.com".length() + 1);
|
||||
if (path.endsWith(".git")) path = path.substring(0, path.length() - 4);
|
||||
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
|
||||
String[] parts = path.split("/", 2);
|
||||
if (parts.length == 2) {
|
||||
owner = parts[0];
|
||||
repo = parts[1];
|
||||
}
|
||||
return "https://" + owner + ".gitlab.io/" + repo + "/";
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
public void removePages(String siteId) {
|
||||
GitRemote remote = gitRemoteRepository.findBySiteId(UUID.fromString(siteId))
|
||||
.orElse(null);
|
||||
@@ -330,20 +341,21 @@ public class GitService {
|
||||
String authUrl = httpsUrl.replace("https://",
|
||||
"https://git:" + remote.getEncryptedToken() + "@");
|
||||
|
||||
boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com");
|
||||
String branch = isGitLab ? "gl-pages" : "gh-pages";
|
||||
PagesProvider provider = PagesProvider.forRemote(remote);
|
||||
String branch = provider.branchName();
|
||||
|
||||
if (!branchExistsOnRemote(authUrl, branch)) {
|
||||
log.info("No {} branch found for site {}, nothing to remove", branch, siteId);
|
||||
return;
|
||||
}
|
||||
|
||||
Path tempDir = Files.createTempDirectory("indie-pages-remove-" + siteId);
|
||||
try {
|
||||
String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'";
|
||||
runGit(tempDir,
|
||||
"git init",
|
||||
"git " + authorConfig + " commit --allow-empty -m 'Remove site'",
|
||||
"git checkout --orphan " + branch,
|
||||
"git " + authorConfig + " commit --allow-empty -m 'Remove site'",
|
||||
"git push --force " + authUrl + " HEAD:" + branch
|
||||
);
|
||||
log.info("Removed pages for site {} from {} branch {}", siteId, remote.getProvider(), branch);
|
||||
Path contentRoot = provider.contentRoot(tempDir);
|
||||
Files.createDirectories(contentRoot);
|
||||
Map<String, byte[]> removalFiles = createRemovalPage();
|
||||
pushPagesBranch(tempDir, authUrl, branch, contentRoot, provider, removalFiles, "Unpublish site");
|
||||
log.info("Unpublished site {} from {} branch {}", siteId, remote.getProvider(), branch);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
|
||||
@@ -142,14 +142,9 @@ public class LLMResponseParser {
|
||||
if (pageNode.has("orderIndex")) {
|
||||
builder.orderIndex(pageNode.get("orderIndex").asInt());
|
||||
}
|
||||
|
||||
List<SiteStructure.ComponentStructure> components = new ArrayList<>();
|
||||
if (pageNode.has("components") && pageNode.get("components").isArray()) {
|
||||
for (JsonNode compNode : pageNode.get("components")) {
|
||||
components.add(parseComponent(compNode));
|
||||
}
|
||||
if (pageNode.has("rawHtml")) {
|
||||
builder.rawHtml(pageNode.get("rawHtml").asText());
|
||||
}
|
||||
builder.components(components);
|
||||
|
||||
SiteStructure.PageStructure page = builder.build();
|
||||
if (page.getTitle() == null || page.getSlug() == null) {
|
||||
@@ -158,34 +153,4 @@ public class LLMResponseParser {
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
private SiteStructure.ComponentStructure parseComponent(JsonNode compNode) {
|
||||
SiteStructure.ComponentStructure.ComponentStructureBuilder builder = SiteStructure.ComponentStructure.builder();
|
||||
|
||||
if (compNode.has("id")) {
|
||||
builder.id(compNode.get("id").asText());
|
||||
} else {
|
||||
builder.id(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
if (compNode.has("type")) {
|
||||
builder.type(compNode.get("type").asText());
|
||||
}
|
||||
if (compNode.has("orderIndex")) {
|
||||
builder.orderIndex(compNode.get("orderIndex").asInt());
|
||||
}
|
||||
if (compNode.has("config")) {
|
||||
builder.config(objectMapper.convertValue(compNode.get("config"), Map.class));
|
||||
}
|
||||
if (compNode.has("styles")) {
|
||||
builder.styles(objectMapper.convertValue(compNode.get("styles"), Map.class));
|
||||
}
|
||||
|
||||
SiteStructure.ComponentStructure component = builder.build();
|
||||
if (component.getType() == null) {
|
||||
throw new IllegalArgumentException("Component must have a type");
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
24
backend/src/main/java/com/krrishg/service/PagesProvider.java
Normal file
24
backend/src/main/java/com/krrishg/service/PagesProvider.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.krrishg.service;
|
||||
|
||||
import com.krrishg.model.GitRemote;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
interface PagesProvider {
|
||||
|
||||
String branchName();
|
||||
|
||||
Path contentRoot(Path tempDir);
|
||||
|
||||
void writeProviderFiles(Path tempDir) throws IOException;
|
||||
|
||||
String buildPagesUrl(GitRemote remote);
|
||||
|
||||
static PagesProvider forRemote(GitRemote remote) {
|
||||
if (remote.getRemoteUrl().contains("gitlab.com")) {
|
||||
return new GitLabPagesProvider();
|
||||
}
|
||||
return new GitHubPagesProvider();
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class RenderService {
|
||||
@@ -58,52 +58,16 @@ public class RenderService {
|
||||
}
|
||||
}
|
||||
|
||||
List<Section> sections = buildSections(page.getComponents());
|
||||
Set<Integer> lastIndices = sections.stream()
|
||||
.map(s -> s.indices.get(s.indices.size() - 1))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
StringBuilder sectionsHtml = new StringBuilder();
|
||||
for (Section section : sections) {
|
||||
String bgStyle = section.bg != null && !section.bg.isEmpty()
|
||||
? " style=\"background:" + section.bg + "\"" : "";
|
||||
sectionsHtml.append("<section").append(bgStyle).append(">\n <div class=\"container\">\n");
|
||||
for (int idx : section.indices) {
|
||||
SiteStructure.ComponentStructure comp = page.getComponents().get(idx);
|
||||
sectionsHtml.append(" ").append(renderComponent(comp, idx)).append("\n");
|
||||
}
|
||||
sectionsHtml.append(" </div>\n</section>\n");
|
||||
}
|
||||
|
||||
StringBuilder compStyles = new StringBuilder();
|
||||
for (int i = 0; i < page.getComponents().size(); i++) {
|
||||
SiteStructure.ComponentStructure c = page.getComponents().get(i);
|
||||
if (c.getStyles() == null || c.getStyles().isEmpty()) continue;
|
||||
List<Map.Entry<String, Object>> entries = c.getStyles().entrySet().stream()
|
||||
.filter(e -> !"backgroundColor".equals(e.getKey()))
|
||||
.collect(Collectors.toList());
|
||||
if (lastIndices.contains(i)) {
|
||||
entries = entries.stream()
|
||||
.filter(e -> !"marginBottom".equals(e.getKey()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (entries.isEmpty()) continue;
|
||||
compStyles.append(".comp-").append(i).append(" {\n");
|
||||
for (Map.Entry<String, Object> e : entries) {
|
||||
compStyles.append(" ").append(cssProperty(e.getKey())).append(": ").append(e.getValue()).append(";\n");
|
||||
}
|
||||
compStyles.append("}\n");
|
||||
}
|
||||
|
||||
String title = page.getSeoTitle() != null && !page.getSeoTitle().isEmpty()
|
||||
? page.getSeoTitle() : page.getTitle();
|
||||
String bodyContent = page.getRawHtml() != null ? page.getRawHtml() : "";
|
||||
|
||||
return "<!DOCTYPE html>\n"
|
||||
+ "<html lang=\"en\">\n"
|
||||
+ "<head>\n"
|
||||
+ " <meta charset=\"UTF-8\">\n"
|
||||
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
|
||||
+ " <title>" + esc(title) + "</title>\n"
|
||||
+ " <title>" + title + "</title>\n"
|
||||
+ " <link rel=\"stylesheet\" href=\"style.css\">\n"
|
||||
+ " <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&family=Merriweather:wght@300;400;700&family=Lato:wght@300;400;700&family=Open+Sans:wght@300;400;600;700&family=DM+Sans:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n"
|
||||
+ " <style>\n"
|
||||
@@ -118,200 +82,14 @@ public class RenderService {
|
||||
+ " line-height: 1.6;\n"
|
||||
+ " -webkit-font-smoothing: antialiased;\n"
|
||||
+ " }\n"
|
||||
+ " .container { max-width: var(--spacing-containerWidth, 1100px); margin: 0 auto; padding: 0 1.5rem; }\n"
|
||||
+ " section { padding: var(--spacing-sectionPadding, 3rem) 0; }\n"
|
||||
+ compStyles.toString()
|
||||
+ " </style>\n"
|
||||
+ "</head>\n"
|
||||
+ "<body>\n"
|
||||
+ sectionsHtml.toString()
|
||||
+ "</body>\n"
|
||||
+ bodyContent
|
||||
+ "\n</body>\n"
|
||||
+ "</html>";
|
||||
}
|
||||
|
||||
private String renderComponent(SiteStructure.ComponentStructure c, int index) {
|
||||
Map<String, Object> config = c.getConfig() != null ? c.getConfig() : new HashMap<>();
|
||||
String type = (c.getType() != null ? c.getType() : "").toUpperCase();
|
||||
String cls = "comp-" + index;
|
||||
|
||||
return switch (type) {
|
||||
case "HEADING" -> renderHeading(config, cls);
|
||||
case "TEXT" -> renderText(config, cls);
|
||||
case "IMAGE" -> renderImage(config, cls);
|
||||
case "BUTTON" -> renderButton(config, cls);
|
||||
case "DIVIDER" -> renderDivider(config, cls);
|
||||
case "GALLERY" -> renderGallery(config, cls);
|
||||
case "VIDEO" -> renderVideo(config, cls);
|
||||
case "CONTACT_FORM" -> renderContactForm(config, cls);
|
||||
case "MAP" -> renderMap(config, cls);
|
||||
case "CUSTOM_HTML" -> renderCustomHtml(config, cls);
|
||||
default -> "<div class=\"" + cls + "\">[" + esc(type) + "]</div>";
|
||||
};
|
||||
}
|
||||
|
||||
private String renderHeading(Map<String, Object> config, String cls) {
|
||||
String level = str(config.get("level"), "h2");
|
||||
String text = str(config.get("text"), "Heading");
|
||||
String tag = List.of("h1", "h2", "h3", "h4", "h5", "h6").contains(level) ? level : "h2";
|
||||
return "<" + tag + " class=\"" + cls + "\">" + esc(text) + "</" + tag + ">";
|
||||
}
|
||||
|
||||
private String renderText(Map<String, Object> config, String cls) {
|
||||
String content = str(config.get("content"), "");
|
||||
return "<div class=\"" + cls + "\">" + content + "</div>";
|
||||
}
|
||||
|
||||
private String renderImage(Map<String, Object> config, String cls) {
|
||||
String src = str(config.get("src"), "");
|
||||
String alt = str(config.get("alt"), "");
|
||||
String caption = str(config.get("caption"), "");
|
||||
String linkTo = str(config.get("linkTo"), "");
|
||||
String img = "<img src=\"" + esc(src) + "\" alt=\"" + esc(alt) + "\" class=\"" + cls + "\" style=\"max-width:100%;height:auto\">";
|
||||
String imgHtml = linkTo.isEmpty() ? img : "<a href=\"" + esc(linkTo) + "\">" + img + "</a>";
|
||||
if (!caption.isEmpty()) {
|
||||
return "<figure style=\"margin:0\">" + imgHtml
|
||||
+ "<figcaption style=\"text-align:center;font-size:0.875rem;color:#6b7280;margin-top:0.5rem\">"
|
||||
+ esc(caption) + "</figcaption></figure>";
|
||||
}
|
||||
return imgHtml;
|
||||
}
|
||||
|
||||
private String renderButton(Map<String, Object> config, String cls) {
|
||||
String text = str(config.get("text"), "Button");
|
||||
String link = str(config.get("link"), "#");
|
||||
String variant = str(config.get("variant"), "primary");
|
||||
String btnStyle = "display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;transition:all 0.2s;cursor:pointer;border:2px solid transparent";
|
||||
btnStyle += switch (variant) {
|
||||
case "outline" -> ";background:transparent;color:var(--color-primary,#4f46e5);border-color:var(--color-primary,#4f46e5)";
|
||||
case "secondary" -> ";background:var(--color-secondary,#6366f1);color:#ffffff";
|
||||
default -> ";background:var(--color-primary,#4f46e5);color:#ffffff";
|
||||
};
|
||||
return "<a href=\"" + esc(link) + "\" class=\"" + cls + "\" style=\"" + btnStyle + "\">" + esc(text) + "</a>";
|
||||
}
|
||||
|
||||
private String renderDivider(Map<String, Object> config, String cls) {
|
||||
String style = str(config.get("style"), "solid");
|
||||
String color = str(config.get("color"), "var(--color-text, #d1d5db)");
|
||||
return "<hr class=\"" + cls + "\" style=\"border:none;border-top:1px " + style + " " + color + "\">";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String renderGallery(Map<String, Object> config, String cls) {
|
||||
List<String> images = (List<String>) config.getOrDefault("images", List.of());
|
||||
int columns = Math.min(Math.max(toInt(config.get("columns"), 3), 1), 6);
|
||||
if (images.isEmpty()) {
|
||||
return "<div class=\"" + cls + "\">Gallery (no images)</div>";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<div class=\"").append(cls).append("\" style=\"display:grid;grid-template-columns:repeat(")
|
||||
.append(columns).append(",1fr);gap:1rem\">\n");
|
||||
for (String src : images) {
|
||||
sb.append(" <img src=\"").append(esc(src))
|
||||
.append("\" style=\"width:100%;height:250px;object-fit:cover;border-radius:8px\">\n");
|
||||
}
|
||||
sb.append(" </div>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String renderVideo(Map<String, Object> config, String cls) {
|
||||
String src = str(config.get("src"), "");
|
||||
boolean controls = config.get("controls") != Boolean.FALSE;
|
||||
boolean autoplay = config.get("autoplay") == Boolean.TRUE;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<video src=\"").append(esc(src)).append("\" class=\"").append(cls)
|
||||
.append("\"");
|
||||
if (controls) sb.append(" controls");
|
||||
if (autoplay) sb.append(" autoplay");
|
||||
sb.append(" muted style=\"max-width:100%\"></video>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String renderContactForm(Map<String, Object> config, String cls) {
|
||||
List<String> fields = (List<String>) config.getOrDefault("fields", List.of("name", "email", "message"));
|
||||
String submitText = str(config.get("submitText"), "Send");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<form class=\"").append(cls).append("\">\n");
|
||||
for (String field : fields) {
|
||||
sb.append(" <div style=\"margin-bottom:1rem\">\n");
|
||||
sb.append(" <label style=\"display:block;font-size:0.875rem;font-weight:600;margin-bottom:0.375rem;color:var(--color-text,#374151)\">")
|
||||
.append(esc(capitalize(field))).append("</label>\n");
|
||||
if ("message".equals(field)) {
|
||||
sb.append(" <textarea rows=\"4\" style=\"width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none\" onfocus=\"this.style.borderColor='var(--color-primary,#4f46e5)'\" onblur=\"this.style.borderColor='#d1d5db'\"></textarea>\n");
|
||||
} else {
|
||||
String inputType = "email".equals(field) ? "email" : "text";
|
||||
sb.append(" <input type=\"").append(inputType)
|
||||
.append("\" style=\"width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none\" onfocus=\"this.style.borderColor='var(--color-primary,#4f46e5)'\" onblur=\"this.style.borderColor='#d1d5db'\">\n");
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
}
|
||||
sb.append(" <button type=\"submit\" style=\"padding:0.75rem 2rem;background:var(--color-primary,#4f46e5);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:1rem;font-weight:500;transition:opacity 0.2s\" onmouseover=\"this.style.opacity='0.9'\" onmouseout=\"this.style.opacity='1'\">")
|
||||
.append(esc(submitText)).append("</button>\n");
|
||||
sb.append(" </form>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String renderMap(Map<String, Object> config, String cls) {
|
||||
String address = str(config.get("address"), "New York, NY");
|
||||
return "<div class=\"" + cls + "\" style=\"display:flex;align-items:center;justify-content:center;background:#f3f4f6\">\n"
|
||||
+ " <span style=\"color:#9ca3af\">Map: " + esc(address) + "</span>\n"
|
||||
+ " </div>";
|
||||
}
|
||||
|
||||
private String renderCustomHtml(Map<String, Object> config, String cls) {
|
||||
String html = str(config.get("html"), "");
|
||||
return "<div class=\"" + cls + "\">" + html + "</div>";
|
||||
}
|
||||
|
||||
private List<Section> buildSections(List<SiteStructure.ComponentStructure> components) {
|
||||
List<Section> sections = new ArrayList<>();
|
||||
if (components == null) return sections;
|
||||
for (int i = 0; i < components.size(); i++) {
|
||||
SiteStructure.ComponentStructure c = components.get(i);
|
||||
String bg = "";
|
||||
if (c.getStyles() != null && c.getStyles().get("backgroundColor") != null) {
|
||||
bg = String.valueOf(c.getStyles().get("backgroundColor"));
|
||||
}
|
||||
if ((bg != null && !bg.isEmpty()) || i == 0) {
|
||||
sections.add(new Section(bg, new ArrayList<>()));
|
||||
}
|
||||
sections.get(sections.size() - 1).indices.add(i);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
private String cssProperty(String key) {
|
||||
return key.replaceAll("([a-z])([A-Z])", "$1-$2").toLowerCase();
|
||||
}
|
||||
|
||||
private String esc(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
private String capitalize(String s) {
|
||||
if (s == null || s.isEmpty()) return s;
|
||||
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
||||
}
|
||||
|
||||
private String str(Object value, String defaultValue) {
|
||||
return value != null ? String.valueOf(value) : defaultValue;
|
||||
}
|
||||
|
||||
private int toInt(Object value, int defaultValue) {
|
||||
if (value instanceof Number n) return n.intValue();
|
||||
if (value instanceof String s) {
|
||||
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return defaultValue; }
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private record Section(String bg, List<Integer> indices) {}
|
||||
|
||||
public record SiteOutput(String css, String js, Map<String, String> pages) {
|
||||
public Map<String, byte[]> asFileMap() {
|
||||
Map<String, byte[]> files = new LinkedHashMap<>();
|
||||
@@ -321,7 +99,7 @@ public class RenderService {
|
||||
for (Map.Entry<String, String> entry : pages.entrySet()) {
|
||||
String slug = entry.getKey();
|
||||
String html = entry.getValue();
|
||||
if (slug == null || slug.isEmpty() || "home".equals(slug)) {
|
||||
if (slug == null || slug.isEmpty() || "home".equals(slug) || "index".equals(slug)) {
|
||||
files.put("index.html", html.getBytes());
|
||||
} else {
|
||||
files.put(slug + "/index.html", html.getBytes());
|
||||
|
||||
Reference in New Issue
Block a user