aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-10-07 13:47:34 -0700
committerPinapelz <yukais@pinapelz.com>2025-10-07 13:49:40 -0700
commit539ef6568697c9dc1712fc7a2a379d4e3fb9235c (patch)
tree7335354113834d9db965fb60d41d81c36ec09d42
Initial commit
-rw-r--r--.gitignore5
-rw-r--r--CMakeLists.txt40
-rw-r--r--README.md4
-rw-r--r--src/main.cpp87
-rw-r--r--src/markdown_translator.cpp264
-rw-r--r--src/markdown_translator.h35
-rw-r--r--styles/ffxiv-style.css649
7 files changed, 1084 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7929f93
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+build/
+*.o
+*.exe
+*.out
+sample.md
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..7c6221e
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.16)
+project(FFXIV-MD-GENERATOR VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+if (MSVC)
+ add_compile_options(/W4 /permissive-)
+else()
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# Debug information
+message(STATUS "Source directory: ${CMAKE_SOURCE_DIR}")
+message(STATUS "Binary directory: ${CMAKE_BINARY_DIR}")
+message(STATUS "Source styles dir: ${CMAKE_SOURCE_DIR}/styles")
+message(STATUS "Target styles dir: ${CMAKE_BINARY_DIR}/styles")
+
+# Main executable links to the library
+add_executable(${PROJECT_NAME}
+ src/main.cpp
+ src/markdown_translator.cpp
+)
+# target_link_libraries(${PROJECT_NAME} PRIVATE mylib)
+
+# Create a custom target that will always be built
+add_custom_target(copy_styles ALL
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ ${CMAKE_SOURCE_DIR}/styles
+ ${CMAKE_BINARY_DIR}/styles
+ COMMENT "Copying styles directory to build directory"
+ VERBATIM
+)
+
+# Make sure the styles are copied before the main executable is built
+add_dependencies(${PROJECT_NAME} copy_styles)
+
+# Create build/styles directory if it doesn't exist (as a fallback)
+file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/styles)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..97590d4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# Wiki-MD2HTML (WIP)
+A basic C++ parser that converts a specialized format of Markdown to a themed HTML style.
+
+Designed for presenting information on-screen in a more "markdown-like" Wiki format
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..caee08c
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,87 @@
+#include <iostream>
+#include <fstream>
+#include <string>
+#include <sstream>
+#include <unordered_map>
+#include "markdown_translator.h"
+
+bool parseArguments(int argc, char* argv[], std::unordered_map<std::string, std::string>& params, std::string& inputFile) {
+ if (argc < 2) {
+ std::cerr << "Usage: COMMAND <input_file> [-o <output_file>] [-css <css_file_path>] [other options]\n";
+ return false;
+ }
+ params["-o"] = "index.html";
+ params["-css"] = "styles/ffxiv-style.css";
+ for (int i = 1; i < argc; ++i) {
+ std::string arg = argv[i];
+ if (arg[0] == '-') {
+ if (i + 1 < argc) {
+ params[arg] = argv[i + 1];
+ ++i;
+ } else {
+ std::cerr << "Error: Missing value for option " << arg << "\n";
+ return false;
+ }
+ } else {
+ if (inputFile.empty()) {
+ inputFile = arg;
+ } else {
+ std::cerr << "Error: Multiple input files specified: " << inputFile << " and " << arg << "\n";
+ return false;
+ }
+ }
+ }
+
+ if (inputFile.empty()) {
+ std::cerr << "Error: No input file specified\n";
+ return false;
+ }
+
+ return true;
+}
+
+int main(int argc, char* argv[]) {
+ std::unordered_map<std::string, std::string> params;
+ std::string inputFile;
+
+ if (!parseArguments(argc, argv, params, inputFile)) {
+ return 1;
+ }
+
+ std::ifstream file(inputFile);
+ if (!file) {
+ std::cerr << "Error: Could not open file " << inputFile << "\n";
+ return 1;
+ }
+
+ // Read entire file content
+ std::stringstream buffer;
+ buffer << file.rdbuf();
+ std::string markdownContent = buffer.str();
+ file.close();
+
+ // Create translator and convert markdown to HTML
+ MarkdownTranslator translator;
+
+ // Get CSS path from parameters or use default
+ std::string cssPath = "styles/ffxiv-style.css";
+ if (params.find("-css") != params.end()) {
+ cssPath = params["-css"];
+ }
+
+ std::string htmlOutput = translator.translate(markdownContent, cssPath, "XIV Lore");
+
+ // Write to output file
+ std::string outputFile = params["-o"];
+ std::ofstream outFile(outputFile);
+ if (!outFile) {
+ std::cerr << "Error: Could not open output file " << outputFile << "\n";
+ return 1;
+ }
+
+ outFile << htmlOutput;
+ outFile.close();
+
+ std::cout << "HTML output written to " << outputFile << std::endl;
+ return 0;
+}
diff --git a/src/markdown_translator.cpp b/src/markdown_translator.cpp
new file mode 100644
index 0000000..d362f82
--- /dev/null
+++ b/src/markdown_translator.cpp
@@ -0,0 +1,264 @@
+#include "markdown_translator.h"
+#include <regex>
+#include <iostream>
+#include <algorithm>
+
+MarkdownTranslator::MarkdownTranslator(){
+}
+
+MarkdownTranslator::~MarkdownTranslator() {
+}
+
+std::string MarkdownTranslator::translate(const std::string& markdownContent, const std::string& cssPath, const std::string& title) {
+ std::stringstream htmlOutput;
+ std::stringstream markdownStream(markdownContent);
+ std::string line;
+ std::vector<std::string> headers;
+
+ std::string currentLine;
+
+
+ while (std::getline(markdownStream, currentLine)) {
+ std::regex headerRegex("^(#{1,3})\\s+(.*)$");
+ std::smatch matches;
+ if (std::regex_match(currentLine, matches, headerRegex)) {
+ int level = matches[1].length();
+ std::string content = matches[2];
+ if (level <= 3) {
+ headers.push_back(std::to_string(level) + ":" + content);
+ }
+ }
+ }
+
+ // Reset the stream to start over
+ markdownStream.clear();
+ markdownStream.str(markdownContent);
+
+ // Start with basic HTML structure
+ htmlOutput << "<!DOCTYPE html>\n";
+ htmlOutput << "<html lang=\"en\">\n";
+ htmlOutput << "<head>\n";
+ htmlOutput << " <meta charset=\"UTF-8\">\n";
+ htmlOutput << " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n";
+ htmlOutput << " <title>" + title + "</title>\n";
+ htmlOutput << " <link rel=\"stylesheet\" href=\"" << cssPath << "\">\n";
+ htmlOutput << "</head>\n";
+ htmlOutput << "<body>\n";
+
+ // Add navigation sidebar
+ generateSideBar(htmlOutput, headers, title);
+
+ // Main content container
+ htmlOutput << " <div class=\"main-content\">\n";
+ htmlOutput << " <div class=\"container\">\n";
+
+ // Process each line of markdown
+
+ // State of current parse
+ bool inFigureBlock{false};
+ std::vector<std::string> figureLines;
+
+ while (std::getline(markdownStream, line)) {
+ if(line.find(":::figure") == 0){
+ inFigureBlock = true;
+ figureLines.clear();
+ continue;
+ }
+
+ if(inFigureBlock && line == ":::"){
+ inFigureBlock = false;
+ htmlOutput << " " << processFigureBlock(figureLines) << "\n";
+ continue;
+ }
+ else if(inFigureBlock){
+ figureLines.push_back(line);
+ continue;
+ }
+
+ htmlOutput << " " << processLine(line);
+ }
+
+ // Add article meta information section
+ htmlOutput << " <div class=\"article-meta\">\n";
+ htmlOutput << " <p>Last updated: " << getCurrentDateTime() << "</p>\n";
+ htmlOutput << " </div>\n";
+
+ htmlOutput << " </div>\n";
+ htmlOutput << " </div>\n";
+ htmlOutput << "</body>\n";
+ htmlOutput << "</html>\n";
+ return htmlOutput.str();
+}
+
+void MarkdownTranslator::generateSideBar(std::stringstream& output, const std::vector<std::string>& headers, const std::string& title) {
+ output << " <div class=\"nav-sidebar\">\n";
+ output << " <div class=\"nav-logo\">\n";
+ output << " <h3>" + title + "</h3>\n";
+ output << " </div>\n";
+ output << " <ul class=\"nav-menu\">\n";
+
+ output << " <h4>Table of Contents</h4>\n";
+ output << " <li><a href=\"index.html\">Home</a></li>\n";
+ output << " <li><a href=\"#\">Recent Changes</a></li>\n";
+ output << " <li><a href=\"#\">Random Page</a></li>\n";
+ output << " <ul class=\"nav-submenu\">\n";
+
+ // Generate navigation from headers
+ for (const auto& header : headers) {
+ size_t separatorPos = header.find(':');
+ if (separatorPos != std::string::npos) {
+ int level = std::stoi(header.substr(0, separatorPos));
+ std::string content = header.substr(separatorPos + 1);
+
+ // Create anchor ID from header content
+ std::string anchorId = createAnchorId(content);
+
+ // Add indentation based on header level
+ std::string indentation = "";
+ for (int i = 1; i < level; ++i) {
+ indentation += " ";
+ }
+
+ output << " " << indentation << "<li><a href=\"#" << anchorId << "\">" << content << "</a></li>\n";
+ }
+ }
+
+ output << " </ul>\n";
+ output << " </ul>\n";
+ output << " </div>\n";
+}
+
+std::string MarkdownTranslator::createAnchorId(const std::string& text) {
+ std::string id = text;
+ // Convert to lowercase
+ std::transform(id.begin(), id.end(), id.begin(), ::tolower);
+
+ // Replace spaces with hyphens
+ std::replace(id.begin(), id.end(), ' ', '-');
+
+ // Remove any non-alphanumeric characters except hyphens
+ id.erase(
+ std::remove_if(
+ id.begin(),
+ id.end(),
+ [](char c) { return !(std::isalnum(c) || c == '-'); }
+ ),
+ id.end()
+ );
+
+ return id;
+}
+
+std::string MarkdownTranslator::getCurrentDateTime() {
+ std::time_t now = std::time(nullptr);
+ std::tm* localTime = std::localtime(&now);
+
+ char buffer[80];
+ std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime);
+
+ return std::string(buffer);
+}
+
+std::string MarkdownTranslator::processLine(const std::string& line) {
+ std::string processed = line;
+
+ // Check for headers first (they start at beginning of line)
+ processed = processHeaders(processed);
+
+ // If it wasn't a header, process inline elements
+ if (processed == line) {
+ processed = processBold(processed);
+ processed = processItalic(processed);
+ processed = processLinks(processed);
+
+ // Wrap in paragraph tags if it's regular text
+ if (!processed.empty() && processed[0] != '<') {
+ processed = processParagraph(processed);
+ }
+ }
+
+ return processed + "\n";
+}
+
+std::string MarkdownTranslator::processHeaders(const std::string& line) {
+ // Check for H1-H6
+ std::regex headerRegex("^(#{1,6})\\s+(.*)$");
+ std::smatch matches;
+ if (std::regex_match(line, matches, headerRegex)) {
+ int level = matches[1].length();
+ std::string content = matches[2];
+ std::string anchorId = createAnchorId(content);
+ return "<h" + std::to_string(level) + " id=\"" + anchorId + "\">" + content + "</h" + std::to_string(level) + ">";
+ }
+
+ return line;
+}
+
+std::string MarkdownTranslator::processBold(const std::string& text) {
+ // Replace **text** or __text__ with <strong>text</strong>
+ std::string result = text;
+ std::regex boldRegex("\\*\\*([^\\*]+)\\*\\*|__([^_]+)__");
+ result = std::regex_replace(result, boldRegex, "<strong>$1$2</strong>");
+ return result;
+}
+
+std::string MarkdownTranslator::processItalic(const std::string& text) {
+ // Replace *text* or _text_ with <em>text</em>
+ std::string result = text;
+ std::regex italicRegex("\\*([^\\*]+)\\*|_([^_]+)_");
+ result = std::regex_replace(result, italicRegex, "<em>$1$2</em>");
+ return result;
+}
+
+std::string MarkdownTranslator::processLinks(const std::string& text) {
+ // Replace [text](url) with <a href="url">text</a>
+ std::string result = text;
+ std::regex linkRegex("\\[([^\\]]+)\\]\\(([^\\)]+)\\)");
+ result = std::regex_replace(result, linkRegex, "<a href=\"$2\">$1</a>");
+ return result;
+}
+
+std::string MarkdownTranslator::processParagraph(const std::string& text) {
+ if (text.empty()) return "";
+ return "<p>" + text + "</p>";
+}
+
+std::string MarkdownTranslator::processSingleFigure(const std::string& text) {
+ // Extract image details using regex
+ std::regex imageRegex("!\\[(.*?)\\]\\(([^\\s\"]+)(\\s+\"(.*?)\")?\\)");
+ std::smatch matches;
+
+ if (std::regex_search(text, matches, imageRegex)) {
+ std::string alt = matches[1].str();
+ std::string src = matches[2].str();
+ std::string title = matches.size() > 4 && matches[4].matched ? " title=\"" + matches[4].str() + "\"" : "";
+ // Create the HTML for the image
+ return "<img src=\"" + src + "\" alt=\"" + alt + "\"" + title + ">";
+ }
+ return text;
+}
+
+std::string MarkdownTranslator::processFigureBlock(const std::vector<std::string>& lines){
+ std::stringstream html;
+ std::string imageHtml;
+ std::string caption;
+ html << "<figure class=\"custom-figure\">\n";
+
+ // Process each line in the figure block
+ for (const auto& line : lines) {
+ if (line.find("![") == 0) {
+ // Process the image
+ imageHtml = processSingleFigure(line);
+ html << " " << imageHtml << "\n";
+ } else if (line.find("Caption:") == 0) {
+ // Extract caption
+ caption = line.substr(8);
+ html << " <figcaption>" << caption << "</figcaption>\n";
+ } else if (!line.empty()) {
+ // Process any other content
+ html << " <p>" << line << "</p>\n";
+ }
+ }
+ html << "</figure>";
+ return html.str();
+}
diff --git a/src/markdown_translator.h b/src/markdown_translator.h
new file mode 100644
index 0000000..6d24ac7
--- /dev/null
+++ b/src/markdown_translator.h
@@ -0,0 +1,35 @@
+#ifndef MARKDOWN_TRANSLATOR_H
+#define MARKDOWN_TRANSLATOR_H
+
+#include <string>
+#include <vector>
+#include <sstream>
+#include <ctime>
+
+class MarkdownTranslator {
+public:
+ // Constructor
+ MarkdownTranslator();
+ // Destructor
+ ~MarkdownTranslator();
+ // Main translation function - takes markdown content and returns HTML
+ std::string translate(const std::string& markdownContent, const std::string& cssPath = "styles/ffxiv-style.css", const std::string& title = "Title");
+ std::string processLine(const std::string& line);
+
+private:
+ // Helper functions for different markdown elements
+ std::string processHeaders(const std::string& line);
+ std::string processBold(const std::string& text);
+ std::string processItalic(const std::string& text);
+ std::string processLinks(const std::string& text);
+ std::string processParagraph(const std::string& text);
+ std::string processSingleFigure(const std::string& text);
+ std::string processFigureBlock(const std::vector<std::string>& lines);
+ // Navigation and table of contents
+ void generateSideBar(std::stringstream& output, const std::vector<std::string>& headers, const std::string& title);
+ std::string createAnchorId(const std::string& text);
+ // Utility functions
+ std::string getCurrentDateTime();
+};
+
+#endif // MARKDOWN_TRANSLATOR_H
diff --git a/styles/ffxiv-style.css b/styles/ffxiv-style.css
new file mode 100644
index 0000000..2b1c0e9
--- /dev/null
+++ b/styles/ffxiv-style.css
@@ -0,0 +1,649 @@
+/* FFXIV Wiki-inspired CSS */
+/* Base typography and colors inspired by the Lodestone and Wiki style */
+
+:root {
+ /* Color palette inspired by FFXIV UI */
+ --primary-bg: #1a1a1a;
+ --secondary-bg: #252525;
+ --tertiary-bg: #2d2d2d;
+ --accent-gold: #c9aa71;
+ --accent-blue: #5b8fc7;
+ --text-primary: #e2e2e2;
+ --text-secondary: #b8b8b8;
+ --text-dim: #888888;
+ --border-color: #3a3a3a;
+ --link-color: #7db8e8;
+ --link-hover: #a5d0f0;
+ --header-gradient-start: #2a2a2a;
+ --header-gradient-end: #1a1a1a;
+ --code-bg: #0d0d0d;
+ --nav-bg: #1f1f1f;
+ --nav-hover: #333333;
+}
+
+/* Reset and base styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Meiryo", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ background-color: var(--primary-bg);
+ color: var(--text-primary);
+ line-height: 1.7;
+ min-height: 100vh;
+ display: flex;
+ background-image: repeating-linear-gradient(
+ 45deg,
+ transparent,
+ transparent 10px,
+ rgba(255, 255, 255, 0.01) 10px,
+ rgba(255, 255, 255, 0.01) 20px
+ );
+}
+
+/* Navigation sidebar */
+.nav-sidebar {
+ width: 250px;
+ background-color: var(--nav-bg);
+ border-right: 1px solid var(--border-color);
+ height: 100vh;
+ position: fixed;
+ overflow-y: auto;
+ padding: 20px 0;
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
+}
+
+.nav-logo {
+ padding: 15px 20px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid var(--border-color);
+ text-align: center;
+}
+
+.nav-logo h3 {
+ color: var(--accent-gold);
+ font-size: 1.2em;
+ margin: 0;
+}
+
+.nav-menu {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.nav-menu li {
+ margin: 0;
+}
+
+.nav-menu a {
+ display: block;
+ padding: 10px 20px;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-left: 3px solid transparent;
+ transition: all 0.3s ease;
+}
+
+.nav-menu a:hover {
+ background-color: var(--nav-hover);
+ color: var(--accent-gold);
+ border-left-color: var(--accent-gold);
+ text-shadow: none;
+}
+
+.nav-menu a::after {
+ display: none;
+}
+
+.nav-menu h4 {
+ padding: 15px 20px 5px;
+ color: var(--accent-gold);
+ font-size: 1em;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.nav-submenu {
+ list-style-type: none;
+ padding-left: 15px;
+}
+
+/* Main content */
+.main-content {
+ flex: 1;
+ margin-left: 250px;
+ width: calc(100% - 250px);
+}
+
+/* Container */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 40px 20px;
+ background-color: rgba(26, 26, 26, 0.95);
+ min-height: 100vh;
+ box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
+}
+
+/* Headers with FFXIV styling */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Jupiter Pro", "Cinzel", "Georgia", serif;
+ color: var(--accent-gold);
+ margin-top: 1.5em;
+ margin-bottom: 0.8em;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ position: relative;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+h1 {
+ font-size: 2.2em;
+ padding-bottom: 0.5em;
+ border-bottom: 3px solid var(--accent-gold);
+ background: linear-gradient(90deg, var(--accent-gold), transparent 70%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ text-shadow: none;
+ margin-top: 0.5em;
+}
+
+h1::after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: -3px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: linear-gradient(90deg, var(--accent-gold), transparent 70%);
+}
+
+h2 {
+ font-size: 1.8em;
+ color: var(--accent-blue);
+ padding-left: 15px;
+ border-left: 4px solid var(--accent-blue);
+ margin-left: -15px;
+}
+
+h3 {
+ font-size: 1.4em;
+ color: var(--text-primary);
+}
+
+h4 {
+ font-size: 1.2em;
+ color: var(--text-secondary);
+}
+
+h5,
+h6 {
+ font-size: 1.1em;
+ color: var(--text-secondary);
+}
+
+/* Paragraphs and text */
+p {
+ margin-bottom: 1.2em;
+ color: var(--text-secondary);
+ text-align: left;
+}
+
+/* Links with FFXIV styling */
+a {
+ color: var(--link-color);
+ text-decoration: none;
+ position: relative;
+ transition: color 0.3s ease;
+ font-weight: 500;
+}
+
+a:hover {
+ color: var(--link-hover);
+ text-shadow: 0 0 10px rgba(125, 184, 232, 0.5);
+}
+
+a::after {
+ content: "";
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 0;
+ height: 1px;
+ background-color: var(--link-hover);
+ transition: width 0.3s ease;
+}
+
+a:hover::after {
+ width: 100%;
+}
+
+/* Bold and Italic */
+strong,
+b {
+ color: var(--accent-gold);
+ font-weight: 600;
+}
+
+em,
+i {
+ color: var(--text-primary);
+ font-style: italic;
+}
+
+/* Lists with FFXIV styling */
+ul,
+ol {
+ margin-left: 30px;
+ margin-bottom: 1.2em;
+}
+
+li {
+ margin-bottom: 0.5em;
+ color: var(--text-secondary);
+}
+
+ul li::marker {
+ color: var(--accent-gold);
+}
+
+ol li::marker {
+ color: var(--accent-blue);
+ font-weight: bold;
+}
+
+/* Code blocks with FFXIV terminal style */
+pre {
+ background-color: var(--code-bg);
+ border: 1px solid var(--border-color);
+ border-left: 4px solid var(--accent-blue);
+ padding: 15px;
+ margin: 1.5em 0;
+ overflow-x: auto;
+ font-family: "Consolas", "Monaco", monospace;
+ font-size: 0.9em;
+ line-height: 1.5;
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5);
+}
+
+code {
+ background-color: var(--code-bg);
+ color: var(--accent-blue);
+ padding: 3px 6px;
+ border-radius: 3px;
+ font-family: "Consolas", "Monaco", monospace;
+ font-size: 0.9em;
+ border: 1px solid var(--border-color);
+}
+
+pre code {
+ background-color: transparent;
+ color: var(--text-primary);
+ padding: 0;
+ border: none;
+}
+
+/* Blockquotes with FFXIV quest text styling */
+blockquote {
+ border-left: 4px solid var(--accent-gold);
+ background: linear-gradient(90deg, rgba(201, 170, 113, 0.1), transparent);
+ padding: 15px 20px;
+ margin: 1.5em 0;
+ font-style: italic;
+ color: var(--text-secondary);
+ position: relative;
+}
+
+blockquote::before {
+ content: '"';
+ font-size: 3em;
+ color: var(--accent-gold);
+ opacity: 0.3;
+ position: absolute;
+ top: -10px;
+ left: 10px;
+}
+
+/* Tables with FFXIV UI styling */
+table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin: 1.5em 0;
+ background-color: var(--secondary-bg);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+}
+
+thead {
+ background: linear-gradient(
+ 180deg,
+ var(--tertiary-bg),
+ var(--secondary-bg)
+ );
+}
+
+th {
+ padding: 12px 15px;
+ text-align: left;
+ color: var(--accent-gold);
+ font-weight: 600;
+ border-bottom: 2px solid var(--accent-gold);
+ text-transform: uppercase;
+ font-size: 0.9em;
+ letter-spacing: 1px;
+}
+
+td {
+ padding: 10px 15px;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+tr:hover td {
+ background-color: rgba(201, 170, 113, 0.05);
+}
+
+/* Horizontal rules with FFXIV ornamental style */
+hr {
+ border: none;
+ height: 2px;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ var(--accent-gold) 20%,
+ var(--accent-gold) 50%,
+ var(--accent-gold) 80%,
+ transparent
+ );
+ margin: 2em 0;
+ position: relative;
+}
+
+hr::before {
+ content: "◆";
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ background-color: var(--primary-bg);
+ color: var(--accent-gold);
+ padding: 0 10px;
+ font-size: 1.2em;
+}
+
+/* Images */
+img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 1.5em auto;
+ border: 2px solid var(--border-color);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
+}
+
+/* Wiki-style additional elements */
+.wiki-infobox {
+ float: right;
+ width: 300px;
+ margin: 0 0 20px 20px;
+ background-color: var(--secondary-bg);
+ border: 1px solid var(--border-color);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+}
+
+.wiki-infobox-header {
+ background-color: var(--tertiary-bg);
+ color: var(--accent-gold);
+ padding: 10px 15px;
+ text-align: center;
+ font-weight: bold;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.wiki-infobox-content {
+ padding: 15px;
+}
+
+.wiki-infobox-row {
+ display: flex;
+ margin-bottom: 10px;
+}
+
+.wiki-infobox-label {
+ flex: 1;
+ font-weight: bold;
+ color: var(--accent-blue);
+}
+
+.wiki-infobox-value {
+ flex: 2;
+ color: var(--text-secondary);
+}
+
+/* Article meta information */
+.article-meta {
+ border-top: 1px solid var(--border-color);
+ margin-top: 40px;
+ padding-top: 20px;
+ font-size: 0.9em;
+ color: var(--text-dim);
+}
+
+.article-categories {
+ margin-top: 10px;
+}
+
+.article-category {
+ display: inline-block;
+ background-color: var(--tertiary-bg);
+ color: var(--accent-gold);
+ padding: 3px 10px;
+ margin-right: 5px;
+ margin-bottom: 5px;
+ border-radius: 3px;
+ font-size: 0.85em;
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 12px;
+ height: 12px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--secondary-bg);
+ border: 1px solid var(--border-color);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--accent-gold);
+ border: 1px solid var(--border-color);
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-blue);
+}
+
+/* Selection styling */
+::selection {
+ background-color: rgba(201, 170, 113, 0.3);
+ color: var(--text-primary);
+}
+
+::-moz-selection {
+ background-color: rgba(201, 170, 113, 0.3);
+ color: var(--text-primary);
+}
+
+/* Custom Figure styling */
+.custom-figure {
+ margin: 2.5rem auto;
+ padding: 1.5rem;
+ background-color: var(--secondary-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
+ position: relative;
+ overflow: hidden;
+ max-width: 95%;
+}
+
+/* Image gallery layout for multiple images */
+.custom-figure .image-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ grid-gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+/* Single image in figure */
+.custom-figure img {
+ margin: 0 auto 1rem;
+ border: 1px solid var(--border-color);
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
+ transition:
+ transform 0.3s ease,
+ box-shadow 0.3s ease;
+ max-height: 500px;
+ width: auto;
+}
+
+/* Hover effect for images */
+.custom-figure img:hover {
+ transform: scale(1.02);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+ border-color: var(--accent-gold);
+}
+
+/* Image titles (from title attribute) */
+.custom-figure .image-title {
+ display: block;
+ text-align: center;
+ font-style: italic;
+ color: var(--accent-gold);
+ font-size: 0.9em;
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+/* Caption styling */
+.custom-figure figcaption {
+ font-family: "Jupiter Pro", "Cinzel", "Georgia", serif;
+ text-align: center;
+ color: var(--text-primary);
+ font-size: 1.1em;
+ padding: 0.8rem 0;
+ border-top: 1px solid rgba(201, 170, 113, 0.3);
+ margin-top: 0.5rem;
+ position: relative;
+}
+
+/* Special styling for captions to match FFXIV aesthetic */
+.custom-figure figcaption::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 4px;
+ background: linear-gradient(90deg, var(--accent-gold), var(--accent-blue));
+}
+
+/* Responsive design */
+@media (max-width: 1200px) {
+ .container {
+ max-width: 100%;
+ }
+}
+
+@media (max-width: 992px) {
+ .nav-sidebar {
+ width: 200px;
+ }
+
+ .main-content {
+ margin-left: 200px;
+ width: calc(100% - 200px);
+ }
+}
+
+@media (max-width: 768px) {
+ body {
+ flex-direction: column;
+ }
+
+ .nav-sidebar {
+ width: 100%;
+ height: auto;
+ position: relative;
+ border-right: none;
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ .main-content {
+ margin-left: 0;
+ width: 100%;
+ }
+
+ .container {
+ padding: 20px 15px;
+ }
+
+ .wiki-infobox {
+ float: none;
+ width: 100%;
+ margin: 1.5em 0;
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ h2 {
+ font-size: 1.6em;
+ }
+
+ h3 {
+ font-size: 1.3em;
+ }
+}
+
+/* Animation for page load */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ .custom-figure {
+ padding: 1rem;
+ margin: 2rem auto;
+ }
+
+ .custom-figure .image-gallery {
+ grid-template-columns: 1fr;
+ }
+
+ .custom-figure img {
+ max-height: 350px;
+ }
+}
+
+.main-content > * {
+ animation: fadeIn 0.6s ease-out;
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage