Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Generate tag cloud for static site

A tag cloud is just a fun way to present data that is loosely measured and displayed in varying weights of importance. And it was a fun, amusing challenge, er, copy-paste job.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/sh
# File: generate-tag-cloud.sh
# Locatiosn:
#    doc7-01a:/usr/local/bin/generate-tag-cloud.sh
#    server1:/mnt/public/Support/Programs/nikola/scripts/for-doc7-01a
# Author: bgstack15
# Startdate: 2021-08-30 09:35
# Title: Generate tag cloud
# Purpose: generate an html sidebar-worthy tag cloud of the article tags on my static site.
# Usage:
#    generate-tag-cloud.sh
#    Called by deploy-part2.sh which is called by deploy.sh on server1
# References:
#    https://dev.to/alvaromontoro/create-a-tag-cloud-with-html-and-css-1e90
# Improve:
# Dependencies:
#    `nikola build` has already run
#    the nikola conf.py points to tagcloud.html
#    awk, bc

INDIR=/var/www/blog
test -z "${TAG_COUNT}" && TAG_COUNT=30
test -z "${TRIM_TAGS_SMALLER_THAN}" && TRIM_TAGS_SMALLER_THAN=3
test -z "${LINK_STRING}" && LINK_STRING="/blog/categories/\${tag}"
test -z "${MAX_SCALE}" && MAX_SCALE=9 # so 1 to 9
test -z "${SHOW_COUNTS}" && SHOW_COUNTS=1   # set undefined 
test -z "${OUT_CSS}" && OUT_CSS=/var/www/blog/tagcloud.css
test -z "${OUT_HTML}" && OUT_HTML=/var/www/blog/tagcloud.html

# FUNCTIONS
math_scale() {
   # call: math_scale "${weights}" "${MAX_SCALE}" "${MIN_SCALE}"
   # math scale is fun to technically implement, but not very useful for what we are doing.
   _weights="${1}"
   _outmax="${2}"
   _outmin="${3}"
   # we need to build a scale for min-max of the given numbers, to scale from 1-9 for the data-weights.
   max="$( echo "${_weights}" | awk 'BEGIN{a=0}{if($2>a){a=$2}} END{print a}' )"
   min="$( echo "${_weights}" | awk 'BEGIN{a=50}{if($2<a){a=$2}} END{print a}' )"
   #echo "So we need to scale ${min}-${max} down to 1-${MAX_SCALE}" 1>&2
   echo "${_weights}" | sort -k2 | awk -v "outmax=${_outmax}" -v "outmin=${_outmin}" -v "max=${max}" -v "min=${min}" '{a=int(($2-min)/(max-min)*((outmax-outmin)+outmin));print $1,a,$2}'
   #/usr/bin/printf "%d\n" "$( printf '%s\n' \
   #   "(${_value}-${_min})/(${_max}-${_min})*((${_outmax}-${_outmin})+${_outmin})" | bc )" 2>/dev/null
}

smooth_scale() {
   # goal: list all items in weight order, then add an integer that scales as the list increments, where this new integer scales from 1-MAX
   # call: smooth_scale "${weights}" "${MAX_SCALE}"
   _weights="${1}"
   _max_scale="${2}"
   _max="$( echo "${_weights}" | wc -l )"
   echo "${weights}" | sort -k2 | awk -v "max_scale=${_max_scale}" -v "max=${_max}" '{a=int((NR/max)*max_scale);print $1,a,$2}'
}

# MAIN
cd "${INDIR}"
# Nikola generates "article:tag" contents which we can parse easily
# This outputs "ansible 24\ncentos 14\ncli 14\n"
weights="$( grep -h --include '*html' -riIE 'article:tag' . | awk '{print $3}' | awk -F'"' -v"t=${TRIM_TAGS_SMALLER_THAN}" '{a[$2]++} END {for(i in a){if(a[i]>=t){print i,a[i]}}}' | sort -k2 -n -r | head -n"${TAG_COUNT}" )"

# Choose how to scale the sizes of the words. smooth_scale is better.
#out_weights="$( math_scale "${weights}" "${MAX_SCALE}" 1 )"
out_weights="$( smooth_scale "${weights}" "${MAX_SCALE}" 1 )"

# Sort here. I might change my mind about how to sort them. Wordpress sorts the visible tags alphabetically.
# column 1 is name, 2 is the scaled weight, 3 is raw number
sorted_weights="$( echo "${out_weights}" | sort -k1 )"
#echo "${sorted_weights}" 1>&2

# build css
{
   cat <<'EOF'
ul.tagcloud a[data-weight="0"] { --size: 1; }
ul.tagcloud a[data-weight="1"] { --size: 2; }
ul.tagcloud a[data-weight="2"] { --size: 3; }
ul.tagcloud a[data-weight="3"] { --size: 4; }
ul.tagcloud a[data-weight="4"] { --size: 5; }
ul.tagcloud a[data-weight="5"] { --size: 6; }
ul.tagcloud a[data-weight="6"] { --size: 7; }
ul.tagcloud a[data-weight="7"] { --size: 8; }
ul.tagcloud a[data-weight="8"] { --size: 9; }
ul.tagcloud a[data-weight="9"] { --size: 10; }
ul.tagcloud a[data-weight="10"] { --size: 11; }

ul.tagcloud {
   list-style: none;
   padding-left: 0;
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   justify-content: center;
}

ul.tagcloud a {
   color: #a33;
   display: block;
   padding: 0.125rem 0.125rem;
   text-decoration: none;
   position: relative;
   --size: 2;
   font-size: calc(var(--size) * 0.125rem + .6666rem);
}

ul.tagcloud[show-data-value] a::after {
   content: " (" attr(data-value) ")";
   font-size: 1rem;
}

ul.tagcloud a::before {
   content: "";
   position: absolute;
   top: 0;
   left: 50%;
   width: 0;
   height: 100%;
   background: #000;
   transform: translate(-50%, 0);
   opacity: 0.15;
   transition: width 0.25s;
}

ul.tagcloud a:focus::before,
ul.tagcloud a:hover::before {
  width: 100%;
}

@media (prefers-reduced-motion) {
  ul.tagcloud * {
    transition: none !important;
  }
}
EOF
} > "${OUT_CSS}"

# build html
{
   # the base target=_parent enables the links to control the page outside this iframe-included tagcloud.html
   echo "<html><head><base target=\"_parent\"><link rel=\"stylesheet\" href=\"tagcloud.css\"></head><body>"
   echo "<ul class=\"tagcloud\" role=\"navigation\" ${SHOW_COUNTS:+show-data-value}>"
   echo "${sorted_weights}" | while read tag weight value ;
   do
      eval this_link="${LINK_STRING}"
      echo "   <li><a href=\"${this_link}\" data-weight=\"${weight}\" data-value=\"${value}\">${tag}</a></li>"
   done
   echo "</ul>"
   echo "</body></html>"
} > "${OUT_HTML}"

My nikola config file defines the sidebar with an iframe (so sue me) that loads this html file.

Comments