diff --git a/ChangeLog.txt b/ChangeLog.md similarity index 64% rename from ChangeLog.txt rename to ChangeLog.md index 396c029..246b8e9 100644 --- a/ChangeLog.txt +++ b/ChangeLog.md @@ -1,18 +1,31 @@ -0.3 - 30 March 2015 +# 0.4 +## 12 October 2015 + + Live preview function, rendering locally and refreshing the + browser on save (for Linux only). EXIF functions for jpeg images: + optional cleanup of metadata (location, camera model, time), auto + rotate and addition of a comment field. Social network SEO: + opengraph and new image tags. The backend is now using zuper + extended library of zfunctions. + +# 0.3 +## 30 March 2015 Several bugfixes and enhancements. Added support for recursive indexing of directory structures, integration with Jaro Mail for public maildir archiving, Spanish translation of documentation, render test function for local preview. -0.2 - 2 September 2013 +# 0.2 +## 2 September 2013 Refactoring based on bootstrap-2 and bootswatch themes plus the blueimp gallery. Project renamed to WebNomad. Possibility to interlace sections in HTML. Slideshow functiond developed and used for the website of http://yurt.in -0.1 - 19 October 2012 +# 0.1 +## 19 October 2012 Initial release following a minimalist design and a directory structure inspired by ruby projects. Called JaroWeb for lack @@ -20,7 +33,7 @@ http://jaromil.dyne.org as well stuff like http://pirati5stelle.it or http://democrazialiquida.eu -Earlier around 2010 +# Earlier around 2010 Various scripts descend from a rewrite of some Dyne.org websites made by Jaromil and Hellekin in Ruysdael Squat. diff --git a/README-es.md b/README-es.md index 9d7d462..141c375 100644 --- a/README-es.md +++ b/README-es.md @@ -3,7 +3,7 @@ .--.--.--.-----| |--.-----.-----.--------.---.-.--| | | | | | -__| _ | | _ | | _ | _ | |________|_____|_____|__|__|_____|__|__|__|___._|_____| - Un astuto publicador de sitios web estáticos v 0.3 + Un astuto publicador de sitios web estáticos http://dyne.org/software/webnomad diff --git a/README.md b/README.md index eb1cec5..41f4700 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,17 @@ Now go customise files in `tmpl/` with your favorite HTML editor and then go as well in `views/` to create your web pages, better start from index.html. +## LIVE PREVIEW ON SAVE + To preview all changes, start `./webnomad/preview`: this will launch a firefox browser on the locally rendered `test/index.html` and will -refresh it every time a file is modified in views/ or tmpl/. A -different browser than firefox can be specified as an argument, for +refresh it every time a file is modified in views/ or tmpl/. + +A different browser than firefox can be specified as an argument, for instance `./webnomad/preview chromium`. +## RENDER FINAL RESULT + To upload results, run `./webnomad/render` and your webpage will be in the `pub/` directory ready for upload with a recursive Scp or Rsync on any webserver, for instance `rsync -PraX pub/* dyne.org:public_html`. diff --git a/render b/render index 6b2beaa..f5bedb9 100755 --- a/render +++ b/render @@ -39,7 +39,7 @@ arrs+=(custom_fonts) source $SYS/zuper.init -VERSION=0.5 +VERSION=0.4 # fill path to the source website root DIR="`pwd`" diff --git a/zuper b/zuper deleted file mode 120000 index 20ed873..0000000 --- a/zuper +++ /dev/null @@ -1 +0,0 @@ -/home/jrml/devel/zuper/zuper \ No newline at end of file diff --git a/zuper b/zuper new file mode 100644 index 0000000..789c862 --- /dev/null +++ b/zuper @@ -0,0 +1,706 @@ +#!/usr/bin/env zsh +# +# Zuper - Zsh Ultimate Programmer's Extensions Refurbished +# +# Copyright (C) 2015 Dyne.org Foundation +# +# Zuper is designed, written and maintained by Denis Roio +# +# This source code is free software; you can redistribute it and/or +# modify it under the terms of the GNU Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This source code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# Please refer to the GNU Public License for more details. +# +# You should have received a copy of the GNU Public License along with +# this source code; if not, write to: +# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + +########################## +typeset -aU vars +typeset -aU arrs +typeset -aU maps + +vars=(DEBUG QUIET LOG) +arrs=(req freq) + +vars+=(zuper_version) +zuper_version=0.2 + +# Messaging function with pretty coloring +autoload colors +colors + +vars+=(last_act last_func last_notice) + +function _msg() { + local msg="$2" + command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")" + for i in $(seq 3 ${#}); + do + msg=${(S)msg//::$(($i - 2))*::/$*[$i]} + done + + local command="print -P" + local progname="$fg[magenta]${PROGRAM##*/}$reset_color" + local message="$fg_bold[normal]$fg_no_bold[normal]$msg$reset_color" + local -i returncode + + case "$1" in + inline) + command+=" -n"; pchars=" > "; pcolor="yellow" + ;; + message) + last_act="$msg" + pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]$msg$reset_color" + ;; + verbose) + last_func="$msg" + pchars="[D]"; pcolor="blue" + ;; + success) + last_notice="$msg" + pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]$msg$reset_color" + ;; + warning) + pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]$msg$reset_color" + ;; + failure) + pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]$msg$reset_color" + returncode=1 + ;; + print) + progname="" + ;; + *) + pchars="[F]"; pcolor="red" + message="Developer oops! Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\"" + returncode=127 + zerr + ;; + esac + ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >&2 + + # write the log if its configured + [[ "$LOG" = "" ]] || { + touch $LOG || return $? + ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >> $LOG + } + + return $returncode +} + +function _message say act() { + local notice="message" + [[ "$1" = "-n" ]] && shift && notice="inline" + [[ $QUIET = 1 ]] || _msg "$notice" $@ + return 0 +} + +function _verbose xxx func() { + [[ $DEBUG = 1 ]] && _msg verbose $@ + return 0 +} + +function _success yes notice() { + [[ $QUIET = 1 ]] || _msg success $@ + return 0 +} + +function _warning no warn warning() { + [[ $QUIET = 1 ]] || _msg warning $@ + return 0 +} + +function _failure fatal die error() { + # typeset -i exitcode=${exitv:-1} + [[ $QUIET = 1 ]] || _msg failure $@ + return 1 +} + +function _print() { + [[ $QUIET = 1 ]] || _msg print $@ + return 0 +} + + +fn() { + fun="$@" + req=() + freq=() + func "$fun" +} + +zerr() { + error "error in: ${fun:-$last_notice}" + [[ "$last_func" = "" ]] || warn "called in: $last_func" + [[ "$last_act" = "" ]] || warn "called in: $last_act" + [[ "$last_notice" = "" ]] || warn "called in: $last_notice" + # [[ "$fun" = "" ]] || warn "called in: $fun" + TRAPEXIT() { + error "error reported, operation aborted." + } + return 1 +} + + +ckreq reqck() { + err=0 + for v in $req; do + [[ "${(P)v}" = "" ]] && { + warn "required setting is blank: $v" + err=1 + } + done + + [[ $err = 1 ]] && return $err + + for f in $freq; do + # exists and has size greater than zero + [[ -s $f ]] || { + warn "required file empty: $f" + err=1 + } + done + [[ $err == 1 ]] && zerr + return $err +} + +zdump() { + fn zdump + [[ ${#vars} -gt 0 ]] && { + print "Global variables:" + for _v in $vars; do + print " $_v = \t ${(P)_v}" + done + } + [[ ${#arrs} -gt 0 ]] && { + print "Global arrays:" + for _a in $arrs; do + print " $_a \t ( ${(P)_a} )" + done + } + [[ ${#maps} -gt 0 ]] && { + print "Global maps:" + for _m in $maps; do + print " $_m [key] \t ( ${(Pk)_m} )" + print " $_m [val] \t ( ${(Pv)_m} )" + done + } +} + +# handy wrappers for throw/catch execution of blocks where we need the +# program to exit on any error (non-zero) returned by any function +throw() { function TRAPZERR() { zerr; return 1 } } +catch() { function TRAPZERR() { } } + +########################## +# Endgame handling + +arrs+=(destruens) +destruens=() + +# Trap functions for the endgame event +TRAPINT() { endgame INT; return $? } +# TRAPEXIT() { endgame EXIT; return $? } +TRAPHUP() { endgame HUP; return $? } +TRAPQUIT() { endgame QUIT; return $? } +TRAPABRT() { endgame ABORT; return $? } +TRAPKILL() { endgame KILL; return $? } +# TRAPPIPE() { endgame PIPE; return $? } +TRAPTERM() { endgame TERM; return $? } +TRAPSTOP() { endgame STOP; return $? } +# TRAPZERR() { func "function returns non-zero." } + + +endgame() { + fn "endgame $*" + + # execute all no matter what + TRAPZERR() { } + + # process registered destructors + for d in $destruens; do + fn "destructor: $d" + $d + done + return 0 +} + +# Register endgame() to be called at exit. +# unlike TRAPEXIT, the zshexit() hook is not called when functions exit. +zshexit() { endgame EXIT; return $? } + +########################## +# Temp file handling + +vars+=(ztmpfile) +# ztmp() fills in $ztmpfile global. Caller must copy that variable as +# it will be overwritten at every call. +ztmp() { + fn ztmp + + ztmpfile=`mktemp` + tmpfiles+=($ztmpfile) +} + +# All tempfiles are freed in endgame() +_ztmp_destructor() { + fn _ztmp_destructor + + for f in $tmpfiles; do + rm -f "$f" + done + tmpfiles=() +} + +arrs+=(tmpfiles) +destruens+=(_ztmp_destructor) + +# tokenizer, works only with one char length delimiters +# saves everything in global array tok=() +arrs+=(tok) +strtok() { + fun="strtok $*" + _string="$1" + _delim="$2" + req=(_string _delim) + ckreq || return $? + + tok=() + local f=0 + local c=0 + for c in {1..${#_string}}; do + if [[ "${_string[(e)$c]}" == "$_delim" ]]; then + # check if not empty + t=${_string[(e)$(($f + 1)),$(($c - 1))]} + [[ "$t" == "" ]] || tok+=($t) + # save last found + f=$c + fi + done + # add last token + t=${_string[(e)$(($f + 1)),$c]} + [[ "$t" == "" ]] || tok+=($t) +} + +# optional: define zkv=1 on source + +[[ "$zkv" = "" ]] || { + + ########################## + # Key/Value file storage using ZSh associative maps + + zmodload zsh/system + + # load a map from a file + # map must be already instantiated with typeset -A by called + # name of map is defined inside the file + function zkv.load() { + fn "zkv-load $*" + + file=$1 + [[ "$file" = "" ]] && { + error "zkv-open() missing argument: file-path" + zerr + return 1 } + [[ -r "$file" ]] || { + error "zkv-open() file not found $file" + zerr + return 1 } + [[ -s "$file" ]] || { + error "zkv-open() file is empty" + zerr + return 1 } + + source $file + } + + # save a map in a file + # $1 = name of the map associative array + # $2 = full path to the file + function zkv.save() { + fn "zkv.save $*" + + _map=$1 + _path=$2 + [[ "$_path" = "" ]] && { + error "zkv.save() missing argument: map-name path-to-file" + zerr + return 1 + } + [[ -r $_path ]] && { + func "zkv.close() overwriting $_path" + func "backup turd left behind: ${_path}~" + mv $_path $_path~ + } + touch $_path + + # wondering about http://www.zsh.org/mla/users/2015/msg00286.html + # meanwhile solved using a double array, wasting a full map memcpy + _karr=(${(Pk)_map}) + _varr=(${(Pv)_map}) + _num="${#_karr}" + for c in {1..$_num}; do + # can also be cat here, however for speed we use builtins + # switch to cat if compatibility is an issue + sysread -o 1 <> $_path +$_map+=("${_karr[$c]}" "${(v)_varr[$c]}") +EOF + done + func "$_num key/values stored in $_path" + } + +} + +# optional: define restful=1 on source + +[[ "$restful" = "" ]] || { + + ######## + # Restful API client + # there is a clear zsh optimization here in get/set kv + # using zsh/tcp instead of spawning curl + # and perhaps querying with one call using ?recursive + + zmodload zsh/net/tcp + + function restful.put() { + # $1 = hostname + # $2 = port + # $3 = path + # $4 = key + # $5 = value + + fn "restful.put $*" + + # to check if the http service is running is up to the caller + + _host=${1} # ip address + _port=${2} + _path=${3} + _k="$4" # key name + _v="$5" # value + + req=(_host _k _v) + ckreq || return $? + + if ztcp $_host $_port; then + + + # TODO: work out various parsers, this one works with consul.io + + _fd=$REPLY + # func "tcp open on fd $fd" + cat <& $_fd +PUT ${_path}${_k} HTTP/1.1 +User-Agent: Zuper/$zuper_version +Host: ${_host}:${_port} +Accept: */* +Content-Length: ${#_v} +Content-Type: application/x-www-form-urlencoded + +EOF + + print -n "$_v" >& $_fd + + sysread -i $_fd _res + + # close connection + ztcp -c $_fd + + [[ "$_res" =~ "true" ]] || { + warn "failed PUT on restful key/value" + warn "endpoint: ${_host}:${_port}${_path}" + warn "resource: $_k = $_v" + print - "$_res" + zerr + return 1 + } + + else + error "cannot connect to restful service: $_host:$_port" + zerr + return 1 + fi + + return 0 + + } + + restful.get() { + fn "restful.get $*" + + _host=${1} + _port=${2} + _path=${3} + _k=$4 # key name + + req=(_host _k) + ckreq || return $? + + _k=$1 + + ztcp $_host $_port || { + zerr + return 1 + } + + _fd=$REPLY + + # TODO: work out various parsers, this one works with consul.io + + cat <& $_fd +GET ${_path}${_k} HTTP/1.1 +User-Agent: Zuper/$zuper_version +Host: $_host:$_port +Accept: */* + +EOF + sysread -i $_fd -o 1 | awk -F: ' +/"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d + + # close connection + ztcp -c $_fd + + return 0 + + } + +} + +# {{{ Helpers +[[ "$helpers" = "" ]] || { + + function helper.isfound isfound() { + command -v $1 1>/dev/null 2>/dev/null + return $? + } + + # remote leading and trailing spaces in a string taken from stdin + function helper.trim trim() { + sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g' + } + + zmodload zsh/mapfile + # faster substitute for cat + function helper.printfile printfile() { + print ${mapfile[$1]} + } + + # extract all emails found in a text from stdin + # outputs them one per line + function helper.extract-emails extract_emails() { + awk '{ for (i=1;i<=NF;i++) + if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) { + gsub(/<|>|,/ , "" , $i); print $i } }' + } + + + zmodload zsh/regex + # takes a string as argument, returns success if is an email + function helper.isemail isemail() { + [[ "$1" -regex-match "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0 + + return 1 + } + + # takes a numeric argument and prints out a human readable size + function helper.human-size human_size() { + [[ $1 -gt 0 ]] || { + error "human_size() called with invalid argument" + return 1 + } + + # we use the binary operation for speed + # shift right 10 is divide by 1024 + + # gigabytes + [[ $1 -gt 1073741824 ]] && { + print -n "$(( $1 >> 30 )) GB" + return 0 + } + + # megabytes + [[ $1 -gt 1048576 ]] && { + print -n "$(( $1 >> 20 )) MB" + return 0 + } + # kilobytes + [[ $1 -gt 1024 ]] && { + print -n "$(( $1 >> 10 )) KB" + return 0 + } + # bytes + print -n "$1 Bytes" + return 0 + } + + + # strips out all html/xml tags (everything between < >) + function helper.html-strip xml_strip html_strip() { sed 's/<[^>]\+>//g' } + + # changes stdin string special chars to be shown in html + function helper.escape-html escape_html() { + sed -e ' +s/\&/\&/g +s/>/\>/g +s/