bb.sh 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. #!/usr/bin/env bash
  2. # BashBlog, a simple blog system written in a single bash script
  3. # Copyright: Carlos Fenollosa <carlos.fenollosa@gmail.com>, 2011-2014
  4. # With contributions from many others:
  5. # https://github.com/carlesfe/bashblog/contributors
  6. #########################################################################################
  7. #
  8. # README
  9. #
  10. #########################################################################################
  11. #
  12. # This is a very basic blog system
  13. #
  14. # Basically it asks the user to create a text file, then converts it into a .html file
  15. # and then rebuilds the index.html and feed.rss.
  16. #
  17. # Comments are supported via external service (Disqus).
  18. # Markdown syntax is supported via third party library (e.g. Gruber's Markdown.pl)
  19. #
  20. # This script is standalone, it doesn't require any other file to run
  21. #
  22. # Files that this script generates:
  23. # - main.css (inherited from my web page) and blog.css (blog-specific stylesheet)
  24. # - one .html for each post
  25. # - one tag_*.html file for each tag
  26. # - index.html (regenerated each run)
  27. # - feed.rss (idem)
  28. # - all_posts.html (idem)
  29. # - all_tags.html (idem)
  30. # - it also generates temporal files, which are removed afterwards
  31. #
  32. # It generates valid html and rss files, so keep care to use valid xhtml when editing a post
  33. #
  34. # There are many loops which iterate on '*.html' so make sure not to manually put other
  35. # html files on this folder.
  36. #
  37. # Read more: https://github.com/cfenollosa/bashblog
  38. #########################################################################################
  39. #
  40. # LICENSE
  41. #
  42. #########################################################################################
  43. #
  44. # This program is free software: you can redistribute it and/or modify
  45. # it under the terms of the GNU General Public License as published by
  46. # the Free Software Foundation, either version 3 of the License, or
  47. # (at your option) any later version.
  48. #
  49. # This program is distributed in the hope that it will be useful,
  50. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  51. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  52. # GNU General Public License for more details.
  53. #
  54. # You should have received a copy of the GNU General Public License
  55. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  56. #########################################################################################
  57. #
  58. # CHANGELOG
  59. #
  60. #########################################################################################
  61. #
  62. # 2.1 Support for tags/categories
  63. # 'delete' command
  64. # 2.0.3 Support for other analytics code, via external file
  65. # 2.0.2 Fixed bug when $body_begin_file was empty
  66. # Added extra line in the footer linking to the github project
  67. # 2.0.1 Allow personalized header/footer files
  68. # 2.0 Added Markdown support
  69. # Fully support BSD date
  70. # 1.6.4 Fixed bug in localized dates
  71. # 1.6.3 Now supporting BSD date
  72. # 1.6.2 Simplified some functions and variables to avoid duplicated information
  73. # 1.6.1 'date' fix when hours are 1 digit.
  74. # 1.6.0 Disqus comments. External configuration file. Check of 'date' command version.
  75. # 1.5.1 Misc bugfixes and parameter checks
  76. # 1.5 Đurađ Radojičić (djura-san) refactored some code and added flexibility and i18n
  77. # 1.4.2 Now issues are handled at Github
  78. # 1.4.1 Some code refactoring
  79. # 1.4 Using twitter for comments, improved 'rebuild' command
  80. # 1.3 'edit' command
  81. # 1.2.2 Feedburner support
  82. # 1.2.1 Fixed the timestamps bug
  83. # 1.2 'list' command
  84. # 1.1 Draft and preview support
  85. # 1.0 Read http://is.gd/Bkdoru
  86. #########################################################################################
  87. #
  88. # CODE
  89. #
  90. #########################################################################################
  91. #
  92. # As usual with bash scripts, scroll all the way to the bottom for the main routine
  93. # All other functions are declared above main.
  94. # Global variables
  95. # It is recommended to perform a 'rebuild' after changing any of this in the code
  96. # Config file. Any settings "key=value" written there will override the
  97. # global_variables defaults. Useful to avoid editing bb.sh and having to deal
  98. # with merges in VCS
  99. global_config=".config"
  100. # This function will load all the variables defined here. They might be overriden
  101. # by the 'global_config' file contents
  102. global_variables() {
  103. global_software_name="BashBlog"
  104. global_software_version="2.1"
  105. # Blog title
  106. global_title="My fancy blog"
  107. # The typical subtitle for each blog
  108. global_description="A blog about turtles and carrots"
  109. # The public base URL for this blog
  110. global_url="http://example.com/blog"
  111. # Your name
  112. global_author="John Smith"
  113. # You can use twitter or facebook or anything for global_author_url
  114. global_author_url="http://twitter.com/example"
  115. # Your email
  116. global_email="john@smith.com"
  117. # CC by-nc-nd is a good starting point, you can change this to "&copy;" for Copyright
  118. global_license="CC by-nc-nd"
  119. # If you have a Google Analytics ID (UA-XXXXX) and wish to use the standard
  120. # embedding code, put it on global_analytics
  121. # If you have custom analytics code (i.e. non-google) or want to use the Universal
  122. # code, leave global_analytics empty and specify a global_analytics_file
  123. global_analytics=""
  124. global_analytics_file=""
  125. # Leave this empty (i.e. "") if you don't want to use feedburner,
  126. # or change it to your own URL
  127. global_feedburner=""
  128. # Change this to your username if you want to use twitter for comments
  129. global_twitter_username=""
  130. # Change this to your disqus username to use disqus for comments
  131. global_disqus_username=""
  132. # Blog generated files
  133. # index page of blog (it is usually good to use "index.html" here)
  134. index_file="index.html"
  135. number_of_index_articles="8"
  136. # global archive
  137. archive_index="all_posts.html"
  138. tags_index="all_tags.html"
  139. # feed file (rss in this case)
  140. blog_feed="feed.rss"
  141. number_of_feed_articles="10"
  142. # "cut" blog entry when putting it to index page
  143. # i.e. include only up to first <hr> (---- in markdown)
  144. # possible values: "cut", ""
  145. cut_do="cut"
  146. # Regexp matching the HTML line where to do the cut
  147. # note that slash is regexp separator so you need to prepend it with backslash
  148. cut_line='<hr ?\/?>'
  149. # prefix for tags/categories files
  150. # please make sure that no other html file starts with this prefix
  151. prefix_tags="tag_"
  152. # personalized header and footer (only if you know what you're doing)
  153. # DO NOT name them .header.html, .footer.html or they will be overwritten
  154. # leave blank to generate them, recommended
  155. header_file=""
  156. footer_file=""
  157. # extra content to add just after we open the <body> tag
  158. # and before the actual blog content
  159. body_begin_file=""
  160. # Localization and i18n
  161. # "Comments?" (used in twitter link after every post)
  162. template_comments="Comments?"
  163. # "Read more..." (link under cut article on index page)
  164. template_read_more="Read more..."
  165. # "View more posts" (used on bottom of index page as link to archive)
  166. template_archive="View more posts"
  167. # "All posts" (title of archive page)
  168. template_archive_title="All posts"
  169. # "All tags"
  170. template_tags_title="All tags"
  171. # "posts" (on "All tags" page, text at the end of each tag line, like "2. Music - 15 posts")
  172. template_tags_posts="posts"
  173. # "Posts tagged" (text on a title of a page with index of one tag, like "My Blog - Posts tagged "Music"")
  174. template_tag_title="Posts tagged"
  175. # "Tags:" (beginning of line in HTML file with list of all tags for this article)
  176. template_tags_line_header="Tags:"
  177. # "Back to the index page" (used on archive page, it is link to blog index)
  178. template_archive_index_page="Back to the index page"
  179. # "Subscribe" (used on bottom of index page, it is link to RSS feed)
  180. template_subscribe="Subscribe"
  181. # "Subscribe to this page..." (used as text for browser feed button that is embedded to html)
  182. template_subscribe_browser_button="Subscribe to this page..."
  183. # "Tweet" (used as twitter text button for posting to twitter)
  184. template_twitter_button="Tweet"
  185. template_twitter_comment="&lt;Type your comment here but please leave the URL so that other people can follow the comments&gt;"
  186. # The locale to use for the dates displayed on screen (not for the timestamps)
  187. date_format="%B %d, %Y"
  188. date_locale="C"
  189. # Markdown location. Trying to autodetect by default.
  190. # The invocation must support the signature 'markdown_bin in.html > out.md'
  191. markdown_bin="$(which Markdown.pl)"
  192. }
  193. # Check for the validity of some variables
  194. # DO NOT EDIT THIS FUNCTION unless you know what you're doing
  195. global_variables_check() {
  196. [[ "$header_file" == ".header.html" ]] &&
  197. echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" &&
  198. exit
  199. [[ "$footer_file" == ".footer.html" ]] &&
  200. echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" &&
  201. exit
  202. }
  203. # Test if the markdown script is working correctly
  204. test_markdown() {
  205. [[ -z "$markdown_bin" ]] && return 1
  206. [[ -z "$(which diff)" ]] && return 1
  207. in="/tmp/md-in-$(echo $RANDOM).md"
  208. out="/tmp/md-out-$(echo $RANDOM).html"
  209. good="/tmp/md-good-$(echo $RANDOM).html"
  210. echo -e "line 1\n\nline 2" > $in
  211. echo -e "<p>line 1</p>\n\n<p>line 2</p>" > $good
  212. $markdown_bin $in > $out 2> /dev/null
  213. diff $good $out &> /dev/null # output is irrelevant, we'll check $?
  214. if [[ $? -ne 0 ]]; then
  215. rm -f $in $good $out
  216. return 1
  217. fi
  218. rm -f $in $good $out
  219. return 0
  220. }
  221. # Parse a Markdown file into HTML and return the generated file
  222. markdown() {
  223. out="$(echo $1 | sed 's/md$/html/g')"
  224. while [ -f "$out" ]; do out="$(echo $out | sed 's/\.html$/\.'$RANDOM'\.html')"; done
  225. $markdown_bin $1 > $out
  226. echo $out
  227. }
  228. # Prints the required google analytics code
  229. google_analytics() {
  230. [[ -z "$global_analytics" ]] && [[ -z "$global_analytics_file" ]] && return
  231. if [[ -z "$global_analytics_file" ]]; then
  232. echo "<script type=\"text/javascript\">
  233. var _gaq = _gaq || [];
  234. _gaq.push(['_setAccount', '"$global_analytics"']);
  235. _gaq.push(['_trackPageview']);
  236. (function() {
  237. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  238. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  239. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  240. })();
  241. </script>"
  242. else
  243. cat "$global_analytics_file"
  244. fi
  245. }
  246. # Prints the required code for disqus comments
  247. disqus_body() {
  248. [[ -z "$global_disqus_username" ]] && return
  249. echo '<div id="disqus_thread"></div>
  250. <script type="text/javascript">
  251. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  252. var disqus_shortname = '\'$global_disqus_username\''; // required: replace example with your forum shortname
  253. /* * * DONT EDIT BELOW THIS LINE * * */
  254. (function() {
  255. var dsq = document.createElement("script"); dsq.type = "text/javascript"; dsq.async = true;
  256. dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";
  257. (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(dsq);
  258. })();
  259. </script>
  260. <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
  261. <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>'
  262. }
  263. # Prints the required code for disqus in the footer
  264. disqus_footer() {
  265. [[ -z "$global_disqus_username" ]] && return
  266. echo '<script type="text/javascript">
  267. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  268. var disqus_shortname = '\'$global_disqus_username\''; // required: replace example with your forum shortname
  269. /* * * DONT EDIT BELOW THIS LINE * * */
  270. (function () {
  271. var s = document.createElement("script"); s.async = true;
  272. s.type = "text/javascript";
  273. s.src = "//" + disqus_shortname + ".disqus.com/count.js";
  274. (document.getElementsByTagName("HEAD")[0] || document.getElementsByTagName("BODY")[0]).appendChild(s);
  275. }());
  276. </script>'
  277. }
  278. # Reads HTML file from stdin, prints its content to stdout
  279. # $1 where to start ("text" or "entry")
  280. # $2 where to stop ("text" or "entry")
  281. # $3 "cut" to remove text from <hr /> to <!-- text end -->
  282. # note that this does not remove <hr /> line itself,
  283. # so you can see if text was cut or not
  284. get_html_file_content() {
  285. awk '/<!-- '$1' begin -->/, /<!-- '$2' end -->/{
  286. if (!/<!-- '$1' begin -->/ && !/<!-- '$2' end -->/) print
  287. if ("'$3'" == "cut" && /'"$cut_line"'/){
  288. if ("'$2'" == "text") exit # no need to read further
  289. while (getline > 0 && !/<!-- text end -->/) {}
  290. }
  291. }'
  292. }
  293. # Edit an existing, published .html file while keeping its original timestamp
  294. # Please note that this function does not automatically republish anything, as
  295. # it is usually called from 'main'.
  296. #
  297. # 'edit' is kind of an advanced feature, as it leaves to the user the responsibility
  298. # of editing an html file
  299. #
  300. # $1 the file to edit
  301. edit() {
  302. timestamp="$(date -r $1 +'%Y%m%d%H%M')"
  303. $EDITOR "$1"
  304. touch -t $timestamp "$1"
  305. }
  306. # Adds the code needed by the twitter button
  307. #
  308. # $1 the post URL
  309. twitter() {
  310. [[ -z "$global_twitter_username" ]] && return
  311. if [[ -z "$global_disqus_username" ]]; then
  312. echo "<p id='twitter'>$template_comments&nbsp;"
  313. else
  314. echo "<p id='twitter'><a href=\"$1#disqus_thread\">$template_comments</a> &nbsp;"
  315. fi
  316. echo "<a href=\"https://twitter.com/share\" class=\"twitter-share-button\" data-text=\"$template_twitter_comment\" data-url=\"$1\""
  317. echo " data-via=\"$global_twitter_username\""
  318. echo ">$template_twitter_button</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=\"//platform.twitter.com/widgets.js\";fjs.parentNode.insertBefore(js,fjs);}}(document,\"script\",\"twitter-wjs\");</script>"
  319. echo "</p>"
  320. }
  321. # Check if the file is a 'boilerplate' (i.e. not a post)
  322. # The return values are designed to be used like this inside a loop:
  323. # is_boilerplate_file <file> && continue
  324. #
  325. # $1 the file
  326. #
  327. # Return 0 (bash return value 'true') if the input file is am index, feed, etc
  328. # or 1 (bash return value 'false') if it is a blogpost
  329. is_boilerplate_file() {
  330. if [[ "$1" == "$index_file" ]] || [[ "$1" == "$archive_index" ]] || [[ "$1" == "$tags_index" ]] || [[ "$1" == "$footer_file" ]] || [[ "$1" == "$header_file" ]] || [[ "$1" == "$global_analytics_file" ]] || [[ "$1" = "$prefix_tags"* ]] ; then return 0
  331. else return 1
  332. fi
  333. }
  334. # Adds all the bells and whistles to format the html page
  335. # Every blog post is marked with a <!-- entry begin --> and <!-- entry end -->
  336. # which is parsed afterwards in the other functions. There is also a marker
  337. # <!-- text begin --> to determine just the beginning of the text body of the post
  338. #
  339. # $1 a file with the body of the content
  340. # $2 the output file
  341. # $3 "yes" if we want to generate the index.html,
  342. # "no" to insert new blog posts
  343. # $4 title for the html header
  344. # $5 original blog timestamp
  345. create_html_page() {
  346. content="$1"
  347. filename="$2"
  348. index="$3"
  349. title="$4"
  350. timestamp="$5"
  351. # Create the actual blog post
  352. # html, head
  353. cat ".header.html" > "$filename"
  354. echo "<title>$title</title>" >> "$filename"
  355. google_analytics >> "$filename"
  356. echo "</head><body>" >> "$filename"
  357. # stuff to add before the actual body content
  358. [[ -n "$body_begin_file" ]] && cat "$body_begin_file" >> "$filename"
  359. # body divs
  360. echo '<div id="divbodyholder">' >> "$filename"
  361. echo '<div class="headerholder"><div class="header">' >> "$filename"
  362. # blog title
  363. echo '<div id="title">' >> "$filename"
  364. cat .title.html >> "$filename"
  365. echo '</div></div></div>' >> "$filename" # title, header, headerholder
  366. echo '<div id="divbody"><div class="content">' >> "$filename"
  367. file_url="$(sed 's/.rebuilt//g' <<< $filename)" # Get the correct URL when rebuilding
  368. # one blog entry
  369. if [[ "$index" == "no" ]]; then
  370. echo '<!-- entry begin -->' >> "$filename" # marks the beginning of the whole post
  371. echo '<h3><a class="ablack" href="'$file_url'">' >> "$filename"
  372. # remove possible <p>'s on the title because of markdown conversion
  373. echo "$(echo "$title" | sed 's/<\/*p>//g')" >> "$filename"
  374. echo '</a></h3>' >> "$filename"
  375. if [[ "$timestamp" == "" ]]; then
  376. echo '<div class="subtitle">'$(LC_ALL=$date_locale date +"$date_format")' &mdash; ' >> "$filename"
  377. else
  378. echo '<div class="subtitle">'$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp") ' &mdash; ' >> "$filename"
  379. fi
  380. echo "$global_author</div>" >> "$filename"
  381. echo '<!-- text begin -->' >> "$filename" # This marks the text body, after the title, date...
  382. fi
  383. cat "$content" >> "$filename" # Actual content
  384. if [[ "$index" == "no" ]]; then
  385. echo '<!-- text end -->' >> "$filename"
  386. twitter "$global_url/$file_url" >> "$filename"
  387. echo '<!-- entry end -->' >> "$filename" # absolute end of the post
  388. fi
  389. echo '</div>' >> "$filename" # content
  390. # Add disqus commments except for index and all_posts pages
  391. if [[ ${filename%.*.*} != "index" && ${filename%.*.*} != "all_posts" ]]; then
  392. disqus_body >> "$filename"
  393. fi
  394. # page footer
  395. cat .footer.html >> "$filename"
  396. # close divs
  397. echo '</div></div>' >> "$filename" # divbody and divbodyholder
  398. disqus_footer >> "$filename"
  399. echo '</body></html>' >> "$filename"
  400. }
  401. # Parse the plain text file into an html file
  402. parse_file() {
  403. # Read for the title and check that the filename is ok
  404. title=""
  405. while read line; do
  406. if [[ "$title" == "" ]]; then
  407. # set title and
  408. # remove extra <p> and </p> added by markdown
  409. title=$(echo "$line" | sed 's/<\/*p>//g')
  410. filename="$(echo $title | tr [:upper:] [:lower:])"
  411. filename="$(echo $filename | sed 's/\ /-/g')"
  412. filename="$(echo $filename | tr -dc '[:alnum:]-')" # html likes alphanumeric
  413. filename="$filename.html"
  414. content="$filename.tmp"
  415. # Check for duplicate file names
  416. while [ -f "$filename" ]; do
  417. suffix="$RANDOM"
  418. filename="$(echo $filename | sed 's/\.html/'$suffix'\.html/g')"
  419. done
  420. else
  421. echo "$line" >> "$content"
  422. fi
  423. done < "$1"
  424. # Create the actual html page
  425. create_html_page "$content" "$filename" no "$title"
  426. rm "$content"
  427. }
  428. # Manages the creation of the text file and the parsing to html file
  429. # also the drafts
  430. write_entry() {
  431. fmt="html"; f="$2"
  432. [[ "$2" == "-m" ]] && fmt="md" && f="$3"
  433. if [[ "$fmt" == "md" ]]; then
  434. test_markdown
  435. if [[ "$?" -ne 0 ]]; then
  436. echo "Markdown is not working, please use HTML. Press a key to continue..."
  437. fmt="html"
  438. read
  439. fi
  440. fi
  441. if [[ "$f" != "" ]]; then
  442. TMPFILE="$f"
  443. if [[ ! -f "$TMPFILE" ]]; then
  444. echo "The file doesn't exist"
  445. delete_includes
  446. exit
  447. fi
  448. # check if TMPFILE is markdown even though the user didn't specify it
  449. extension="${TMPFILE##*.}"
  450. [[ "$extension" == "md" ]] && fmt="md"
  451. else
  452. TMPFILE=".entry-$RANDOM.$fmt"
  453. echo -e "Title on this line\n" >> "$TMPFILE"
  454. [[ "$fmt" == "html" ]] && cat << EOF >> "$TMPFILE"
  455. <p>The rest of the text file is an <b>html</b> blog post. The process will continue as soon
  456. as you exit your editor.</p>
  457. <p>$template_tags_line_header keep-this-tag-format, tags-are-optional, example</p>
  458. EOF
  459. [[ "$fmt" == "md" ]] && cat << EOF >> "$TMPFILE"
  460. The rest of the text file is a **Markdown** blog post. The process will continue
  461. as soon as you exit your editor.
  462. $template_tags_line_header keep-this-tag-format, tags-are-optional, beware-with-underscores-in-markdown, example
  463. EOF
  464. fi
  465. chmod 600 "$TMPFILE"
  466. post_status="E"
  467. while [ "$post_status" != "p" ] && [ "$post_status" != "P" ]; do
  468. $EDITOR "$TMPFILE"
  469. if [[ "$fmt" == "md" ]]; then
  470. html_from_md="$(markdown "$TMPFILE")"
  471. parse_file "$html_from_md"
  472. rm "$html_from_md"
  473. else
  474. parse_file "$TMPFILE" # this command sets $filename as the html processed file
  475. fi
  476. chmod 600 "$filename"
  477. echo -n "Preview? (Y/n) "
  478. read p
  479. if [[ "$p" != "n" ]] && [[ "$p" != "N" ]]; then
  480. chmod 644 "$filename"
  481. echo "Open $global_url/$filename in your browser"
  482. fi
  483. echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) "
  484. read post_status
  485. if [[ "$post_status" == "d" ]] || [[ "$post_status" == "D" ]]; then
  486. mkdir -p "drafts/"
  487. chmod 700 "drafts/"
  488. title="$(head -n 1 $TMPFILE)"
  489. title="$(echo $title | tr [:upper:] [:lower:])"
  490. title="$(echo $title | sed 's/\ /-/g')"
  491. title="$(echo $title | tr -dc '[:alnum:]-')"
  492. draft="drafts/$title.$fmt"
  493. while [ -f "$draft" ]; do draft="drafts/$title-$RANDOM.$fmt"; done
  494. mv "$TMPFILE" "$draft"
  495. chmod 600 "$draft"
  496. rm "$filename"
  497. delete_includes
  498. echo "Saved your draft as '$draft'"
  499. exit
  500. fi
  501. if [[ "$post_status" == "e" ]] || [[ "$post_status" == "E" ]]; then
  502. rm "$filename" # Delete the html file as it will be generated again
  503. fi
  504. done
  505. rm "$TMPFILE"
  506. # Parse possible tags
  507. cp "$filename" "$filename.bak"
  508. while read line; do
  509. if [[ "$line" = "<p>$template_tags_line_header"* ]]; then
  510. tags="$(echo "$line" | cut -d ":" -f 2- | sed -e 's/<\/p>//g' -e 's/^ *//' -e 's/ *$//' -e 's/, /,/g')"
  511. IFS=, read -r -a array <<< "$tags"
  512. echo -n "<p>$template_tags_line_header "
  513. (for item in "${array[@]}"; do
  514. echo -n "<a href='$prefix_tags$item.html'>$item</a>, "
  515. done ) | sed 's/, $//g'
  516. echo -e "</p>"
  517. else echo "$line"
  518. fi
  519. done < "$filename.bak" > "$filename"
  520. rm "$filename.bak"
  521. chmod 644 "$filename"
  522. echo "Posted $filename"
  523. }
  524. # Create an index page with all the posts
  525. all_posts() {
  526. echo -n "Creating an index page with all the posts "
  527. contentfile="$archive_index.$RANDOM"
  528. while [ -f "$contentfile" ]; do
  529. contentfile="$archive_index.$RANDOM"
  530. done
  531. echo "<h3>$template_archive_title</h3>" >> "$contentfile"
  532. echo "<ul>" >> "$contentfile"
  533. for i in $(ls -t *.html); do
  534. is_boilerplate_file "$i" && continue
  535. echo -n "."
  536. # Title
  537. title="$(get_post_title "$i")"
  538. echo -n '<li><a href="'$i'">'$title'</a> &mdash;' >> "$contentfile"
  539. # Date
  540. date="$(LC_ALL=$date_locale date -r "$i" +"$date_format")"
  541. echo " $date</li>" >> "$contentfile"
  542. done
  543. echo ""
  544. echo "</ul>" >> "$contentfile"
  545. echo '<div id="all_posts"><a href="'./'">'$template_archive_index_page'</a></div>' >> "$contentfile"
  546. create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title &mdash; All posts"
  547. mv "$archive_index.tmp" "$archive_index"
  548. chmod 644 "$archive_index"
  549. rm "$contentfile"
  550. }
  551. # Create an index page with all the tags
  552. all_tags() {
  553. echo -n "Creating an index page with all the tags "
  554. contentfile="$tags_index.$RANDOM"
  555. while [ -f "$contentfile" ]; do
  556. contentfile="$tags_index.$RANDOM"
  557. done
  558. echo "<h3>$template_tags_title</h3>" >> "$contentfile"
  559. echo "<ul>" >> "$contentfile"
  560. for i in $(ls $prefix_tags*.html); do
  561. echo -n "."
  562. nposts="$(grep -c "^<\!-- entry begin -->$" $i)"
  563. tagname="$(echo $i | cut -c $((${#prefix_tags}+1))- | sed 's/\.html//g')"
  564. echo "<li><a href="$i">$tagname</a> &mdash; $nposts $template_tags_posts</li>" >> "$contentfile"
  565. done
  566. echo ""
  567. echo "</ul>" >> "$contentfile"
  568. echo '<div id="all_posts"><a href="'./'">'$template_archive_index_page'</a></div>' >> "$contentfile"
  569. create_html_page "$contentfile" "$tags_index.tmp" yes "$global_title &mdash; $template_tags_title"
  570. mv "$tags_index.tmp" "$tags_index"
  571. chmod 644 "$tags_index"
  572. rm "$contentfile"
  573. }
  574. # Generate the index.html with the content of the latest posts
  575. rebuild_index() {
  576. echo -n "Rebuilding the index "
  577. newindexfile="$index_file.$RANDOM"
  578. contentfile="$newindexfile.content"
  579. while [ -f "$newindexfile" ]; do
  580. newindexfile="$index_file.$RANDOM"
  581. contentfile="$newindexfile.content"
  582. done
  583. # Create the content file
  584. n=0
  585. for i in $(ls -t *.html); do # sort by date, newest first
  586. is_boilerplate_file "$i" && continue;
  587. if [[ "$n" -ge "$number_of_index_articles" ]]; then break; fi
  588. if [ "$cut_do" ]; then
  589. get_html_file_content 'entry' 'entry' 'cut' <$i | awk '/'"$cut_line"'/ { print "<p class=\"readmore\"><a href=\"'$i'\">'"$template_read_more"'</a></p>" ; next } 1' >> "$contentfile"
  590. else
  591. get_html_file_content 'entry' 'entry' <$i >> "$contentfile"
  592. fi
  593. echo -n "."
  594. n=$(( $n + 1 ))
  595. done
  596. feed="$blog_feed"
  597. if [[ "$global_feedburner" != "" ]]; then feed="$global_feedburner"; fi
  598. echo '<div id="all_posts"><a href="'$archive_index'">'$template_archive'</a> &mdash; <a href="'$tags_index'">'$template_tags_title'</a> &mdash; <a href="'$feed'">'$template_subscribe'</a></div>' >> "$contentfile"
  599. echo ""
  600. create_html_page "$contentfile" "$newindexfile" yes "$global_title"
  601. rm "$contentfile"
  602. mv "$newindexfile" "$index_file"
  603. chmod 644 "$index_file"
  604. }
  605. # Rebuilds all tag_*.html files
  606. rebuild_tags() {
  607. echo -n "Rebuilding tag pages "
  608. n=0
  609. rm $prefix_tags*.tmp.html &> /dev/null
  610. # First we will process all files and create temporal tag files
  611. # with just the content of the posts
  612. for i in $(ls -t *.html); do
  613. is_boilerplate_file "$i" && continue;
  614. echo -n "."
  615. tmpfile="$(mktemp tmp.XXX)"
  616. awk '/<!-- entry begin -->/, /<!-- entry end -->/' "$i" >> "$tmpfile"
  617. while read line; do
  618. if [[ "$line" = "<p>$template_tags_line_header"* ]]; then
  619. # 'split' tags by commas
  620. echo "$line" | cut -c 10- | while IFS="," read -a tags; do
  621. for dirty_tag in "${tags[@]}"; do # extract html around it
  622. tag="$(echo $dirty_tag | grep -o ">.*</a>" | awk '{print substr($0, 2, length($0)-5)}' | tr " " "_")"
  623. # Add the content of this post to the tag file
  624. cat "$tmpfile" >> "$prefix_tags$tag".tmp.html
  625. done
  626. done
  627. fi
  628. done < "$tmpfile"
  629. rm "$tmpfile"
  630. done
  631. # Now generate the tag files with headers, footers, etc
  632. for i in $(ls -t $prefix_tags*.tmp.html); do
  633. tagname="$(echo $i | cut -c $((${#prefix_tags}+1))- | sed 's/\.tmp\.html//g')"
  634. create_html_page "$i" "$prefix_tags$tagname.html" yes "$global_title &mdash; $template_tag_title \"$tagname\""
  635. rm "$i"
  636. done
  637. echo
  638. }
  639. # Return the post title
  640. #
  641. # $1 the html file
  642. get_post_title() {
  643. awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' "$1"
  644. }
  645. # Displays a list of the posts
  646. list_posts() {
  647. ls *.html &> /dev/null
  648. [[ $? -ne 0 ]] && echo "No posts yet. Use 'bb.sh post' to create one" && return
  649. lines=""
  650. n=1
  651. for i in $(ls -t *.html); do
  652. is_boilerplate_file "$i" && continue
  653. line="$n # $(get_post_title "$i") # $(LC_ALL=$date_locale date -r $i +"$date_format")"
  654. lines="${lines}""$line""\n" # Weird stuff needed for the newlines
  655. n=$(( $n + 1 ))
  656. done
  657. echo -e "$lines" | column -t -s "#"
  658. }
  659. # Generate the feed file
  660. make_rss() {
  661. echo -n "Making RSS "
  662. rssfile="$blog_feed.$RANDOM"
  663. while [ -f "$rssfile" ]; do rssfile="$blog_feed.$RANDOM"; done
  664. echo '<?xml version="1.0" encoding="UTF-8" ?>' >> "$rssfile"
  665. echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">' >> "$rssfile"
  666. echo '<channel><title>'$global_title'</title><link>'$global_url'</link>' >> "$rssfile"
  667. echo '<description>'$global_description'</description><language>en</language>' >> "$rssfile"
  668. echo '<lastBuildDate>'$(date +"%a, %d %b %Y %H:%M:%S %z")'</lastBuildDate>' >> "$rssfile"
  669. echo '<pubDate>'$(date +"%a, %d %b %Y %H:%M:%S %z")'</pubDate>' >> "$rssfile"
  670. echo '<atom:link href="'$global_url/$blog_feed'" rel="self" type="application/rss+xml" />' >> "$rssfile"
  671. n=0
  672. for i in $(ls -t *.html); do
  673. is_boilerplate_file "$i" && continue
  674. [[ "$n" -ge "$number_of_feed_articles" ]] && break # max 10 items
  675. echo -n "."
  676. echo '<item><title>' >> "$rssfile"
  677. echo "$(get_post_title "$i")" >> "$rssfile"
  678. echo '</title><description><![CDATA[' >> "$rssfile"
  679. echo "$(get_html_file_content 'text' 'entry' $cut_do <$i)" >> "$rssfile"
  680. echo "]]></description><link>$global_url/$i</link>" >> "$rssfile"
  681. echo "<guid>$global_url/$i</guid>" >> "$rssfile"
  682. echo "<dc:creator>$global_author</dc:creator>" >> "$rssfile"
  683. echo '<pubDate>'$(date -r "$i" +"%a, %d %b %Y %H:%M:%S %z")'</pubDate></item>' >> "$rssfile"
  684. n=$(( $n + 1 ))
  685. done
  686. echo '</channel></rss>' >> "$rssfile"
  687. echo ""
  688. mv "$rssfile" "$blog_feed"
  689. chmod 644 "$blog_feed"
  690. }
  691. # generate headers, footers, etc
  692. create_includes() {
  693. echo '<h1 class="nomargin"><a class="ablack" href="'$global_url'">'$global_title'</a></h1>' > ".title.html"
  694. echo '<div id="description">'$global_description'</div>' >> ".title.html"
  695. if [[ -f "$header_file" ]]; then cp "$header_file" .header.html
  696. else
  697. echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' > ".header.html"
  698. echo '<html xmlns="http://www.w3.org/1999/xhtml"><head>' >> ".header.html"
  699. echo '<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />' >> ".header.html"
  700. echo '<link rel="stylesheet" href="main.css" type="text/css" />' >> ".header.html"
  701. echo '<link rel="stylesheet" href="blog.css" type="text/css" />' >> ".header.html"
  702. if [[ "$global_feedburner" == "" ]]; then
  703. echo '<link rel="alternate" type="application/rss+xml" title="'$template_subscribe_browser_button'" href="'$blog_feed'" />' >> ".header.html"
  704. else
  705. echo '<link rel="alternate" type="application/rss+xml" title="'$template_subscribe_browser_button'" href="'$global_feedburner'" />' >> ".header.html"
  706. fi
  707. fi
  708. if [[ -f "$footer_file" ]]; then cp "$footer_file" .footer.html
  709. else
  710. protected_mail="$(echo "$global_email" | sed 's/@/\&#64;/g' | sed 's/\./\&#46;/g')"
  711. echo '<div id="footer">'$global_license '<a href="'$global_author_url'">'$global_author'</a> &mdash; <a href="mailto:'$protected_mail'">'$protected_mail'</a><br/>' >> ".footer.html"
  712. echo 'Generated with <a href="https://github.com/cfenollosa/bashblog">bashblog</a>, a single bash script to easily create blogs like this one</div>' >> ".footer.html"
  713. fi
  714. }
  715. # Delete the temporarily generated include files
  716. delete_includes() {
  717. rm ".title.html" ".footer.html" ".header.html"
  718. }
  719. # Create the css file from scratch
  720. create_css() {
  721. # To avoid overwriting manual changes. However it is recommended that
  722. # this function is modified if the user changes the blog.css file
  723. if [[ ! -f "blog.css" ]]; then
  724. # blog.css directives will be loaded after main.css and thus will prevail
  725. echo '#title{font-size: x-large;}
  726. a.ablack{color:black !important;}
  727. li{margin-bottom:8px;}
  728. ul,ol{margin-left:24px;margin-right:24px;}
  729. #all_posts{margin-top:24px;text-align:center;}
  730. .subtitle{font-size:small;margin:12px 0px;}
  731. .content p{margin-left:24px;margin-right:24px;}
  732. h1{margin-bottom:12px !important;}
  733. #description{font-size:large;margin-bottom:12px;}
  734. h3{margin-top:42px;margin-bottom:8px;}
  735. h4{margin-left:24px;margin-right:24px;}
  736. #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;}' > blog.css
  737. fi
  738. # If there is a style.css from the parent page (i.e. some landing page)
  739. # then use it. This directive is here for compatibility with my own
  740. # home page. Feel free to edit it out, though it doesn't hurt
  741. if [[ -f "../style.css" ]] && [[ ! -f "main.css" ]]; then
  742. ln -s "../style.css" "main.css"
  743. elif [[ ! -f "main.css" ]]; then
  744. echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;}
  745. #divbodyholder{padding:5px;background-color:#DDD;width:874px;margin:24px auto;}
  746. #divbody{width:776px;border:solid 1px #ccc;background-color:#fff;padding:0px 48px 24px 48px;top:0;}
  747. .headerholder{background-color:#f9f9f9;border-top:solid 1px #ccc;border-left:solid 1px #ccc;border-right:solid 1px #ccc;}
  748. .header{width:800px;margin:0px auto;padding-top:24px;padding-bottom:8px;}
  749. .content{margin-bottom:45px;}
  750. .nomargin{margin:0;}
  751. .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;}
  752. h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;}
  753. .clear{clear:both;}
  754. #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;}
  755. a{text-decoration:none;color:#003366 !important;}
  756. a:visited{text-decoration:none;color:#336699 !important;}
  757. blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;}
  758. blockquote img{margin:12px 0px;}
  759. blockquote iframe{margin:12px 0px;}' > main.css
  760. fi
  761. }
  762. # Regenerates all the single post entries, keeping the post content but modifying
  763. # the title, html structure, etc
  764. rebuild_all_entries() {
  765. echo -n "Rebuilding all entries "
  766. for i in *.html; do # no need to sort
  767. is_boilerplate_file "$i" && continue;
  768. contentfile=".tmp.$RANDOM"
  769. while [ -f "$contentfile" ]; do contentfile=".tmp.$RANDOM"; done
  770. echo -n "."
  771. # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...)
  772. title="$(get_post_title "$i")"
  773. get_html_file_content 'text' 'text' <$i >> "$contentfile"
  774. # Original post timestamp
  775. timestamp="$(LC_ALL=$date_locale date -r $i +"%a, %d %b %Y %H:%M:%S %z" )"
  776. create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp"
  777. # keep the original timestamp!
  778. timestamp="$(LC_ALL=$date_locale date -r $i +'%Y%m%d%H%M')"
  779. mv "$i.rebuilt" "$i"
  780. chmod 644 "$i"
  781. touch -t $timestamp "$i"
  782. rm "$contentfile"
  783. done
  784. echo ""
  785. }
  786. # Displays the help
  787. function usage() {
  788. echo "$global_software_name v$global_software_version"
  789. echo "Usage: $0 command [filename]"
  790. echo ""
  791. echo "Commands:"
  792. echo " post [-m] [filename] insert a new blog post, or the filename of a draft to continue editing it"
  793. echo " use '-m' to edit the post as Markdown text"
  794. echo " edit [filename] edit an already published .html file. **NEVER** edit manually a published .html file,"
  795. echo " always use this function as it keeps internal data and rebuilds the blog"
  796. echo " delete [filename] deletes the post and rebuilds the blog"
  797. echo " rebuild regenerates all the pages and posts, preserving the content of the entries"
  798. echo " reset deletes everything except this script. Use with a lot of caution and back up first!"
  799. echo " list list all posts"
  800. echo ""
  801. echo "For more information please open $0 in a code editor and read the header and comments"
  802. }
  803. # Delete all generated content, leaving only this script
  804. reset() {
  805. echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" "
  806. read line
  807. if [[ "$line" == "Yes, I am!" ]]; then
  808. rm .*.html *.html *.css *.rss &> /dev/null
  809. echo
  810. echo "Deleted all posts, stylesheets and feeds."
  811. echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed."
  812. else
  813. echo "Phew! You dodged a bullet there. Nothing was modified."
  814. fi
  815. }
  816. # Detects if GNU date is installed
  817. date_version_detect() {
  818. date --version >/dev/null 2>&1
  819. if [[ $? -ne 0 ]]; then
  820. # date utility is BSD. Test if gdate is installed
  821. if gdate --version >/dev/null 2>&1 ; then
  822. date() {
  823. gdate "$@"
  824. }
  825. else
  826. # BSD date
  827. date() {
  828. if [[ "$1" == "-r" ]]; then
  829. # Fall back to using stat for 'date -r'
  830. format=$(echo $3 | sed 's/\+//g')
  831. stat -f "%Sm" -t "$format" "$2"
  832. elif [[ $(echo $@ | grep '\-\-date') ]]; then
  833. # convert between dates using BSD date syntax
  834. /bin/date -j -f "%a, %d %b %Y %H:%M:%S %z" "$(echo $2 | sed 's/\-\-date\=//g')" "$1"
  835. else
  836. # acceptable format for BSD date
  837. /bin/date -j "$@"
  838. fi
  839. }
  840. fi
  841. fi
  842. }
  843. # Main function
  844. # Encapsulated on its own function for readability purposes
  845. #
  846. # $1 command to run
  847. # $2 file name of a draft to continue editing (optional)
  848. do_main() {
  849. # Detect if using BSD date or GNU date
  850. date_version_detect
  851. # Load default configuration, then override settings with the config file
  852. global_variables
  853. [[ -f "$global_config" ]] && source "$global_config" &> /dev/null
  854. global_variables_check
  855. # Check for $EDITOR
  856. [[ -z "$EDITOR" ]] &&
  857. echo "Please set your \$EDITOR environment variable" && exit
  858. # Check for validity of argument
  859. [[ "$1" != "reset" ]] && [[ "$1" != "post" ]] && [[ "$1" != "rebuild" ]] && [[ "$1" != "list" ]] && [[ "$1" != "edit" ]] && [[ "$1" != "delete" ]] &&
  860. usage && exit
  861. [[ "$1" == "list" ]] &&
  862. list_posts && exit
  863. if [[ "$1" == "edit" ]]; then
  864. if [[ $# -lt 2 ]] || [[ ! -f "$2" ]]; then
  865. echo "Please enter a valid html file to edit"
  866. exit
  867. fi
  868. fi
  869. # Test for existing html files
  870. ls *.html &> /dev/null
  871. [[ $? -ne 0 ]] && [[ "$1" == "rebuild" ]] &&
  872. echo "Can't find any html files, nothing to rebuild" && exit
  873. # We're going to back up just in case
  874. ls *.html &> /dev/null
  875. [[ $? -eq 0 ]] &&
  876. tar cfz ".backup.tar.gz" *.html &&
  877. chmod 600 ".backup.tar.gz"
  878. [[ "$1" == "reset" ]] &&
  879. reset && exit
  880. create_includes
  881. create_css
  882. [[ "$1" == "post" ]] && write_entry "$@"
  883. [[ "$1" == "rebuild" ]] && rebuild_all_entries
  884. [[ "$1" == "edit" ]] && edit "$2"
  885. [[ "$1" == "delete" ]] && rm "$2" &> /dev/null
  886. rebuild_index
  887. all_posts
  888. rebuild_tags
  889. all_tags
  890. make_rss
  891. delete_includes
  892. }
  893. #
  894. # MAIN
  895. # Do not change anything here. If you want to modify the code, edit do_main()
  896. #
  897. do_main $*