diff --git a/Makefile b/Makefile index 3a8a9c4..e45b2df 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ CXXFLAGS=-Wall -Wextra -g -O0 -std=c++17 .PHONY: clean -$(PROGRAM_NAME): build/ build/obj/ build/obj/main.o build/obj/ConfigUtils.o build/obj/WebsiteBuilder.o build/obj/SwgRuntime.o build/obj/MarkdownParser.o build/obj/Article.o - $(CXXC) -o $(PROGRAM_NAME) build/obj/main.o build/obj/ConfigUtils.o build/obj/WebsiteBuilder.o build/obj/SwgRuntime.o build/obj/MarkdownParser.o build/obj/Article.o +$(PROGRAM_NAME): build/ build/obj/ build/obj/main.o build/obj/ConfigUtils.o build/obj/WebsiteBuilder.o build/obj/SwgRuntime.o build/obj/MarkdownParser.o build/obj/Article.o build/obj/Common.o build/obj/BlogBuilder.o + $(CXXC) -o $(PROGRAM_NAME) build/obj/main.o build/obj/ConfigUtils.o build/obj/WebsiteBuilder.o build/obj/SwgRuntime.o build/obj/MarkdownParser.o build/obj/Article.o build/obj/Common.o build/obj/BlogBuilder.o build/obj/main.o: src/main.cxx src/ConfigUtils.hxx src/SwgContext.hxx src/WebsiteBuilder.hxx src/Common.hxx $(CXXC) $(CXXFLAGS) -o build/obj/main.o -c src/main.cxx @@ -45,6 +45,12 @@ build/obj/MarkdownParser.o: src/MarkdownParser.hxx src/MarkdownParser.cxx build/obj/Article.o: src/Article.hxx src/Article.cxx $(CXXC) $(CXXFLAGS) -o build/obj/Article.o -c src/Article.cxx +build/obj/Common.o: src/Common.hxx src/Common.cxx + $(CXXC) $(CXXFLAGS) -o build/obj/Common.o -c src/Common.cxx + +build/obj/BlogBuilder.o: src/BlogBuilder.hxx src/BlogBuilder.cxx + $(CXXC) $(CXXFLAGS) -o build/obj/BlogBuilder.o -c src/BlogBuilder.cxx + build/: mkdir -p build/ diff --git a/src/BlogBuilder.cxx b/src/BlogBuilder.cxx new file mode 100644 index 0000000..d653b42 --- /dev/null +++ b/src/BlogBuilder.cxx @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 luca0N! + * + * This file is part of Static Website Generator (swg). + * + * Static Website Generator (swg) is free software: you can redistribute it + * and/or modify it under the terms of the version 3 of the GNU Lesser General + * Public License as published by the Free Software Foundation. + * + * Static Website Generator (swg) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Static Website Generator (swg). If not, see + * . + * + * Contact luca0N! by e-mail: . + */ + +#include "BlogBuilder.hxx" + +#include +#include + +#include "Article.hxx" +#include "WebsiteBuilder.hxx" + +std::string blog_relative_path(std::string const &pathPrefix, std::string const &path) { + return path.substr(pathPrefix.length() - 1); +} + +bool is_valid_article(std::string const &pathPrefix, std::string const &path) { + return std::regex_search(blog_relative_path(pathPrefix, path), std::regex("^/\\d{4}/\\d{2}/.*\\.md")); +} +void build_blog_structure(std::string const &path, std::string const &prefix, std::list const &articles, Blog *blog) { + std::filesystem::path obp = get_output_path(path); // Output Blog Path + obp /= "blog"; + obp /= blog->dir; + try { + // Create blog directory + if (!std::filesystem::exists(obp)) std::filesystem::create_directories(obp); + } catch (std::filesystem::filesystem_error const &e) { + std::cerr << "error: failed to create directory for blog \"" << blog->name << "\": " << e.what() << std::endl; + exit(RETURN_FAILED_UNKNOWN_ERROR); + } + + std::list am; + //std::map> sorted_articles; + + for (std::string const &a : articles) { + std::string articlePath = blog_relative_path(prefix, a); + std::regex yearMonth("(\\d+)"); + auto match = std::sregex_iterator(articlePath.begin(), articlePath.end(), yearMonth); + + std::string year = match->str(), + month = (++match)->str(); + + // Go ahead and parse article metadata. + Article::Metadata *articleMetadata = (Article::Metadata*) malloc(sizeof(Article::Metadata)); + Article::get_metadata(a, articleMetadata); + std::cout << "Parsed metadata for article \"" << articleMetadata->title << "\"\n\tPublished on " + << ctime(&(articleMetadata->publish_ts)) << "\n"; + + // TODO: This code could be optimized by removing directory + // checks for every single article. Instead, add + // directory-checks for years and months to a queue (skipping + // existing ones) and then checking and creating the + // directories later as needed. + try { + // Create directory for the year of this article if it doesn't exist. + std::filesystem::path oad = obp, // Output Article Directory + rap; // Relative Article Path + oad /= year; + rap /= year; + if (!std::filesystem::exists(oad)) std::filesystem::create_directory(oad); + + // Do the same for the article month. + oad /= month; + rap /= month; + if (!std::filesystem::exists(oad)) std::filesystem::create_directory(oad); + + // Now create the article file. + std::string new_article_filename = getFilename(a, false); + new_article_filename += ".html"; + oad /= new_article_filename; + rap /= new_article_filename; + compile_markdown(path, a, oad, articleMetadata); + strncpy(articleMetadata->path, rap.c_str(), sizeof(articleMetadata->path)); + am.push_back(articleMetadata); + } catch (std::filesystem::filesystem_error const &e) { + std::cerr << "error: failed to create directory for an article from blog \"" + << blog->name << "\": " << e.what() << std::endl; + exit(RETURN_FAILED_UNKNOWN_ERROR); + } + } + // Sort am list. + am.sort(Article::Comparator::comp); + // Generate blog catalog. + std::string blog_html_catalog, + last_date = ""; + blog_html_catalog += "

"; + blog_html_catalog += blog->name; + blog_html_catalog += " - Catalog

\n
    "; + + for (Article::Metadata *m : am) { + struct tm *article_time = gmtime(&(m->publish_ts)); + std::string hr_date = get_hr_month(article_time->tm_mon); + hr_date += " "; + hr_date += std::to_string(article_time->tm_year + 1900); + // Check if this article belongs to the same "month + year" group as + // the last article. If it doesn't, add a new group to the catalog. + if (hr_date != last_date) { + last_date = hr_date; + blog_html_catalog += "\n\t"; + blog_html_catalog += hr_date; + blog_html_catalog += "\n"; + } + blog_html_catalog += "\t
  • path; + blog_html_catalog += "\">"; + blog_html_catalog += m->title; + blog_html_catalog += "
  • \n"; + // Free memory as it's no longer needed. + free(m); + } + blog_html_catalog += "
"; + // Put catalog HTML into template and save it to the blog directory. + std::filesystem::path catalog_output_path = obp; + catalog_output_path /= "index.html"; + + std::string html_template = get_template(path); + std::regex content_placeholder(""); + std::string catalog_html_contents = std::regex_replace(html_template, content_placeholder, blog_html_catalog); + + FILE *catalog_output_file = fopen(catalog_output_path.c_str(), "w"); + fputs(catalog_html_contents.c_str(), catalog_output_file); + fclose(catalog_output_file); +} + diff --git a/src/BlogBuilder.hxx b/src/BlogBuilder.hxx new file mode 100644 index 0000000..91a369f --- /dev/null +++ b/src/BlogBuilder.hxx @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 luca0N! + * + * This file is part of Static Website Generator (swg). + * + * Static Website Generator (swg) is free software: you can redistribute it + * and/or modify it under the terms of the version 3 of the GNU Lesser General + * Public License as published by the Free Software Foundation. + * + * Static Website Generator (swg) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Static Website Generator (swg). If not, see + * . + * + * Contact luca0N! by e-mail: . + */ + +#pragma once + +#include +#include + +#include "Common.hxx" + +bool is_valid_article(std::string const &pathPrefix, std::string const &path); +void build_blog_structure(std::string const &path, std::string const &prefix, std::list const &articles, Blog *blog); diff --git a/src/Common.cxx b/src/Common.cxx new file mode 100644 index 0000000..170cd45 --- /dev/null +++ b/src/Common.cxx @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 luca0N! + * + * This file is part of Static Website Generator (swg). + * + * Static Website Generator (swg) is free software: you can redistribute it + * and/or modify it under the terms of the version 3 of the GNU Lesser General + * Public License as published by the Free Software Foundation. + * + * Static Website Generator (swg) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Static Website Generator (swg). If not, see + * . + * + * Contact luca0N! by e-mail: . + */ + +#include "Common.hxx" + +#include + +const char* HR_MONTH[] = { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" +}; + +const char* get_hr_month(unsigned short month) { + return HR_MONTH[month]; +} + +std::string getFilename(std::string const &path, bool const ext) { + int extSeparator = -1; + // Backwards search for directory separator. + for (int x = path.length(); x > 0; --x) { + if (path[x] == '/') return ext ? path.substr(x + 1) : path.substr(x + 1, extSeparator - x - 1); + // If this function was called with "ext" set to false, + // generate a substring of the path containing the filename + // only. + else if (!ext && path[x] == '.' && extSeparator == -1) + extSeparator = x; + } + return path; +} + +std::filesystem::path get_output_path(std::string const &path) { + std::filesystem::path output = path; + return output /= "output"; +} + +/** + * Used to determine whether a file should be copied onto the output directory + * or not. Certain files, like the config file shouldn't be copied. + */ +bool is_special_file(std::string const &filename) { + if (filename == "swg.cfg" || filename.find("__swg_") == 0) + return true; + return false; +} + +std::string get_template(std::string const &path) { + // Template lookup + std::string stPath = path; + stPath += "/__swg_template.html"; + FILE *swgTemplate = fopen(stPath.c_str(), "r"); + if (swgTemplate == NULL) { + std::cerr << "error: couldn't open the SWG HTML template file; does it exist?\n"; + perror(stPath.c_str()); + exit(RETURN_FAILED_INVALID_DIRECTORY); + } + + // Check for content placeholder + int buflen = 8; + char buf[buflen]; + std::string htmlTemplate; + while (fgets(buf, buflen, swgTemplate) != NULL) { + htmlTemplate += buf; + } + + fclose(swgTemplate); + std::cout << "Loaded HTML template into memory.\n"; + return htmlTemplate; +} + diff --git a/src/Common.hxx b/src/Common.hxx index 449f707..806f4a8 100644 --- a/src/Common.hxx +++ b/src/Common.hxx @@ -21,17 +21,33 @@ #pragma once -#define PROGRAM_NAME "SWG" -#define PROGRAM_VERSION 1 -#define PROGRAM_VERSION_NAME "0.1-dev" -#define PROGRAM_COPYRIGHT "luca0N!" -#define PROGRAM_COPYRIGHT_YEARS "2022" +#define PROGRAM_NAME "SWG" +#define PROGRAM_VERSION 1 +#define PROGRAM_VERSION_NAME "0.1-dev" +#define PROGRAM_COPYRIGHT "luca0N!" +#define PROGRAM_COPYRIGHT_YEARS "2022" -#define RETURN_SUCCESSFUL 0 +#define RETURN_SUCCESSFUL 0 #define RETURN_FAILED_INVALID_SYNTAX 1 #define RETURN_FAILED_CONFIG_INVALID_SYNTAX 2 #define RETURN_FAILED_INVALID_DIRECTORY 3 #define RETURN_FAILED_WEBSITE_BUILD_EXISTS 4 -#define RETURN_FAILED_UNKNOWN_ERROR 10 +#define RETURN_FAILED_UNKNOWN_ERROR 10 + +#include +#include + +struct Blog { + char name[64], + path[64], + dir[64]; + int test; +}; + +const char* get_hr_month(unsigned short month); +std::string getFilename(std::string const &path, bool const ext = true); +std::filesystem::path get_output_path(std::string const &path); +bool is_special_file(std::string const &filename); +std::string get_template(std::string const &path); diff --git a/src/SwgContext.hxx b/src/SwgContext.hxx index df0d3de..61e92ea 100644 --- a/src/SwgContext.hxx +++ b/src/SwgContext.hxx @@ -24,12 +24,7 @@ #include #include -struct Blog { - char name[64], - path[64], - dir[64]; - int test; -}; +#include "Common.hxx" struct SwgContext { std::string websiteName, diff --git a/src/WebsiteBuilder.cxx b/src/WebsiteBuilder.cxx index 36d1635..15a1de3 100644 --- a/src/WebsiteBuilder.cxx +++ b/src/WebsiteBuilder.cxx @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -30,53 +29,11 @@ #include "SwgRuntime.hxx" #include "SwgContext.hxx" #include "ConfigUtils.hxx" -// #include "Blog.h" +#include "BlogBuilder.hxx" #include "Common.hxx" #include "MarkdownParser.hxx" #include "Article.hxx" -static std::string HR_MONTH[] = { - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" -}; - -std::string blog_relative_path(std::string const &pathPrefix, std::string const &path) { - return path.substr(pathPrefix.length() - 1); -} - -std::string getFilename(std::string const &path, bool const ext = true) { - int extSeparator = -1; - // Backwards search for directory separator. - for (int x = path.length(); x > 0; --x) { - if (path[x] == '/') return ext ? path.substr(x + 1) : path.substr(x + 1, extSeparator - x - 1); - // If this function was called with "ext" set to false, - // generate a substring of the path containing the filename - // only. - else if (!ext && path[x] == '.' && extSeparator == -1) - extSeparator = x; - } - return path; -} - -bool isValidArticle(std::string const &pathPrefix, std::string const &path) { - return std::regex_search(blog_relative_path(pathPrefix, path), std::regex("^/\\d{4}/\\d{2}/.*\\.md")); -} - -std::filesystem::path get_output_path(std::string const &path) { - std::filesystem::path output = path; - return output /= "output"; -} - void build_dir_structure(std::string const &path) { // Create directory tree, which will be used for the website. std::filesystem::path rootDir = get_output_path(path); @@ -103,30 +60,6 @@ void build_dir_structure(std::string const &path) { } } -std::string get_template(std::string const &path) { - // Template lookup - std::string stPath = path; - stPath += "/__swg_template.html"; - FILE *swgTemplate = fopen(stPath.c_str(), "r"); - if (swgTemplate == NULL) { - std::cerr << "error: couldn't open the SWG HTML template file; does it exist?\n"; - perror(stPath.c_str()); - exit(RETURN_FAILED_INVALID_DIRECTORY); - } - - // Check for content placeholder - int buflen = 8; - char buf[buflen]; - std::string htmlTemplate; - while (fgets(buf, buflen, swgTemplate) != NULL) { - htmlTemplate += buf; - } - - fclose(swgTemplate); - std::cout << "Loaded HTML template into memory.\n"; - return htmlTemplate; -} - void compile_markdown(std::string const &path, std::string const &md, std::string const &to, Article::Metadata *metadata = NULL) { FILE *articleOutput = fopen(to.c_str(), "w"); @@ -144,7 +77,7 @@ void compile_markdown(std::string const &path, std::string const &md, std::strin if (metadata->publish_str[0] == '\0') { struct tm *publish_tm = gmtime(&(metadata->publish_ts)); //article_html += ctime(&(metadata->publish_ts)); - article_html += HR_MONTH[publish_tm->tm_mon]; + article_html += get_hr_month(publish_tm->tm_mon); article_html += " "; article_html += std::to_string(publish_tm->tm_mday); article_html += ", "; @@ -165,118 +98,6 @@ void compile_markdown(std::string const &path, std::string const &md, std::strin fclose(articleOutput); } -void build_blog_structure(std::string const &path, std::string const &prefix, std::list const &articles, Blog *blog) { - std::filesystem::path obp = get_output_path(path); // Output Blog Path - obp /= "blog"; - obp /= blog->dir; - try { - // Create blog directory - if (!std::filesystem::exists(obp)) std::filesystem::create_directories(obp); - } catch (std::filesystem::filesystem_error const &e) { - std::cerr << "error: failed to create directory for blog \"" << blog->name << "\": " << e.what() << std::endl; - exit(RETURN_FAILED_UNKNOWN_ERROR); - } - - std::list am; - //std::map> sorted_articles; - - for (std::string const &a : articles) { - std::string articlePath = blog_relative_path(prefix, a); - std::regex yearMonth("(\\d+)"); - auto match = std::sregex_iterator(articlePath.begin(), articlePath.end(), yearMonth); - - std::string year = match->str(), - month = (++match)->str(); - - // Go ahead and parse article metadata. - Article::Metadata *articleMetadata = (Article::Metadata*) malloc(sizeof(Article::Metadata)); - Article::get_metadata(a, articleMetadata); - std::cout << "Parsed metadata for article \"" << articleMetadata->title << "\"\n\tPublished on " - << ctime(&(articleMetadata->publish_ts)) << "\n"; - - // TODO: This code could be optimized by removing directory - // checks for every single article. Instead, add - // directory-checks for years and months to a queue (skipping - // existing ones) and then checking and creating the - // directories later as needed. - try { - // Create directory for the year of this article if it doesn't exist. - std::filesystem::path oad = obp, // Output Article Directory - rap; // Relative Article Path - oad /= year; - rap /= year; - if (!std::filesystem::exists(oad)) std::filesystem::create_directory(oad); - - // Do the same for the article month. - oad /= month; - rap /= month; - if (!std::filesystem::exists(oad)) std::filesystem::create_directory(oad); - - // Now create the article file. - std::string new_article_filename = getFilename(a, false); - new_article_filename += ".html"; - oad /= new_article_filename; - rap /= new_article_filename; - compile_markdown(path, a, oad, articleMetadata); - strncpy(articleMetadata->path, rap.c_str(), sizeof(articleMetadata->path)); - am.push_back(articleMetadata); - } catch (std::filesystem::filesystem_error const &e) { - std::cerr << "error: failed to create directory for an article from blog \"" - << blog->name << "\": " << e.what() << std::endl; - exit(RETURN_FAILED_UNKNOWN_ERROR); - } - } - // Sort am list. - am.sort(Article::Comparator::comp); - // Generate blog catalog. - std::string blog_html_catalog = "
    \n", - last_date = ""; - - for (Article::Metadata *m : am) { - struct tm *article_time = gmtime(&(m->publish_ts)); - std::string hr_date = HR_MONTH[article_time->tm_mon]; - hr_date += " "; - hr_date += std::to_string(article_time->tm_year + 1900); - // Check if this article belongs to the same "month + year" group as - // the last article. If it doesn't, add a new group to the catalog. - if (hr_date != last_date) { - last_date = hr_date; - blog_html_catalog += "\n\t"; - blog_html_catalog += hr_date; - blog_html_catalog += "\n"; - } - blog_html_catalog += "\t
  • path; - blog_html_catalog += "\">"; - blog_html_catalog += m->title; - blog_html_catalog += "
  • \n"; - // Free memory as it's no longer needed. - free(m); - } - blog_html_catalog += "
"; - // Put catalog HTML into template and save it to the blog directory. - std::filesystem::path catalog_output_path = obp; - catalog_output_path /= "index.html"; - - std::string html_template = get_template(path); - std::regex content_placeholder(""); - std::string catalog_html_contents = std::regex_replace(html_template, content_placeholder, blog_html_catalog); - - FILE *catalog_output_file = fopen(catalog_output_path.c_str(), "w"); - fputs(catalog_html_contents.c_str(), catalog_output_file); - fclose(catalog_output_file); -} - -/** - * Used to determine whether a file should be copied onto the output directory - * or not. Certain files, like the config file shouldn't be copied. - */ -bool is_special_file(std::string const &filename) { - if (filename == "swg.cfg" || filename.find("__swg_") == 0) - return true; - return false; -} - void build_website(SwgContext &ctx, std::string const &path) { build_dir_structure(path); @@ -360,7 +181,7 @@ void build_website(SwgContext &ctx, std::string const &path) { filename.find(".md") == filename.length() - 3) { std::cout << "\t\tIs file: " << getFilename(dir_entry.path()) << "\n"; // Markdown files should be insite a YYYY/MM directory. - if (!isValidArticle(relativePath, dir_entry.path())) + if (!is_valid_article(relativePath, dir_entry.path())) failedArticles.insert(failedArticles.end(), dir_entry.path()); else parsedArticles.insert(parsedArticles.end(), dir_entry.path()); } diff --git a/src/WebsiteBuilder.hxx b/src/WebsiteBuilder.hxx index 68b10e8..96cd556 100644 --- a/src/WebsiteBuilder.hxx +++ b/src/WebsiteBuilder.hxx @@ -23,5 +23,10 @@ #include "SwgContext.hxx" -void build_website(SwgContext &ctx, std::string const &path); +#include "Article.hxx" + +#include + +void build_website(SwgContext &ctx, std::string const &path); +void compile_markdown(std::string const &path, std::string const &md, std::string const &to, Article::Metadata *metadata = NULL);