#!/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() { # This function updates #updated and #version tags in the provided string local content="${1}" local debug=false if (${debug}) echo "${red}_last_updated: Setting date and version in footer${end}" # Perform the replacements local updated_content=$(echo "${content}" | sed \ -e "s|#updated|${TODAY}|" \ -e "s|#version|${QSGEN} ${VERSION}|") # Return the updated content echo "${updated_content}" } 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=false # 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 -2 | 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} ) # 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 [[ $( echo ${page_content} | grep \#link ) ]]; then if (${debug}) echo "_pages: If #link is present, run _link: page_content" page_content=$( _link "${page_content}" ) fi if [[ $( echo ${page_content} | grep \#showimg ) ]]; then if (${debug}) echo "_pages: If #showimg is present, run _image: page_content" page_content=$( _image "${page_content}" ) fi if [[ $( echo ${page_content} | grep \#ytvideo ) ]]; then if (${debug}) echo "_pages: If #ytvideo is present, run _youtube: page_content" page_content=$( _youtube "${page_content}" ) fi # 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 using Perl" # Use Perl for multi-line and special character handling pages_tpl=$( echo "${pages_tpl}" | perl -pe "s|BODY|${page_content}|gs" ) # Replace every #pagetitle in pages_tpl if (${debug}) echo "_pages: Replacing #pagetitle in pages_tpl" pages_tpl=$( echo ${pages_tpl} | perl -pe "s|#pagetitle|${page_title}|gs" ) # Replace every #tagline in pages_tpl if (${debug}) echo "_pages: Replacing tagline" pages_tpl=$( echo ${pages_tpl} | perl -pe "s|#tagline|${site_tagline}|gs" ) # Replace #updated with today's date and #version with Name and Version to footer if (${debug}) echo "_pages: _last_updated in pages_tpl" pages_tpl=$( _last_updated ${pages_tpl} ) # Clean up unused tags, if any if (${debug}) echo "_pages: Running _cleanup" pages_tpl=$( _cleanup "${pages_tpl}" ) # Always use lowercase for file names if (${debug}) echo "_pages: Lowercase filnames, always" 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}" echo "${pages_tpl}" > ${www_root}/${pages_title_lower%.*}.html done else echo "${yellow}No new or updated Pages${end}" fi } function _blogs() { # Rewritten _blogs function # This function either generates blog files or exports metadata based on the argument local debug=false local mode=${1} # If ", exports metadata; otherwise, creates blog posts # Running function _list_blog if (${debug}) echo "_blogs: Running function _list_blog" _list_blog # Running function _blog_cache if (${debug}) echo "_blogs: Running function _blog_cache" _blog_cache if (( ${#make_blog_array[@]} > 0 )); then # Regular blog creation process if [[ ! ${mode} ]]; then if [[ -f ${project_dir}/templates/${theme}/blogs.tpl ]]; then local blog_tpl=$(<"${project_dir}/templates/${theme}/blogs.tpl") else echo "Unable to find theme template for Blogs." exit fi for blog in "${make_blog_array[@]}"; do if (${debug}) echo "_blogs: Processing pre-data for ${blog}" local content="$(<"${blog}")" local sdate btitle ingress body blog_index blog_dir blog_url # Extract blog information sed -i "s/GETDATE/${blogdate}/" ${blog} # Array sdate = Name day=1, Year=2, Month=3, Number day=4 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="${btitle:l}" blog_index=$(echo "${blog_index}" | sed 's/ /_/g; s/,//g; s/\.//g; s/://g; s/[()]//g') blog_dir="/blog/${sdate[2]}/${sdate[3]:l}/${sdate[4]}" blog_url="${blog_dir}/${blog_index}.html" if (${debug}) echo "_blogs: Processing ${blog}" # Prepare the blog template if (${debug}) echo "_blogs: Processing substitutes in ${blog}" local blog_content=$(echo "${blog_tpl}" | perl -pe "s|BLOGTITLE|${btitle}|g") blog_content=$(echo "${blog_content}" | perl -pe "s|CALADAY|${sdate[1]}|g") blog_content=$(echo "${blog_content}" | perl -pe "s|CALNDAY|${sdate[4]}|g") blog_content=$(echo "${blog_content}" | perl -pe "s|CALMONTH|${sdate[3]}|g") blog_content=$(echo "${blog_content}" | perl -pe "s|CALYEAR|${sdate[2]}|g") blog_content=$(echo "${blog_content}" | perl -pe "s|BLOGURL|${blog_url}|g") # Replace INGRESS placeholder with actual content using Perl blog_content=$(echo "${blog_content}" | perl -pe "s|\QINGRESS\E|${ingress}|g") # blog_content="${blog_content//INGRESS/${ingress}}" # Replace BODY placeholder with actual content using Perl blog_content=$(echo "${blog_content}" | perl -pe "s|\QBODY\E|${body}|g") # Apply transformations blog_content="$(_html "${blog_content}")" # Look for links, images and videos and convert them if present. if (${debug}) echo "_pages: Checking for #link, #showimg and #ytvideo in blog_content" if [[ $( echo ${blog_content} | grep \#link ) ]]; then if (${debug}) echo "_blogs: If #link is present, run _link: blog_content" blog_content="$(_link "${blog_content}")" fi if [[ $( echo ${blog_content} | grep \#showimg ) ]]; then if (${debug}) echo "_blogs: If #showimg is present, run _link: blog_content" blog_content="$(_image "${blog_content}")" fi if [[ $( echo ${blog_content} | grep \#ytvideo ) ]]; then if (${debug}) echo "_blogs: If #ytvideo is present, run _link: blog_content" blog_content="$(_youtube "${blog_content}")" fi blog_content="$(_cleanup "${blog_content}")" blog_content="$(_last_updated "${blog_content}")" # Create directory if it doesn't exist if (${debug}) echo "_blogs: Creating directoty ${www_root}/${blog_dir}" [[ ! -d "${www_root}/${blog_dir}" ]] && mkdir -p "${www_root}/${blog_dir}" # Write to file if (${debug}) echo "_blogs: Writing ${blog} to disk: ${www_root}${blog_url}" echo "${blog_content}" > "${www_root}${blog_url}" done else # Export metadata mode if (${debug}) echo "_blogs: Exporting metadata for ${blog}" export BLOG_SDATE="${sdate[@]}" export BLOG_BTITLE="${btitle}" export BLOG_INGRESS="${ingress}" export BLOG_URL="${www_root}${blog_url}" fi 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 in a provided string local content="${1}" local modified_content="" local debug=false # Process the content line by line echo "${content}" | while IFS= read -r line; do if [[ ${line} == *"#link"* ]]; then if (${debug}) echo "${red}URL_MAIN(line): ${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 modified_content+="${line}\n" done # Return the modified content echo -e "${modified_content}" } function _image() { # This replaces #showimg tags with actual HTML img tags in a provided string local content="${1}" local modified_content="" local debug=false # Process the content line by line echo "${content}" | while IFS= read -r line; do if [[ ${line} == *"#showimg"* ]]; then if (${debug}) echo "${red}_image: Processing line: ${line}${end}" # Extract image link and alt text local img_link=$(echo "${line}" | awk -F'#showimg ' '{print $2}') local image=$(echo "${img_link}" | awk -F'¤' '{print $1}') local img_alt=$(echo "${img_link}" | awk -F'¤' '{print $2}') # Determine the source of the image local real_image="" if [[ ${image} =~ ^https?:// ]]; then real_image=${image} elif [[ ${image} =~ ^\/ ]]; then real_image=${image} else real_image="/images/${image}" fi # Form the replacement HTML image tag local img_tag="\"${img_alt}\"" line=${line//"#showimg ${img_link}"/${img_tag}} fi modified_content+="${line}\n" done # Return the modified content echo -e "${modified_content}" } function _youtube() { # This embeds a YouTube video in a provided string local content="${1}" local modified_content="" local debug=false # Process the content line by line echo "${content}" | while IFS= read -r line; do if [[ ${line} == *"#ytvideo"* ]]; then if (${debug}) echo "${red}_youtube: Processing line: ${line}${end}" # Extract YouTube video ID local yt_id=$(echo "${line}" | awk -F'#ytvideo ' '{print $2}') # Form the replacement YouTube iframe embed local yt_iframe="" line=${line//"#ytvideo ${yt_id}"/${yt_iframe}} fi modified_content+="${line}\n" done # Return the modified content echo -e "${modified_content}" } function _cleanup() { # This removes tags used in the templates that may be left over for some reason local content="${1}" local debug=false if (${debug}) echo "${red}_cleanup: Cleaning up tags in content${end}" # Perform the cleanup local cleaned_content=$(echo "${content}" | sed \ -e "s|¤||g" \ -e "s|#showimg\ ||g" \ -e "s|#ytvideo\ ||g" \ -e "s|#link\ ||g") # Return the cleaned content echo "${cleaned_content}" } function _html() { # This function converts formatting tags into their HTML equivalents in a provided string local content="${1}" local debug=false if (${debug}) echo "${red}_html: Generating HTML from content${end}" # Perform HTML tag substitutions local html_content=$(echo "${content}" | sed \ -e "s|#BR|
|g" \ -e "s|#BD||g" \ -e "s|#EBD||g" \ -e "s|#UN||g" \ -e "s|#EUN||g" \ -e "s|#P|

|g" \ -e "s|#EP|

|g" \ -e "s|#Q|
|g" \ -e "s|#EQ|
|g" \ -e "s|#STRONG||g" \ -e "s|#ESTRONG||g" \ -e "s|#I||g" \ -e "s|#EI||g" \ -e "s|#C||g" \ -e "s|#EC||g" \ -e "s|#EM||g" \ -e "s|#SEM||g" \ -e "s|#OT|"|g" \ -e "s|#UL|
    |g" \ -e "s|#OL|
      |g" \ -e "s|#LI|
    1. |g" \ -e "s|#ELI|
    2. |g" \ -e "s|#EUL|
|g" \ -e "s|#EOL||g" \ -e "s|#H1|

|g" \ -e "s|#H2|

|g" \ -e "s|#H3|

|g" \ -e "s|#H4|

|g" \ -e "s|#H5|

|g" \ -e "s|#H6|
|g" \ -e "s|#EH1|
|g" \ -e "s|#EH2||g" \ -e "s|#EH3||g" \ -e "s|#EH4||g" \ -e "s|#EH5||g" \ -e "s|#EH6||g" \ -e "s|#LT|<|g" \ -e "s|#GT|>|g" \ -e "s|#NUM|#|g") # Return the HTML content echo "${html_content}" } # Time to test the first function echo "${green}Running function _blogs${end}" _blogs echo "${green}Running function _pages${end}" _pages