bb.sh 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  1. #!/usr/bin/env bash
  2. # BashBlog, a simple blog system written in a single bash script
  3. # (C) Carlos Fenollosa <carlos.fenollosa@gmail.com>, 2011-2016 and contributors
  4. # https://github.com/carlesfe/bashblog/contributors
  5. # Check out README.md for more details
  6. # Global variables
  7. # It is recommended to perform a 'rebuild' after changing any of this in the code
  8. # Config file. Any settings "key=value" written there will override the
  9. # global_variables defaults. Useful to avoid editing bb.sh and having to deal
  10. # with merges in VCS
  11. global_config=".config"
  12. # This function will load all the variables defined here. They might be overridden
  13. # by the 'global_config' file contents
  14. global_variables() {
  15. global_software_name="BashBlog"
  16. global_software_version="2.10"
  17. # Blog title
  18. global_title="My fancy blog"
  19. # The typical subtitle for each blog
  20. global_description="A blog about turtles and carrots"
  21. # The public base URL for this blog
  22. global_url="http://example.com/blog"
  23. # Your name
  24. global_author="John Smith"
  25. # You can use twitter or facebook or anything for global_author_url
  26. global_author_url="http://twitter.com/example"
  27. # Your email
  28. global_email="john@smith.com"
  29. # CC by-nc-nd is a good starting point, you can change this to "&copy;" for Copyright
  30. global_license="CC by-nc-nd"
  31. # If you have a Google Analytics ID (UA-XXXXX) and wish to use the standard
  32. # embedding code, put it on global_analytics
  33. # If you have custom analytics code (i.e. non-google) or want to use the Universal
  34. # code, leave global_analytics empty and specify a global_analytics_file
  35. global_analytics=""
  36. global_analytics_file=""
  37. # Leave this empty (i.e. "") if you don't want to use feedburner,
  38. # or change it to your own URL
  39. global_feedburner=""
  40. # Change this to your username if you want to use twitter for comments
  41. global_twitter_username=""
  42. # Default image for the Twitter cards. Use an absolute URL
  43. global_twitter_card_image=""
  44. # Set this to false for a Twitter button with share count. The cookieless version
  45. # is just a link.
  46. global_twitter_cookieless="true"
  47. # Default search page, where tweets more than a week old are hidden
  48. global_twitter_search="twitter"
  49. # Change this to your disqus username to use disqus for comments
  50. global_disqus_username=""
  51. # Blog generated files
  52. # index page of blog (it is usually good to use "index.html" here)
  53. index_file="index.html"
  54. number_of_index_articles="8"
  55. # global archive
  56. archive_index="all_posts.html"
  57. tags_index="all_tags.html"
  58. # Non blogpost files. Bashblog will ignore these. Useful for static pages and custom content
  59. # Add them as a bash array, e.g. non_blogpost_files=("news.html" "test.html")
  60. non_blogpost_files=()
  61. # feed file (rss in this case)
  62. blog_feed="feed.rss"
  63. number_of_feed_articles="10"
  64. # "cut" blog entry when putting it to index page. Leave blank for full articles in front page
  65. # i.e. include only up to first '<hr>', or '----' in markdown
  66. cut_do="cut"
  67. # When cutting, cut also tags? If "no", tags will appear in index page for cut articles
  68. cut_tags="yes"
  69. # Regexp matching the HTML line where to do the cut
  70. # note that slash is regexp separator so you need to prepend it with backslash
  71. cut_line='<hr ?\/?>'
  72. # save markdown file when posting with "bb post -m". Leave blank to discard it.
  73. save_markdown="yes"
  74. # prefix for tags/categories files
  75. # please make sure that no other html file starts with this prefix
  76. prefix_tags="tag_"
  77. # personalized header and footer (only if you know what you're doing)
  78. # DO NOT name them .header.html, .footer.html or they will be overwritten
  79. # leave blank to generate them, recommended
  80. header_file=""
  81. footer_file=""
  82. # extra content to add just after we open the <body> tag
  83. # and before the actual blog content
  84. body_begin_file=""
  85. # extra content to add just before we close </body>
  86. body_end_file=""
  87. # extra content to ONLY on the index page AFTER `body_begin_file` contents
  88. # and before the actual content
  89. body_begin_file_index=""
  90. # CSS files to include on every page, f.ex. css_include=('main.css' 'blog.css')
  91. # leave empty to use generated
  92. css_include=()
  93. # HTML files to exclude from index, f.ex. post_exclude=('imprint.html 'aboutme.html')
  94. html_exclude=()
  95. # Localization and i18n
  96. # "Comments?" (used in twitter link after every post)
  97. template_comments="Comments?"
  98. # "Read more..." (link under cut article on index page)
  99. template_read_more="Read more..."
  100. # "View more posts" (used on bottom of index page as link to archive)
  101. template_archive="View more posts"
  102. # "All posts" (title of archive page)
  103. template_archive_title="All posts"
  104. # "All tags"
  105. template_tags_title="All tags"
  106. # "posts" (on "All tags" page, text at the end of each tag line, like "2. Music - 15 posts")
  107. template_tags_posts="posts"
  108. template_tags_posts_2_4="posts" # Some slavic languages use a different plural form for 2-4 items
  109. template_tags_posts_singular="post"
  110. # "Posts tagged" (text on a title of a page with index of one tag, like "My Blog - Posts tagged "Music"")
  111. template_tag_title="Posts tagged"
  112. # "Tags:" (beginning of line in HTML file with list of all tags for this article)
  113. template_tags_line_header="Tags:"
  114. # "Back to the index page" (used on archive page, it is link to blog index)
  115. template_archive_index_page="Back to the index page"
  116. # "Subscribe" (used on bottom of index page, it is link to RSS feed)
  117. template_subscribe="Subscribe"
  118. # "Subscribe to this page..." (used as text for browser feed button that is embedded to html)
  119. template_subscribe_browser_button="Subscribe to this page..."
  120. # "Tweet" (used as twitter text button for posting to twitter)
  121. template_twitter_button="Tweet"
  122. template_twitter_comment="&lt;Type your comment here but please leave the URL so that other people can follow the comments&gt;"
  123. # The locale to use for the dates displayed on screen
  124. date_format="%B %d, %Y"
  125. date_locale="C"
  126. date_inpost="bashblog_timestamp"
  127. # Don't change these dates
  128. date_format_full="%a, %d %b %Y %H:%M:%S %z"
  129. date_format_timestamp="%Y%m%d%H%M.%S"
  130. date_allposts_header="%B %Y"
  131. # Perform the post title -> filename conversion
  132. # Experts only. You may need to tune the locales too
  133. # Leave empty for no conversion, which is not recommended
  134. # This default filter respects backwards compatibility
  135. convert_filename="iconv -f utf-8 -t ascii//translit | sed 's/^-*//' | tr [:upper:] [:lower:] | tr ' ' '-' | tr -dc '[:alnum:]-'"
  136. # URL where you can view the post while it's being edited
  137. # same as global_url by default
  138. # You can change it to path on your computer, if you write posts locally
  139. # before copying them to the server
  140. preview_url=""
  141. # Markdown location. Trying to autodetect by default.
  142. # The invocation must support the signature 'markdown_bin in.md > out.html'
  143. [[ -f Markdown.pl ]] && markdown_bin=./Markdown.pl || markdown_bin=$(which Markdown.pl 2>/dev/null || which markdown 2>/dev/null)
  144. }
  145. # Check for the validity of some variables
  146. # DO NOT EDIT THIS FUNCTION unless you know what you're doing
  147. global_variables_check() {
  148. [[ $header_file == .header.html ]] &&
  149. echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" &&
  150. exit
  151. [[ $footer_file == .footer.html ]] &&
  152. echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" &&
  153. exit
  154. }
  155. # Test if the markdown script is working correctly
  156. test_markdown() {
  157. [[ -n $markdown_bin ]] &&
  158. (
  159. [[ $("$markdown_bin" <<< $'line 1\n\nline 2') == $'<p>line 1</p>\n\n<p>line 2</p>' ]] ||
  160. [[ $("$markdown_bin" <<< $'line 1\n\nline 2') == $'<p>line 1</p>\n<p>line 2</p>' ]]
  161. )
  162. }
  163. # Parse a Markdown file into HTML and return the generated file
  164. markdown() {
  165. out=${1%.md}.html
  166. while [[ -f $out ]]; do out=${out%.html}.$RANDOM.html; done
  167. $markdown_bin "$1" > "$out"
  168. echo "$out"
  169. }
  170. # Prints the required google analytics code
  171. google_analytics() {
  172. [[ -z $global_analytics && -z $global_analytics_file ]] && return
  173. if [[ -z $global_analytics_file ]]; then
  174. echo "<script type=\"text/javascript\">
  175. var _gaq = _gaq || [];
  176. _gaq.push(['_setAccount', '${global_analytics}']);
  177. _gaq.push(['_trackPageview']);
  178. (function() {
  179. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  180. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  181. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  182. })();
  183. </script>"
  184. else
  185. cat "$global_analytics_file"
  186. fi
  187. }
  188. # Prints the required code for disqus comments
  189. disqus_body() {
  190. [[ -z $global_disqus_username ]] && return
  191. echo '<div id="disqus_thread"></div>
  192. <script type="text/javascript">
  193. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  194. var disqus_shortname = '"'$global_disqus_username'"'; // required: replace example with your forum shortname
  195. /* * * DONT EDIT BELOW THIS LINE * * */
  196. (function() {
  197. var dsq = document.createElement("script"); dsq.type = "text/javascript"; dsq.async = true;
  198. dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";
  199. (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(dsq);
  200. })();
  201. </script>
  202. <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
  203. <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>'
  204. }
  205. # Prints the required code for disqus in the footer
  206. disqus_footer() {
  207. [[ -z $global_disqus_username ]] && return
  208. echo '<script type="text/javascript">
  209. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  210. var disqus_shortname = '"'$global_disqus_username'"'; // required: replace example with your forum shortname
  211. /* * * DONT EDIT BELOW THIS LINE * * */
  212. (function () {
  213. var s = document.createElement("script"); s.async = true;
  214. s.type = "text/javascript";
  215. s.src = "//" + disqus_shortname + ".disqus.com/count.js";
  216. (document.getElementsByTagName("HEAD")[0] || document.getElementsByTagName("BODY")[0]).appendChild(s);
  217. }());
  218. </script>'
  219. }
  220. # Reads HTML file from stdin, prints its content to stdout
  221. # $1 where to start ("text" or "entry")
  222. # $2 where to stop ("text" or "entry")
  223. # $3 "cut" to remove text from <hr /> to <!-- text end -->
  224. # note that this does not remove <hr /> line itself,
  225. # so you can see if text was cut or not
  226. get_html_file_content() {
  227. awk "/<!-- $1 begin -->/, /<!-- $2 end -->/{
  228. if (!/<!-- $1 begin -->/ && !/<!-- $2 end -->/) print
  229. if (\"$3\" == \"cut\" && /$cut_line/){
  230. if (\"$2\" == \"text\") exit # no need to read further
  231. while (getline > 0 && !/<!-- text end -->/) {
  232. if (\"$cut_tags\" == \"no\" && /^<p>$template_tags_line_header/ ) print
  233. }
  234. }
  235. }"
  236. }
  237. # Edit an existing, published .html file while keeping its original timestamp
  238. # Please note that this function does not automatically republish anything, as
  239. # it is usually called from 'main'.
  240. #
  241. # Note that it edits HTML file, even if you wrote the post as markdown originally
  242. # Note that if you edit title then filename might also change
  243. #
  244. # $1 the file to edit
  245. # $2 (optional) edit mode:
  246. # "keep" to keep old filename
  247. # "full" to edit full HTML, and not only text part (keeps old filename)
  248. # leave empty for default behavior (edit only text part and change name)
  249. edit() {
  250. [[ ! -f "${1%%.*}.html" ]] && echo "Can't edit post "${1%%.*}.html", did you mean to use \"bb.sh post <draft_file>\"?" && exit -1
  251. # Original post timestamp
  252. edit_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_full" )
  253. touch_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_timestamp")
  254. tags_before=$(tags_in_post "${1%%.*}.html")
  255. if [[ $2 == full ]]; then
  256. $EDITOR "$1"
  257. filename=$1
  258. else
  259. if [[ ${1##*.} == md ]]; then
  260. test_markdown
  261. if (($? != 0)); then
  262. echo "Markdown is not working, please edit HTML file directly."
  263. exit
  264. fi
  265. # editing markdown file
  266. $EDITOR "$1"
  267. TMPFILE=$(markdown "$1")
  268. filename=${1%%.*}.html
  269. else
  270. # Create the content file
  271. TMPFILE=$(basename "$1").$RANDOM.html
  272. # Title
  273. get_post_title "$1" > "$TMPFILE"
  274. # Post text with plaintext tags
  275. get_html_file_content 'text' 'text' <"$1" | sed "/^<p>$template_tags_line_header/s|<a href='$prefix_tags\([^']*\).html'>\\1</a>|\\1|g" >> "$TMPFILE"
  276. $EDITOR "$TMPFILE"
  277. filename=$1
  278. fi
  279. rm "$filename"
  280. if [[ $2 == keep ]]; then
  281. parse_file "$TMPFILE" "$edit_timestamp" "$filename"
  282. else
  283. parse_file "$TMPFILE" "$edit_timestamp" # this command sets $filename as the html processed file
  284. [[ ${1##*.} == md ]] && mv "$1" "${filename%%.*}.md" 2>/dev/null
  285. fi
  286. rm "$TMPFILE"
  287. fi
  288. touch -t "$touch_timestamp" "$filename"
  289. touch -t "$touch_timestamp" "$1"
  290. chmod 644 "$filename"
  291. echo "Posted $filename"
  292. tags_after=$(tags_in_post "$filename")
  293. relevant_tags=$(echo "$tags_before $tags_after" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ')
  294. if [[ ! -z $relevant_tags ]]; then
  295. relevant_posts="$(posts_with_tags $relevant_tags) $filename"
  296. rebuild_tags "$relevant_posts" "$relevant_tags"
  297. fi
  298. }
  299. # Create a Twitter summary (twitter "card") for the post
  300. #
  301. # $1 the post file
  302. # $2 the title
  303. twitter_card() {
  304. [[ -z $global_twitter_username ]] && return
  305. echo "<meta name='twitter:card' content='summary' />"
  306. echo "<meta name='twitter:site' content='@$global_twitter_username' />"
  307. echo "<meta name='twitter:title' content='$2' />" # Twitter truncates at 70 char
  308. description=$(grep -v "^<p>$template_tags_line_header" "$1" | sed -e 's/<[^>]*>//g' | tr '\n' ' ' | sed "s/\"/'/g" | head -c 250)
  309. echo "<meta name='twitter:description' content=\"$description\" />"
  310. # For the image we try to locate the first image in the article
  311. image=$(sed -n '2,$ d; s/.*<img.*src="\([^"]*\)".*/\1/p' "$1")
  312. # If none, then we try a global setting image
  313. [[ -z $image ]] && [[ -n $global_twitter_card_image ]] && image=$global_twitter_card_image
  314. # If none, return
  315. [[ -z $image ]] && return
  316. # Final housekeeping
  317. [[ $image =~ ^https?:// ]] || image=$global_url/$image # Check that URL is absolute
  318. echo "<meta name='twitter:image' content='$image' />"
  319. }
  320. # Adds the code needed by the twitter button
  321. #
  322. # $1 the post URL
  323. twitter() {
  324. [[ -z $global_twitter_username ]] && return
  325. if [[ -z $global_disqus_username ]]; then
  326. if [[ $global_twitter_cookieless == true ]]; then
  327. id=$RANDOM
  328. search_engine="https://twitter.com/search?q="
  329. echo "<p id='twitter'><a href='http://twitter.com/intent/tweet?url=$1&text=$template_twitter_comment&via=$global_twitter_username'>$template_comments $template_twitter_button</a> "
  330. echo "<a href='$search_engine""$1'><span id='count-$id'></span></a>&nbsp;</p>"
  331. return;
  332. else
  333. echo "<p id='twitter'>$template_comments&nbsp;";
  334. fi
  335. else
  336. echo "<p id='twitter'><a href=\"$1#disqus_thread\">$template_comments</a> &nbsp;"
  337. fi
  338. echo "<a href=\"https://twitter.com/share\" class=\"twitter-share-button\" data-text=\"$template_twitter_comment\" data-url=\"$1\""
  339. echo " data-via=\"$global_twitter_username\""
  340. 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>"
  341. echo "</p>"
  342. }
  343. # Check if the file is a 'boilerplate' (i.e. not a post)
  344. # The return values are designed to be used like this inside a loop:
  345. # is_boilerplate_file <file> && continue
  346. #
  347. # $1 the file
  348. #
  349. # Return 0 (bash return value 'true') if the input file is an index, feed, etc
  350. # or 1 (bash return value 'false') if it is a blogpost
  351. is_boilerplate_file() {
  352. name=${1#./}
  353. # First check against user-defined non-blogpost pages
  354. for item in "${non_blogpost_files[@]}"; do
  355. [[ "$name" == "$item" ]] && return 0
  356. done
  357. case $name in
  358. ( "$index_file" | "$archive_index" | "$tags_index" | "$footer_file" | "$header_file" | "$global_analytics_file" | "$prefix_tags"* )
  359. return 0 ;;
  360. ( * ) # Check for excluded
  361. for excl in "${html_exclude[@]}"; do
  362. [[ $name == "$excl" ]] && return 0
  363. done
  364. return 1 ;;
  365. esac
  366. }
  367. # Adds all the bells and whistles to format the html page
  368. # Every blog post is marked with a <!-- entry begin --> and <!-- entry end -->
  369. # which is parsed afterwards in the other functions. There is also a marker
  370. # <!-- text begin --> to determine just the beginning of the text body of the post
  371. #
  372. # $1 a file with the body of the content
  373. # $2 the output file
  374. # $3 "yes" if we want to generate the index.html,
  375. # "no" to insert new blog posts
  376. # $4 title for the html header
  377. # $5 original blog timestamp
  378. # $6 post author
  379. create_html_page() {
  380. content=$1
  381. filename=$2
  382. index=$3
  383. title=$4
  384. timestamp=$5
  385. author=$6
  386. # Create the actual blog post
  387. # html, head
  388. {
  389. cat ".header.html"
  390. echo "<title>$title</title>"
  391. google_analytics
  392. twitter_card "$content" "$title"
  393. echo "</head><body>"
  394. # stuff to add before the actual body content
  395. [[ -n $body_begin_file ]] && cat "$body_begin_file"
  396. [[ $filename = $index_file* ]] && [[ -n $body_begin_file_index ]] && cat "$body_begin_file_index"
  397. # body divs
  398. echo '<div id="divbodyholder">'
  399. echo '<div class="headerholder"><div class="header">'
  400. # blog title
  401. echo '<div id="title">'
  402. cat .title.html
  403. echo '</div></div></div>' # title, header, headerholder
  404. echo '<div id="divbody"><div class="content">'
  405. file_url=${filename#./}
  406. file_url=${file_url%.rebuilt} # Get the correct URL when rebuilding
  407. # one blog entry
  408. if [[ $index == no ]]; then
  409. echo '<!-- entry begin -->' # marks the beginning of the whole post
  410. echo "<h3><a class=\"ablack\" href=\"$file_url\">"
  411. # remove possible <p>'s on the title because of markdown conversion
  412. title=${title//<p>/}
  413. title=${title//<\/p>/}
  414. echo "$title"
  415. echo '</a></h3>'
  416. if [[ -z $timestamp ]]; then
  417. echo "<!-- $date_inpost: #$(LC_ALL=$date_locale date +"$date_format_timestamp")# -->"
  418. else
  419. echo "<!-- $date_inpost: #$(LC_ALL=$date_locale date +"$date_format_timestamp" --date="$timestamp")# -->"
  420. fi
  421. if [[ -z $timestamp ]]; then
  422. echo -n "<div class=\"subtitle\">$(LC_ALL=$date_locale date +"$date_format")"
  423. else
  424. echo -n "<div class=\"subtitle\">$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp")"
  425. fi
  426. [[ -n $author ]] && echo -e " &mdash; \n$author"
  427. echo "</div>"
  428. echo '<!-- text begin -->' # This marks the text body, after the title, date...
  429. fi
  430. cat "$content" # Actual content
  431. if [[ $index == no ]]; then
  432. echo -e '\n<!-- text end -->'
  433. twitter "$global_url/$file_url"
  434. echo '<!-- entry end -->' # absolute end of the post
  435. fi
  436. echo '</div>' # content
  437. # Add disqus commments except for index and all_posts pages
  438. [[ $index == no ]] && disqus_body
  439. # page footer
  440. cat .footer.html
  441. # close divs
  442. echo '</div></div>' # divbody and divbodyholder
  443. disqus_footer
  444. [[ -n $body_end_file ]] && cat "$body_end_file"
  445. echo '</body></html>'
  446. } > "$filename"
  447. }
  448. # Parse the plain text file into an html file
  449. #
  450. # $1 source file name
  451. # $2 (optional) timestamp for the file
  452. # $3 (optional) destination file name
  453. # note that although timestamp is optional, something must be provided at its
  454. # place if destination file name is provided, i.e:
  455. # parse_file source.txt "" destination.html
  456. parse_file() {
  457. # Read for the title and check that the filename is ok
  458. title=""
  459. while IFS='' read -r line; do
  460. if [[ -z $title ]]; then
  461. # remove extra <p> and </p> added by markdown
  462. title=$(echo "$line" | sed 's/<\/*p>//g')
  463. if [[ -n $3 ]]; then
  464. filename=$3
  465. else
  466. filename=$title
  467. [[ -n $convert_filename ]] &&
  468. filename=$(echo "$title" | eval "$convert_filename")
  469. [[ -n $filename ]] ||
  470. filename=$RANDOM # don't allow empty filenames
  471. filename=$filename.html
  472. # Check for duplicate file names
  473. while [[ -f $filename ]]; do
  474. filename=${filename%.html}$RANDOM.html
  475. done
  476. fi
  477. content=$filename.tmp
  478. # Parse possible tags
  479. elif [[ $line == "<p>$template_tags_line_header"* ]]; then
  480. tags=$(echo "$line" | cut -d ":" -f 2- | sed -e 's/<\/p>//g' -e 's/^ *//' -e 's/ *$//' -e 's/, /,/g')
  481. IFS=, read -r -a array <<< "$tags"
  482. echo -n "<p>$template_tags_line_header " >> "$content"
  483. for item in "${array[@]}"; do
  484. echo -n "<a href='$prefix_tags$item.html'>$item</a>, "
  485. done | sed 's/, $/<\/p>/g' >> "$content"
  486. else
  487. echo "$line" >> "$content"
  488. fi
  489. done < "$1"
  490. # Create the actual html page
  491. create_html_page "$content" "$filename" no "$title" "$2" "$global_author"
  492. rm "$content"
  493. }
  494. # Manages the creation of the text file and the parsing to html file
  495. # also the drafts
  496. write_entry() {
  497. test_markdown && fmt=md || fmt=html
  498. f=$2
  499. [[ $2 == -html ]] && fmt=html && f=$3
  500. if [[ -n $f ]]; then
  501. TMPFILE=$f
  502. if [[ ! -f $TMPFILE ]]; then
  503. echo "The file doesn't exist"
  504. delete_includes
  505. exit
  506. fi
  507. # guess format from TMPFILE
  508. extension=${TMPFILE##*.}
  509. [[ $extension == md || $extension == html ]] && fmt=$extension
  510. # but let user override it (`bb.sh post -html file.md`)
  511. [[ $2 == -html ]] && fmt=html
  512. # Test if Markdown is working before re-posting a .md file
  513. if [[ $extension == md ]]; then
  514. test_markdown
  515. if (($? != 0)); then
  516. echo "Markdown is not working, please edit HTML file directly."
  517. exit
  518. fi
  519. fi
  520. else
  521. TMPFILE=.entry-$RANDOM.$fmt
  522. echo -e "Title on this line\n" >> "$TMPFILE"
  523. [[ $fmt == html ]] && cat << EOF >> "$TMPFILE"
  524. <p>The rest of the text file is an <b>html</b> blog post. The process will continue as soon
  525. as you exit your editor.</p>
  526. <p>$template_tags_line_header keep-this-tag-format, tags-are-optional, example</p>
  527. EOF
  528. [[ $fmt == md ]] && cat << EOF >> "$TMPFILE"
  529. The rest of the text file is a **Markdown** blog post. The process will continue
  530. as soon as you exit your editor.
  531. $template_tags_line_header keep-this-tag-format, tags-are-optional, beware-with-underscores-in-markdown, example
  532. EOF
  533. fi
  534. chmod 600 "$TMPFILE"
  535. post_status="E"
  536. filename=""
  537. while [[ $post_status != "p" && $post_status != "P" ]]; do
  538. [[ -n $filename ]] && rm "$filename" # Delete the generated html file, if any
  539. $EDITOR "$TMPFILE"
  540. if [[ $fmt == md ]]; then
  541. html_from_md=$(markdown "$TMPFILE")
  542. parse_file "$html_from_md"
  543. rm "$html_from_md"
  544. else
  545. parse_file "$TMPFILE" # this command sets $filename as the html processed file
  546. fi
  547. chmod 644 "$filename"
  548. [[ -n $preview_url ]] || preview_url=$global_url
  549. echo "To preview the entry, open $preview_url/$filename in your browser"
  550. echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) "
  551. read -r post_status
  552. if [[ $post_status == d || $post_status == D ]]; then
  553. mkdir -p "drafts/"
  554. chmod 700 "drafts/"
  555. title=$(head -n 1 $TMPFILE)
  556. [[ -n $convert_filename ]] && title=$(echo "$title" | eval "$convert_filename")
  557. [[ -n $title ]] || title=$RANDOM
  558. draft=drafts/$title.$fmt
  559. mv "$TMPFILE" "$draft"
  560. chmod 600 "$draft"
  561. rm "$filename"
  562. delete_includes
  563. echo "Saved your draft as '$draft'"
  564. exit
  565. fi
  566. done
  567. if [[ $fmt == md && -n $save_markdown ]]; then
  568. mv "$TMPFILE" "${filename%%.*}.md"
  569. else
  570. rm "$TMPFILE"
  571. fi
  572. chmod 644 "$filename"
  573. echo "Posted $filename"
  574. relevant_tags=$(tags_in_post $filename)
  575. if [[ -n $relevant_tags ]]; then
  576. relevant_posts="$(posts_with_tags $relevant_tags) $filename"
  577. rebuild_tags "$relevant_posts" "$relevant_tags"
  578. fi
  579. }
  580. # Create an index page with all the posts
  581. all_posts() {
  582. echo -n "Creating an index page with all the posts "
  583. contentfile=$archive_index.$RANDOM
  584. while [[ -f $contentfile ]]; do
  585. contentfile=$archive_index.$RANDOM
  586. done
  587. {
  588. echo "<h3>$template_archive_title</h3>"
  589. prev_month=""
  590. while IFS='' read -r i; do
  591. is_boilerplate_file "$i" && continue
  592. echo -n "." 1>&3
  593. # Month headers
  594. month=$(LC_ALL=$date_locale date -r "$i" +"$date_allposts_header")
  595. if [[ $month != "$prev_month" ]]; then
  596. [[ -n $prev_month ]] && echo "</ul>" # Don't close ul before first header
  597. echo "<h4 class='allposts_header'>$month</h4>"
  598. echo "<ul>"
  599. prev_month=$month
  600. fi
  601. # Title
  602. title=$(get_post_title "$i")
  603. echo -n "<li><a href=\"$i\">$title</a> &mdash;"
  604. # Date
  605. date=$(LC_ALL=$date_locale date -r "$i" +"$date_format")
  606. echo " $date</li>"
  607. done < <(ls -t ./*.html)
  608. echo "" 1>&3
  609. echo "</ul>"
  610. echo "<div id=\"all_posts\"><a href=\"./$index_file\">$template_archive_index_page</a></div>"
  611. } 3>&1 >"$contentfile"
  612. create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title &mdash; $template_archive_title" "$global_author"
  613. mv "$archive_index.tmp" "$archive_index"
  614. chmod 644 "$archive_index"
  615. rm "$contentfile"
  616. }
  617. # Create an index page with all the tags
  618. all_tags() {
  619. echo -n "Creating an index page with all the tags "
  620. contentfile=$tags_index.$RANDOM
  621. while [[ -f $contentfile ]]; do
  622. contentfile=$tags_index.$RANDOM
  623. done
  624. {
  625. echo "<h3>$template_tags_title</h3>"
  626. echo "<ul>"
  627. for i in $prefix_tags*.html; do
  628. [[ -f "$i" ]] || break
  629. echo -n "." 1>&3
  630. nposts=$(grep -c "<\!-- text begin -->" "$i")
  631. tagname=${i#"$prefix_tags"}
  632. tagname=${tagname%.html}
  633. case $nposts in
  634. 1) word=$template_tags_posts_singular;;
  635. 2|3|4) word=$template_tags_posts_2_4;;
  636. *) word=$template_tags_posts;;
  637. esac
  638. echo "<li><a href=\"$i\">$tagname</a> &mdash; $nposts $word</li>"
  639. done
  640. echo "" 1>&3
  641. echo "</ul>"
  642. echo "<div id=\"all_posts\"><a href=\"./$index_file\">$template_archive_index_page</a></div>"
  643. } 3>&1 > "$contentfile"
  644. create_html_page "$contentfile" "$tags_index.tmp" yes "$global_title &mdash; $template_tags_title" "$global_author"
  645. mv "$tags_index.tmp" "$tags_index"
  646. chmod 644 "$tags_index"
  647. rm "$contentfile"
  648. }
  649. # Generate the index.html with the content of the latest posts
  650. rebuild_index() {
  651. echo -n "Rebuilding the index "
  652. newindexfile=$index_file.$RANDOM
  653. contentfile=$newindexfile.content
  654. while [[ -f $newindexfile ]]; do
  655. newindexfile=$index_file.$RANDOM
  656. contentfile=$newindexfile.content
  657. done
  658. # Create the content file
  659. {
  660. n=0
  661. while IFS='' read -r i; do
  662. is_boilerplate_file "$i" && continue;
  663. if ((n >= number_of_index_articles)); then break; fi
  664. if [[ -n $cut_do ]]; then
  665. 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"
  666. else
  667. get_html_file_content 'entry' 'entry' <"$i"
  668. fi
  669. echo -n "." 1>&3
  670. n=$(( n + 1 ))
  671. done < <(ls -t ./*.html) # sort by date, newest first
  672. feed=$blog_feed
  673. if [[ -n $global_feedburner ]]; then feed=$global_feedburner; fi
  674. 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>"
  675. } 3>&1 >"$contentfile"
  676. echo ""
  677. create_html_page "$contentfile" "$newindexfile" yes "$global_title" "$global_author"
  678. rm "$contentfile"
  679. mv "$newindexfile" "$index_file"
  680. chmod 644 "$index_file"
  681. }
  682. # Finds all tags referenced in one post.
  683. # Accepts either filename as first argument, or post content at stdin
  684. # Prints one line with space-separated tags to stdout
  685. tags_in_post() {
  686. sed -n "/^<p>$template_tags_line_header/{s/^<p>$template_tags_line_header//;s/<[^>]*>//g;s/[ ,]\+/ /g;p;}" "$1" | tr ', ' ' '
  687. }
  688. # Finds all posts referenced in a number of tags.
  689. # Arguments are tags
  690. # Prints one line with space-separated tags to stdout
  691. posts_with_tags() {
  692. (($# < 1)) && return
  693. set -- "${@/#/$prefix_tags}"
  694. set -- "${@/%/.html}"
  695. sed -n '/^<h3><a class="ablack" href="[^"]*">/{s/.*href="\([^"]*\)">.*/\1/;p;}' "$@" 2> /dev/null
  696. }
  697. # Rebuilds tag_*.html files
  698. # if no arguments given, rebuilds all of them
  699. # if arguments given, they should have this format:
  700. # "FILE1 [FILE2 [...]]" "TAG1 [TAG2 [...]]"
  701. # where FILEn are files with posts which should be used for rebuilding tags,
  702. # and TAGn are names of tags which should be rebuilt.
  703. # example:
  704. # rebuild_tags "one_post.html another_article.html" "example-tag another-tag"
  705. # mind the quotes!
  706. rebuild_tags() {
  707. if (($# < 2)); then
  708. # will process all files and tags
  709. files=$(ls -t ./*.html)
  710. all_tags=yes
  711. else
  712. # will process only given files and tags
  713. files=$(printf '%s\n' $1 | sort -u)
  714. files=$(ls -t $files)
  715. tags=$2
  716. fi
  717. echo -n "Rebuilding tag pages "
  718. n=0
  719. if [[ -n $all_tags ]]; then
  720. rm ./"$prefix_tags"*.html &> /dev/null
  721. else
  722. for i in $tags; do
  723. rm "./$prefix_tags$i.html" &> /dev/null
  724. done
  725. fi
  726. # First we will process all files and create temporal tag files
  727. # with just the content of the posts
  728. tmpfile=tmp.$RANDOM
  729. while [[ -f $tmpfile ]]; do tmpfile=tmp.$RANDOM; done
  730. while IFS='' read -r i; do
  731. is_boilerplate_file "$i" && continue;
  732. echo -n "."
  733. if [[ -n $cut_do ]]; then
  734. 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"
  735. else
  736. get_html_file_content 'entry' 'entry' <"$i"
  737. fi >"$tmpfile"
  738. for tag in $(tags_in_post "$i"); do
  739. if [[ -n $all_tags || " $tags " == *" $tag "* ]]; then
  740. cat "$tmpfile" >> "$prefix_tags$tag".tmp.html
  741. fi
  742. done
  743. done <<< "$files"
  744. rm "$tmpfile"
  745. # Now generate the tag files with headers, footers, etc
  746. while IFS='' read -r i; do
  747. tagname=${i#./"$prefix_tags"}
  748. tagname=${tagname%.tmp.html}
  749. create_html_page "$i" "$prefix_tags$tagname.html" yes "$global_title &mdash; $template_tag_title \"$tagname\"" "$global_author"
  750. rm "$i"
  751. done < <(ls -t ./"$prefix_tags"*.tmp.html 2>/dev/null)
  752. echo
  753. }
  754. # Return the post title
  755. #
  756. # $1 the html file
  757. get_post_title() {
  758. awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' "$1"
  759. }
  760. # Return the post author
  761. #
  762. # $1 the html file
  763. get_post_author() {
  764. awk '/<div class="subtitle">.+/, /<!-- text begin -->/{if (!/<div class="subtitle">.+/ && !/<!-- text begin -->/) print}' "$1" | sed 's/<\/div>//g'
  765. }
  766. # Displays a list of the tags
  767. #
  768. # $2 if "-n", tags will be sorted by number of posts
  769. list_tags() {
  770. if [[ $2 == -n ]]; then do_sort=1; else do_sort=0; fi
  771. ls ./$prefix_tags*.html &> /dev/null
  772. (($? != 0)) && echo "No posts yet. Use 'bb.sh post' to create one" && return
  773. lines=""
  774. for i in $prefix_tags*.html; do
  775. [[ -f "$i" ]] || break
  776. nposts=$(grep -c "<\!-- text begin -->" "$i")
  777. tagname=${i#"$prefix_tags"}
  778. tagname=${tagname#.html}
  779. ((nposts > 1)) && word=$template_tags_posts || word=$template_tags_posts_singular
  780. line="$tagname # $nposts # $word"
  781. lines+=$line\\n
  782. done
  783. if (( do_sort == 1 )); then
  784. echo -e "$lines" | column -t -s "#" | sort -nrk 2
  785. else
  786. echo -e "$lines" | column -t -s "#"
  787. fi
  788. }
  789. # Displays a list of the posts
  790. list_posts() {
  791. ls ./*.html &> /dev/null
  792. (($? != 0)) && echo "No posts yet. Use 'bb.sh post' to create one" && return
  793. lines=""
  794. n=1
  795. while IFS='' read -r i; do
  796. is_boilerplate_file "$i" && continue
  797. line="$n # $(get_post_title "$i") # $(LC_ALL=$date_locale date -r "$i" +"$date_format")"
  798. lines+=$line\\n
  799. n=$(( n + 1 ))
  800. done < <(ls -t ./*.html)
  801. echo -e "$lines" | column -t -s "#"
  802. }
  803. # Generate the feed file
  804. make_rss() {
  805. echo -n "Making RSS "
  806. rssfile=$blog_feed.$RANDOM
  807. while [[ -f $rssfile ]]; do rssfile=$blog_feed.$RANDOM; done
  808. {
  809. pubdate=$(LC_ALL=C date +"$date_format_full")
  810. echo '<?xml version="1.0" encoding="UTF-8" ?>'
  811. echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">'
  812. echo "<channel><title>$global_title</title><link>$global_url/$index_file</link>"
  813. echo "<description>$global_description</description><language>en</language>"
  814. echo "<lastBuildDate>$pubdate</lastBuildDate>"
  815. echo "<pubDate>$pubdate</pubDate>"
  816. echo "<atom:link href=\"$global_url/$blog_feed\" rel=\"self\" type=\"application/rss+xml\" />"
  817. n=0
  818. while IFS='' read -r i; do
  819. is_boilerplate_file "$i" && continue
  820. ((n >= number_of_feed_articles)) && break # max 10 items
  821. echo -n "." 1>&3
  822. echo '<item><title>'
  823. get_post_title "$i"
  824. echo '</title><description><![CDATA['
  825. get_html_file_content 'text' 'entry' $cut_do <"$i"
  826. echo "]]></description><link>$global_url/${i#./}</link>"
  827. echo "<guid>$global_url/$i</guid>"
  828. echo "<dc:creator>$(get_post_author "$i")</dc:creator>"
  829. echo "<pubDate>$(LC_ALL=C date -r "$i" +"$date_format_full")</pubDate></item>"
  830. n=$(( n + 1 ))
  831. done < <(ls -t ./*.html)
  832. echo '</channel></rss>'
  833. } 3>&1 >"$rssfile"
  834. echo ""
  835. mv "$rssfile" "$blog_feed"
  836. chmod 644 "$blog_feed"
  837. }
  838. # generate headers, footers, etc
  839. create_includes() {
  840. {
  841. echo "<h1 class=\"nomargin\"><a class=\"ablack\" href=\"$global_url/$index_file\">$global_title</a></h1>"
  842. echo "<div id=\"description\">$global_description</div>"
  843. } > ".title.html"
  844. if [[ -f $header_file ]]; then cp "$header_file" .header.html
  845. else {
  846. echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
  847. echo '<html xmlns="http://www.w3.org/1999/xhtml"><head>'
  848. echo '<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />'
  849. echo '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
  850. printf '<link rel="stylesheet" href="%s" type="text/css" />\n' "${css_include[@]}"
  851. if [[ -z $global_feedburner ]]; then
  852. echo "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$template_subscribe_browser_button\" href=\"$blog_feed\" />"
  853. else
  854. echo "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$template_subscribe_browser_button\" href=\"$global_feedburner\" />"
  855. fi
  856. } > ".header.html"
  857. fi
  858. if [[ -f $footer_file ]]; then cp "$footer_file" .footer.html
  859. else {
  860. protected_mail=${global_email//@/&#64;}
  861. protected_mail=${protected_mail//./&#46;}
  862. echo "<div id=\"footer\">$global_license <a href=\"$global_author_url\">$global_author</a> &mdash; <a href=\"mailto:$protected_mail\">$protected_mail</a><br/>"
  863. echo 'Generated with <a href="https://github.com/cfenollosa/bashblog">bashblog</a>, a single bash script to easily create blogs like this one</div>'
  864. } >> ".footer.html"
  865. fi
  866. }
  867. # Delete the temporarily generated include files
  868. delete_includes() {
  869. rm ".title.html" ".footer.html" ".header.html"
  870. }
  871. # Create the css file from scratch
  872. create_css() {
  873. # To avoid overwriting manual changes. However it is recommended that
  874. # this function is modified if the user changes the blog.css file
  875. (( ${#css_include[@]} > 0 )) && return || css_include=('main.css' 'blog.css')
  876. if [[ ! -f blog.css ]]; then
  877. # blog.css directives will be loaded after main.css and thus will prevail
  878. echo '#title{font-size: x-large;}
  879. a.ablack{color:black !important;}
  880. li{margin-bottom:8px;}
  881. ul,ol{margin-left:24px;margin-right:24px;}
  882. #all_posts{margin-top:24px;text-align:center;}
  883. .subtitle{font-size:small;margin:12px 0px;}
  884. .content p{margin-left:24px;margin-right:24px;}
  885. h1{margin-bottom:12px !important;}
  886. #description{font-size:large;margin-bottom:12px;}
  887. h3{margin-top:42px;margin-bottom:8px;}
  888. h4{margin-left:24px;margin-right:24px;}
  889. img{max-width:100%;}
  890. #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;}' > blog.css
  891. fi
  892. # If there is a style.css from the parent page (i.e. some landing page)
  893. # then use it. This directive is here for compatibility with my own
  894. # home page. Feel free to edit it out, though it doesn't hurt
  895. if [[ -f ../style.css ]] && [[ ! -f main.css ]]; then
  896. ln -s "../style.css" "main.css"
  897. elif [[ ! -f main.css ]]; then
  898. echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;}
  899. #divbodyholder{padding:5px;background-color:#DDD;width:100%;max-width:874px;margin:24px auto;}
  900. #divbody{border:solid 1px #ccc;background-color:#fff;padding:0px 48px 24px 48px;top:0;}
  901. .headerholder{background-color:#f9f9f9;border-top:solid 1px #ccc;border-left:solid 1px #ccc;border-right:solid 1px #ccc;}
  902. .header{width:100%;max-width:800px;margin:0px auto;padding-top:24px;padding-bottom:8px;}
  903. .content{margin-bottom:5%;}
  904. .nomargin{margin:0;}
  905. .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;}
  906. h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;}
  907. .clear{clear:both;}
  908. #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;}
  909. a{text-decoration:none;color:#003366 !important;}
  910. a:visited{text-decoration:none;color:#336699 !important;}
  911. blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;}
  912. blockquote img{margin:12px 0px;}
  913. blockquote iframe{margin:12px 0px;}' > main.css
  914. fi
  915. }
  916. # Regenerates all the single post entries, keeping the post content but modifying
  917. # the title, html structure, etc
  918. rebuild_all_entries() {
  919. echo -n "Rebuilding all entries "
  920. for i in ./*.html; do
  921. is_boilerplate_file "$i" && continue;
  922. contentfile=.tmp.$RANDOM
  923. while [[ -f $contentfile ]]; do contentfile=.tmp.$RANDOM; done
  924. echo -n "."
  925. # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...)
  926. title=$(get_post_title "$i")
  927. get_html_file_content 'text' 'text' <"$i" >> "$contentfile"
  928. # Read timestamp from post, if present, and sync file timestamp
  929. timestamp=$(awk '/<!-- '$date_inpost': .+ -->/ { print }' "$i" | cut -d '#' -f 2)
  930. [[ -n $timestamp ]] && touch -t "$timestamp" "$i"
  931. # Read timestamp from file in correct format for 'create_html_page'
  932. timestamp=$(LC_ALL=C date -r "$i" +"$date_format_full")
  933. create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp" "$(get_post_author "$i")"
  934. # keep the original timestamp!
  935. timestamp=$(LC_ALL=C date -r "$i" +"$date_format_timestamp")
  936. mv "$i.rebuilt" "$i"
  937. chmod 644 "$i"
  938. touch -t "$timestamp" "$i"
  939. rm "$contentfile"
  940. done
  941. echo ""
  942. }
  943. # Displays the help
  944. usage() {
  945. echo "$global_software_name v$global_software_version"
  946. echo "Usage: $0 command [filename]"
  947. echo ""
  948. echo "Commands:"
  949. echo " post [-html] [filename] insert a new blog post, or the filename of a draft to continue editing it"
  950. echo " it tries to use markdown by default, and falls back to HTML if it's not available."
  951. echo " use '-html' to override it and edit the post as HTML even when markdown is available"
  952. echo " edit [-n|-f] [filename] edit an already published .html or .md file. **NEVER** edit manually a published .html file,"
  953. echo " always use this function as it keeps internal data and rebuilds the blog"
  954. echo " use '-n' to give the file a new name, if title was changed"
  955. echo " use '-f' to edit full html file, instead of just text part (also preserves name)"
  956. echo " delete [filename] deletes the post and rebuilds the blog"
  957. echo " rebuild regenerates all the pages and posts, preserving the content of the entries"
  958. echo " reset deletes everything except this script. Use with a lot of caution and back up first!"
  959. echo " list list all posts"
  960. echo " tags [-n] list all tags in alphabetical order"
  961. echo " use '-n' to sort list by number of posts"
  962. echo ""
  963. echo "For more information please open $0 in a code editor and read the header and comments"
  964. }
  965. # Delete all generated content, leaving only this script
  966. reset() {
  967. echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" "
  968. read -r line
  969. if [[ $line == "Yes, I am!" ]]; then
  970. rm .*.html ./*.html ./*.css ./*.rss &> /dev/null
  971. echo
  972. echo "Deleted all posts, stylesheets and feeds."
  973. echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed."
  974. else
  975. echo "Phew! You dodged a bullet there. Nothing was modified."
  976. fi
  977. }
  978. # Detects if GNU date is installed
  979. date_version_detect() {
  980. date --version >/dev/null 2>&1
  981. if (($? != 0)); then
  982. # date utility is BSD. Test if gdate is installed
  983. if gdate --version >/dev/null 2>&1 ; then
  984. date() {
  985. gdate "$@"
  986. }
  987. else
  988. # BSD date
  989. date() {
  990. if [[ $1 == -r ]]; then
  991. # Fall back to using stat for 'date -r'
  992. format=${3//+/}
  993. stat -f "%Sm" -t "$format" "$2"
  994. elif [[ $2 == --date* ]]; then
  995. # convert between dates using BSD date syntax
  996. command date -j -f "$date_format_full" "${2#--date=}" "$1"
  997. else
  998. # acceptable format for BSD date
  999. command date -j "$@"
  1000. fi
  1001. }
  1002. fi
  1003. fi
  1004. }
  1005. # Main function
  1006. # Encapsulated on its own function for readability purposes
  1007. #
  1008. # $1 command to run
  1009. # $2 file name of a draft to continue editing (optional)
  1010. do_main() {
  1011. # Detect if using BSD date or GNU date
  1012. date_version_detect
  1013. # Load default configuration, then override settings with the config file
  1014. global_variables
  1015. [[ -f $global_config ]] && source "$global_config" &> /dev/null
  1016. global_variables_check
  1017. # Check for $EDITOR
  1018. [[ -z $EDITOR ]] &&
  1019. echo "Please set your \$EDITOR environment variable. For example, to use nano, add the line 'export EDITOR=nano' to your \$HOME/.bashrc file" && exit
  1020. # Check for validity of argument
  1021. [[ $1 != "reset" && $1 != "post" && $1 != "rebuild" && $1 != "list" && $1 != "edit" && $1 != "delete" && $1 != "tags" ]] &&
  1022. usage && exit
  1023. [[ $1 == list ]] &&
  1024. list_posts && exit
  1025. [[ $1 == tags ]] &&
  1026. list_tags "$@" && exit
  1027. if [[ $1 == edit ]]; then
  1028. if (($# < 2)) || [[ ! -f ${!#} ]]; then
  1029. echo "Please enter a valid .md or .html file to edit"
  1030. exit
  1031. fi
  1032. fi
  1033. # Test for existing html files
  1034. if ls ./*.html &> /dev/null; then
  1035. # We're going to back up just in case
  1036. tar -c -z -f ".backup.tar.gz" -- *.html &&
  1037. chmod 600 ".backup.tar.gz"
  1038. elif [[ $1 == rebuild ]]; then
  1039. echo "Can't find any html files, nothing to rebuild"
  1040. exit
  1041. fi
  1042. # Keep first backup of this day containing yesterday's version of the blog
  1043. [[ ! -f .yesterday.tar.gz || $(date -r .yesterday.tar.gz +'%d') != "$(date +'%d')" ]] &&
  1044. cp .backup.tar.gz .yesterday.tar.gz &> /dev/null
  1045. [[ $1 == reset ]] &&
  1046. reset && exit
  1047. create_css
  1048. create_includes
  1049. [[ $1 == post ]] && write_entry "$@"
  1050. [[ $1 == rebuild ]] && rebuild_all_entries && rebuild_tags
  1051. [[ $1 == delete ]] && rm "$2" &> /dev/null && rebuild_tags
  1052. if [[ $1 == edit ]]; then
  1053. if [[ $2 == -n ]]; then
  1054. edit "$3"
  1055. elif [[ $2 == -f ]]; then
  1056. edit "$3" full
  1057. else
  1058. edit "$2" keep
  1059. fi
  1060. fi
  1061. rebuild_index
  1062. all_posts
  1063. all_tags
  1064. make_rss
  1065. delete_includes
  1066. }
  1067. #
  1068. # MAIN
  1069. # Do not change anything here. If you want to modify the code, edit do_main()
  1070. #
  1071. do_main "$@"
  1072. # vim: set shiftwidth=4 tabstop=4 expandtab: