#!/usr/bin/zsh # Quick Site Generator 2 is a static website generator inspired by Nikola. # It is written for the Z shell (zsh) because that's what I use and also because I like it better than Bash. # # This script is an almost complete rewrite of my old script because it became overly complicated and # had way too many bugs, even though it worked on simple sites. # # qsgen2 uses different templates and its own formattings tags to generate the static HTML pages. # # The file structure in the project directory should look like this: # # .blog_cache # .pages_cache # config # index.tpl # templates/ # templates//pages.tpl # templates//blogs.tpl # templates//blog_index.tpl # blog/ # blog/2024-01-26-1.blog # # Explanation of the file structure. # The file 'config' contains the settings for this specific project. # The file 'index.tpl' is what becomes index.html that is served to the public. # The 'templates/' directory contains the templates used to generate the pages and blog posts. # The 'blog/' directory contains the blog posts. The date format of the files are used to create a blog/index.html with the newest post first. # # Contents of the file 'config' --> CHANGE THESE <-- # export site_name="The name of the website" # - This is the project directory # export project_dir=${HOME}/www/vikingo # - This is where the generated files will be put # export www_root=${HOME}/www_root/smelror.com # ################################################################################################# # I don't think these need to be here as they'll always be in the same place with the same names # export pages=${project_dir}/templates//pages.tpl # export blogs=${project_dir}/templates//blogs.tpl # export blog_list=${project_dir}/templates//blog_list.tpl # export blog_index=${project_dir}/templates//blog_index.tpl ################################################################################################### # export blog_in_index=false VERSION="0.0.1 alpha" # Mon-2024-01-29 QSGEN="Quick Site Generator 2" # Set to true or false # This will show debug information from almost every function in this script debug=false function include () { # This function is used to include other functions that will normally be in # ${HOME}/bin/include/ # Edit this path to reflect your installation local inc_file=${HOME}/bin/include/${1}.inc if [[ ! -f ${inc_file} ]]; then local inc_opt=$( echo ${1} | cut -d\/ -f2 ) echo "Supplied option \"${inc_opt}\" is not a valid include." else builtin source ${inc_file} ${2} fi } # Including some colors to the script include common/colors echo "${magenta}${QSGEN} ${VERSION}${end}" function _version() { echo "${yellow}- Created by kekePower - 2018-$(date +%Y)${end}" echo "${yellow}- https://github.com/kekePower/qsgen2/${end}" echo "${yellow}- See '${1} help' for more information." exit } function _help() { echo "This is where I'll write the Help documentation." exit } if [[ "$1" == "version" || "$1" == "-v" || "$1" == "--version" ]]; then _version ${0:t} elif [[ "$1" == "help" || "$1" == "-h" || "$1" == "--help" ]]; then _help ${0:t} fi # Loading Zsh modules zmodload zsh/files # Check for, an source, the config file for this specific website if [[ -f $(pwd)/config ]]; then if (${debug}) echo "${red}Config file found and sourced${end}\n${yellow} - $(pwd)/config${end}" # CONFIG=$(pwd)/config builtin source $(pwd)/config else echo "${red}Cannot find configuration file.${end}" echo "${yellow} - Please create the file 'config' in your project directory.${end}" exit fi if (${debug}); then echo "${red}Contents of Config file:${end}" echo "${yellow} - site_name=${site_name}${end}" echo "${yellow} - site_tagline=${site_tagline}${end}" echo "${yellow} - theme=${theme}${end}" echo "${yellow} - project_dir=${project_dir}${end}" echo "${yellow} - www_root=${www_root}${end}" echo "${yellow} - blog_in_index=${blog_in_index}${end}" echo "${yellow} - generator=${generator}${end}" fi # Let's check if qsgen2 can generate this site by checking if 'generator' is available if [[ ! ${generator} ]]; then echo "${0:t} cannot parse this site. Exiting." exit fi # We define the variable 'engine' based on what's in the 'config' file. if [[ ${generator} == "native" ]]; then # Usage: ${engine} ${1} - Where 1 is the file you want to convert engine=_html elif [[ ${generator} == "markdown" ]]; then if [[ ! -f /usr/bin/markdown ]]; then echo "Please install the 'discount' package to use Markdown." exit else # Let's make sure that the Markdown executable gets all its variables: 1 and 2 if [[ ! ${2} ]] || [[ ${2} == "" ]]; then echo "Engine: To use Markdown, please provide a second variable, the output file." echo "Engine: Example: engine file.tpl www_root/file.html" exit else # Usage: ${engine} ${1} - Where 1 is the file you want parsed engine=$( /usr/bin/markdown ${1} -d ) fi fi fi if (${debug}) echo "${red}Using the ${generator} engine${end}" # Define cache files for blogs and pages blog_cache_file="${project_dir}/.blog_cache" pages_cache_file="${project_dir}/.pages_cache" builtin cd ${project_dir} # Let's put these here for now. export today=$( date "+%Y-%m-%d - %T" ) export blogdate=$( date +%a-%Y-%b-%d ) # Let's create arrays of all the files we'll be working on function _list_blog_idx() { ls -har blog/*.idx | while read -r file; do blog_idx_array+=($file) done } function _list_blog_tmp_idx() { ls -har blog/*.tmp.html | while read -r file; do blog_tmp_idx_array+=($file) done } function _list_blog() { ls -1btar blog/*.blog | while read -r file; do blog_index_file_array+=($file) done } # BLOG CACHE function _blog_cache() { local debug=false # Create an associative array for the blog cache typeset -A blog_cache # Load the existing blog cache if [[ -f $blog_cache_file ]]; then while IFS=':' read -r name hash; do blog_cache[$name]=$hash if (${debug}) echo "${red}HASH VALUE: ${blog_cache[${name}]}${end}" done < "$blog_cache_file" fi # Initialize the array for storing blog files to process make_blog_array=() # Process blog files for blog_file in $(ls -har blog/*.blog); do # Compute the current blog file hash current_hash=$(md5sum "$blog_file" | awk '{print $1}') if (${debug}) echo "${red}1. blog_cache: ${blog_file}${end}" if (${debug}) echo "${red}2. current_cache: ${current_hash}${end}" # Check if the blog file is new or has changed if [[ ${blog_cache[$blog_file]} != "$current_hash" ]]; then if (${debug}) echo "${red}3. new_cache_file: ${blog_file}${end}" if (${debug}) echo "${red}4. new_current_cache: ${current_hash}${end}" # Blog file is new or has changed; add it to the processing array make_blog_array+=("$blog_file") # Update the blog cache with the new hash blog_cache[$blog_file]=$current_hash fi done # Rebuild the blog cache file from scratch : >| "$blog_cache_file" # Truncate the file before writing for name in "${(@k)blog_cache}"; do echo "$name:${blog_cache[$name]}" >> "$blog_cache_file" done } # PAGES CACHE # Returns the array pages_array() function _pages_cache() { local debug=false # Create an associative array for the pages cache typeset -A pages_cache # Load the existing pages cache if [[ -f $pages_cache_file ]]; then while IFS=':' read -r name hash; do pages_cache[$name]=$hash if (${debug}) echo "${red}PAGES HASH VALUE: ${pages_cache[${name}]}${end}" done < "$pages_cache_file" fi # Initialize the array for storing pages files to process pages_array=() # Process pages files for file in $(ls -1bt *tpl); do # Compute the current blog file hash current_hash=$(md5sum "$file" | awk '{print $1}') if (${debug}) echo "${red}1. pages_cache: ${pages_cache[$file]}${end}" if (${debug}) echo "${red}1. current_cache: ${current_hash}${end}" # Check if the pages file is new or has changed if [[ ${pages_cache[$file]} != "$current_hash" ]]; then if (${debug}) echo "${red}2. pages_file: ${pages_cache[$file]}${end}" if (${debug}) echo "${red}2. current_cache: ${current_hash}${end}" # Pages file is new or has changed; add it to the processing array pages_array+=("$file") # Update the pages cache with the new hash pages_cache[$file]=$current_hash fi done # Rebuild the pages cache file from scratch : >| "$pages_cache_file" # Truncate the file before writing for name in "${(@k)pages_cache}"; do echo "$name:${pages_cache[$name]}" >> "$pages_cache_file" done } function _last_updated() { cat ${1} | sed -e "s|#updated|${TODAY}|" | sed -e "s|\#version|${QSGEN} ${VERSION}|" > ${1} } function _file_to_lower() { local filename=${1} # Replace spaces with dashes filename="${filename// /-}" # Convert to lowercase and remove invalid characters filename=$(echo "${filename}" | sed -e 's/^[^a-zA-Z0-9_.]+//g' -e 's/[^a-zA-Z0-9_-]+/-/g') echo ${filename} } function _pages() { # This function generates all the new and updated Pages local debug=true # Load the cache for Pages if (${debug}) echo "_pages: Running function _pages_cache" _pages_cache if (( ${#pages_array[@]} > 0 )); then if (${debug}) echo "_pages: Setting Pages template" local pages=${project_dir}/templates/${theme}/pages.tpl # Let's check if we can access the pages.tpl file. # It not, exit script. if [[ ! -f ${pages} ]]; then echo "Unable to find the Pages template: ${pages}" exit else # Read template once if (${debug}) echo "_pages: Reading Pages template into pages_tpl" pages_tpl="$(<${pages})" fi # If pages_array is not empty, we do work if (${debug}) echo "_pages: pages_array is not empty" for pages_in_array in ${pages_array[@]} do echo "${green}Generating Page: ${pages_in_array}${end}" # Read the file once if (${debug}) echo "_pages: Loading page_content once" local page_content="$(<${pages_in_array})" # Grab the title from the Page if (${debug}) echo "_pages: Grepping for page_title" page_title=$( echo ${page_content} | head -1 | grep \#title | cut -d= -f2 ) # Remove the #title line from the buffer. No longer needed. if (${debug}) echo "_pages: Removing #title line from page_content" page_content=$( echo ${page_content} | grep -v \#title ) # HTML'ify the page content if (${debug}) echo "_pages: Running engine on ${pages_in_array}" page_content=$( ${engine} ${page_content} ) #if (${debug}) echo "_pages: Writing page_content to disk" #echo ${page_content} > ${project_dir}/${pages_in_array%.*}.idx # local pages_tpl="$(<${pages})" # Replace every #pagetitle in pages_tpl if (${debug}) echo "_pages: Replacing #pagetitle in pages_tpl" pages_tpl=$( echo ${pages_tpl} | sed -e "s|#pagetitle|${page_title}|g" ) # Replace every #tagline in pages_tpl if (${debug}) echo "_pages: Replacing tagline" pages_tpl=$( echo ${pages_tpl} | sed -e "s|#tagline|${site_tagline}|g" ) # Insert page_content into pages_tpl by replacing the BODY tag present there #if (${debug}) echo "_pages: Replacing BODY with page_content in pages_tpl" #pages_tpl=$( echo ${pages_tpl} | sed "s|BODY|${page_content}|g" ) # Always use lowercase for file names pages_title_lower=$( _file_to_lower "${pages_in_array}" ) # Write pages_tpl to disk echo "${green}Writing ${www_root}/${pages_title_lower%.*}.html to disk.${end}" tee < ${pages_tpl} | sed \ -e "s|BODY|$( echo ${page_content} )|" \ > ${www_root}/${pages_title_lower%.*}.html # Replace #updated with today's date and #version with Name and Version to footer if (${debug}) echo "_pages: _last_updated in pages_tpl" _last_updated ${www_root}/${pages_title_lower%.*}.html # Look for links, images and videos and convert them if present. if (${debug}) echo "_pages: Checking for #link, #showimg and #ytvideo in page_content" if [[ $( cat ${www_root}/${pages_title_lower%.*}.html | grep \#link ) ]]; then echo "If #link is present, run _link: ${page_content}" #_link ${www_root}/${pages_in_array%.*}.html elif [[ $( cat ${www_root}/${pages_title_lower%.*}.html | grep \#showimg ) ]]; then echo "If #showimg is present, run _image: ${page_content}" #_image ${www_root}/${pages_in_array%.*}.html elif [[ $( cat ${www_root}/${pages_title_lower%.*}.html | grep \#ytvideo ) ]]; then echo "If #ytvideo is present, run _youtube: ${page_content}" #_youtube ${www_root}/${pages_in_array%.*}.html fi # Run a cleanup if in case something was left out # _cleanup ${pages_tpl} done else echo "${yellow}No new or updated Pages${end}" fi } function _blogs() { # This function generates the blog files local debug=true # Running function _list_blog # It returns the array: blog_index_file_array echo "_blogs: Running function _list_blog" _list_blog # Running function _blog_cache # It returns the array: make_blog_array echo "_blogs: Running function _blog_cache" _blog_cache if (( ${#make_blog_array[@]} > 0 )); then local blog_tpl=${project_dir}/templates/${theme}/blogs.tpl if [[ ! -f ${blog_tpl} ]]; then echo "Unable to find the Blog template: ${blog_tpl}" exit fi local sdate btitle ingress body blog_index blog_dir blog_url if (${debug}) echo "_blogs: _blog_list_for_index: Just before the for loop: make_blog_array" for blog in ${make_blog_array[@]} do if (${debug}) echo "0001: ${red}_blogs: _blog_list_for_index: Processing ${blog}${end}" # Read the file once if (${debug}) echo "0002: ${red}_blogs: _blog_list_for_index: Reading blog from array into content: ${blog}${end}" local content="$(<${blog})" sed -i "s/GETDATE/${BLOGDATE}/" ${blog} # Array sdate = Name day=4, Year=2, Month=3, Number day=1 sdate=( $( echo ${content} | grep DATE | sed "s|DATE\ ||" | sed "s|\-|\ |g" ) ) btitle=$( echo ${content} | grep BLOG_TITLE | cut -d' ' -f2- ) ingress=$( echo ${content} | sed "s/'/\\\'/g" | xargs | grep -Po "#INGRESS_START\K(.*?)#INGRESS_STOP" | sed "s|\ \#INGRESS_STOP||" | sed "s|^\ ||" ) body=$( echo ${content} | sed "s/'/\\\'/g" | xargs | grep -Po "#BODY_START\K(.*?)#BODY_STOP" | sed "s|\ \#BODY_STOP||" | sed "s|^\ ||" ) blog_index=$( echo ${btitle:l} | sed -e "s|-|_|" | sed -e "s|\ |-|g" | sed -e "s|\,||g" | sed -e "s|\.||" ) blog_dir="/blog/${sdate[2]}/${sdate[3]}/${sdate[4]}" blog_url="${blog_dir}/${blog_index}.html" if [[ ! -d ${www_root}${blog_dir} ]]; then if (${debug}) echo "0003: ${red}_blogs: Creating blog directory: ${www_root}${blog_dir}${end}" mkdir -p ${www_root}${blog_dir} fi if (${debug}) echo "0004: ${cyan}_blogs: blog_index: ${blog_tpl}${end}" tee < ${blog_tpl} | sed \ -e "s|BLOGTITLE|${btitle}|" \ -e "s|CALADAY|${sdate[1]}|" \ -e "s|CALNDAY|${sdate[4]}|" \ -e "s|CALMONTH|${sdate[3]}|" \ -e "s|CALYEAR|${sdate[2]}|" \ -e "s|BLOGURL|${blog_url}|" \ -e "s|INGRESS|${ingress}|" \ -e "s|DATE ||" \ > ${blog%.*}.idx if (${debug}) echo "0005: ---------- ${red}_blogs: TO_IDX_FILE: ${blog%.*}.idx${end}" if (${debug}) echo "0006: ${cyan}_blogs: BLOG_URL_TPL: ${www_root}${blog_url}${end}" tee < ${blog_tpl} | sed \ -e "s|BLOGTITLE|${btitle}|" \ -e "s|CALADAY|${sdate[1]}|" \ -e "s|CALNDAY|${sdate[4]}|" \ -e "s|CALMONTH|${sdate[3]}|" \ -e "s|CALYEAR|${sdate[2]}|" \ -e "s|INGRESS|${ingress}|" \ -e "s|BODY|${body}|" \ -e "s|DATE ||" \ -e "s|\#title||" \ >> ${www_root}${blog_url} if (${debug}) echo "0007: ---------- ${red}_blogs: ${blog_tpl} -- TO_BLOG_URL_FILE: ${www_root}${blog_url}${end}" echo "_blogs: _last_updated: Updating footer for ${www_root}${blog_url}" _last_updated ${www_root}${blog_url} echo "_blogs: Running HTML engine: ${engine}" ${engine} ${blog_url} if [[ $( grep \#link ${www_root}${blog_url} ) ]]; then echo "If #link is present, run _link: ${www_root}${blog_url}" _link ${blog_url} elif [[ $( grep \#showimg ${www_root}${blog_url} ) ]]; then echo "If #showimg is present, run _image: ${www_root}${blog_url}" _image ${blog_url} elif [[ $( grep \#ytvideo ${www_root}${blog_url} ) ]]; then echo "If #ytvideo is present, run _youtube: ${www_root}${blog_url}" _youtube ${blog_url} fi _cleanup ${blog_url} done else echo "${yellow}No new or updated Blogs detected.${end}" fi } function _blog_idx_for_index() { # This function generates the file blog/index.idx local debug=false if [[ -f ${project_dir}/blog/index.tmp.html ]]; then echo "Remove temporary file: ${project_dir}/blog/index.tmp.html" rm -f ${project_dir}/blog/index.tmp.html fi } function _blog_index() { # This function generates the /blog/index.html file that gets its data from _blog_list_for_index() local debug=false local pages=${project_dir}/templates/${theme}/pages.tpl if [[ ! -f ${pages} ]]; then echo "Unable to find the Pages template: ${pages}" exit fi local blog_index_title="Blog" # Running function _list_blog_idx # It returns the array: blog_tmp_idx_array _list_blog_tmp_idx if (( ${#blog_tmp_idx_array[@]} > 0 )); then local blog_list=${project_dir}/templates/${theme}/blog_list.tpl if [[ ! -f ${blog_list} ]]; then echo "Unable to find the Pages template: ${blog_list}" exit fi for blog_files in ${blog_tmp_idx_array[@]} do local content="$(<${blog_files})" sed -i "s/GETDATE/${BLOGDATE}/" ${blog_files} # Array sdate = Name day=4, Year=2, Month=3, Number day=1 sdate=( $( echo ${content} | grep DATE | sed "s|DATE\ ||" | sed "s|\-|\ |g" ) ) ingress=$( echo ${content} | sed "s/'/\\\'/g" | xargs | grep -Po "#INGRESS_START\K(.*?)#INGRESS_STOP" | sed "s|\ \#INGRESS_STOP||" | sed "s|^\ ||" ) body=$( echo ${content} | sed "s/'/\\\'/g" | xargs | grep -Po "#BODY_START\K(.*?)#BODY_STOP" | sed "s|\ \#BODY_STOP||" | sed "s|^\ ||" ) blog_date="${sdate[1]} ${sdate[4]}-${sdate[3]}-${sdate[2]}" sed \ -e "s|BLOGDATE|${blog_date}|" \ -e "s|BLOGURL|${blog_url}|" \ -e "s|INGRESS|${ingress}|" > ${project_dir}/blog/index.tmp.x done fi tee < ${pages} | sed \ -e "s|BODY|$(cat ${project_dir}/blog/index.tmp.x)|" \ -s "s|#pagetitle|${blog_index_title}|g" \ > ${www_root}/blog/index.html _last_updated ${www_root}/blog/index.html ${engine} ${www_root}/blog/index.html } function _link() { # This converts #link tags to actual clickable links local debug=false if (${debug}) echo "${red}_link: Generating links for ${1}${end}" # Process the file line by line while IFS= read -r line; do if [[ ${line} == *"#link"* ]]; then if (${debug}) echo "${red}URL_MAIN(line): (${1}) ${line}${end}" # Extract the URL and the link text local url_full=$(echo "$line" | awk -F'#link ' '{print $2}' | awk -F'¤' '{print $1 "¤" $2}') local url_dest=$(echo "$url_full" | awk -F'¤' '{print $1}') local url_txt=$(echo "$url_full" | awk -F'¤' '{print $2}') if (${debug}) echo "${red}URL: ${url_dest}${end}" if (${debug}) echo "${red}Text: ${url_txt}${end}" # Form the replacement HTML link local modified_link="${url_txt}" if [[ ${url_dest} =~ ^https?:// ]]; then # Add external link icon for external URLs modified_link+="\"External" fi modified_link+="" line=${line//"#link $url_full"/$modified_link} fi echo "$line" >> "${www_root}/${1%.*}.tmp.html" done < "${www_root}/${1%.*}.html" # Replace the original file with the modified one builtin mv "${www_root}/${1%.*}.tmp.html" "${www_root}/${1%.*}.html" } function _image() { # This replaces #showimg to actual HTML img tag local get_img img_link image img_alt local debug=false if (${debug}) echo "${red}_image: Generating image tags for ${1}${end}" cat ${www_root}/${1%.*}.html | sed "s/\.\ /\.\\n/g" | sed "s/\,/\,\\n/g" | sed "s/\#/\\n\#/g" | grep -P '(?=.*?#showimg)' |\ while read img do if [[ ${img} != "" ]]; then get_img=$( echo "${img}" | awk '/#showimg/' | cut -d# -f2- ) if (${debug}) echo "${red}GET_IMG: ${get_img}${end}" img_link=$( echo ${get_img} | cut -d' ' -f2- ) if (${debug}) echo "${red}IMG_LINK: ${img_link}${end}" image=$( echo ${img_link} | cut -d¤ -f1 | cut -d¤ -f1- ) if (${debug}) echo "${red}IMAGE: ${image}${end}" img_alt=$( echo ${img_link} | cut -d¤ -f2- | cut -d¤ -f1- ) if (${debug}) echo "${red}IMAGE_ALT: ${image_alt}${end}" if [[ ${image} =~ ^https* ]]; then # Images on another server real_image=${image} if (${debug}) echo "${red}HTTPS REAL_IMAGE: ${real_image}${end}" elif [[ ${image} =~ ^\/ ]]; then # This is for images in another directory and the image link begins with a / real_image=${image} if (${debug}) echo "${red}SLASH REAL_IMAGE: ${real_image}${end}" else # This is for images in the '/images/' directory real_image="/images/${image}" if (${debug}) echo "${red}IMAGES REAL_IMAGE: ${real_image}${end}" fi if (${debug}) echo "${red}REAL_IMAGE: $real_image${end}" if (${debug}) echo "${red}IMG_ALT: $img_alt${end}" echo ${img_link} |\ sed -i -- "s|${image}|\"'${img_alt}'\"|' ${www_root}/${1%.*}.html fi done } function _youtube() { # This embeds a YouTube video on a page or a blog local yt_id local debug=false if (${debug}) echo "${red}_youtube: Creating YouTube player embed${end}" cat ${www_root}/${1%.*}.html | sed "s/\.\ /\.\\n/g" | sed "s/\,/\,\\n/g" | sed "s/\#/\\n\#/g" | grep -P '(?=.*?#ytvideo)' |\ while read video do if [[ ${video} != "" ]]; then yt_id=$( echo "${video}" | awk '/#ytvideo/' | cut -d" " -f2 ) if (${debug}) echo "${red}YT VIDEO ID: ${yt_id}${end}" sed -i -- "s|${yt_id}|