├── .github └── workflows │ └── publish-image.yml ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── action.yml ├── bin └── jenny └── share └── jenny ├── layout ├── index.sh ├── post.sh └── rss2.sh └── lib ├── components.sh ├── helpers.sh ├── md2html.awk └── subcommands.sh /.github/workflows/publish-image.yml: -------------------------------------------------------------------------------- 1 | # 2 | name: publish on gh packages 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | tags: 8 | - '*' # Push events to every tag not containing / 9 | 10 | # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. 16 | jobs: 17 | build-and-push-image: 18 | runs-on: ubuntu-latest 19 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 20 | permissions: 21 | contents: read 22 | packages: write 23 | attestations: write 24 | id-token: write 25 | # 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. 30 | - name: Log in to the Container registry 31 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 32 | with: 33 | registry: ${{ env.REGISTRY }} 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 37 | - name: Extract metadata (tags, labels) for Docker 38 | id: meta 39 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 43 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 44 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 45 | - name: Build and push Docker image 46 | id: push 47 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 48 | with: 49 | context: . 50 | push: true 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} 53 | 54 | # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 55 | - name: Generate artifact attestation 56 | uses: actions/attest-build-provenance@v1 57 | with: 58 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 59 | subject-digest: ${{ steps.push.outputs.digest }} 60 | push-to-registry: true 61 | 62 | 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Container image that runs your code 2 | FROM guff666/docker-multimarkdown-6:latest 3 | COPY ./ /tmp/jenny 4 | RUN (cd /tmp/jenny; make install) 5 | 6 | WORKDIR /blog 7 | RUN mkdir .dist 8 | ENTRYPOINT ["jenny"] 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Conrado Patricio Ambrosio 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | PREFIX?=/usr/local 3 | BIN=${PREFIX}/bin 4 | SHARE=${PREFIX}/share 5 | 6 | sym-install: 7 | @echo "Symlinking jenny script" 8 | ln -s $(shell pwd)/bin/jenny $(BIN)/jenny 9 | 10 | install: 11 | @echo "Installing jenny script" 12 | install -m 755 ./bin/jenny $(BIN)/ 13 | 14 | @echo "Installing jenny assets" 15 | cp -r ./share/jenny $(SHARE)/ 16 | find $(SHARE)/jenny -type f -exec chmod 644 {} \; 17 | find $(SHARE)/jenny -type d -exec chmod 755 {} \; 18 | chmod +x $(SHARE)/jenny/lib/md2html.awk 19 | chmod +x $(SHARE)/jenny/lib/*.sh 20 | chmod +x $(SHARE)/jenny/layout/*.sh 21 | 22 | uninstall: 23 | @echo "Removing script and assets" 24 | rm -rf $(BIN)/jenny $(SHARE)/jenny 25 | 26 | reinstall: 27 | @echo "Running reinstall" 28 | make uninstall && make install 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenny 2 | 3 | _Jenny_ is a static blog generator. Its aim is to work with minimal requirements and a small footprint while providing some modern conveniences. 4 | 5 | - [x] Lightweight default theme 6 | - [x] Markdown with Footnotes support, care of a modified [md2html.awk](https://bitbucket.org/yiyus/md2html.awk) (currently broken, please byo multimarkdown parser) 7 | - [x] Basic pagination with fixed page numbers 8 | - [x] Plug your own Markdown parser 9 | - [x] Heredocs-based template syntax 10 | - [x] Draft/ignore support (by leaving out the date) 11 | - [x] Forward-posting, i.e. ignores posts with dates in the future 12 | - [x] Tags support 13 | - [x] Modifiable installation prefix 14 | - [x] Define run-time settings in command arguments 15 | - [x] Include static directories in build 16 | - [x] Unique URL slug generation 17 | - [x] Render only changed or new posts 18 | - [x] RSS/Atom feed 19 | - [ ] Tests 20 | 21 | [![publish on gh packages](https://github.com/hmngwy/jenny/actions/workflows/publish-image.yml/badge.svg)](https://github.com/hmngwy/jenny/actions/workflows/publish-image.yml) 22 | 23 | 24 | ## Getting started, four ways 25 | 26 | #### 1. You can install `jenny` to your local bin folder 27 | 28 | ``` 29 | git clone https://github.com/hmngwy/jenny.git 30 | make install 31 | ``` 32 | 33 | #### 2. Or, skip installation and use Docker, all references to the command `jenny` is interchangeable with the below 34 | 35 | ``` 36 | docker run -it -v $PWD:/blog ghcr.io/hmngwy/jenny:latest 37 | ``` 38 | 39 | The Docker image contains Multimarkdown 6 so you can have `MARKDOWN_COMMAND="multimarkdown "` in your `.blogrc`. 40 | 41 | #### 3. Or, as a Github Action to automatically build your blog 42 | 43 | This repository is also the Github Action source, simply reference it in your Worflow steps, below is an example. 44 | 45 | ``` 46 | on: [push] 47 | 48 | jobs: 49 | build_job: 50 | runs-on: ubuntu-latest 51 | name: Build with Jenny 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: hmngwy/jenny@master 55 | ``` 56 | 57 | #### 4. Or, fork this template repo 58 | 59 | You can fork the [hmngwy/jenny-template](https://github.com/hmngwy/jenny-template) repo to start using `jenny` without installing a thing. 60 | 61 | --- 62 | 63 | ## Setting up a Blog 64 | 65 | Prepare the directory for your blog posts. 66 | 67 | ``` 68 | mkdir ./blog && cd blog 69 | ``` 70 | 71 | Use the subcommand below to initialize your blog folder with a `.blogrc` file and a sample first post. 72 | 73 | ``` 74 | jenny init 75 | ``` 76 | 77 | ## Creating a Blog Post 78 | 79 | A dated filename will be recognized as a published post, everything else is a draft and ignored, the format is: 80 | 81 | ``` 82 | YYYY-MM-DD a-published-post.md 83 | ``` 84 | 85 | Create a file with a date so that Jenny recognizes it as a published post. 86 | 87 | ``` 88 | cat <> "$(date +%Y-%m-%d) hello-world.md" 89 | # Hello World 90 | 91 | Jenny is a static blog generator using bash, sed, and awk. 92 | EOT 93 | ``` 94 | 95 | --- 96 | 97 | ## Building the Blog 98 | 99 | Run `jenny` on your blog directory. 100 | 101 | ## CLI Arguments 102 | 103 | To override `.blogrc` settings at run-time use command line arguments. Other options are also available, use `jenny -h` to display the message below: 104 | 105 | ``` 106 | jenny usage: 107 | -d ./dir Override the build directory setting 108 | -p 10 Override posts per page setting 109 | -l ./dir Override layout directory setting 110 | -m mmd Override the markdown parser command setting 111 | -v Display more information during building 112 | -n Ignore the .bloglock file when building 113 | -c Empty out the build folder prior to building 114 | -h Show this help message 115 | ``` 116 | 117 | ### Handy Shortcuts 118 | 119 | ```bash 120 | # Publish file with current date 121 | jenny publish filename.md 122 | 123 | # Edit file without typing date/full filename 124 | jenny edit partial-filename 125 | ``` 126 | 127 | ## Customize defualt behaviors with .blogrc settings 128 | 129 | To configure posts per page: 130 | 131 | ``` 132 | POSTS_PER_PAGE=10 133 | ``` 134 | 135 | To use your own markdown parser, set `MARKDOWN_COMMAND`: 136 | 137 | ``` 138 | MARKDOWN_COMMAND=multimarkdown 139 | ``` 140 | 141 | To run a script after the build process, write a `post_hook` function in .blogrc: 142 | 143 | ``` 144 | function post_hook() { 145 | echo "Done!" 146 | } 147 | ``` 148 | 149 | To set a different build folder, set `DIST`: 150 | 151 | ``` 152 | DIST=~/blog/.dist 153 | ``` 154 | 155 | To copy directories into the build folder: 156 | 157 | ``` 158 | STATIC_DIRS="images files" 159 | ``` 160 | 161 | ## Post Formatting 162 | 163 | Titles are Markdown H1s. Multimarkdown footnotes syntax supported in the vendored parser. 164 | 165 | To use tags add the below into a post where `tagname` is both filename and URL friendly: 166 | 167 | ``` 168 | tags: tagname anothertag 169 | ``` 170 | 171 | ## Theme Customization 172 | 173 | From the project folder, copy the layout folder contents and modify to your liking, mind the template tags. 174 | 175 | ```cp -R ./share/jenny/layout ~/blog/.layout``` 176 | 177 | Let Jenny know where to find your customized layout files. 178 | 179 | ``` 180 | cat <> .blogrc 181 | LAYOUT_DIR=~/blog/.layout 182 | EOT 183 | ``` 184 | 185 | ## Plugs 🔌 186 | 187 | Sites built with `jenny`. 188 | 189 | Me - [@hmngwy](https://github.com/hmngwy): [manilafunctional.com](https://manilafunctional.com/)[src](https://github.com/hmngwy/blog) 190 | 191 | Send a PR my way if you want to be on here. 192 | 193 | 194 | ## Alternative Installation Modes 195 | 196 | To soft install or symlink into your bin folder, good for contributing/development: 197 | ``` 198 | sudo make sym-install [PREFIX=$PWD] 199 | ``` 200 | 201 | To install into a custom location do: ```make install [PREFIX=~/.local]``` 202 | 203 | To uninstall, in the project folder run: ```make uninstall``` 204 | 205 | ## Credits 206 | 207 | - Layout inspired by n-o-d-e.net 208 | - Some colors from Solarized by Ethan Schoonover 209 | - Makefile inspired by [moebiuseye/skf](https://github.com/moebiuseye/skf) 210 | 211 | ## License 212 | 213 | MIT License 214 | 215 | Copyright (c) 2024 216 | 217 | Permission is hereby granted, free of charge, to any person obtaining a copy 218 | of this software and associated documentation files (the "Software"), to deal 219 | in the Software without restriction, including without limitation the rights 220 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 221 | copies of the Software, and to permit persons to whom the Software is 222 | furnished to do so, subject to the following conditions: 223 | 224 | The above copyright notice and this permission notice shall be included in all 225 | copies or substantial portions of the Software. 226 | 227 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 228 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 229 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 230 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 231 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 232 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 233 | SOFTWARE. 234 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # action.yml 2 | name: 'jenny-blog' 3 | description: 'blog with jenny' 4 | branding: 5 | icon: 'book' 6 | color: 'yellow' 7 | runs: 8 | using: 'docker' 9 | image: 'docker://ghcr.io/hmngwy/jenny:latest' 10 | -------------------------------------------------------------------------------- /bin/jenny: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Declare CONSTANTS 4 | PUBLISH_PATTERN="^[0-9]{4}-[0-9]{2}-[0-9]{2}(.*)" 5 | BLOG_LOCK=$(pwd)/.bloglock 6 | SOURCE_TEMP_FILE=$(mktemp) 7 | RENDER_TEMP_FILE=$(mktemp) 8 | TAGS_TEMP_DIR=$(mktemp -d) 9 | T="[jenny]" 10 | 11 | if readlink $0 > /dev/null; then 12 | SHARE_DIR=$(readlink $0 | rev | cut -d '/' -f3- | rev)/share/jenny 13 | else 14 | SHARE_DIR=$(echo $0 | rev | cut -d '/' -f3- | rev)/share/jenny 15 | fi 16 | 17 | # Load library 18 | source $SHARE_DIR/lib/helpers.sh 19 | source $SHARE_DIR/lib/components.sh 20 | source $SHARE_DIR/lib/subcommands.sh 21 | 22 | BASENAME=$(basename $0) 23 | SUBCOMMAND=$1 24 | type sub_${SUBCOMMAND} >/dev/null 2>/dev/null 25 | 26 | if [ $? -eq 0 ]; then 27 | sub_${SUBCOMMAND} $@ 28 | fi 29 | 30 | # Checks if user is on macOS and checks for dependencies 31 | # Also handles suppresion of this very message 32 | if is_utils_installed; then 33 | echo -e "$T macOS users need ggrep, gsed, gawk, and gdate, install them via: \n\n brew install gawk gnu-sed grep coreutils\n" 34 | echo -e "$T If you installed them with --with-default-names, i.e. as sed and awk, then add the below to your .blogrc file to suppress this warning:\n\n SUPPRESS_UTILS_WARN=1\n" 35 | exit 0 36 | fi 37 | 38 | # Use GNU utils if present 39 | [ "$(which gsed 2>/dev/null)" ] && SED=$(which gsed) || SED=$(which sed) 40 | [ "$(which gawk 2>/dev/null)" ] && AWK=$(which gawk) || AWK=$(which awk) 41 | [ "$(which gdate 2>/dev/null)" ] && DATE=$(which gdate) || DATE=$(which date) 42 | [ "$(which md5 2>/dev/null)" ] && SUM=$(which md5) || SUM=$(which md5sum) 43 | [ "$(which ggrep 2>/dev/null)" ] && GREP=$(which ggrep) || GREP=$(which grep) 44 | 45 | function usage() { 46 | echo "$(basename $0) usage:" && grep " .)\ #" $0 | sed 's/) #//' | sed 's/\ */ -/' | sed 's/ | \*//' 47 | exit 0 48 | } 49 | 50 | # Load configuration on current dir 51 | if source_blogrc; then 52 | echo "$T Sourced .blogrc" 53 | 54 | # Set defaults if not set in .blogrc 55 | [ -z $POSTS_PER_PAGE ] && POSTS_PER_PAGE=5 56 | [ -z $DIST ] && DIST=$PWD/.dist 57 | [ -z "$LAYOUT_DIR" ] && LAYOUT_DIR=$SHARE_DIR/layout 58 | [ -z "$MARKDOWN_COMMAND" ] && MARKDOWN_COMMAND="\$AWK -f \$SHARE_DIR/lib/md2html.awk" 59 | 60 | while getopts ":d:p:l:m:vnc" opt; do 61 | case $opt in 62 | d) # ./dir Override the build directory setting 63 | DIST="$OPTARG" 64 | ;; 65 | l) # ./dir Override layout directory setting 66 | LAYOUT_DIR="$OPTARG" 67 | ;; 68 | p) # 10 Override posts per page setting 69 | POSTS_PER_PAGE="$OPTARG" 70 | ;; 71 | m) # mmd Override the markdown parser command setting 72 | MARKDOWN_COMMAND="$OPTARG" 73 | ;; 74 | v) # Display more information during building 75 | VERBOSE="yes" 76 | ;; 77 | n) # Ignore the .bloglock file when building 78 | echo "$T Emptying $BLOG_LOCK" 79 | : > $BLOG_LOCK 80 | ;; 81 | c) # Empty out the build folder prior to building 82 | echo "$T Cleaning $DIST" 83 | rm -r $DIST 84 | ;; 85 | h | *) # Show this help message 86 | usage 87 | exit 0 88 | ;; 89 | \?) echo "Invalid option -$OPTARG" >&2 90 | ;; 91 | esac 92 | done 93 | 94 | # Sets up directory 95 | touch $BLOG_LOCK 96 | mkdir -p $DIST/post 97 | mkdir -p $DIST/page 98 | 99 | if [ "$STATIC_DIRS" ]; then 100 | eval "cp -R $STATIC_DIRS $DIST" 101 | fi 102 | else 103 | echo "$T Could not find .blogrc in this directory, exiting." 104 | exit 1 105 | fi 106 | 107 | [ ! -z "$VERBOSE" ] && echo 108 | [ ! -z "$VERBOSE" ] && echo "$T Build directory: $DIST" 109 | [ ! -z "$VERBOSE" ] && echo "$T Layout directory: $LAYOUT_DIR" 110 | [ ! -z "$VERBOSE" ] && echo "$T Posts per page: $POSTS_PER_PAGE" 111 | [ ! -z "$VERBOSE" ] && echo "$T Markdown command: $MARKDOWN_COMMAND" 112 | [ ! -z "$VERBOSE" ] && echo 113 | 114 | [ ! -z "$VERBOSE" ] && echo "$T grep: ${GREP}" 115 | [ ! -z "$VERBOSE" ] && echo "$T sed: ${SED}" 116 | [ ! -z "$VERBOSE" ] && echo "$T awk: ${AWK}" 117 | [ ! -z "$VERBOSE" ] && echo "$T date: ${DATE}" 118 | [ ! -z "$VERBOSE" ] && echo "$T sum: ${SUM}" 119 | [ ! -z "$VERBOSE" ] && echo 120 | 121 | # Declare GlobalVariables 122 | declare -a IndexList 123 | 124 | 125 | begin () { 126 | 127 | if [ -z "$_TAGNAME" ]; then 128 | echo "$T Generating main index" 129 | else 130 | echo "$T Generating $_TAGNAME tag index" 131 | fi 132 | 133 | # Grabbing the array argument 134 | local list=("$@") 135 | 136 | local total_post_count=$(get_total_post_count $@) 137 | local total_page_count=$(get_total_page_count $total_post_count $POSTS_PER_PAGE) 138 | 139 | # Reset variables 140 | local post_index=0 141 | 142 | # Loop through file list in expanded array 143 | for f in "${list[@]}"; do 144 | local full_file_name=$(get_full_filename "$f") 145 | local filename_sum=$(echo "$full_file_name" | $SUM | cut -d ' ' -f 1) 146 | local title=$(get_title "$f") 147 | 148 | # Process source files if not working on tag index 149 | if [ -z "$_TAGNAME" ]; then 150 | 151 | if is_draft "$f"; then 152 | echo "$T • Draft: $full_file_name file.."; 153 | continue 154 | fi 155 | 156 | if is_scheduled "$f"; then 157 | echo "$T • Scheduled: $full_file_name file.."; 158 | continue 159 | fi 160 | 161 | local content_sum=$(cat "$f" | $SUM | cut -d ' ' -f 1) 162 | 163 | echo "$T ☶ $full_file_name" 164 | forRendering=false 165 | if is_new "$filename_sum"; then 166 | local slug=$(get_unique_slug "$f") 167 | forRendering=true 168 | echo "$T + /post/$slug.html" 169 | echo "$slug $filename_sum $content_sum" >> $BLOG_LOCK 170 | else 171 | # not new 172 | local slug=$(get_existing_slug "$filename_sum") 173 | if is_changed "$filename_sum $content_sum"; then 174 | echo "$T ↑ /post/$slug.html" 175 | $SED -i "/$filename_sum/d" $BLOG_LOCK 176 | echo "$slug $filename_sum $content_sum" >> $BLOG_LOCK 177 | forRendering=true 178 | fi 179 | fi 180 | 181 | # Grab the tags 182 | tags=$(get_tags "$f") 183 | for i in $tags; do 184 | echo "$f" >> "$TAGS_TEMP_DIR/$i" 185 | done 186 | 187 | # If page is for rendering 188 | if [ $forRendering=true ]; then 189 | local destination="$_DIST/post/$slug.html" 190 | render "$f" "$destination" "$title" "$slug" "$tags" 191 | fi 192 | 193 | else 194 | local slug=$(get_existing_slug "$filename_sum") 195 | echo "$T ∙ /post/$slug.html" 196 | fi # Checks if working on tag index 197 | 198 | let post_index++ 199 | 200 | local page=$((($post_index+$POSTS_PER_PAGE-1)/$POSTS_PER_PAGE)) 201 | 202 | # Finally insert to the current index page 203 | _DIST=$_DIST _TAGNAME=$_TAGNAME \ 204 | index_insert "$f" "$slug" "$title" \ 205 | $total_post_count $total_page_count $post_index $page 206 | 207 | done 208 | 209 | echo 210 | } 211 | 212 | # Clean DIST folder first 213 | _DIST=$DIST clean 214 | 215 | # Get files in current directory 216 | fileList=(*.md) 217 | 218 | # Start generation 219 | _DIST=$DIST begin "${fileList[@]}" 220 | 221 | # Now generate indexes for tags 222 | OIFS="$IFS" 223 | if compgen -G "$TAGS_TEMP_DIR/*" > /dev/null; then 224 | for i in `ls $TAGS_TEMP_DIR/*`; do 225 | tag_name=$(basename $i) 226 | mkdir -p $DIST/tag/$tag_name 227 | IFS=$'\r\n' GLOBIGNORE='*' command eval "fileList=(\$(cat $i))" 228 | _DIST=$DIST/tag/$tag_name _TAGNAME=$tag_name begin "${fileList[@]}" 229 | done 230 | fi 231 | IFS="$OIFS" 232 | 233 | # Run the user defined hook 234 | [ "$(type -t post_hook)" = function ] && post_hook 235 | 236 | echo "$T Your rendered files are at $DIST" 237 | 238 | exit 0 239 | -------------------------------------------------------------------------------- /share/jenny/layout/index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Break apart the LIST payload 4 | IFS='✂︎' read -r -a array <<< "$LIST" 5 | 6 | function index_loop { 7 | for (( idx=${#array[@]}-1 ; idx>=0 ; idx-- )) ; do 8 | [ "${array[idx]}" ] && eval "${array[idx]} list_item" 9 | done 10 | } 11 | 12 | function list_item { 13 | if [ -z "$BREAK" ]; then 14 | cat << _LOOP_ 15 |
  • $(date -d "$POST_DATE_RFC822" +%m/%d/%Y) $POST_TITLE
  • 16 | _LOOP_ 17 | else 18 | cat << _LOOP_ 19 |
  • In page $BREAK
  • 20 | _LOOP_ 21 | fi 22 | } 23 | 24 | function nav { 25 | if [ "$PAGE_OLD" ] || [ "$PAGE_NEW" ]; then 26 | cat << _NAV_ 27 | 31 | _NAV_ 32 | fi 33 | } 34 | 35 | cat << _EOF_ 36 | 37 | 38 | 39 | 40 | 41 | ${BLOG_TITLE} 42 | 43 | 44 | 66 | 67 | 68 |
    ${BLOG_TITLE} 69 | $(if [ "$TAGNAME" ]; then echo "-> TAG == $TAGNAME"; fi) 70 | $(if [ "$PAGE_NUM" ]; then echo "-> Page $PAGE_NUM"; fi) 71 |
    72 | $(if [ "$TAGNAME" ]; then echo "
    TAG: $TAGNAME
    "; fi) 73 | 76 | $(nav) 77 | 78 | 79 | _EOF_ 80 | -------------------------------------------------------------------------------- /share/jenny/layout/post.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat << _EOF_ 4 | 5 | 6 | 7 | 8 | 9 | $POST_TITLE - Jenny 10 | 11 | 12 | 13 | 57 | 58 | 59 |
    $BLOG_TITLE
    60 | 67 | 68 | 69 | _EOF_ 70 | -------------------------------------------------------------------------------- /share/jenny/layout/rss2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Break apart the LIST payload 4 | IFS='✂︎' read -r -a array <<< "$LIST" 5 | 6 | function index_loop { 7 | for (( idx=${#array[@]}-1 ; idx>=0 ; idx-- )) ; do 8 | [ "${array[idx]}" ] && eval "${array[idx]} list_item" 9 | done 10 | } 11 | 12 | function list_item { 13 | cat << _LOOP_ 14 | 15 | $(echo $POST_TITLE) 16 | $(echo $BLOG_HOST)$(echo $POST_URL) 17 | 18 | $(echo $POST_DATE_RFC822) 19 | 20 | _LOOP_ 21 | } 22 | 23 | cat << _EOF_ 24 | 25 | 26 | 27 | $(echo $BLOG_TITLE) 28 | $(echo $BLOG_HOST) 29 | 30 | 31 | $(index_loop) 32 | 33 | 34 | 35 | _EOF_ 36 | 37 | -------------------------------------------------------------------------------- /share/jenny/lib/components.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Creates individual post pages 4 | render () { 5 | local file=$1 6 | local destination=$2 7 | local title=$3 8 | local slug=$4 9 | local tags=$5 10 | # Copy source file to a temp file 11 | cp "$file" $SOURCE_TEMP_FILE 12 | 13 | # Delete the tagline from the temp file 14 | $SED -i '/^tags\: .*/d' $SOURCE_TEMP_FILE 15 | 16 | # Run markdown interpreter 17 | eval "$MARKDOWN_COMMAND $SOURCE_TEMP_FILE > $RENDER_TEMP_FILE" 18 | 19 | # Delete the tag 20 | $SED -i 's/.*<\/h1>//' $RENDER_TEMP_FILE 21 | 22 | # Push variables to template file 23 | POST_TITLE=$title \ 24 | POST_URL="/post/$slug.html" \ 25 | POST_DATE=$(get_post_date "$file") \ 26 | POST_DATE_RFC822=$(get_post_date_rfc822 "$file") \ 27 | POST_CONTENTS=$(cat $RENDER_TEMP_FILE) \ 28 | BLOG_HOST=$BLOG_HOST \ 29 | BLOG_TITLE=$BLOG_TITLE \ 30 | TAGS=$tags \ 31 | $LAYOUT_DIR/post.sh > $destination 32 | } 33 | 34 | # Inserts each article into an index or pagination page 35 | index_insert () { 36 | local file=$1 37 | local slug=$2 38 | local title=$3 39 | local total_post_count=$4 40 | local total_page_count=$5 41 | local post_index=$6 42 | local page=$7 43 | 44 | local post_date=$(get_post_date "$file") 45 | local post_date_rfc822=$(get_post_date_rfc822 "$file") 46 | local is_page_new=$(( $post_index % $POSTS_PER_PAGE )) 47 | local url_prefix="" 48 | local root="./.." 49 | 50 | # If working on a tag index page, adjust pagination links 51 | if [ $_TAGNAME ]; then 52 | url_prefix="./.." 53 | root="." 54 | fi 55 | 56 | # Create the export line for the index.sh template 57 | IndexList+=("POST_URL=\"$url_prefix/post/$slug.html\" POST_TITLE=\"$(echo $title | sed 's#\"#\\\"#')\" POST_DATE=\"$post_date\" POST_DATE_RFC822=\"$post_date_rfc822\" TAGNAME=\"$_TAGNAME\"") 58 | 59 | # Create page when we have enough for a page 60 | # Or when we don't have any more 61 | if (( $is_page_new == 0 )) || (( $post_index == $total_post_count )); then 62 | 63 | # Add the older page nav 64 | PAGE_OLD=$(ROOT=$root get_page_old_url $page ) 65 | PAGE_NEW=$(ROOT=$root get_page_new_url $page $total_page_count) 66 | 67 | # This is where we should generate the heredocs template for index 68 | if (( $page == $total_page_count )); then 69 | # This is the generation for the main index, i.e. / 70 | IndexList=$(join_by '✂︎' "${IndexList[@]}") 71 | if (( ${#IndexList[@]} < $POSTS_PER_PAGE )) && (( $total_page_count > 1 )); then 72 | # If main index page is not full, fill from next page 73 | IFS='✂︎' read -r -a append <<< "$LastList" 74 | IndexList="BREAK=$(($total_page_count - 1))✂︎$IndexList" 75 | for (( idx=$POSTS_PER_PAGE ; idx>${#IndexList[@]} ; idx-- )) ; do 76 | IndexList="${append[idx]}✂︎$IndexList" 77 | done 78 | fi 79 | LIST="$IndexList" \ 80 | URL_PREFIX="." \ 81 | PAGE_OLD=$PAGE_OLD \ 82 | TAGNAME=$_TAGNAME \ 83 | BLOG_HOST=$BLOG_HOST \ 84 | BLOG_TITLE=$BLOG_TITLE \ 85 | $LAYOUT_DIR/index.sh > "$_DIST/index.html" 86 | 87 | echo "$T ⌁ Generating RSS feed" 88 | 89 | LIST="$IndexList" \ 90 | PAGE_OLD=$PAGE_OLD \ 91 | TAGNAME=$_TAGNAME \ 92 | BLOG_HOST=$BLOG_HOST \ 93 | BLOG_TITLE=$BLOG_TITLE \ 94 | $LAYOUT_DIR/rss2.sh > "$_DIST/feed.xml" 95 | 96 | else 97 | # This is the generation for paged indexes, i.e. page/* 98 | IndexList=$(join_by '✂︎' "${IndexList[@]}") 99 | LastList=$IndexList # this is used in later loop iterations 100 | mkdir -p $_DIST/page 101 | LIST="$IndexList" \ 102 | URL_PREFIX=".." \ 103 | PAGE_NUM=$page \ 104 | PAGE_OLD=$PAGE_OLD \ 105 | PAGE_NEW=$PAGE_NEW \ 106 | TAGNAME=$_TAGNAME \ 107 | BLOG_HOST=$BLOG_HOST \ 108 | BLOG_TITLE=$BLOG_TITLE \ 109 | $LAYOUT_DIR/index.sh > "$_DIST/page/${page}.html" 110 | fi 111 | 112 | # Reset array for a new page 113 | unset IndexList 114 | 115 | fi 116 | 117 | } 118 | -------------------------------------------------------------------------------- /share/jenny/lib/helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Load configuration on current dir 4 | function source_blogrc() { 5 | BLOGRC="$(pwd)/.blogrc" 6 | if [ -f "$BLOGRC" ]; then 7 | source $(pwd)/.blogrc 8 | return 0 9 | else 10 | return 1; 11 | fi 12 | } 13 | 14 | # Check if darwin 15 | function is_utils_installed() { 16 | if [ -z "$SUPPRESS_UTILS_WARN" ] && \ 17 | [[ "$(uname -a)" == *"Darwin"* && ( -z "$(which gsed)" || -z "$(which gawk)" || -z "$(which ggrep)" || -z "$(which gdate)" ) ]]; then 18 | return 0 19 | else 20 | return 1 21 | fi 22 | } 23 | 24 | ceiling_divide() { 25 | ceiling_result=$((($1+$2-1)/$2)) 26 | } 27 | 28 | clean() { 29 | rm -f $_DIST/index.html 30 | rm -rf $_DIST/page/ 31 | rm -rf $_DIST/tag/ 32 | rm -rf /tmp/jenny* 33 | } 34 | 35 | function join_by { local IFS="$1"; shift; echo "$*"; } 36 | 37 | function script_dir() { 38 | if [[ "$(uname -a)" == *"Darwin"* ]]; then 39 | echo $(dirname "$(readlink $(which $BASH_SOURCE))") 40 | else 41 | echo $(dirname -- "$(readlink -e -- $BASH_SOURCE)") 42 | fi 43 | } 44 | 45 | function is_installed() { 46 | # Use project dir when symlinked 47 | if [[ script_dir = "." ]]; then 48 | return 0 # Installed 49 | else 50 | return 1 # Not installed 51 | fi 52 | } 53 | 54 | function get_total_post_count() { 55 | local list=("$@") 56 | local non_draft=($(printf "%s\n" "${list[@]}" | $GREP -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}(.*)')) 57 | local count=0 58 | for f1 in "${non_draft[@]}"; do 59 | if ! is_scheduled "$f1"; then 60 | let count++ 61 | fi 62 | done 63 | echo $count 64 | } 65 | 66 | function get_total_page_count() { 67 | # usage fn(total post count, posts per page) 68 | echo $((($1+$2-1)/$2)) 69 | } 70 | 71 | function get_full_filename() { 72 | echo $(basename "$1") 73 | } 74 | 75 | function get_extension() { 76 | echo $(get_full_filename "$1") | rev | cut -d. -f1 | rev 77 | } 78 | 79 | function get_filename() { 80 | echo $(get_full_filename "$1") | cut -d. -f1 81 | } 82 | 83 | function get_unique_slug() { 84 | local slug=$(echo $(get_filename "$1").md | sed 's|\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\) \(.*\).md$|\4|g' | sed 's| |-|g') 85 | 86 | $GREP "^$slug " $BLOG_LOCK > /dev/null 87 | if [ $? -eq 0 ]; then 88 | # slug exists 89 | local i=2 90 | while $GREP "^$slug-$i " $BLOG_LOCK > /dev/null; do 91 | let i++ 92 | done 93 | echo "$slug-$i" 94 | else 95 | echo $slug 96 | fi 97 | 98 | } 99 | 100 | function get_existing_slug() { 101 | local filename_sum=$1 102 | local match=$($GREP "$filename_sum" $BLOG_LOCK) 103 | if [ $? -eq 0 ]; then 104 | echo $match | cut -d " " -f 1 105 | exit 0 106 | fi 107 | } 108 | 109 | function get_id() { 110 | echo $(get_full_filename "$1") | rev | cut -d ' ' -f 2 | rev 111 | } 112 | 113 | function get_timestamp() { 114 | echo $(get_filename "$1") | cut -d ' ' -f -1 115 | } 116 | 117 | function get_post_date() { 118 | echo $(get_timestamp "$1") | $SED -e 's#\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)#\2/\3#' 119 | } 120 | 121 | function get_post_date_full() { 122 | echo $(get_timestamp "$1") | $SED -e 's#\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)#\1\ \2\-\3#' 123 | } 124 | 125 | function get_post_date_int() { 126 | echo $(get_timestamp "$1") | $SED -e 's#\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)#\1\2\3#' 127 | } 128 | 129 | function get_post_date_rfc822() { 130 | $DATE --rfc-822 --date="$(echo $(get_timestamp "$1") | $SED -e 's#\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)#\1/\2/\3#')" 131 | } 132 | 133 | function get_tags() { 134 | local tag_line=$(cat "$1" | $GREP -m 1 "^[Tt]ags: ") 135 | echo $tag_line | $SED -e 's/[Tt]ags\: \(.*\)/\1/' 136 | } 137 | 138 | function is_draft() { 139 | # if filename doesn't match publish pattern 140 | local publish_pattern="^[0-9]{4}-[0-9]{2}-[0-9]{2}(.*)" 141 | if ! [[ $(get_filename "$1") =~ $PUBLISH_PATTERN ]]; then 142 | return 0 # is draft 143 | else 144 | return 1 145 | fi 146 | } 147 | 148 | function is_scheduled() { 149 | # if the date is in the future, then it's scheduled 150 | if (( $(get_post_date_int "$1") > $(date +"%Y%m%d") )); then 151 | return 0 # is scheduled 152 | else 153 | return 1 154 | fi 155 | } 156 | 157 | function get_last_modified_timestamp() { 158 | date -r "$1" +%s 159 | } 160 | 161 | function is_new() { 162 | $GREP $1 $BLOG_LOCK > /dev/null 163 | if [ ! $? -eq 0 ]; then 164 | return 0 # is new 165 | else 166 | return 1 167 | fi 168 | } 169 | 170 | function is_changed() { 171 | $GREP "$1" $BLOG_LOCK > /dev/null 172 | if [ ! $? -eq 0 ]; then 173 | return 0 174 | else 175 | return 1 176 | fi 177 | } 178 | 179 | function get_title() { 180 | echo $($GREP -E "^#\s(.*?)" "$1" | \ 181 | $SED 's/^\#\ \(.*\)/\1/' | \ 182 | $SED -r 's/\\(.)/\1/g' ) 183 | } 184 | 185 | function get_page_old_url() { 186 | local page=$1 187 | if [[ $(( page - 1 )) > 0 ]]; then 188 | echo "$ROOT/page/$(( $page - 1 )).html" 189 | else 190 | echo "" 191 | fi 192 | } 193 | 194 | function get_page_new_url() { 195 | local page=$1 196 | local total_page_count=$2 197 | if [[ $(( page + 1 )) == $total_page_count ]]; then 198 | echo "../" 199 | else 200 | echo "$ROOT/page/$(( page + 1 )).html" 201 | fi 202 | } 203 | 204 | -------------------------------------------------------------------------------- /share/jenny/lib/md2html.awk: -------------------------------------------------------------------------------- 1 | #!/bin/awk -f 2 | # 3 | # by: Jesus Galan (yiyus) 2009 4 | # 5 | # Usage: md2html.awk file.md > file.html 6 | # See: http://4l77.com/src/md2html.awk 7 | 8 | function eschtml(t) { 9 | gsub("&", "\\&", t); 10 | gsub("<", "\\<", t); 11 | return t; 12 | } 13 | 14 | function oprint(t){ 15 | if(nr == 0) 16 | print t; 17 | else 18 | otext = otext "\n" t; 19 | } 20 | 21 | function subref(id){ 22 | for(; nr > 0 && sub("<<" id, ref[id], otext); nr--); 23 | if(nr == 0 && otext) { 24 | print otext; 25 | otext = ""; 26 | } 27 | } 28 | 29 | function nextil(t) { 30 | if(!match(t, /[`<&\[*_\\-]|(\!\[)|(\[\^)/)) 31 | return t; 32 | t1 = substr(t, 1, RSTART - 1); 33 | tag = substr(t, RSTART, RLENGTH); 34 | t2 = substr(t, RSTART + RLENGTH); 35 | if(ilcode && tag != "`") 36 | return eschtml(t1 tag) nextil(t2); 37 | # Backslash escaping 38 | if(tag == "\\"){ 39 | if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){ 40 | tag = substr(t2, 1, 1); 41 | t2 = substr(t2, 2); 42 | } 43 | return t1 tag nextil(t2); 44 | } 45 | # Dashes 46 | if(tag == "-"){ 47 | if(sub(/^-/, "", t2)) 48 | tag = "—"; 49 | return t1 tag nextil(t2); 50 | } 51 | # Inline Code 52 | if(tag == "`"){ 53 | if(sub(/^`/, "", t2)){ 54 | if(!match(t2, /``/)) 55 | return t1 "”" nextil(t2); 56 | ilcode2 = !ilcode2; 57 | } 58 | else if(ilcode2) 59 | return t1 tag nextil(t2); 60 | tag = ""; 61 | if(ilcode){ 62 | t1 = eschtml(t1); 63 | tag = ""; 64 | } 65 | ilcode = !ilcode; 66 | return t1 tag nextil(t2); 67 | } 68 | if(tag == "<"){ 69 | # Autolinks 70 | if(match(t2, /^[^ ]+[\.@][^ ]+>/)){ 71 | url = eschtml(substr(t2, 1, RLENGTH - 1)); 72 | t2 = substr(t2, RLENGTH + 1); 73 | linktext = url; 74 | if(match(url, /@/) && !match(url, /^mailto:/)) 75 | url = "mailto:" url; 76 | return t1 "" linktext "" nextil(t2); 77 | } 78 | # Html tags 79 | if(match(t2, /^[A-Za-z\/!][^>]*>/)){ 80 | tag = tag substr(t2, RSTART, RLENGTH); 81 | t2 = substr(t2, RLENGTH + 1); 82 | return t1 tag nextil(t2); 83 | } 84 | return t1 "<" nextil(t2); 85 | } 86 | # Html special entities 87 | if(tag == "&"){ 88 | if(match(t2, /^#?[A-Za-z0-9]+;/)){ 89 | tag = tag substr(t2, RSTART, RLENGTH); 90 | t2 = substr(t2, RLENGTH + 1); 91 | return t1 tag nextil(t2); 92 | } 93 | return t1 "&" nextil(t2); 94 | } 95 | # Images 96 | if(tag == "!["){ 97 | if(!match(t2, /(\[.*\])|(\(.*\))/)) 98 | return t1 tag nextil(t2); 99 | match(t2, /^[^\]]*/); 100 | alt = substr(t2, 1, RLENGTH); 101 | t2 = substr(t2, RLENGTH + 2); 102 | if(match(t2, /^\(/)){ 103 | # Inline 104 | sub(/^\(/, "", t2); 105 | match(t2, /^[^\)]+/); 106 | url = eschtml(substr(t2, 1, RLENGTH)); 107 | t2 = substr(t2, RLENGTH + 2); 108 | title = ""; 109 | if(match(url, /[ ]+\".*\"[ ]*$/)) { 110 | title = substr(url, RSTART, RLENGTH); 111 | url = substr(url, 1, RSTART - 1); 112 | match(title, /\".*\"/); 113 | title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; 114 | } 115 | if(match(url, /^<.*>$/)) 116 | url = substr(url, 2, RLENGTH - 2); 117 | return t1 "\""" nextil(t2); 118 | } 119 | else{ 120 | # Referenced 121 | sub(/^ ?\[/, "", t2); 122 | id = alt; 123 | if(match(t2, /^[^\]]+/)) 124 | id = substr(t2, 1, RLENGTH); 125 | t2 = substr(t2, RLENGTH + 2); 126 | if(ref[id]) 127 | r = ref[id]; 128 | else{ 129 | r = "<<" id; 130 | nr++; 131 | } 132 | return t1 "\""" nextil(t2); 133 | } 134 | } 135 | # Footnotes 136 | if(tag == "[^"){ 137 | match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); 138 | linktext = substr(t2, 1, RLENGTH); 139 | t2 = substr(t2, RLENGTH + 2); 140 | return t1 "" linktext "" nextil(t2); 141 | } 142 | # Links 143 | if(tag == "["){ 144 | if(!match(t2, /(\[.*\])|(\(.*\))/)) 145 | return t1 tag nextil(t2); 146 | match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); 147 | linktext = substr(t2, 1, RLENGTH); 148 | t2 = substr(t2, RLENGTH + 2); 149 | if(match(t2, /^\(/)){ 150 | # Inline 151 | match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/); 152 | url = substr(t2, 2, RLENGTH - 1); 153 | pt2 = substr(t2, RLENGTH + 2); 154 | title = ""; 155 | if(match(url, /[ ]+\".*\"[ ]*$/)) { 156 | title = substr(url, RSTART, RLENGTH); 157 | url = substr(url, 1, RSTART - 1); 158 | match(title, /\".*\"/); 159 | title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; 160 | } 161 | if(match(url, /^<.*>$/)) 162 | url = substr(url, 2, RLENGTH - 2); 163 | url = eschtml(url); 164 | return t1 "" nextil(linktext) "" nextil(pt2); 165 | } 166 | else{ 167 | # Referenced 168 | sub(/^ ?\[/, "", t2); 169 | id = linktext; 170 | if(match(t2, /^[^\]]+/)) 171 | id = substr(t2, 1, RLENGTH); 172 | t2 = substr(t2, RLENGTH + 2); 173 | if(ref[id]) 174 | r = ref[id]; 175 | else{ 176 | r = "<<" id; 177 | nr++; 178 | } 179 | pt2 = t2; 180 | return t1 "" nextil(linktext) "" nextil(pt2); 181 | } 182 | } 183 | # Emphasis 184 | if(match(tag, /[*_]/)){ 185 | ntag = tag; 186 | if(sub("^" tag, "", t2)){ 187 | if(stag[ns] == tag && match(t2, "^" tag)) 188 | t2 = tag t2; 189 | else 190 | ntag = tag tag 191 | } 192 | n = length(ntag); 193 | tag = (n == 2) ? "strong" : "em"; 194 | if(match(t1, / $/) && match(t2, /^ /)) 195 | return t1 tag nextil(t2); 196 | if(stag[ns] == ntag){ 197 | tag = "/" tag; 198 | ns--; 199 | } 200 | else 201 | stag[++ns] = ntag; 202 | tag = "<" tag ">"; 203 | return t1 tag nextil(t2); 204 | } 205 | } 206 | 207 | function inline(t) { 208 | ilcode = 0; 209 | ilcode2 = 0; 210 | ns = 0; 211 | 212 | return nextil(t); 213 | } 214 | 215 | function printp(tag) { 216 | if(!match(text, /^[ ]*$/)){ 217 | text = inline(text); 218 | if(tag != "") 219 | oprint("<" tag ">" text ""); 220 | else 221 | oprint(text); 222 | } 223 | text = ""; 224 | } 225 | 226 | BEGIN { 227 | blank = 0; 228 | code = 0; 229 | hr = 0; 230 | html = 0; 231 | nl = 0; 232 | nr = 0; 233 | otext = ""; 234 | text = ""; 235 | par = "p"; 236 | } 237 | 238 | # References 239 | !code && /^ *\[\^![^\]]*\]:[ ]+/ { 240 | sub(/^ *\[\^!/, ""); 241 | match($0, /\]/); 242 | id = substr($0, 1, RSTART - 1); 243 | sub(id "\\]:[ ]+", ""); 244 | title = ""; 245 | if(match($0, /\".*\"$/)) 246 | title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2); 247 | sub(/[ ]+\".*\"$/, ""); 248 | url = eschtml($0); 249 | ref[id] = url title; 250 | 251 | subref(id); 252 | next; 253 | } 254 | 255 | !code && /^ *\[\^[^\]]*\]:[ ]+/ { 256 | sub(/^ *\[\^/, ""); 257 | match($0, /\]/); 258 | id = substr($0, 1, RSTART - 1); 259 | sub(id "\\]:[ ]+", ""); 260 | sub(/[ ]+\".*\"$/, ""); 261 | url = eschtml($0); 262 | fnref[id] = url; 263 | 264 | subref(id); 265 | next; 266 | } 267 | 268 | # html 269 | !html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ 270 | isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ { 271 | if(code) 272 | oprint(""); 273 | for(; !text && block[nl] == "blockquote"; nl--) 274 | oprint(""); 275 | match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ 276 | isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/); 277 | htag = substr($0, 2, RLENGTH - 1); 278 | if(!match($0, "(<\\/" htag ">)|((^
    $)")) 279 | html = 1; 280 | if(html && match($0, /^
    $/ || 288 | (hr && />$/)) { 289 | html = 0; 290 | hr = 0; 291 | oprint($0); 292 | next; 293 | } 294 | 295 | html { 296 | oprint($0); 297 | next; 298 | } 299 | 300 | # List and quote blocks 301 | 302 | # Remove indentation 303 | { 304 | for(nnl = 0; nnl < nl; nnl++) 305 | if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \ 306 | (block[nnl + 1] == "blockquote" && !sub(/^> ?/, ""))) 307 | break; 308 | } 309 | nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; } 310 | # Quote blocks 311 | { 312 | while(sub(/^> /, "")) 313 | nblock[++nnl] = "blockquote"; 314 | } 315 | # Horizontal rules 316 | { hr = 0; } 317 | (blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ { 318 | if(code){ 319 | oprint(""); 320 | code = 0; 321 | } 322 | blank = 0; 323 | nnl = 0; 324 | hr = 1; 325 | } 326 | # List items 327 | block[nl] ~ /[ou]l/ && /^$/ { 328 | blank = 1; 329 | next; 330 | } 331 | { newli = 0; } 332 | !hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ { 333 | sub(/^ ? ? ?[*+-]( +| )/, ""); 334 | nnl++; 335 | nblock[nnl] = "ul"; 336 | newli = 1; 337 | } 338 | (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ { 339 | sub(/^ ? ? ?([0-9]+\.)+( +| )/, ""); 340 | nnl++; 341 | nblock[nnl] = "ol"; 342 | newli = 1; 343 | } 344 | newli { 345 | if(blank && nnl == nl && !par) 346 | par = "p"; 347 | blank = 0; 348 | printp(par); 349 | if(nnl == nl && block[nl] == nblock[nl]) 350 | oprint("
  • "); 351 | } 352 | blank && ! /^$/ { 353 | if(match(block[nnl], /[ou]l/) && !par) 354 | par = "p"; 355 | printp(par); 356 | par = "p"; 357 | blank = 0; 358 | } 359 | 360 | # Close old blocks and open new ones 361 | nnl != nl || nblock[nl] != block[nl] { 362 | if(code){ 363 | oprint(""); 364 | code = 0; 365 | } 366 | printp(par); 367 | b = (nnl > nl) ? nblock[nnl] : block[nnl]; 368 | par = (match(b, /[ou]l/)) ? "" : "p"; 369 | } 370 | nnl < nl || (nnl == nl && nblock[nl] != block[nl]) { 371 | for(; nl > nnl || (nnl == nl && pblock[nl] != block[nl]); nl--){ 372 | if(match(block[nl], /[ou]l/)) 373 | oprint("
  • "); 374 | oprint(""); 375 | } 376 | } 377 | nnl > nl { 378 | for(; nl < nnl; nl++){ 379 | block[nl + 1] = nblock[nl + 1]; 380 | oprint("<" block[nl + 1] ">"); 381 | if(match(block[nl + 1], /[ou]l/)) 382 | oprint("
  • "); 383 | } 384 | } 385 | hr { 386 | oprint("
    "); 387 | next; 388 | } 389 | 390 | # Code blocks 391 | code && /^$/ { 392 | if(blanK) 393 | oprint(""); 394 | blank = 1; 395 | next; 396 | } 397 | !text && sub(/^( | )/, "") { 398 | if(blanK) 399 | oprint(""); 400 | blank = 0; 401 | if(!code) 402 | oprint("
    ");
    403 | 	code = 1;
    404 | 	$0 = eschtml($0);
    405 | 	oprint($0);
    406 | 	next;
    407 | }
    408 | code {
    409 | 	oprint("
    "); 410 | code = 0; 411 | } 412 | 413 | # Setex-style Headers 414 | text && /^=+$/ {printp("h1"); next;} 415 | text && /^-+$/ {printp("h2"); next;} 416 | 417 | # Atx-Style headers 418 | /^#+/ && (!newli || par=="p" || /^##/) { 419 | for(n = 0; n < 6 && sub(/^# */, ""); n++) 420 | sub(/#$/, ""); 421 | par = "h" n; 422 | } 423 | 424 | # Paragraph 425 | /^$/ { 426 | printp(par); 427 | par = "p"; 428 | next; 429 | } 430 | 431 | # Add text 432 | { text = (text ? text " " : "") $0; } 433 | 434 | function alen(a, ix, k) { 435 | k = 0 436 | for(ix in a) k++ 437 | return k 438 | } 439 | 440 | END { 441 | if(code){ 442 | oprint(""); 443 | code = 0; 444 | } 445 | printp(par); 446 | for(; nl > 0; nl--){ 447 | if(match(block[nl], /[ou]l/)) 448 | oprint("
  • "); 449 | oprint(""); 450 | } 451 | gsub(/<<[^\"]*/, "", otext); 452 | print(otext); 453 | 454 | # Print footnotes 455 | if(alen(fnref)>0) { 456 | print "
      "; 457 | for (i in fnref) print "
    • " i ": " inline(fnref[i]) " ↩︎
    • "; 458 | print "
    "; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /share/jenny/lib/subcommands.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function sub_init() { 4 | if [ -f .blogrc ]; then 5 | echo "$T Found a .blogrc file, halting" 6 | exit 1 7 | fi 8 | echo "$T Initializing directory as blog." 9 | echo 10 | read -p " URL: " -i "http://localhost:8000" -e init_hostname 11 | read -p " Blog title: " -i "My Blog" -e init_title 12 | read -p " Posts per page: " -i 8 -e init_posts_per_page 13 | read -p " Build directory: " -i ".dist" -e init_build_dir 14 | read -p " File directories: " -i "images files" -e init_static_dirs 15 | cat << EOF > .blogrc 16 | #!/usr/bin/env bash 17 | 18 | BLOG_HOST="${init_hostname}" 19 | BLOG_TITLE="${init_title}" 20 | POSTS_PER_PAGE=$init_posts_per_page 21 | 22 | # This is your build directory 23 | DIST=$init_build_dir 24 | 25 | # These directories are copied into your build directory 26 | STATIC_DIRS="${init_static_dirs}" 27 | 28 | # Use custom template files, see instructions at https://github.com/hmngwy/jenny#customization 29 | #LAYOUT_DIR= 30 | 31 | # Use a custom Markdown command 32 | #MARKDOWN_COMMAND= 33 | 34 | # Commands to run after succesful build 35 | #function post_hook() { 36 | # echo "Done." 37 | #} 38 | EOF 39 | echo 40 | echo "$T .blogrc created." 41 | mkdir $init_static_dirs 2> /dev/null 42 | echo "$T File directories created." 43 | cat <> "$(date +%Y-%m-%d) hello-world.md" 44 | # Hello World 45 | 46 | Jenny is a static blog generator using bash, sed, and awk. 47 | EOT 48 | echo "$T Sample post created, you can run jenny now." 49 | exit 0 50 | } 51 | 52 | function sub_publish() { 53 | echo "$T Published $(date +%Y-%m-%d) ${*:2}" 54 | mv "${*:2}" "$(date +%Y-%m-%d) ${*:2}" 55 | exit 0 56 | } 57 | 58 | function sub_edit () { 59 | editor "$(ls | grep $2)" 60 | exit 0 61 | } 62 | --------------------------------------------------------------------------------