├── README.md └── bb.sh /README.md: -------------------------------------------------------------------------------- 1 | bashblog-ng 2 | ======== 3 | 4 | This is the Next Generation version of (https://github.com/cfenollosa/bashblog). Me, and [lots of other people](https://www.google.com/search?q=%22Generated+with+bashblog,+a+single+bash+script+to+easily+create+blogs+like+this+one%22), love bashblog as a simple tool to maintain a blog. The reason for bashblog-ng is simply that I wanted more features added but did not want to pollute the original bashblog with lots of pull requests. 5 | 6 | A single Bash script to create blogs. 7 | 8 | Created because someone wanted a very, very simple way to post entries to a blog by using a public folder on a server, without any special requirements and dependencies. Works on GNU/Linux, OSX and BSD. 9 | 10 | You can see a sample here: (https://yagni.rocks). That page was 100% generated using bashblog-ng, no additional tweaking. 11 | 12 | Check out [other bashblog-ng users](https://www.google.com/search?q=%22Generated+with+bashblog-ng+-+%28B%29e+%28A%29wesome+%26+%28S%29imple+%28H%29omie%22) 13 | 14 | 15 | Usage 16 | ----- 17 | 18 | Download the code and copy bb.sh into a public folder (for example, `$HOME/public_html/blog`) and run 19 | 20 | ./bb.sh 21 | 22 | This will show the available commands. If the file is not executable, type `chmod +x bb.sh` and retry. 23 | 24 | **Before creating your first post, you may want to configure the blog settings (title, author, etc). 25 | Read the Configuration section below for more information** 26 | 27 | To create your first post, just run: 28 | 29 | ./bb.sh post 30 | 31 | It will try to use Markdown, if installed. To force HTML: 32 | 33 | ./bb.sh post -html 34 | 35 | The script will handle the rest. 36 | 37 | When you're done, access the public URL for that folder (e.g. `http://server.com/~username/blog`) 38 | and you should see the index file and a new page for that post! 39 | 40 | 41 | Features 42 | -------- 43 | 44 | - Ultra simple usage: Just type a post with your favorite editor and the script does the rest. No templating. 45 | - No installation required. Download `bb.sh` and start blogging. 46 | - Zero dependencies. It runs just on base utils (`date`, `basename`, `grep`, `sed`, `head`, etc) 47 | - GNU/Linux, BSD and OSX compatible out of the box, no need for GNU `coreutils` on a Mac. 48 | It does some magic to autodetect which command switches it needs to run depending on your system. 49 | - All content is static. You only need shell access to a machine with a public web folder. 50 | *Tip: advanced users could mount a remote public folder via `ftpfs` and run this script locally* 51 | - Allows drafts, includes a simple but clean stylesheet, generates the RSS file automatically. 52 | - Support for tags/categories 53 | - Support for Markdown, Disqus comments, Twitter, Feedburner, Google Analytics. 54 | - The project is still maintained as of 2016. Bugs are fixed, and new features are considered (see "Contributing") 55 | - Everything stored in a single ~1k lines bash script, how cool is that?! ;) 56 | 57 | 58 | Configuration 59 | ------------- 60 | 61 | Configuration is not required for a test drive, but if you plan on running your blog with bashblog, you will 62 | want to change the default titles, author names, etc, to match your own. 63 | 64 | There are two ways to configure the blog strings: 65 | 66 | - Edit `bb.sh` and modify the variables in the `global_variables()` function 67 | - Create a `.config` file with your configuration values -- useful if you don't want to touch the script and be able to update it regularly with git 68 | 69 | The software will load the values in the script first, then overwrite them with the values in the `.config` file. 70 | This means that you don't need to define all variables in the config file, only those which you need to override 71 | from the defaults. 72 | 73 | The format of the `.config` file is just one `variablename="value"` per line, just like in the `global_variables()` 74 | function. **Please remember:** quote the values, do not declare a variable with the dollar sign, do not use 75 | spaces around the equal sign. 76 | 77 | bashblog uses the `$EDITOR` environment value to open the text editor. 78 | 79 | 80 | Detailed features 81 | ----------------- 82 | 83 | - HTML5 compatable 84 | - Support for github username with corner-link 85 | - A simple but nice and readable design, with nothing but the blog posts 86 | - **NEW on 2.0** Markdown support via a third-party library. 87 | The easiest method is to download 88 | Gruber's [Markdown.pl](http://daringfireball.net/projects/markdown/) 89 | - Post preview 90 | - Save posts as drafts and resume editing later 91 | - HTML page for each post, using its title as the URL 92 | - Configurable number of posts on the front page 93 | - Automatic generation of an RSS file, feedburner support 94 | - Additional page containing an index of all posts 95 | - Automatically generates pages for each tag 96 | - Rebuild all files while keeping the original data 97 | - Comments delegated to Twitter, with additional Disqus support 98 | - An option for cookieless Twitter sharing, to comply with the 99 | [EU cookie law](https://github.com/cfenollosa/eu-cookie-law) 100 | - Google Analytics code support 101 | - Contains its own CSS so that everything is reasonably styled by default 102 | - Headers, footers, and in general everything that a well-structured html file needs 103 | - Support to add extra content on top of every page (e.g. banners, images, etc) 104 | - xhtml validation, CSS validation, RSS validation by the w3c 105 | - Automatic backup of the site every time you post (stored as `.backup.tar.gz`) 106 | 107 | Read the Changelog section for more updates or [check out the news on my blog](http://cfenollosa.com/blog/tag_bashblog.html) 108 | 109 | 110 | Contributing 111 | ------------ 112 | 113 | Bashblog started at 500 SLOC and it now has hit the 1000 SLOC barrier. 114 | If we want to keep the code minimal and understandable, we need to make the difficult effort to restrain ourselves 115 | from adding too many features. 116 | 117 | All bugfixes are welcome, but brand new features need to be strongly justified to get into the main tree. 118 | Every new request will be honestly and civilly discussed on the comments. 119 | As a guideline, pull requests should: 120 | 121 | - Fix a use case for some people (e.g. internationalization) 122 | - Add a use case which is arguably very common (e.g. disqus integration for comments) 123 | - Be very small when possible (a couple lines of code) 124 | - Don't require a significant rewrite of the code (Don't break `create_html_file()` or `write_entry()`, etc) 125 | - It must work on Linux, BSD and Mac. Beware of using GNU coreutils with non-POSIX flags (i.e. `date` or `grep`) 126 | - Follow the UNIX philosophy: do one thing and do it well, rely on third party software for external features, etc 127 | - **Always** keep backwards compatibility when using the default configuration 128 | 129 | 130 | License 131 | ------- 132 | 133 | This program is free software: you can redistribute it and/or modify 134 | it under the terms of the GNU General Public License as published by 135 | the Free Software Foundation, either version 3 of the License, or 136 | (at your option) any later version. 137 | 138 | This program is distributed in the hope that it will be useful, 139 | but WITHOUT ANY WARRANTY; without even the implied warranty of 140 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 141 | GNU General Public License for more details. 142 | 143 | You should have received a copy of the GNU General Public License 144 | along with this program. If not, see . 145 | -------------------------------------------------------------------------------- /bb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # BashBlog-ng, Bash script to create and manage blogs 4 | # (C) David Satime Wallin , 2019, 2020, 2021, 2022 and contributors 5 | # https://github.com/dvwallin/bashblog-ng/contributors 6 | # (C) Carlos Fenollosa , 2011-2016 and contributors 7 | # https://github.com/carlesfe/bashblog/contributors 8 | # Check out README.md for more details 9 | 10 | # Global variables 11 | # It is recommended to perform a 'rebuild' after changing any of this in the code 12 | 13 | # Config file. Any settings "key=value" written there will override the 14 | # global_variables defaults. Useful to avoid editing bb.sh and having to deal 15 | # with merges in VCS 16 | global_config=".config" 17 | 18 | # This function will load all the variables defined here. They might be overridden 19 | # by the 'global_config' file contents 20 | global_variables() { 21 | global_software_name="bashblog-ng" 22 | global_software_version="1.0" 23 | 24 | # Blog title 25 | global_title="My Personal Blog" 26 | # The typical subtitle for each blog 27 | global_description="Statically Generated by bashblog-ng" 28 | # The public base URL for this blog 29 | global_url="http://example.com/blog" 30 | 31 | # Your name 32 | global_author="John Smith" 33 | # You can use twitter or facebook or anything for global_author_url 34 | global_author_url="http://twitter.com/example" 35 | # Your email 36 | global_email="john.doe@example.org" 37 | 38 | # CC by-nc-nd is a good starting point, you can change this to "©" for Copyright 39 | global_license="CC by-nc-nd" 40 | 41 | # If you have a Google Analytics ID (UA-XXXXX) and wish to use the standard 42 | # embedding code, put it on global_analytics 43 | # If you have custom analytics code (i.e. non-google) or want to use the Universal 44 | # code, leave global_analytics empty and specify a global_analytics_file 45 | global_analytics="" 46 | global_analytics_file="" 47 | 48 | # Leave this empty (i.e. "") if you don't want to use feedburner, 49 | # or change it to your own URL 50 | global_feedburner="" 51 | 52 | # Change this to your username if you want to use twitter for comments 53 | global_twitter_username="" 54 | # Set this to false for a Twitter button with share count. The cookieless version 55 | # is just a link. 56 | global_twitter_cookieless="true" 57 | # Default search page, where tweets more than a week old are hidden 58 | global_twitter_search="twitter" 59 | 60 | # Set your github username here to display a github-corner linking to your github profile 61 | global_github_username="" 62 | 63 | # Set to _blank if you want your github profile to open in a new tab/window. Default is _self 64 | github_link_target="" 65 | 66 | 67 | # Blog generated files 68 | # index page of blog (it is usually good to use "index.html" here) 69 | index_file="index.html" 70 | number_of_index_articles="8" 71 | # global archive 72 | archive_index="all_posts.html" 73 | tags_index="all_tags.html" 74 | 75 | # Non blogpost files. Bashblog will ignore these. Useful for static pages and custom content 76 | # Add them as a bash array, e.g. non_blogpost_files=("news.html" "test.html") 77 | non_blogpost_files=() 78 | 79 | # Declare your menu here. Key is the printed name, value is the file to point to 80 | # as for example: 81 | # global_main_menu=( "Home:index.html" "About:about.html" ) 82 | 83 | global_main_menu=() 84 | declare -a global_main_menu 85 | 86 | # set global_enable_duckduckgo to true in order to show a DuckDuckGo search-box 87 | # so that visitors can search your website. 88 | global_enable_duckduckgo=false 89 | 90 | # feed file (rss in this case) 91 | blog_feed="feed.rss" 92 | number_of_feed_articles="10" 93 | # "cut" blog entry when putting it to index page. Leave blank for full articles in front page 94 | # i.e. include only up to first '
', or '----' in markdown 95 | cut_do="cut" 96 | # When cutting, cut also tags? If "no", tags will appear in index page for cut articles 97 | cut_tags="yes" 98 | # Regexp matching the HTML line where to do the cut 99 | # note that slash is regexp separator so you need to prepend it with backslash 100 | cut_line='
' 101 | # save markdown file when posting with "bb post -m". Leave blank to discard it. 102 | save_markdown="yes" 103 | # prefix for tags/categories files 104 | # please make sure that no other html file starts with this prefix 105 | prefix_tags="tag_" 106 | # personalized header and footer (only if you know what you're doing) 107 | # DO NOT name them .header.html, .footer.html or they will be overwritten 108 | # leave blank to generate them, recommended 109 | header_file="" 110 | footer_file="" 111 | # extra content to add just after we open the tag 112 | # and before the actual blog content 113 | body_begin_file="" 114 | # extra content to add just before we close ) 116 | body_end_file="" 117 | # CSS files to include on every page, f.ex. css_include=('main.css' 'blog.css') 118 | # leave empty to use generated 119 | css_include=() 120 | # HTML files to exclude from index, f.ex. post_exclude=('imprint.html 'aboutme.html') 121 | html_exclude=() 122 | 123 | # Localization and i18n 124 | # "Comments?" (used in twitter link after every post) 125 | template_comments="Comments?" 126 | # "Read more..." (link under cut article on index page) 127 | template_read_more="Read more..." 128 | # "View more posts" (used on bottom of index page as link to archive) 129 | template_archive="View more posts" 130 | # "All posts" (title of archive page) 131 | template_archive_title="All posts" 132 | # "All tags" 133 | template_tags_title="All tags" 134 | # "posts" (on "All tags" page, text at the end of each tag line, like "2. Music - 15 posts") 135 | template_tags_posts="posts" 136 | template_tags_posts_2_4="posts" # Some slavic languages use a different plural form for 2-4 items 137 | template_tags_posts_singular="post" 138 | # "Posts tagged" (text on a title of a page with index of one tag, like "My Blog - Posts tagged "Music"") 139 | template_tag_title="Posts tagged" 140 | # "Tags:" (beginning of line in HTML file with list of all tags for this article) 141 | template_tags_line_header="Tags:" 142 | # "Back to the index page" (used on archive page, it is link to blog index) 143 | template_archive_index_page="Back to the index page" 144 | # "Subscribe" (used on bottom of index page, it is link to RSS feed) 145 | template_subscribe="Subscribe" 146 | # "Subscribe to this page..." (used as text for browser feed button that is embedded to html) 147 | template_subscribe_browser_button="Subscribe to this page..." 148 | # "Tweet" (used as twitter text button for posting to twitter) 149 | template_twitter_button="Tweet" 150 | template_twitter_comment="<Type your comment here but please leave the URL so that other people can follow the comments>" 151 | 152 | # Core Theming 153 | core_theme_body_max_width="874" 154 | core_theme_header_max_width="860" 155 | 156 | # The locale to use for the dates displayed on screen 157 | date_format="%B %d, %Y" 158 | date_locale="C" 159 | date_inpost="bashblog_timestamp" 160 | # Don't change these dates 161 | date_format_full="%a, %d %b %Y %H:%M:%S %z" 162 | date_format_timestamp="%Y%m%d%H%M.%S" 163 | date_allposts_header="%B %Y" 164 | 165 | # Perform the post title -> filename conversion 166 | # Experts only. You may need to tune the locales too 167 | # Leave empty for no conversion, which is not recommended 168 | # This default filter respects backwards compatibility 169 | convert_filename="iconv -f utf-8 -t ascii//translit | sed 's/^-*//' | tr [:upper:] [:lower:] | tr ' ' '-' | tr -dc '[:alnum:]-'" 170 | 171 | # URL where you can view the post while it's being edited 172 | # same as global_url by default 173 | # You can change it to path on your computer, if you write posts locally 174 | # before copying them to the server 175 | preview_url="" 176 | 177 | # Markdown location. Trying to autodetect by default. 178 | # The invocation must support the signature 'converter_bin convert_args in.md > out.html' 179 | converter_bin="Markdown.pl" 180 | converter_args="" 181 | [[ -f ${converter_bin} ]] && converter_bin=./${converter_bin} || converter_bin=$(which ${converter_bin} 2>/dev/null || which markdown 2>/dev/null) 182 | } 183 | 184 | # Check for the validity of some variables 185 | # DO NOT EDIT THIS FUNCTION unless you know what you're doing 186 | global_variables_check() { 187 | [[ $header_file == .header.html ]] && 188 | echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" && 189 | exit 190 | [[ $footer_file == .footer.html ]] && 191 | echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" && 192 | exit 193 | } 194 | 195 | 196 | # Test if the markdown script is working correctly 197 | test_markdown() { 198 | [[ -n $converter_bin ]] && 199 | ( 200 | [[ $("$converter_bin" <<< $'line 1\n\nline 2') == $'

line 1

\n\n

line 2

' ]] || 201 | [[ $("$converter_bin" <<< $'line 1\n\nline 2') == $'

line 1

\n

line 2

' ]] 202 | ) 203 | } 204 | 205 | 206 | # Parse a Markdown file into HTML and return the generated file 207 | markdown() { 208 | out=${1%.md}.html 209 | while [[ -f $out ]]; do out=${out%.html}.$RANDOM.html; done 210 | $converter_bin ${convert_args} "$1" > "$out" 211 | echo "$out" 212 | } 213 | 214 | 215 | # Prints the required google analytics code 216 | google_analytics() { 217 | [[ -z $global_analytics && -z $global_analytics_file ]] && return 218 | 219 | if [[ -z $global_analytics_file ]]; then 220 | 221 | echo " 222 | 223 | " 230 | else 231 | cat "$global_analytics_file" 232 | fi 233 | } 234 | 235 | # Prints the iframe used to show a duckduckgo search box 236 | duckduckgo_body() { 237 | if $global_enable_duckduckgo; then 238 | site=`echo "${global_url}" | awk -F/ '{print $3}'` 239 | echo "
" 240 | else 241 | echo "" 242 | fi 243 | } 244 | 245 | # Reads HTML file from stdin, prints its content to stdout 246 | # $1 where to start ("text" or "entry") 247 | # $2 where to stop ("text" or "entry") 248 | # $3 "cut" to remove text from
to 249 | # note that this does not remove
line itself, 250 | # so you can see if text was cut or not 251 | get_html_file_content() { 252 | awk "//, //{ 253 | if (!// && !//) print 254 | if (\"$3\" == \"cut\" && /$cut_line/){ 255 | if (\"$2\" == \"text\") exit # no need to read further 256 | while (getline > 0 && !//) { 257 | if (\"$cut_tags\" == \"no\" && /^

$template_tags_line_header/ ) print 258 | } 259 | } 260 | }" 261 | } 262 | 263 | # Edit an existing, published .html file while keeping its original timestamp 264 | # Please note that this function does not automatically republish anything, as 265 | # it is usually called from 'main'. 266 | # 267 | # Note that it edits HTML file, even if you wrote the post as markdown originally 268 | # Note that if you edit title then filename might also change 269 | # 270 | # $1 the file to edit 271 | # $2 (optional) edit mode: 272 | # "keep" to keep old filename 273 | # "full" to edit full HTML, and not only text part (keeps old filename) 274 | # leave empty for default behavior (edit only text part and change name) 275 | edit() { 276 | [[ ! -f "${1%%.*}.html" ]] && echo "Can't edit post "${1%%.*}.html", did you mean to use \"bb.sh post \"?" && exit -1 277 | # Original post timestamp 278 | edit_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_full" ) 279 | touch_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_timestamp") 280 | tags_before=$(tags_in_post "${1%%.*}.html") 281 | if [[ $2 == full ]]; then 282 | $EDITOR "$1" 283 | filename=$1 284 | else 285 | if [[ ${1##*.} == md ]]; then 286 | test_markdown 287 | if (($? != 0)); then 288 | echo "Markdown is not working, please edit HTML file directly." 289 | exit 290 | fi 291 | # editing markdown file 292 | $EDITOR "$1" 293 | TMPFILE=$(markdown "$1") 294 | filename=${1%%.*}.html 295 | else 296 | # Create the content file 297 | TMPFILE=$(basename "$1").$RANDOM.html 298 | # Title 299 | get_post_title "$1" > "$TMPFILE" 300 | # Post text with plaintext tags 301 | get_html_file_content 'text' 'text' <"$1" | sed "/^

$template_tags_line_header/s|\\1|\\1|g" >> "$TMPFILE" 302 | $EDITOR "$TMPFILE" 303 | filename=$1 304 | fi 305 | rm "$filename" 306 | if [[ $2 == keep ]]; then 307 | parse_file "$TMPFILE" "$edit_timestamp" "$filename" 308 | else 309 | parse_file "$TMPFILE" "$edit_timestamp" # this command sets $filename as the html processed file 310 | [[ ${1##*.} == md ]] && mv "$1" "${filename%%.*}.md" 2>/dev/null 311 | fi 312 | rm "$TMPFILE" 313 | fi 314 | touch -t "$touch_timestamp" "$filename" 315 | touch -t "$touch_timestamp" "$1" 316 | chmod 644 "$filename" 317 | echo "Posted $filename" 318 | tags_after=$(tags_in_post "$filename") 319 | relevant_tags=$(echo "$tags_before $tags_after" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ') 320 | if [[ ! -z $relevant_tags ]]; then 321 | relevant_posts="$(posts_with_tags $relevant_tags) $filename" 322 | rebuild_tags "$relevant_posts" "$relevant_tags" 323 | fi 324 | } 325 | 326 | # Create a Twitter summary (twitter "card") for the post 327 | # 328 | # $1 the post file 329 | # $2 the title 330 | twitter_card() { 331 | [[ -z $global_twitter_username ]] && return 332 | 333 | echo "" 334 | echo "" 335 | echo "" # Twitter truncates at 70 char 336 | description=$(grep -v "^

$template_tags_line_header" "$1" | sed -e 's/<[^>]*>//g' | head -c 250 | tr '\n' ' ' | sed "s/\"/'/g") 337 | echo "" 338 | image=$(sed -n 's/.*" 342 | } 343 | 344 | # Adds the code needed by the twitter button 345 | # 346 | # $1 the post URL 347 | twitter() { 348 | [[ -z $global_twitter_username ]] && return 349 | 350 | if [[ $global_twitter_cookieless == true ]]; then 351 | id=$RANDOM 352 | 353 | search_engine="https://twitter.com/search?q=" 354 | 355 | echo "

$template_comments $template_twitter_button " 356 | echo " 

" 357 | return; 358 | else 359 | echo "

$template_comments "; 360 | fi 361 | 362 | echo "$template_twitter_button " 365 | echo "

" 366 | } 367 | 368 | # Check if the file is a 'boilerplate' (i.e. not a post) 369 | # The return values are designed to be used like this inside a loop: 370 | # is_boilerplate_file && continue 371 | # 372 | # $1 the file 373 | # 374 | # Return 0 (bash return value 'true') if the input file is an index, feed, etc 375 | # or 1 (bash return value 'false') if it is a blogpost 376 | is_boilerplate_file() { 377 | name=${1#./} 378 | # First check against user-defined non-blogpost pages 379 | for item in "${non_blogpost_files[@]}"; do 380 | [[ "$name" == "$item" ]] && return 0 381 | done 382 | 383 | case $name in 384 | ( "$index_file" | "$archive_index" | "$tags_index" | "$footer_file" | "$header_file" | "$global_analytics_file" | "$prefix_tags"* ) 385 | return 0 ;; 386 | ( * ) # Check for excluded 387 | for excl in "${html_exclude[@]}"; do 388 | [[ $name == "$excl" ]] && return 0 389 | done 390 | return 1 ;; 391 | esac 392 | } 393 | 394 | # Adds all the bells and whistles to format the html page 395 | # Every blog post is marked with a and 396 | # which is parsed afterwards in the other functions. There is also a marker 397 | # to determine just the beginning of the text body of the post 398 | # 399 | # $1 a file with the body of the content 400 | # $2 the output file 401 | # $3 "yes" if we want to generate the index.html, 402 | # "no" to insert new blog posts 403 | # $4 title for the html header 404 | # $5 original blog timestamp 405 | # $6 post author 406 | create_html_page() { 407 | content=$1 408 | filename=$2 409 | index=$3 410 | title=$4 411 | timestamp=$5 412 | author=$6 413 | 414 | # Create the actual blog post 415 | # html, head 416 | { 417 | cat ".header.html" 418 | echo "$title" 419 | google_analytics 420 | twitter_card "$content" "$title" 421 | echo "" 422 | # stuff to add before the actual body content 423 | [[ -n $body_begin_file ]] && cat "$body_begin_file" 424 | # body divs 425 | echo '
' 426 | echo '
' 427 | # blog title 428 | echo '
' 429 | cat .title.html 430 | echo '
' 431 | cat .menu.html 432 | echo '
' # title, header, headerholder 433 | echo '
' 434 | 435 | file_url=${filename#./} 436 | file_url=${file_url%.rebuilt} # Get the correct URL when rebuilding 437 | # one blog entry 438 | if [[ $index == no ]]; then 439 | echo '' # marks the beginning of the whole post 440 | echo "

" 441 | # remove possible

's on the title because of markdown conversion 442 | title=${title//

/} 443 | title=${title//<\/p>/} 444 | echo "$title" 445 | echo '

' 446 | if [[ -z $timestamp ]]; then 447 | echo "" 448 | else 449 | echo "" 450 | fi 451 | if [[ -z $timestamp ]]; then 452 | echo -n "
$(LC_ALL=$date_locale date +"$date_format")" 453 | else 454 | echo -n "
$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp")" 455 | fi 456 | [[ -n $author ]] && echo -e " — \n$author" 457 | echo "
" 458 | echo '' # This marks the text body, after the title, date... 459 | fi 460 | cat "$content" # Actual content 461 | if [[ $index == no ]]; then 462 | echo -e '\n' 463 | 464 | twitter "$global_url/$file_url" 465 | 466 | echo '' # absolute end of the post 467 | fi 468 | 469 | echo '
' # content 470 | 471 | 472 | # page footer 473 | cat .footer.html 474 | # close divs 475 | echo '
' # divbody and divbodyholder 476 | [[ $global_github_username != "" ]] && 477 | if [[ $github_link_target == "" ]]; then 478 | github_link_target="_self" 479 | fi 480 | echo ""; 481 | [[ -n $body_end_file ]] && cat "$body_end_file" 482 | echo '' 483 | } > "$filename" 484 | } 485 | 486 | # Parse the plain text file into an html file 487 | # 488 | # $1 source file name 489 | # $2 (optional) timestamp for the file 490 | # $3 (optional) destination file name 491 | # note that although timestamp is optional, something must be provided at its 492 | # place if destination file name is provided, i.e: 493 | # parse_file source.txt "" destination.html 494 | parse_file() { 495 | # Read for the title and check that the filename is ok 496 | title="" 497 | while IFS='' read -r line; do 498 | if [[ -z $title ]]; then 499 | # remove extra

and

added by markdown 500 | title=$(echo "$line" | sed 's/<\/*p>//g') 501 | if [[ -n $3 ]]; then 502 | filename=$3 503 | else 504 | filename=$title 505 | [[ -n $convert_filename ]] && 506 | filename=$(echo "$title" | eval "$convert_filename") 507 | [[ -n $filename ]] || 508 | filename=$RANDOM # don't allow empty filenames 509 | 510 | filename=$filename.html 511 | 512 | # Check for duplicate file names 513 | while [[ -f $filename ]]; do 514 | filename=${filename%.html}$RANDOM.html 515 | done 516 | fi 517 | content=$filename.tmp 518 | # Parse possible tags 519 | elif [[ $line == "

$template_tags_line_header"* ]]; then 520 | tags=$(echo "$line" | cut -d ":" -f 2- | sed -e 's/<\/p>//g' -e 's/^ *//' -e 's/ *$//' -e 's/, /,/g') 521 | IFS=, read -r -a array <<< "$tags" 522 | 523 | echo -n "

$template_tags_line_header " >> "$content" 524 | for item in "${array[@]}"; do 525 | echo -n "$item, " 526 | done | sed 's/, $/<\/p>/g' >> "$content" 527 | else 528 | echo "$line" >> "$content" 529 | fi 530 | done < "$1" 531 | 532 | # Create the actual html page 533 | create_html_page "$content" "$filename" no "$title" "$2" "$global_author" 534 | rm "$content" 535 | } 536 | 537 | # Manages the creation of the text file and the parsing to html file 538 | # also the drafts 539 | write_entry() { 540 | test_markdown && fmt=md || fmt=html 541 | f=$2 542 | [[ $2 == -html ]] && fmt=html && f=$3 543 | 544 | if [[ -n $f ]]; then 545 | TMPFILE=$f 546 | if [[ ! -f $TMPFILE ]]; then 547 | echo "The file doesn't exist" 548 | delete_includes 549 | exit 550 | fi 551 | # guess format from TMPFILE 552 | extension=${TMPFILE##*.} 553 | [[ $extension == md || $extension == html ]] && fmt=$extension 554 | # but let user override it (`bb.sh post -html file.md`) 555 | [[ $2 == -html ]] && fmt=html 556 | # Test if Markdown is working before re-posting a .md file 557 | if [[ $extension == md ]]; then 558 | test_markdown 559 | if (($? != 0)); then 560 | echo "Markdown is not working, please edit HTML file directly." 561 | exit 562 | fi 563 | fi 564 | else 565 | TMPFILE=.entry-$RANDOM.$fmt 566 | echo -e "Title on this line\n" >> "$TMPFILE" 567 | 568 | [[ $fmt == html ]] && cat << EOF >> "$TMPFILE" 569 |

The rest of the text file is an html blog post. The process will continue as soon 570 | as you exit your editor.

571 | 572 |

$template_tags_line_header keep-this-tag-format, tags-are-optional, example

573 | EOF 574 | [[ $fmt == md ]] && cat << EOF >> "$TMPFILE" 575 | The rest of the text file is a **Markdown** blog post. The process will continue 576 | as soon as you exit your editor. 577 | 578 | $template_tags_line_header keep-this-tag-format, tags-are-optional, beware-with-underscores-in-markdown, example 579 | EOF 580 | fi 581 | chmod 600 "$TMPFILE" 582 | 583 | post_status="E" 584 | filename="" 585 | while [[ $post_status != "p" && $post_status != "P" ]]; do 586 | [[ -n $filename ]] && rm "$filename" # Delete the generated html file, if any 587 | $EDITOR "$TMPFILE" 588 | if [[ $fmt == md ]]; then 589 | html_from_md=$(markdown "$TMPFILE") 590 | parse_file "$html_from_md" 591 | rm "$html_from_md" 592 | else 593 | parse_file "$TMPFILE" # this command sets $filename as the html processed file 594 | fi 595 | 596 | chmod 644 "$filename" 597 | [[ -n $preview_url ]] || preview_url=$global_url 598 | echo "To preview the entry, open $preview_url/$filename in your browser" 599 | 600 | echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) " 601 | read -r post_status 602 | if [[ $post_status == d || $post_status == D ]]; then 603 | mkdir -p "drafts/" 604 | chmod 700 "drafts/" 605 | 606 | title=$(head -n 1 $TMPFILE) 607 | [[ -n $convert_filename ]] && title=$(echo "$title" | eval "$convert_filename") 608 | [[ -n $title ]] || title=$RANDOM 609 | 610 | draft=drafts/$title.$fmt 611 | mv "$TMPFILE" "$draft" 612 | chmod 600 "$draft" 613 | rm "$filename" 614 | delete_includes 615 | echo "Saved your draft as '$draft'" 616 | exit 617 | fi 618 | done 619 | 620 | if [[ $fmt == md && -n $save_markdown ]]; then 621 | mv "$TMPFILE" "${filename%%.*}.md" 622 | else 623 | rm "$TMPFILE" 624 | fi 625 | chmod 644 "$filename" 626 | echo "Posted $filename" 627 | relevant_tags=$(tags_in_post $filename) 628 | if [[ -n $relevant_tags ]]; then 629 | relevant_posts="$(posts_with_tags $relevant_tags) $filename" 630 | rebuild_tags "$relevant_posts" "$relevant_tags" 631 | fi 632 | } 633 | 634 | # Create an index page with all the posts 635 | all_posts() { 636 | echo -n "Creating an index page with all the posts " 637 | contentfile=$archive_index.$RANDOM 638 | while [[ -f $contentfile ]]; do 639 | contentfile=$archive_index.$RANDOM 640 | done 641 | 642 | { 643 | echo "

$template_archive_title

" 644 | prev_month="" 645 | while IFS='' read -r i; do 646 | is_boilerplate_file "$i" && continue 647 | echo -n "." 1>&3 648 | # Month headers 649 | month=$(LC_ALL=$date_locale date -r "$i" +"$date_allposts_header") 650 | if [[ $month != "$prev_month" ]]; then 651 | [[ -n $prev_month ]] && echo "" # Don't close ul before first header 652 | echo "

$month

" 653 | echo "
    " 654 | prev_month=$month 655 | fi 656 | # Title 657 | title=$(get_post_title "$i") 658 | echo -n "
  • $title —" 659 | # Date 660 | date=$(LC_ALL=$date_locale date -r "$i" +"$date_format") 661 | echo " $date
  • " 662 | done < <(ls -t ./*.html) 663 | echo "" 1>&3 664 | echo "
" 665 | echo "" 666 | } 3>&1 >"$contentfile" 667 | 668 | create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title — $template_archive_title" "$global_author" 669 | mv "$archive_index.tmp" "$archive_index" 670 | chmod 644 "$archive_index" 671 | rm "$contentfile" 672 | } 673 | 674 | # Create an index page with all the tags 675 | all_tags() { 676 | echo -n "Creating an index page with all the tags " 677 | contentfile=$tags_index.$RANDOM 678 | while [[ -f $contentfile ]]; do 679 | contentfile=$tags_index.$RANDOM 680 | done 681 | 682 | { 683 | echo "

$template_tags_title

" 684 | echo "
    " 685 | for i in $prefix_tags*.html; do 686 | [[ -f "$i" ]] || break 687 | echo -n "." 1>&3 688 | nposts=$(grep -c "<\!-- text begin -->" "$i") 689 | tagname=${i#"$prefix_tags"} 690 | tagname=${tagname%.html} 691 | case $nposts in 692 | 1) word=$template_tags_posts_singular;; 693 | 2|3|4) word=$template_tags_posts_2_4;; 694 | *) word=$template_tags_posts;; 695 | esac 696 | echo "
  • $tagname — $nposts $word
  • " 697 | done 698 | echo "" 1>&3 699 | echo "
" 700 | echo "" 701 | } 3>&1 > "$contentfile" 702 | 703 | create_html_page "$contentfile" "$tags_index.tmp" yes "$global_title — $template_tags_title" "$global_author" 704 | mv "$tags_index.tmp" "$tags_index" 705 | chmod 644 "$tags_index" 706 | rm "$contentfile" 707 | } 708 | 709 | # Generate the index.html with the content of the latest posts 710 | rebuild_index() { 711 | echo -n "Rebuilding the index " 712 | newindexfile=$index_file.$RANDOM 713 | contentfile=$newindexfile.content 714 | while [[ -f $newindexfile ]]; do 715 | newindexfile=$index_file.$RANDOM 716 | contentfile=$newindexfile.content 717 | done 718 | 719 | # Create the content file 720 | { 721 | n=0 722 | while IFS='' read -r i; do 723 | is_boilerplate_file "$i" && continue; 724 | if ((n >= number_of_index_articles)); then break; fi 725 | if [[ -n $cut_do ]]; then 726 | get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" 727 | else 728 | get_html_file_content 'entry' 'entry' <"$i" 729 | fi 730 | echo -n "." 1>&3 731 | n=$(( n + 1 )) 732 | done < <(ls -t ./*.html) # sort by date, newest first 733 | 734 | feed=$blog_feed 735 | if [[ -n $global_feedburner ]]; then feed=$global_feedburner; fi 736 | echo "" 737 | } 3>&1 >"$contentfile" 738 | 739 | echo "" 740 | 741 | create_html_page "$contentfile" "$newindexfile" yes "$global_title" "$global_author" 742 | rm "$contentfile" 743 | mv "$newindexfile" "$index_file" 744 | chmod 644 "$index_file" 745 | } 746 | 747 | # Finds all tags referenced in one post. 748 | # Accepts either filename as first argument, or post content at stdin 749 | # Prints one line with space-separated tags to stdout 750 | tags_in_post() { 751 | sed -n "/^

$template_tags_line_header/{s/^

$template_tags_line_header//;s/<[^>]*>//g;s/[ ,]\+/ /g;p;}" "$1" | tr ', ' ' ' 752 | } 753 | 754 | # Finds all posts referenced in a number of tags. 755 | # Arguments are tags 756 | # Prints one line with space-separated tags to stdout 757 | posts_with_tags() { 758 | (($# < 1)) && return 759 | set -- "${@/#/$prefix_tags}" 760 | set -- "${@/%/.html}" 761 | sed -n '/^

/{s/.*href="\([^"]*\)">.*/\1/;p;}' "$@" 2> /dev/null 762 | } 763 | 764 | # Rebuilds tag_*.html files 765 | # if no arguments given, rebuilds all of them 766 | # if arguments given, they should have this format: 767 | # "FILE1 [FILE2 [...]]" "TAG1 [TAG2 [...]]" 768 | # where FILEn are files with posts which should be used for rebuilding tags, 769 | # and TAGn are names of tags which should be rebuilt. 770 | # example: 771 | # rebuild_tags "one_post.html another_article.html" "example-tag another-tag" 772 | # mind the quotes! 773 | rebuild_tags() { 774 | if (($# < 2)); then 775 | # will process all files and tags 776 | files=$(ls -t ./*.html) 777 | all_tags=yes 778 | else 779 | # will process only given files and tags 780 | files=$(printf '%s\n' $1 | sort -u) 781 | files=$(ls -t $files) 782 | tags=$2 783 | fi 784 | echo -n "Rebuilding tag pages " 785 | n=0 786 | if [[ -n $all_tags ]]; then 787 | rm ./"$prefix_tags"*.html &> /dev/null 788 | else 789 | for i in $tags; do 790 | rm "./$prefix_tags$i.html" &> /dev/null 791 | done 792 | fi 793 | # First we will process all files and create temporal tag files 794 | # with just the content of the posts 795 | tmpfile=tmp.$RANDOM 796 | while [[ -f $tmpfile ]]; do tmpfile=tmp.$RANDOM; done 797 | while IFS='' read -r i; do 798 | is_boilerplate_file "$i" && continue; 799 | echo -n "." 800 | if [[ -n $cut_do ]]; then 801 | get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" 802 | else 803 | get_html_file_content 'entry' 'entry' <"$i" 804 | fi >"$tmpfile" 805 | for tag in $(tags_in_post "$i"); do 806 | if [[ -n $all_tags || " $tags " == *" $tag "* ]]; then 807 | cat "$tmpfile" >> "$prefix_tags$tag".tmp.html 808 | fi 809 | done 810 | done <<< "$files" 811 | rm "$tmpfile" 812 | # Now generate the tag files with headers, footers, etc 813 | while IFS='' read -r i; do 814 | tagname=${i#./"$prefix_tags"} 815 | tagname=${tagname%.tmp.html} 816 | create_html_page "$i" "$prefix_tags$tagname.html" yes "$global_title — $template_tag_title \"$tagname\"" "$global_author" 817 | rm "$i" 818 | done < <(ls -t ./"$prefix_tags"*.tmp.html 2>/dev/null) 819 | echo 820 | } 821 | 822 | # Return the post title 823 | # 824 | # $1 the html file 825 | get_post_title() { 826 | awk '/

/, /<\/a><\/h3>/{if (!/

/ && !/<\/a><\/h3>/) print}' "$1" 827 | } 828 | 829 | # Return the post author 830 | # 831 | # $1 the html file 832 | get_post_author() { 833 | awk '/
.+/, //{if (!/
.+/ && !//) print}' "$1" | sed 's/<\/div>//g' 834 | } 835 | 836 | # Displays a list of the tags 837 | # 838 | # $2 if "-n", tags will be sorted by number of posts 839 | list_tags() { 840 | if [[ $2 == -n ]]; then do_sort=1; else do_sort=0; fi 841 | 842 | ls ./$prefix_tags*.html &> /dev/null 843 | (($? != 0)) && echo "No posts yet. Use 'bb.sh post' to create one" && return 844 | 845 | lines="" 846 | for i in $prefix_tags*.html; do 847 | [[ -f "$i" ]] || break 848 | nposts=$(grep -c "<\!-- text begin -->" "$i") 849 | tagname=${i#"$prefix_tags"} 850 | tagname=${tagname#.html} 851 | ((nposts > 1)) && word=$template_tags_posts || word=$template_tags_posts_singular 852 | line="$tagname # $nposts # $word" 853 | lines+=$line\\n 854 | done 855 | 856 | if (( do_sort == 1 )); then 857 | echo -e "$lines" | column -t -s "#" | sort -nrk 2 858 | else 859 | echo -e "$lines" | column -t -s "#" 860 | fi 861 | } 862 | 863 | # Displays a list of the posts 864 | list_posts() { 865 | ls ./*.html &> /dev/null 866 | (($? != 0)) && echo "No posts yet. Use 'bb.sh post' to create one" && return 867 | 868 | lines="" 869 | n=1 870 | while IFS='' read -r i; do 871 | is_boilerplate_file "$i" && continue 872 | line="$n # $(get_post_title "$i") # $(LC_ALL=$date_locale date -r "$i" +"$date_format")" 873 | lines+=$line\\n 874 | n=$(( n + 1 )) 875 | done < <(ls -t ./*.html) 876 | 877 | echo -e "$lines" | column -t -s "#" 878 | } 879 | 880 | # Generate the feed file 881 | make_rss() { 882 | echo -n "Making RSS " 883 | 884 | rssfile=$blog_feed.$RANDOM 885 | while [[ -f $rssfile ]]; do rssfile=$blog_feed.$RANDOM; done 886 | 887 | { 888 | pubdate=$(LC_ALL=C date +"$date_format_full") 889 | echo '' 890 | echo '' 891 | echo "$global_title$global_url/$index_file" 892 | echo "$global_descriptionen" 893 | echo "$pubdate" 894 | echo "$pubdate" 895 | echo "" 896 | 897 | n=0 898 | while IFS='' read -r i; do 899 | is_boilerplate_file "$i" && continue 900 | ((n >= number_of_feed_articles)) && break # max 10 items 901 | echo -n "." 1>&3 902 | echo '' 903 | get_post_title "$i" 904 | echo '$global_url/${i#./}" 907 | echo "$global_url/$i" 908 | echo "$(get_post_author "$i")" 909 | echo "$(LC_ALL=C date -r "$i" +"$date_format_full")" 910 | 911 | n=$(( n + 1 )) 912 | done < <(ls -t ./*.html) 913 | 914 | echo '' 915 | } 3>&1 >"$rssfile" 916 | echo "" 917 | 918 | mv "$rssfile" "$blog_feed" 919 | chmod 644 "$blog_feed" 920 | } 921 | 922 | # generate headers, footers, etc 923 | create_includes() { 924 | { 925 | echo "

$global_title

" 926 | echo "
$global_description
" 927 | duckduckgo_body 928 | } > ".title.html" 929 | 930 | # generating the menu 931 | if [[ ${#global_main_menu[@]} > 0 ]]; then 932 | { 933 | echo "" 945 | } > .menu.html 946 | else 947 | { 948 | echo "" 949 | } > .menu.html 950 | fi 951 | 952 | if [[ -f $header_file ]]; then cp "$header_file" .header.html 953 | else { 954 | echo '' 955 | echo '' 956 | echo '' 957 | echo '' 958 | printf '\n' "${css_include[@]}" 959 | if [[ -z $global_feedburner ]]; then 960 | echo "" 961 | else 962 | echo "" 963 | fi 964 | } > ".header.html" 965 | fi 966 | 967 | if [[ -f $footer_file ]]; then cp "$footer_file" .footer.html 968 | else { 969 | protected_mail=${global_email//@/@} 970 | protected_mail=${protected_mail//./.} 971 | copyright_year=$(date +'%Y') 972 | echo "
Copyright © $copyright_year $global_author
$global_license
$global_author$protected_mail
" 973 | echo 'Generated with bashblog-ng - (B)e (A)wesome & (S)imple (H)omie
' 974 | } >> ".footer.html" 975 | fi 976 | } 977 | 978 | # Delete the temporarily generated include files 979 | delete_includes() { 980 | rm ".title.html" ".footer.html" ".header.html" ".menu.html" 981 | } 982 | 983 | # Create the css file from scratch 984 | create_css() { 985 | # To avoid overwriting manual changes. However it is recommended that 986 | # this function is modified if the user changes the blog.css file 987 | (( ${#css_include[@]} > 0 )) && return || css_include=('main.css' 'blog.css') 988 | if [[ ! -f blog.css ]]; then 989 | # blog.css directives will be loaded after main.css and thus will prevail 990 | echo '#title{font-size: x-large;} 991 | a.ablack{color:black !important;} 992 | li{margin-bottom:8px;} 993 | ul,ol{margin-left:24px;margin-right:24px;} 994 | #all_posts{margin-top:24px;text-align:center;} 995 | .subtitle{font-size:small;margin:12px 0px;} 996 | .content p{margin-left:14px;margin-right:14px;} 997 | h1{margin-bottom:12px !important;} 998 | #description{font-size:large;margin-bottom:12px;} 999 | h3{margin-top:42px;margin-bottom:8px;} 1000 | h4{margin-left:24px;margin-right:24px;} 1001 | img{max-width:100%;} 1002 | #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;} 1003 | .navigation-menu{overflow:hidden;position:relative;border-bottom:1px solid #e7e7e7}.main-navigation{display:none;padding-left:0} 1004 | .main-navigation li{list-style-type:none;margin:15px auto;text-align:center} 1005 | .main-navigation a{padding:5px 7px;color:#947aff;margin:0 2px 0 2px;text-decoration:none;display:inline-block} 1006 | .main-navigation a:hover{color:#000;transition:.3s ease}.hamburger-icon{margin:5px 30px 5px 5px;cursor:pointer;font-size:25px;color:#947aff} 1007 | #menu-toggle{display:none}#menu-toggle:checked+.main-navigation{display:block} 1008 | @media screen and (min-width:505px){.navigation-menu{height:70px;display:flex;align-items:center;border-bottom:1px solid #e7e7e7} 1009 | .main-navigation{display:flex;flex-direction:row;justify-content:flex-end;margin-left:30px}.main-navigation a{padding:15px 17px} 1010 | .hamburger-icon{display:none}#menu-toggle:checked+.main-navigation{display:flex} 1011 | .content p{margin-left:24px;margin-right:24px;}}' > blog.css 1012 | fi 1013 | 1014 | # If there is a style.css from the parent page (i.e. some landing page) 1015 | # then use it. This directive is here for compatibility with my own 1016 | # home page. Feel free to edit it out, though it doesn't hurt 1017 | if [[ -f ../style.css ]] && [[ ! -f main.css ]]; then 1018 | ln -s "../style.css" "main.css" 1019 | elif [[ ! -f main.css ]]; then 1020 | echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;} 1021 | #divbodyholder{padding:24px;width:100%;max-width:'"${core_theme_body_max_width}"'px;} 1022 | #divbody{border:solid 1px #ccc;background-color:#fff;padding:0px 18px 24px 18px;top:0;} 1023 | .header{width:100%;max-width:'"${core_theme_header_max_width}"'px;margin:0px 0px 0px 24px;padding-top:24px;padding-bottom:8px;} 1024 | .content{margin-bottom:5%;} 1025 | .nomargin{margin:0;} 1026 | .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;} 1027 | h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;} 1028 | .clear{clear:both;} 1029 | #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;} 1030 | a{text-decoration:none;color:#003366 !important;} 1031 | a:visited{text-decoration:none;color:#336699 !important;} 1032 | blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;} 1033 | blockquote img{margin:12px 0px;} 1034 | blockquote iframe{margin:12px 0px;} 1035 | .search_box_container{padding-right:18px;} 1036 | @media screen and (min-width:505px){ 1037 | #divbody{padding:0px 48px 24px 48px;} 1038 | .search_box_container{padding-right:48px;} 1039 | }' > main.css 1040 | fi 1041 | } 1042 | 1043 | # Regenerates all the single post entries, keeping the post content but modifying 1044 | # the title, html structure, etc 1045 | rebuild_all_entries() { 1046 | echo -n "Rebuilding all entries " 1047 | 1048 | for i in ./*.html; do 1049 | is_boilerplate_file "$i" && continue; 1050 | contentfile=.tmp.$RANDOM 1051 | while [[ -f $contentfile ]]; do contentfile=.tmp.$RANDOM; done 1052 | 1053 | echo -n "." 1054 | # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...) 1055 | title=$(get_post_title "$i") 1056 | 1057 | get_html_file_content 'text' 'text' <"$i" >> "$contentfile" 1058 | 1059 | # Read timestamp from post, if present, and sync file timestamp 1060 | timestamp=$(awk '// { print }' "$i" | cut -d '#' -f 2) 1061 | [[ -n $timestamp ]] && touch -t "$timestamp" "$i" 1062 | # Read timestamp from file in correct format for 'create_html_page' 1063 | timestamp=$(LC_ALL=C date -r "$i" +"$date_format_full") 1064 | 1065 | create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp" "$(get_post_author "$i")" 1066 | # keep the original timestamp! 1067 | timestamp=$(LC_ALL=C date -r "$i" +"$date_format_timestamp") 1068 | mv "$i.rebuilt" "$i" 1069 | chmod 644 "$i" 1070 | touch -t "$timestamp" "$i" 1071 | rm "$contentfile" 1072 | done 1073 | echo "" 1074 | } 1075 | 1076 | # Displays the help 1077 | usage() { 1078 | echo "$global_software_name v$global_software_version" 1079 | echo "Usage: $0 command [filename]" 1080 | echo "" 1081 | echo "Commands:" 1082 | echo " post [-html] [filename] insert a new blog post, or the filename of a draft to continue editing it" 1083 | echo " it tries to use markdown by default, and falls back to HTML if it's not available." 1084 | echo " use '-html' to override it and edit the post as HTML even when markdown is available" 1085 | echo " edit [-n|-f] [filename] edit an already published .html or .md file. **NEVER** edit manually a published .html file," 1086 | echo " always use this function as it keeps internal data and rebuilds the blog" 1087 | echo " use '-n' to give the file a new name, if title was changed" 1088 | echo " use '-f' to edit full html file, instead of just text part (also preserves name)" 1089 | echo " delete [filename] deletes the post and rebuilds the blog" 1090 | echo " rebuild regenerates all the pages and posts, preserving the content of the entries" 1091 | echo " reset deletes everything except this script. Use with a lot of caution and back up first!" 1092 | echo " list list all posts" 1093 | echo " tags [-n] list all tags in alphabetical order" 1094 | echo " use '-n' to sort list by number of posts" 1095 | echo "" 1096 | echo "For more information please open $0 in a code editor and read the header and comments" 1097 | } 1098 | 1099 | # Delete all generated content, leaving only this script 1100 | reset() { 1101 | echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" " 1102 | read -r line 1103 | if [[ $line == "Yes, I am!" ]]; then 1104 | rm .*.html ./*.html ./*.css ./*.rss &> /dev/null 1105 | echo 1106 | echo "Deleted all posts, stylesheets and feeds." 1107 | echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed." 1108 | else 1109 | echo "Phew! You dodged a bullet there. Nothing was modified." 1110 | fi 1111 | } 1112 | 1113 | # Detects if GNU date is installed 1114 | date_version_detect() { 1115 | date --version >/dev/null 2>&1 1116 | if (($? != 0)); then 1117 | # date utility is BSD. Test if gdate is installed 1118 | if gdate --version >/dev/null 2>&1 ; then 1119 | date() { 1120 | gdate "$@" 1121 | } 1122 | else 1123 | # BSD date 1124 | date() { 1125 | if [[ $1 == -r ]]; then 1126 | # Fall back to using stat for 'date -r' 1127 | format=${3//+/} 1128 | stat -f "%Sm" -t "$format" "$2" 1129 | elif [[ $2 == --date* ]]; then 1130 | # convert between dates using BSD date syntax 1131 | command date -j -f "$date_format_full" "${2#--date=}" "$1" 1132 | else 1133 | # acceptable format for BSD date 1134 | command date -j "$@" 1135 | fi 1136 | } 1137 | fi 1138 | fi 1139 | } 1140 | 1141 | # Main function 1142 | # Encapsulated on its own function for readability purposes 1143 | # 1144 | # $1 command to run 1145 | # $2 file name of a draft to continue editing (optional) 1146 | do_main() { 1147 | # Detect if using BSD date or GNU date 1148 | date_version_detect 1149 | # Load default configuration, then override settings with the config file 1150 | global_variables 1151 | [[ -f $global_config ]] && source "$global_config" &> /dev/null 1152 | global_variables_check 1153 | 1154 | # Check for $EDITOR 1155 | [[ -z $EDITOR ]] && 1156 | echo "Please set your \$EDITOR environment variable. For example, to use nano, add the line 'export EDITOR=nano' to your \$HOME/.bashrc file" && exit 1157 | 1158 | # Check for validity of argument 1159 | [[ $1 != "reset" && $1 != "post" && $1 != "rebuild" && $1 != "list" && $1 != "edit" && $1 != "delete" && $1 != "tags" ]] && 1160 | usage && exit 1161 | 1162 | [[ $1 == list ]] && 1163 | list_posts && exit 1164 | 1165 | [[ $1 == tags ]] && 1166 | list_tags "$@" && exit 1167 | 1168 | if [[ $1 == edit ]]; then 1169 | if (($# < 2)) || [[ ! -f ${!#} ]]; then 1170 | echo "Please enter a valid .md or .html file to edit" 1171 | exit 1172 | fi 1173 | fi 1174 | 1175 | # Test for existing html files 1176 | if ls ./*.html &> /dev/null; then 1177 | # We're going to back up just in case 1178 | tar -c -z -f ".backup.tar.gz" -- *.html && 1179 | chmod 600 ".backup.tar.gz" 1180 | elif [[ $1 == rebuild ]]; then 1181 | echo "Can't find any html files, nothing to rebuild" 1182 | exit 1183 | fi 1184 | 1185 | # Keep first backup of this day containing yesterday's version of the blog 1186 | [[ ! -f .yesterday.tar.gz || $(date -r .yesterday.tar.gz +'%d') != "$(date +'%d')" ]] && 1187 | cp .backup.tar.gz .yesterday.tar.gz &> /dev/null 1188 | 1189 | [[ $1 == reset ]] && 1190 | reset && exit 1191 | 1192 | create_css 1193 | create_includes 1194 | [[ $1 == post ]] && write_entry "$@" 1195 | [[ $1 == rebuild ]] && rebuild_all_entries && rebuild_tags 1196 | [[ $1 == delete ]] && rm "$2" &> /dev/null && rebuild_tags 1197 | if [[ $1 == edit ]]; then 1198 | if [[ $2 == -n ]]; then 1199 | edit "$3" 1200 | elif [[ $2 == -f ]]; then 1201 | edit "$3" full 1202 | else 1203 | edit "$2" keep 1204 | fi 1205 | fi 1206 | rebuild_index 1207 | all_posts 1208 | all_tags 1209 | make_rss 1210 | delete_includes 1211 | } 1212 | 1213 | 1214 | # 1215 | # MAIN 1216 | # Do not change anything here. If you want to modify the code, edit do_main() 1217 | # 1218 | do_main "$@" 1219 | 1220 | # vim: set shiftwidth=4 tabstop=4 expandtab: 1221 | --------------------------------------------------------------------------------