Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions frontend/node_modules/.package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 27 additions & 7 deletions frontend/src/pages/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { motion } from 'framer-motion';

const Portfolio = () => {
const projects = [
{
title: 'DTE Rates for Home Assistant',
description: 'A Home Assistant custom integration that pulls the official DTE residential electric rate card PDF, parses rates dynamically, and exposes import/export price entities that track time-of-day and season.',
tech: ['Python', 'Home Assistant', 'HACS', 'Lovelace'],
gradient: 'from-yellow-500 to-orange-500',
link: 'https://github.com/javaDevJT/DTE-Rates-for-Home-Assistant'
},
{
title: 'Interactive Dashboard',
description: 'A sleek dashboard for monitoring automotive performance data with real-time visualizations.',
Expand Down Expand Up @@ -81,13 +88,26 @@ const Portfolio = () => {
</span>
))}
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-6 py-2 rounded-full font-semibold shadow-lg hover:shadow-xl transition"
>
View Project
</motion.button>
{project.link ? (
<motion.a
href={project.link}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-block bg-gradient-to-r from-blue-500 to-purple-600 text-white px-6 py-2 rounded-full font-semibold shadow-lg hover:shadow-xl transition"
>
View Project
</motion.a>
) : (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-6 py-2 rounded-full font-semibold shadow-lg hover:shadow-xl transition"
>
View Project
</motion.button>
)}
</div>
</motion.div>
))}
Expand Down
108 changes: 93 additions & 15 deletions src/main/java/com/jtdev/website/service/ContentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.jtdev.website.model.BlogMetadata;
import com.jtdev.website.model.PortfolioMetadata;
import com.vladsch.flexmark.ext.tables.TablesExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
Expand Down Expand Up @@ -72,6 +73,7 @@ public String getMarkdownContent(String path) throws IOException {

// Parse markdown and convert to ASCII-friendly format
MutableDataSet options = new MutableDataSet();
options.set(Parser.EXTENSIONS, java.util.Arrays.asList(TablesExtension.create()));
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();

Expand All @@ -88,9 +90,12 @@ public String getMarkdownContent(String path) throws IOException {
private String convertHtmlToAscii(String html, String dir) {
// First pass: Convert headers with dynamic borders
html = convertDynamicHeaders(html);

html = formatNestedLists(html);

// Convert tables before the general regex pass
html = convertTablesToAscii(html);

// Convert HTML elements to ASCII formatting
String text = html
// Headers already converted above
Expand Down Expand Up @@ -124,17 +129,7 @@ private String convertHtmlToAscii(String html, String dir) {

// Bold and italic
.replaceAll("<strong[^>]*>([^<]*)</strong>", "**$1**")
.replaceAll("<em[^>]*>([^<]*)</em>", "*$1*")

// Tables (simplified ASCII representation)
.replaceAll("<table[^>]*>", "\n┌")
.replaceAll("</table>", "┘\n")
.replaceAll("<tr[^>]*>", "")
.replaceAll("</tr>", "\n├")
.replaceAll("<th[^>]*>", "─ ")
.replaceAll("</th>", " ─")
.replaceAll("<td[^>]*>", "│ ")
.replaceAll("</td>", " │");
.replaceAll("<em[^>]*>([^<]*)</em>", "*$1*");

// Handle images with ASCII art
Pattern imgPattern = Pattern.compile("<img[^>]*src=\"([^\"]*)\"[^>]*>");
Expand All @@ -150,8 +145,8 @@ private String convertHtmlToAscii(String html, String dir) {

// Handle images without src
text = text.replaceAll("<img[^>]*>", "[Image]");
text

text = text
// Remove any remaining HTML tags
.replaceAll("<[^>]+>", "")

Expand All @@ -162,8 +157,7 @@ private String convertHtmlToAscii(String html, String dir) {
.replaceAll("\\n{3,}", "\n\n")
.trim();

// The formatting is already handled in the regex replacements above
return text.trim();
return text;
}

private String formatNestedLists(String html) {
Expand Down Expand Up @@ -250,6 +244,90 @@ int nextIndex() {
}
}

private String convertTablesToAscii(String html) {
Pattern tablePattern = Pattern.compile("<table[^>]*>(.*?)</table>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher tableMatcher = tablePattern.matcher(html);
StringBuffer sb = new StringBuffer();
while (tableMatcher.find()) {
String asciiTable = renderTableAsAscii(tableMatcher.group(1));
tableMatcher.appendReplacement(sb, Matcher.quoteReplacement(asciiTable));
}
tableMatcher.appendTail(sb);
return sb.toString();
}

private String renderTableAsAscii(String tableHtml) {
List<List<String>> rows = new ArrayList<>();

Pattern rowPattern = Pattern.compile("<tr[^>]*>(.*?)</tr>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher rowMatcher = rowPattern.matcher(tableHtml);
while (rowMatcher.find()) {
List<String> cells = new ArrayList<>();
Pattern cellPattern = Pattern.compile("<t[hd][^>]*>(.*?)</t[hd]>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher cellMatcher = cellPattern.matcher(rowMatcher.group(1));
while (cellMatcher.find()) {
String cell = cellMatcher.group(1)
.replaceAll("<[^>]+>", "")
.replaceAll("&amp;", "&").replaceAll("&lt;", "<")
.replaceAll("&gt;", ">").replaceAll("&quot;", "\"")
.replaceAll("&#39;", "'").replaceAll("&nbsp;", " ")
.trim();
cells.add(cell);
}
if (!cells.isEmpty()) rows.add(cells);
}

if (rows.isEmpty()) return "";

int numCols = rows.stream().mapToInt(List::size).max().orElse(0);
int[] colWidths = new int[numCols];
for (List<String> row : rows) {
for (int i = 0; i < row.size(); i++) {
colWidths[i] = Math.max(colWidths[i], row.get(i).length());
}
}

StringBuilder ascii = new StringBuilder("\n");

// Top border
ascii.append("┌");
for (int i = 0; i < numCols; i++) {
ascii.append("─".repeat(colWidths[i] + 2));
ascii.append(i < numCols - 1 ? "┬" : "┐");
}
ascii.append("\n");

for (int r = 0; r < rows.size(); r++) {
List<String> row = rows.get(r);
ascii.append("│");
for (int i = 0; i < numCols; i++) {
String cell = i < row.size() ? row.get(i) : "";
ascii.append(" ").append(cell).append(" ".repeat(colWidths[i] - cell.length() + 1)).append("│");
}
ascii.append("\n");

if (r == 0 && rows.size() > 1) {
// Header separator
ascii.append("├");
for (int i = 0; i < numCols; i++) {
ascii.append("─".repeat(colWidths[i] + 2));
ascii.append(i < numCols - 1 ? "┼" : "┤");
}
ascii.append("\n");
} else if (r == rows.size() - 1) {
// Bottom border
ascii.append("└");
for (int i = 0; i < numCols; i++) {
ascii.append("─".repeat(colWidths[i] + 2));
ascii.append(i < numCols - 1 ? "┴" : "┘");
}
ascii.append("\n");
}
}

return ascii.toString();
}

private String convertDynamicHeaders(String html) {
// Convert H1 headers with dynamic borders
Pattern h1Pattern = Pattern.compile("<h1[^>]*>(.*?)</h1>", Pattern.DOTALL);
Expand Down
Loading