aboutsummaryrefslogtreecommitdiffhomepage
path: root/sundry/misc
diff options
context:
space:
mode:
Diffstat (limited to 'sundry/misc')
-rw-r--r--sundry/misc/editor-syntax-etc/README90
-rw-r--r--sundry/misc/editor-syntax-etc/emacs/README4
-rw-r--r--sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el10
-rw-r--r--sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode.el493
-rw-r--r--sundry/misc/editor-syntax-etc/gedit/sisu.lang128
-rw-r--r--sundry/misc/editor-syntax-etc/kate/sisu.xml178
-rw-r--r--sundry/misc/editor-syntax-etc/nano/nanorc221
-rw-r--r--sundry/misc/editor-syntax-etc/nedit/sisu_nedit.pats79
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/8.vim65
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def-ruby.vim223
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def-sisu.vim223
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def.vim223
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def256-ruby.vim195
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def256-sisu.vim195
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/def256.vim195
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-ruby.vim66
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-sisu.vim66
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm.vim69
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/frugal.vim66
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/slate.vim75
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-ruby.vim66
-rw-r--r--sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-sisu.vim66
-rw-r--r--sundry/misc/editor-syntax-etc/vim/filetype.vim8
-rw-r--r--sundry/misc/editor-syntax-etc/vim/ftplugin/sisu.vim224
-rw-r--r--sundry/misc/editor-syntax-etc/vim/ftplugin/sisurb.vim7
-rw-r--r--sundry/misc/editor-syntax-etc/vim/rc/vimrc_statusline110
-rw-r--r--sundry/misc/editor-syntax-etc/vim/syntax/sisu.vim277
-rw-r--r--sundry/misc/editor-syntax-etc/vim/templates/ssi.tpl30
-rw-r--r--sundry/misc/editor-syntax-etc/vim/templates/ssm.tpl30
-rw-r--r--sundry/misc/editor-syntax-etc/vim/templates/sst.tpl30
-rw-r--r--sundry/misc/editor-syntax-etc/vim/vim-sisu.yaml7
-rw-r--r--sundry/misc/ext_lib/src/arsd/README2
-rw-r--r--sundry/misc/ext_lib/src/arsd/cgi.d10315
-rw-r--r--sundry/misc/util/d/cgi/search/README11
-rw-r--r--sundry/misc/util/d/cgi/search/dub.sdl16
-rw-r--r--sundry/misc/util/d/cgi/search/src/spine_cgi_sqlite_search.d963
-rw-r--r--sundry/misc/util/d/tools/markup_conversion/README1
-rwxr-xr-xsundry/misc/util/d/tools/markup_conversion/endnotes_inline_from_binary.d123
-rw-r--r--sundry/misc/util/d/tools/markup_conversion/markup_changes.d136
-rwxr-xr-xsundry/misc/util/d/tools/markup_conversion/markup_changes_header_and_content.d244
-rwxr-xr-xsundry/misc/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d367
-rwxr-xr-xsundry/misc/util/d/tools/spine_scaffold.d134
-rwxr-xr-xsundry/misc/util/rb/cgi/spine.search.cgi952
-rwxr-xr-xsundry/misc/util/rb/tex/dr_tex.rb120
44 files changed, 17103 insertions, 0 deletions
diff --git a/sundry/misc/editor-syntax-etc/README b/sundry/misc/editor-syntax-etc/README
new file mode 100644
index 0000000..c03f356
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/README
@@ -0,0 +1,90 @@
+This directory
+ ./data/sisu/conf/editor-syntax-etc/
+ /usr/share/sisu/conf/editor-syntax-etc/
+contains rudimentary sisu syntax highlighting files for:
+
+* (g)vim <http://www.vim.org>
+ * package: sisu-vim
+ * status: largely done
+ there is a vim syntax highlighting and folds component
+
+* gedit <http://www.gnome.org/projects/gedit>
+* gobby <http://gobby.0x539.de/>
+ file: sisu.lang
+ place in:
+ /usr/share/gtksourceview-1.0/language-specs
+ or
+ ~/.gnome2/gtksourceview-1.0/language-specs
+ * status: very basic syntax highlighting
+ * comments: this editor features display line wrap and is used by Goby!
+
+* nano <http://www.nano-editor.org>
+ file: nanorc
+ save as:
+ ~/.nanorc
+ includes:
+ * status: basic syntax highlighting
+ * comments: assumes dark background
+ no display line-wrap
+ does line breaks
+
+* diakonos (an editor written in ruby) <http://purepistos.net/diakonos>
+ file: diakonos.conf
+ save as:
+ ~/.diakonos/diakonos.conf
+ includes:
+ * status: basic syntax highlighting
+ * comments: assumes dark background
+ no display line-wrap
+
+* kate & kwrite http://kate.kde.org
+ file: sisu.xml
+ place in:
+ /usr/share/apps/katepart/syntax
+ or
+ ~/.kde/share/apps/katepart/syntax
+ [settings::configure kate::{highlighting,filetypes}]
+ [tools::highlighting::{markup,scripts}::SiSU]
+
+* nedit <http://www.nedit.org>
+ file: sisu_nedit.pats
+ nedit -import sisu_nedit.pats
+ * status: a very clumsy first attempt
+ [not really done]
+ * comments: this editor features display line wrap
+
+* emacs <http://www.gnu.org/software/emacs/emacs.html>
+ files: sisu-mode.el
+ to file ~/.emacs add the following 2 lines:
+ (add-to-list 'load-path "/usr/share/sisu-examples/config/syntax_hi")
+ (require 'sisu-mode.el)
+ [not done / not yet included]
+
+* vim & gvim <http://www.vim.org>
+ files: sisu-vim <http://www.jus.uio.no/sisu/SiSU/download>
+ package is the most comprehensive sisu syntax highlighting and editor
+ environment provided to date (is for vim/ gvim, and is separate from the
+ contents of this directory)
+ on debian:
+ aptitude install sisu-vim
+ * status: this includes:
+ syntax highlighting
+ vim folds
+ some error checking
+ * comments: this editor features display line wrap
+
+NOTE:
+
+[SiSU parses files with long lines or line breaks,
+but, display linewrap (without line-breaks) is a
+convenient editor feature to have for sisu markup]
+
+kde-config --prefix
+KDEDIR/share/apps/katepart/syntax/
+KDEHOME/share/apps/katepart/syntax
+[In the configure dialog in kate, go to the "Editor->highlighting" page and
+>select a highlight to change the mimetype/pattern associations for it.]
+/etc/mime.types
+/usr/share/mime/text
+#
+# * aeditor (an editor written in ruby)
diff --git a/sundry/misc/editor-syntax-etc/emacs/README b/sundry/misc/editor-syntax-etc/emacs/README
new file mode 100644
index 0000000..6dabf72
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/emacs/README
@@ -0,0 +1,4 @@
+; put this into your .emacs file, then use the mode file:
+
+(load-file "~/emacs/el/sisu-spine-mode.el")
+(add-to-list 'auto-mode-alist '("\\.sst$" . sisu-spine-mode))
diff --git a/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el b/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el
new file mode 100644
index 0000000..4cc6332
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode-autoloads.el
@@ -0,0 +1,10 @@
+(add-to-list 'load-path (or (file-name-directory #$) (car load-path)))
+(autoload 'sisu-spine-mode "sisu-spine-mode" "\
+Major mode for editing SiSU (spine) markup files.
+SiSU (https://www.sisudoc.org/) document structuring, publishing
+and search.
+
+\(fn)" t nil)
+(add-to-list 'auto-mode-alist '("\\.sst\\'" . sisu-spine-mode))
+(add-to-list 'auto-mode-alist '("\\.ssm\\'" . sisu-spine-mode))
+(add-to-list 'auto-mode-alist '("\\.ssi\\'" . sisu-spine-mode))
diff --git a/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode.el b/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode.el
new file mode 100644
index 0000000..773fa5c
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/emacs/sisu-spine-mode.el
@@ -0,0 +1,493 @@
+;;; sisu-spine-mode.el --- Major mode for SiSU (spine parser) markup text
+
+;; Copyright (C) 2011, 2021 Free Software Foundation, Inc.
+
+;; Author: Ralph Amissah & Ambrose Kofi Laing
+;; Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
+;; Keywords: text, syntax, processes, tools
+;; Version: 8.0.0
+;; URL: https://www.sisudoc.org/
+;; originally looked at (based on) doc-mode, with kind permission of the author
+;; Author: SUN, Tong <suntong001@users.sf.net>, (c)2001-6, all right reserved
+;; Version: $Date: 2006/01/19 03:13:41 $ $Revision: 1.14 $
+;; Home URL: https://xpt.sourceforge.net/
+;; with contributions from Kevin Ryde and Stefan Monnier
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This program 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. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;; Viva Software Libre!
+;; Support the free software movement!
+;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;; Commentary:
+
+;; SiSU (https://www.sisudoc.org/) is a document structuring and
+;; publishing framework. This package provides an Emacs major mode
+;; for SiSU markup, as used by the spine parser (in D) which has a different
+;; header (based on yaml) from the original sisu parser (in Ruby) which has
+;; bespoke headers.
+
+;; When this package is installed, files ending in ".sst" are automatically
+;; associated with sisu-spine-mode. If a file doesn't have a
+;; .sst extension, add a first line:
+;; # -*- sisuSpine -*-
+
+;; The documentation for the "Structure Of The Hierarchy Text" can be
+;; found in the sisustring for the sisu-spine-mode function.
+
+;;; Code:
+
+;; Variables:
+
+(defgroup sisu-faces nil
+ "AsciiSisu highlighting"
+ :group 'sisus)
+
+;; == Colors
+; color n is more prominent than color n+1
+
+(defface sisu-title-1-face
+ `((((class color)
+ (background dark))
+ (:foreground "brown3" :bold t :height 1.2 :inherit variable-pitch))
+ (((class color)
+ (background light))
+ (:foreground "brown3" :bold t :height 1.2 :inherit variable-pitch))
+ (t (:weight bold :inherit variable-pitch)))
+ "Face for AsciiSisu titles at level 1."
+ :group 'sisu-faces)
+
+(defface sisu-title-2-face
+ `((((class color)
+ (background dark))
+ (:foreground "yellow4" :bold t :height 1.1 :inherit variable-pitch))
+ (((class color)
+ (background light))
+ (:foreground "yellow4" :bold t :height 1.1 :inherit variable-pitch))
+ (t (:weight bold :inherit variable-pitch)))
+ "Face for AsciiSisu titles at level 2."
+ :group 'sisu-faces)
+
+(defface sisu-title-3-face
+ `((((class color)
+ (background dark))
+ (:foreground "sienna3" :bold t))
+ (((class color)
+ (background light))
+ (:foreground "sienna3" :bold t))
+ (t (:weight bold)))
+ "Face for AsciiSisu titles at level 3."
+ :group 'sisu-faces)
+
+(defface sisu-title-4-face
+ `((((class color)
+ (background dark))
+ (:foreground "burlywood3"))
+ (((class color)
+ (background light))
+ (:foreground "burlywood3"))
+ (t ()))
+ "Face for AsciiSisu titles at level 4."
+ :group 'sisu-faces)
+
+(defface info-node
+ '((((class color) (background light)) (:foreground "brown" :bold t :italic t))
+ (((class color) (background dark)) (:foreground "white" :bold t :italic t))
+ (t (:bold t :italic t)))
+ "Face for Info node names."
+ :group 'sisu-faces)
+
+(defvar sisu-title-1 'sisu-title-1-face)
+(defvar sisu-title-2 'sisu-title-2-face)
+(defvar sisu-title-3 'sisu-title-3-face)
+(defvar sisu-title-4 'sisu-title-4-face)
+
+(defvar sisu-general-font-lock-red1 font-lock-warning-face)
+(defvar sisu-general-font-lock-red2 font-lock-comment-face)
+(defvar sisu-general-font-lock-red3 font-lock-string-face)
+
+(defvar sisu-general-font-lock-green1 font-lock-type-face)
+(defvar sisu-general-font-lock-green2 font-lock-constant-face)
+
+(defvar sisu-general-font-lock-blue1 font-lock-keyword-face)
+(defvar sisu-general-font-lock-blue2 font-lock-function-name-face)
+(defvar sisu-general-font-lock-blue3 font-lock-builtin-face)
+
+(defvar sisu-general-font-lock-yellow1 font-lock-variable-name-face)
+(defvar sisu-general-font-lock-yellow2 font-lock-comment-face)
+
+;; == sisu-spine-mode settings
+
+(defvar sisu-spine-mode-hook nil
+ "Normal hook run when entering Sisu Text mode.")
+
+(defvar sisu-spine-mode-abbrev-table nil
+ "Abbrev table in use in Sisu-spine-mode buffers.")
+(define-abbrev-table 'sisu-spine-mode-abbrev-table ())
+
+(defconst sisu-font-lock-keywords
+ (eval-when-compile
+ (list
+ ;;grouped text ---------
+ ;(cons "^```[ ]code\\(.\\|\n\\)+?\n```\n" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+code.*?$\\|^```$" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+table.*?$\\|^```$" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+group$\\|^```$" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+block$\\|^```$" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+poem$\\|^```$" 'sisu-general-font-lock-red2)
+ (cons "^```[ ]+alt$\\|^```$" 'sisu-general-font-lock-red2)
+ ;;grouped text ---------
+ (cons "^group{\\|^}group" 'sisu-general-font-lock-red2)
+ (cons "^block{\\|^}block" 'sisu-general-font-lock-red2)
+ (cons "^code{\\|^}code" 'sisu-general-font-lock-red2)
+ (cons "^poem{\\|^}poem" 'sisu-general-font-lock-red2)
+ (cons "^alt{\\|^}alt" 'sisu-general-font-lock-red2)
+ (cons "^table{.+\\|^}table" 'sisu-general-font-lock-red2)
+ (cons "^{table[^}]+}" 'sisu-general-font-lock-red2)
+
+ (list
+ (concat
+ "^\`\\{3\\}[ ]+code.*?$"
+ "\\(.\\|\n\\)+?"
+ "\`\\{3\\}$"
+ )
+ '(1 sisu-general-font-lock-red2 t)
+ '(2 nil t)
+ '(3 sisu-general-font-lock-red2 t)
+ )
+ (list
+ (concat
+ "^\`\\{3\\}[ ]+table.*?$"
+ "\\(.\\|\n\\)+?"
+ "\`\\{3\\}$"
+ )
+ '(1 sisu-general-font-lock-red2 t)
+ '(2 nil t)
+ '(3 sisu-general-font-lock-red2 t)
+ )
+ (list
+ (concat
+ "^\`\\{3\\}[ ]+\\(group\\|block\\|alt\\|poem\\)$"
+ "\\(.\\|\n\\)+?"
+ "^\`\\{3\\}$"
+ )
+ '(1 sisu-general-font-lock-red2 t)
+ '(2 nil t)
+ '(3 sisu-general-font-lock-red2 t)
+ )
+
+ ;; footnote/endnote ----
+ ;(cons "\~{.+?}\~" 'sisu-general-font-lock-green1)
+ (cons "\~{\\*\\*\\|\~{\\*\\|\~{\\|}\~" 'sisu-general-font-lock-red2)
+ (cons "\~\\[\\+\\|\~\\[\\*\\|\~\\[\\|\\]\~" 'sisu-general-font-lock-red2)
+ (cons "\~\\^ \\|^\\^\~ " 'sisu-general-font-lock-red2)
+ (list
+ (concat
+ "\\(\*\~\\)"
+ "\\([^ \r\t\n]+\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-blue2 t)
+ )
+
+ ;; emphasis (can be program configured to be bold italics or underscore)
+ (list
+ (concat
+ "\\([*]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[*]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; bold ----------------
+ (list
+ (concat
+ "\\([!]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[!]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+ (cons "\\*[^ ]+\\*" 'sisu-general-font-lock-red1)
+ (cons "^!_ .+" 'sisu-general-font-lock-red1)
+
+ ;; italics -------------
+ (list
+ (concat
+ "\\([/]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[/]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-blue1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; underscore ----------
+ (list
+ (concat
+ "\\([_]{\\)"
+ "\\([^}]+\\)"
+ "\\(\}[_]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; monospace -----------
+ (list
+ (concat
+ "\\([#]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[#]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; citation ------------
+ (list
+ (concat
+ "\\([\"]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[\"]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; inserted text -------
+ (list
+ (concat
+ "\\([\+]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[\+]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; strike through ------
+ (list
+ (concat
+ "\\(\\-{\\)"
+ "\\([^}]+\\)"
+ "\\(}\\-\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; superscript ---------
+ (list
+ (concat
+ "\\(\\^{\\)"
+ "\\([^}]+\\)"
+ "\\(}\\^\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; subscript -----------
+ (list
+ (concat
+ "\\([,]{\\)"
+ "\\([^}]+\\)"
+ "\\(}[,]\\)"
+ )
+ '(1 sisu-general-font-lock-red1 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-red1 t)
+ )
+
+ ;; numbered list
+ (cons "^# \\|^_# " 'sisu-general-font-lock-red1)
+
+ ;; bullet text
+ (cons "^_\\*[1-9] \\|^_\\* " 'sisu-general-font-lock-red1)
+
+ ;; indented text
+ (cons "^_[1-9] " 'sisu-general-font-lock-red1)
+ (cons "^_[1-9]! " 'sisu-general-font-lock-red1)
+
+ ;; hanging indented text [proposed enable when implemented]
+ (cons "^__[1-9] " 'sisu-general-font-lock-red1)
+ (cons "^_[0-9]_[0-9] " 'sisu-general-font-lock-red1)
+ (cons "^__[1-9]! " 'sisu-general-font-lock-red1)
+ (cons "^_[0-9]_[0-9]! " 'sisu-general-font-lock-red1)
+
+ ;; url
+ (cons "\\(^\\|[ ]\\)https?:[/][/][^ \t\n\r<]+" 'sisu-general-font-lock-blue2)
+
+ ;; Comment Lines
+ (cons "^% .*" 'sisu-general-font-lock-blue1)
+
+ ;; page break
+ (cons "^\\(-\\\\\\\\-\\|=\\\\\\\\=\\|-\\.\\.-\\)" 'sisu-general-font-lock-red2)
+
+ ;; line break
+ (cons " \\\\\\\\ " 'sisu-general-font-lock-red1)
+
+ ;; line break (depreciated)
+ (cons "<br>" 'sisu-general-font-lock-red1)
+
+ ;; Section titles
+ (list "^\\(\\([1-4]\\|:?[A-D]\\)\\~\\)\\(.*\\)"
+ '(1 sisu-title-1 t)
+ '(3 sisu-title-2 t)
+ )
+
+ ;; hyper-links
+ (list
+ (concat
+ "\\({~^\\|{\\)"
+ "\\([^}{]+\\)"
+ "\\(}https?:[/][/][^ \r\n\t<]+\\)"
+ )
+ '(1 sisu-general-font-lock-blue2 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-blue2 t)
+ )
+
+ ;; book index
+ (list
+ (concat
+ "^\\(\={\\)"
+ "\\([^}{]+\\)"
+ "\\(}\\)$"
+ )
+ '(1 sisu-general-font-lock-green1 t)
+ '(2 nil t)
+ '(3 sisu-general-font-lock-green1 t)
+ )
+
+ ;(cons "^\={.+}" 'sisu-general-font-lock-green1)
+
+ ;; numbers
+ (cons "\\<[.0-9]+\\>" 'sisu-general-font-lock-green2)
+
+ ;; bullets sisu_normal (nearly copied regexp)
+ (cons "^_\\([1-9*]\\|[1-9]\\*\\) " 'sisu-general-font-lock-blue2)
+
+ ;; image links
+ (list
+ (concat
+ "\\({\\)"
+ "\\([^}{]+\\)"
+ "\\(}image\\)"
+ )
+ '(1 sisu-general-font-lock-blue2 t)
+ '(2 sisu-general-font-lock-red1 t)
+ '(3 sisu-general-font-lock-blue2 t)
+ )
+
+ ;; insert file links
+ (list
+ (concat
+ "\\(<< \\)"
+ "\\([^ \r\t\n]+\\.ss\\)"
+ "\\(i\\|t\\)"
+ )
+ '(1 sisu-general-font-lock-blue2 t)
+ '(2 sisu-general-font-lock-blue2 t)
+ '(3 sisu-general-font-lock-blue2 t)
+ )
+
+ ;; raw keywords
+ (list
+ (concat
+ "^\\(\\("
+ "creator\\|"
+ "title\\|"
+ "date\\|"
+ "rights\\|"
+ "publisher\\|"
+ "classify\\|"
+ "identifier\\|"
+ "original\\|"
+ "notes\\|"
+ "links\\|"
+ "make\\|"
+ "\\):\\)\\(.*\\)"
+ )
+ '(1 sisu-title-2 keep)
+ '(3 sisu-title-3 keep)
+ )
+ )
+ )
+ "Default expressions to highlight in AsciiSisu mode."
+)
+
+;; outline mode evil "folding" if available
+;; (define-key evil-normal-state-map ",0" 'show-all)
+;; (define-key evil-normal-state-map ",-" 'hide-body)
+;; (define-key evil-normal-state-map ",+" 'show-subtree)
+;; (define-key evil-normal-state-map ",=" 'show-subtree)
+
+;;
+
+;; Sisu & Autoload:
+
+;;;###autoload
+(define-derived-mode sisu-spine-mode text-mode "SiSU"
+ "Major mode for editing SiSU files.
+SiSU document structuring, publishing in multiple formats and search.
+URL `https://www.sisudoc.org/'"
+ (modify-syntax-entry ?\' ".")
+ ;;(flyspell-mode nil)
+
+ (make-local-variable 'paragraph-start)
+ (setq paragraph-start (concat "$\\|>" page-delimiter))
+ (make-local-variable 'paragraph-separate)
+ (setq paragraph-separate paragraph-start)
+ (make-local-variable 'paragraph-ignore-fill-prefix)
+ (setq paragraph-ignore-fill-prefix t)
+
+ (set (make-local-variable 'outline-regexp)
+ "^\\(\\([1-4]\\|:?[A-D]\\)\\~\\|\\@[a-z]+:\\( \\|$\\)\\)")
+
+ (make-local-variable 'require-final-newline)
+ (setq require-final-newline t)
+
+ (make-local-variable 'font-lock-defaults)
+ (setq font-lock-defaults
+ '(sisu-font-lock-keywords
+ nil ; KEYWORDS-ONLY: no
+ nil ; CASE-FOLD: no
+ ((?_ . "w")) ; SYNTAX-ALIST
+ ))
+ ;; Enable outlining.
+ ;; TODO with outlining make sure linum (line numbering) is off,
+ ;; else performance penalty, sucks bigtime
+ (outline-minor-mode 1))
+
+;;;###autoload (add-to-list 'auto-mode-alist '("\\.ss[imt]\\'" . sisu-spine-mode))
+
+(provide 'sisu-spine-mode)
+
+;;
+
+;;; sisu-spine-mode.el ends here
diff --git a/sundry/misc/editor-syntax-etc/gedit/sisu.lang b/sundry/misc/editor-syntax-etc/gedit/sisu.lang
new file mode 100644
index 0000000..b82d5f8
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/gedit/sisu.lang
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE language SYSTEM "language.dtd">
+<language _name="SiSU" version="1.0" _section="Markup" mimetypes="text/x-ruby">
+<!-- SiSU syntax highligting for gedit, place in:
+ /usr/share/gtksourceview-1.0/language-specs
+or:
+ ~/gtksourceview-1.0/language-specs
+-->
+
+ <syntax-item _name="Header" style="Data Type">
+ <start-regex>^0~\S|^\@\S+:\s</start-regex>
+ <end-regex>\n\n</end-regex>
+ </syntax-item>
+
+ <pattern-item _name="Bold" style="Keyword" end-at-line-end = "TRUE">
+ <regex>^(:[A-C]|[1-6])~[a-zA-Z0-9.?_-]*\s+.+</regex>
+ </pattern-item>
+
+ <pattern-item _name="Operators" style="Function">
+ <regex>^(!_|_[1-9]|_[1-9]?\*)\s</regex>
+ </pattern-item>
+
+ <syntax-item _name="Note Function" style="Function">
+ <start-regex>~\{</start-regex>
+ <end-regex>\}~</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Bold Function" style="Function">
+ <start-regex>[*]\{</start-regex>
+ <end-regex>\}[*]</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Exclaim Function" style="Function">
+ <start-regex>!\{</start-regex>
+ <end-regex>\}!</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Italics Function" style="Function">
+ <start-regex>/\{</start-regex>
+ <end-regex>\}/</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Underscore Function" style="Function">
+ <start-regex>_\{</start-regex>
+ <end-regex>\}_</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Supercript Function" style="Function">
+ <start-regex>\^{</start-regex>
+ <end-regex>\}\^</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Subscript Function" style="Function">
+ <start-regex>,{</start-regex>
+ <end-regex>\},</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Added Function" style="Function">
+ <start-regex>[+]\{</start-regex>
+ <end-regex>\}[+]</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Strikethrough Function" style="Function">
+ <start-regex>[-]\{</start-regex>
+ <end-regex>\}[-]</end-regex>
+ </syntax-item>
+
+ <pattern-item _name="Bold Single" style="String">
+ <regex>[*]\S+[*]</regex>
+ </pattern-item>
+
+ <pattern-item _name="Link" style="String">
+ <regex>\{[^}]+\}(https?:\/\/\S+|image)\s</regex>
+ </pattern-item>
+
+ <pattern-item _name="Link Internal" style="String">
+ <regex>\s\*~\S+</regex>
+ </pattern-item>
+
+ <pattern-item _name="Url" style="String">
+ <regex>http[s]*:\/\/\S+</regex>
+ </pattern-item>
+
+ <pattern-item _name="Url" style="String">
+ <regex>^&lt;&lt;\s+\S+\.ss[ti]</regex>
+ </pattern-item>
+
+ <line-comment _name="Line Comment" style="Comment">
+ <start-regex>^%+\s.</start-regex>
+ </line-comment>
+
+ <syntax-item _name="Multiline Code" style="Others">
+ <start-regex>^code\{\s*$</start-regex>
+ <end-regex>^\}code\s*$</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Multiline Poem" style="Others">
+ <start-regex>^poem\{\s*$</start-regex>
+ <end-regex>^\}poem\s*$</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Multiline Group" style="Others">
+ <start-regex>^group\{\s*$</start-regex>
+ <end-regex>^\}group\s*$</end-regex>
+ </syntax-item>
+
+ <syntax-item _name="Multiline Alt" style="Others">
+ <start-regex>^alt\{\s*$</start-regex>
+ <end-regex>^\}alt\s*$</end-regex>
+ </syntax-item>
+
+ <pattern-item _name="Page Break" style="Others 2" end-at-line-end = "TRUE">
+ <regex>&lt;:(pb|np)&gt;</regex>
+ </pattern-item>
+
+ <pattern-item _name="Line Break" style="Others 2" end-at-line-end = "TRUE">
+ <regex>&lt;:?br&gt;</regex>
+ </pattern-item>
+
+ <pattern-item _name="Footnote Reference" style="Others" end-at-line-end = "TRUE">
+ <regex>~\^(\s|$)</regex>
+ </pattern-item>
+
+ <pattern-item _name="Footnote Content" style="Others" end-at-line-end = "TRUE">
+ <regex>^\^~\s.+</regex>
+ </pattern-item>
+
+</language>
diff --git a/sundry/misc/editor-syntax-etc/kate/sisu.xml b/sundry/misc/editor-syntax-etc/kate/sisu.xml
new file mode 100644
index 0000000..a8054f2
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/kate/sisu.xml
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE language SYSTEM "language.dtd">
+<language name="SiSU" version="1.00" section="Markup" kateversion="2.4" extensions="*.sst; *.ssm; *.ssi; *._sst; *.-sst;" author="Ralph Amissah (ralph.amissah@gmail.com)" license="LGPL" >
+ <highlighting>
+ <list name="somename">
+ <item> class </item>
+ <item> const </item>
+ </list>
+ <contexts>
+ <context attribute="Normal Text" lineEndContext="#pop" name="Normal Text" >
+ <StringDetect String="&lt;br&gt;" attribute="Break" context="#pop" />
+ <RegExpr String="\s+$" attribute="Warn" context="#pop" />
+ <RegExpr String="&lt;\S+&gt;" attribute="Warn" context="#pop" />
+ <RegExpr String="&lt;a href=\S+&gt;" attribute="Warn" context="#pop" />
+ <RegExpr String="&lt;a href=" attribute="Warn" context="#pop" />
+ <RegExpr String="&lt;/a&gt;" attribute="Warn" context="#pop" />
+ <RegExpr String="&lt;https?:\/\/\S+&gt;" attribute="Warn" context="#pop"/>
+ <RegExpr String="&gt;https?:\/\/\S+" attribute="Warn" context="#pop"/>
+ <RegExpr String="^&lt;&lt;\s+[a-zA-Z._]+\.ss[it]" attribute="Link" context="#pop"/>
+ <RegExpr String="^&lt;&lt;\{\s*[a-zA-Z._]+\.ss[it]\s*\}" attribute="Link" context="#pop"/>
+ <RegExpr String="&lt;&lt;\s+&#124;" attribute="Link" context="#pop"/>
+ <RegExpr String="https?:\/\/\S+&lt;" attribute="Warn" context="#pop"/>
+ <keyword attribute="Keyword" context="#stay" String="somename" />
+ <DetectChar attribute="String" context="string" char="&quot;" />
+ <RegExpr String="^(0~\S+|@\S+)\s.+$" attribute="Header" context="#pop" />
+ <RegExpr String="^:?[A-C1-9]~.+$" attribute="Heading" context="#pop" />
+ <StringDetect String="~{" attribute="Endnote" context="footnote"/>
+ <StringDetect String="^~" attribute="Endnote" context="endnote"/>
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ <RegExpr String="\{[\s\S]+\}https?:\/\/\S+(\s|$)" attribute="Link" context="#pop" minimal="true" />
+ <RegExpr String="\{[\s\S]+\}\.\.\/\S+(\s|$)" attribute="Link" context="#stay" minimal="true" />
+ <RegExpr String="\{[\s\S]+\}image(\s|$)" attribute="Link" context="#stay" minimal="true" />
+ <RegExpr String="\s_?https?:\/\/\S+" attribute="Link" context="#stay"/>
+ <RegExpr String="^_?https?:\/\/\S+" attribute="Link" context="#stay"/>
+ <RegExpr String="^!_ .+" attribute="Bold" context="boldline"/>
+ <RegExpr String="^(_[1-9]|_\*|_[1-9]\*)\s" attribute="Indent" context="#stay"/>
+ <RegExpr String="~\^" attribute="Endnote" context="#stay"/>
+ <RegExpr String="^%+\s.+$" attribute="Comment" context="#stay" endRegion="regionMarker" firstNonSpace="true" />
+ </context>
+ <context attribute="String" lineEndContext="#stay" name="string" >
+ <DetectChar attribute="String" context="#pop" char="&quot;" />
+ </context>
+ <context attribute="Endnote" lineEndContext="#stay" name="footnote" >
+ <StringDetect attribute="Endnote" context="#pop" String="}~" />
+ <RegExpr String="\{[\s\S]+\}https?:\/\/\S+(\s|$)" attribute="Link" context="#pop" minimal="true" />
+ <RegExpr String="\{[\s\S]+\}\.\.\/\S+(\s|$)" attribute="Link" context="#stay" minimal="true" />
+ <RegExpr String="\{[\s\S]+\}image(\s|$)" attribute="Link" context="#stay" minimal="true" />
+ <RegExpr String="\s_?https?:\/\/\S+" attribute="Link" context="#stay"/>
+ <RegExpr String="^_?https?:\/\/\S+" attribute="Link" context="#stay"/>
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Bold" lineEndContext="#stay" name="bold" >
+ <StringDetect attribute="Bold" context="#pop" String="}*" />
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Bold" lineEndContext="#stay" name="emphasis" >
+ <StringDetect attribute="Bold" context="#pop" String="}!" />
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Italic" lineEndContext="#stay" name="italic" >
+ <StringDetect attribute="Italic" context="#pop" String="}/" />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Underscore" lineEndContext="#stay" name="underscore" >
+ <StringDetect attribute="Underscore" context="#pop" String="}_" />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Superscript" lineEndContext="#stay" name="superscript" >
+ <StringDetect attribute="Superscript" context="#pop" String="}^" />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Subscript" lineEndContext="#stay" name="subscript" >
+ <StringDetect attribute="Subscript" context="#pop" String="}," />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Strike" lineEndContext="#stay" name="strike" >
+ <StringDetect attribute="Strike" context="#pop" String="}-" />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="+{" attribute="Insert" context="insert"/>
+ </context>
+ <context attribute="Insert" lineEndContext="#stay" name="insert" >
+ <StringDetect attribute="Insert" context="#pop" String="}+" />
+ <StringDetect String="!{" attribute="Bold" context="emphasis"/>
+ <StringDetect String="*{" attribute="Bold" context="bold"/>
+ <StringDetect String="/{" attribute="Italic" context="italic"/>
+ <StringDetect String="_{" attribute="Underscore" context="underscore"/>
+ <StringDetect String="^{" attribute="Superscript" context="superscript"/>
+ <StringDetect String=",{" attribute="Subscript" context="subscript"/>
+ <StringDetect String="-{" attribute="Strike" context="strike"/>
+ </context>
+ <context attribute="Endnote" lineEndContext="#pop" name="endnote" />
+ <context attribute="Bold" lineEndContext="#pop" name="boldline" />
+ <context attribute="Indent" lineEndContext="#pop" name="indent" />
+ <context attribute="Warn" lineEndContext="#pop" name="warn" />
+ </contexts>
+ <itemDatas>
+ <itemData name="Normal Text" defStyleNum="dsNormal" />
+ <itemData name="Header" defStyleNum="dsNormal" color="#1c869b" selColor="#60FFFF" bold="0" italic="0"/>
+ <itemData name="Heading" defStyleNum="dsNormal" color="#ff0000" selColor="#60FFFF" bold="1" italic="0"/>
+ <itemData name="Bold" defStyleNum="dsNormal" color="#800000" selColor="#60FFFF" bold="1" italic="0"/>
+ <itemData name="Italic" defStyleNum="dsNormal" color="#800000" selColor="#60FFFF" bold="0" italic="1"/>
+ <itemData name="Underscore" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Superscript" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Subscript" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Strike" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Insert" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Keyword" defStyleNum="dsKeyword" />
+ <itemData name="Endnote" defStyleNum="dsNormal" color="#0AAA00" selColor="#60FFFF" bold="0" italic="0"/>
+ <itemData name="Link" defStyleNum="dsNormal" color="#0000FF" selColor="#60FFFF" bold="0" italic="0"/>
+ <itemData name="String" defStyleNum="dsString" />
+ <itemData name="Comment" defStyleNum="dsComment"/>
+ <itemData name="Group" defStyleNum="dsRegionMarker"/>
+ <itemData name="Break" defStyleNum="dsNormal" color="#FF0000" selColor="#60FFFF" bold="0" italic="0"/>
+ <itemData name="Indent" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Structure" defStyleNum="dsNormal" color="#F00000" selColor="#80FFD0" bold="0" italic="0"/>
+ <itemData name="Warn" defStyleNum="dsError" color="#FF00FF" selColor="#000000" bold="1" italic="0"/>
+ </itemDatas>
+ </highlighting>
+ <general>
+ <keywords weakDeliminator="\" wordWrapDeliminator=",{}[]"/>
+ <comments>
+ <comment name="singleLine" start="%" />
+ </comments>
+ </general>
+</language>
diff --git a/sundry/misc/editor-syntax-etc/nano/nanorc b/sundry/misc/editor-syntax-etc/nano/nanorc
new file mode 100644
index 0000000..30ba439
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/nano/nanorc
@@ -0,0 +1,221 @@
+## Sample initialization file for GNU nano
+## Please note that you must have configured nano with --enable-nanorc
+## for this file to be read! Also note that characters specially
+## interpreted by the shell should not be escaped here.
+##
+## To make sure a value is not enabled, use "unset <option>"
+##
+## For the options that take parameters, the default value is given.
+## Other options are unset by default.
+
+## Use auto-indentation.
+# set autoindent
+
+## Backup files to filename~.
+# set backup
+
+## The directory to put unique backup files in.
+# set backupdir ""
+
+## Do backwards searches by default.
+# set backwards
+
+## The characters treated as closing brackets. They cannot contain
+## blank characters. Only closing punctuation, optionally followed by
+## closing brackets, can end sentences.
+##
+# set brackets "'")}]>"
+
+## Do case sensitive searches by default.
+# set casesensitive
+
+## Constantly display the cursor position in the statusbar. Note that
+## this cancels out "quickblank".
+# set const
+
+## Use cut to end of line by default.
+# set cut
+
+## Set the line length for wrapping text and justifying paragraphs.
+## If fill is 0 or less, the line length will be the screen width less
+## this number.
+##
+# set fill -8
+
+## Enable ~/.nano_history for saving and reading search/replace strings.
+# set historylog
+
+## Use the blank line below the titlebar as extra editing space.
+# set morespace
+
+## Enable mouse support, so that mouse clicks can be used to set the
+## mark and run shortcuts.
+# set mouse
+
+## Allow multiple file buffers (inserting a file will put it into a
+## separate buffer). You must have configured with --enable-multibuffer
+## for this to work.
+##
+# set multibuffer
+
+## Don't convert files from DOS/Mac format.
+# set noconvert
+
+## Don't follow symlinks when writing files.
+# set nofollow
+
+## Don't display the helpful shortcut lists at the bottom of the screen.
+# set nohelp
+
+## Don't wrap text at all.
+# set nowrap
+
+## Set operating directory. nano will not read or write files outside
+## this directory and its subdirectories. Also, the current directory
+## is changed to here, so files are inserted from this dir. A blank
+## string means the operating directory feature is turned off.
+##
+# set operatingdir ""
+
+## Preserve the XON and XOFF keys (^Q and ^S).
+# set preserve
+
+## The characters treated as closing punctuation. They cannot contain
+## blank characters. Only closing punctuation, optionally followed by
+## closing brackets, can end sentences.
+##
+# set punct ".?!"
+
+## Do quick statusbar blanking. Statusbar messages will disappear after
+## 1 keystroke instead of 25. Note that "const" cancels this out.
+##
+# set quickblank
+
+## The email-quote string, used to justify email-quoted paragraphs.
+## This is an extended regular expression if your system supports them,
+## otherwise a literal string. Default:
+# set quotestr "^([ ]*[\|>:}#])+"
+## if you have regexps, otherwise:
+# set quotestr "> "
+## You can get old nano quoted-justify behavior via:
+# set quotestr "(> )+"
+
+## Fix Backspace/Delete confusion problem.
+# set rebinddelete
+
+## Do extended regular expression searches by default.
+# set regexp
+
+## Make the Home key smarter. When Home is pressed anywhere but at the
+## very beginning of non-whitespace characters on a line, the cursor
+## will jump to that beginning (either forwards or backwards). If the
+## cursor is already at that position, it will jump to the true
+## beginning of the line.
+# set smarthome
+
+## Use smooth scrolling as the default.
+# set smooth
+
+## Use this spelling checker instead of the internal one. This option
+## does not properly have a default value.
+##
+# set speller "aspell -x -c"
+
+## Allow nano to be suspended.
+# set suspend
+
+## Use this tab size instead of the default; it must be greater than 0.
+# set tabsize 8
+
+## Convert typed tabs to spaces.
+# set tabstospaces
+
+## Save automatically on exit, don't prompt.
+# set tempfile
+
+## Disallow file modification; why would you want this in an rcfile? ;)
+# set view
+
+## The two single-column characters used to display the first characters
+## of tabs and spaces. 187 decimal (00BB hexadecimal) and 183 decimal
+## (00B7 hexadecimal) seem to be good values for these.
+# set whitespace " "
+
+## Color setup
+##
+## Format:
+##
+## syntax "short description" ["filename regex" ...]
+##
+## color foreground,background "regex" ["regex"...]
+## or
+## icolor foreground,background "regex" ["regex"...]
+##
+## "color" will do case sensitive matches, while "icolor" will do case
+## insensitive matches.
+##
+## Legal colors: white, black, red, blue, green, yellow, magenta, cyan.
+## You may use the prefix "bright" to mean a stronger color highlight
+## for the foreground.
+##
+## To use multi-line regexes, use the start="regex" end="regex" format.
+##
+## If your system supports transparency, not specifying a background
+## color will use a transparent color. If you don't want this, be sure
+## to set the background color to black or white.
+##
+# syntax "c-file" "\.(c|C|cc|cpp|cxx|h|H|hh|hpp|hxx)$"
+# color red "\<[A-Z_]{2,}\>"
+# color green "\<(float|double|bool|char|int|short|long|sizeof|enum|void|static|const|struct|union|typedef|extern|signed|unsigned|inline)\>"
+# color green "\<(class|namespace|template|public|protected|private|typename|this|friend|virtual|using|mutable|volatile|register|explicit)\>"
+# color brightyellow "\<(for|if|while|do|else|case|default|switch|goto|continue|break|return)\>"
+# color brightyellow "\<(try|throw|catch|operator|new|delete)\>"
+# color brightcyan "^[[:space:]]*#[[:space:]]*(define|undef|include|ifn?def|endif|elif|else|if|warning|error)"
+# color brightmagenta "'([^\]|(\\['abfnrtv\\]))'" "'\\(([0-7][0-7]?)|([0-3][0-7][0-7]))'" "'\\x[0-9A-Fa-f][0-9A-Fa-f]?'"
+## color foreground,background "regex" ["regex"...]
+##
+#% sisu
+syntax "sisu" "\.ss[tmi]$"
+#color white,black ".*"
+color cyan start="^0~" end="^$"
+color cyan start="^@\S+:" end="^$"
+#color cyan "^0~.*"
+color brightyellow "^[1-6]~.*"
+color brightyellow "^:?[A-C]~.*"
+icolor brightwhite "^(Chapter|Part|Section|Book|Article|Preamble|Appendix)\s.*"
+color brightyellow "^!_\s.*"
+color brightyellow "[!*]\{.+?\}[!*]"
+color yellow "[/_+-^,]\{.+?\}[/_+-^,]"
+color green "~\{.+?\}~"
+color green "~\^(\s|$)"
+color green "^\^~\s.+"
+#color green start="~\{" end="\}~"
+color yellow "\{.+?\}http:\/\/\S+"
+color brightblue "http:\/\/\S+"
+color brightblue "^<<\s\S+?\.ss[it]"
+color yellow "^(_[12]?\*|_[12])"
+color magenta "^%+\s+.*"
+color green start="^poem\{$" end="^\}poem$"
+color green start="^code\{$" end="^\}code$"
+color green start="^group\{$" end="^\}group$"
+color green start="^alt\{$" end="^\}alt$"
+color magenta "<:(pb|np)>"
+color magenta "<(\/\s*)?br>"
+color magenta "[a-zA-Z0-9,.::?!&]+http:\/\/\S+"
+
+# .nanorc
+#
+ syntax "nanorc" "(\.|/|)nanorc$"
+# highlight possible errors and parameters
+ icolor brightwhite "^[[:space:]]*(set|unset|syntax|i?color).*$"
+# set, unset and syntax
+ icolor cyan "^[[:space:]]*(set|unset)[[:space:]]+(autoindent|backup|backupdir|backwards|brackets|casesensitive|const|cut|fill|historylog|morespace|mouse|multibuffer|noconvert|nofollow|nohelp|nowrap|operatingdir|preserve|punct|quickblank|quotestr|rebinddelete)\>" "^[[:space:]]*(set|unset)[[:space:]]+(regexp|smarthome|smooth|speller|suspend|tabsize|tabstospaces|tempfile|view|whitespace)\>"
+ icolor green "^[[:space:]]*(set|unset|syntax)\>"
+# colors
+ icolor yellow "^[[:space:]]*i?color[[:space:]]*(bright)?(white|black|red|blue|green|yellow|magenta|cyan)?(,(white|black|red|blue|green|yellow|magenta|cyan))?\>"
+ icolor magenta "^[[:space:]]*i?color\>" "\<(start|end)="
+# strings
+ icolor white "\"(\\.|[^\"])*\""
+# comments
+ icolor blue "^[[:space:]]*#.*$"
+
diff --git a/sundry/misc/editor-syntax-etc/nedit/sisu_nedit.pats b/sundry/misc/editor-syntax-etc/nedit/sisu_nedit.pats
new file mode 100644
index 0000000..ef01b2b
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/nedit/sisu_nedit.pats
@@ -0,0 +1,79 @@
+! Nedit Syntax highlighting patterns and commenting for SiSU
+! Version 0.1
+!
+! INSTALLATION
+!
+! Load this pattern by starting nedit with:
+!
+! nedit -import <name of this file>
+!
+! Then, check that the patterns were loaded correctly, and choose Save Defaults
+! from the Preferences menu. The new patterns will now be incorporated into
+! your own .nedit file, so the next time you start NEdit, you will no longer
+! need to use -import.
+!
+! These comments will not appear in your ~/.nedit
+!
+nedit.macroCommands: \
+ SiSU Comments>Comment Out Lines@SiSU:::R: {\n\
+ replace_in_selection("^.*$", "%+ &", "regex")\n\
+ }\n\
+ SiSU Comments>Uncomment Lines@SiSU:::R: {\n\
+ replace_in_selection("(^[ \\\\t]*# ?)(.*)$", "\\\\2", "regex")\n\
+ }\n\
+ SiSU Comments>Comment Out Sel.@SiSU:::R: {\n\
+ beginning-of-selection()\n\
+ mark("1")\n\
+ deselect-all()\n\
+ insert-string("=begin\\n")\n\
+ goto-mark("1")\n\
+ beginning-of-selection()\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ backward-character("extend")\n\
+ mark("1")\n\
+ end-of-selection()\n\
+ deselect-all()\n\
+ insert-string("\\n=end")\n\
+ goto-mark("1")\n\
+ }\n\
+ SiSU Comments>Uncomment Sel.@SiSU:::R: {\n\
+ beginning-of-selection()\n\
+ mark("1")\n\
+ deselect-all()\n\
+ replace("=begin\\n","")\n\
+ goto-mark("1")\n\
+ deselect-all()\n\
+ replace("\\n=end","","backward")\n\
+ goto-mark("1")\n\
+ }\n
+nedit.highlightPatterns: SiSU:1:0{\n\
+ comment:"^%+ ":"$"::Comment::\n\
+ endnote1:"~\\^(\s|$)":::Keyword::D\n\
+ endnote2:"^\\^~ ":"$"::Comment::\n\
+ header:"^0~|@\S+:\s":"$"::Keyword::\n\
+ bold_line:"^!_ ":"$"::Keyword::\n\
+ heading:"^[1-9]~":"$"::Keyword::\n\
+ code_block:"^code\\{":"^\\}code"::Comment::\n\
+ poem_block:"^poem\\{":"^\\}poem"::Comment::\n\
+ group_block:"^group\\{":"^\\}group"::Comment::\n\
+ alt_block:"^alt\\{":"^\\}alt"::Comment::\n\
+ endnote:"~\\{":"\\}~":"\\n":String::\n\
+ string2:"'":"'":"\\n":String1::\n\
+ string escape chars:"\\\\(.|\\n)":::String1:endnote:\n\
+ character constant:"'":"'":"[^\\\\][^']":Character Const::\n\
+ numeric constant:"<((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f)?>":::Plain::D\n\
+ storage keyword:"<(class)>":::Storage Type::D\n\
+ line:"^(_[1-9]|_\\*|_[1-9]\\*)":::Keyword::D\n\
+ link:"[{}](http://[a-zA-Z0-9?.!+\\-_/&=#]+)?":::Keyword::D\n\
+ url:"http://[a-zA-Z0-9?.!+\\-_/&=#]+":::Keyword::D\n\
+ }
+! endnote:"~\\{":"\\}~"::Keyword::\n\
+! link:"\\{":"\\}http://\S+"::Keyword::D\n\
+nedit.languageModes: SiSU:.sst::::::".,/\\`'!|@#%^&*()-=+{}[]"":;<>?~"
+! nedit.languageModes: SiSU:.sst::::::".,/\\`'!|@#%^&*()-=+{}[]"":;<>?~"
+! nedit.languageModes: SiSU:.ss[tmi]::::::
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/8.vim b/sundry/misc/editor-syntax-etc/vim/colors/8.vim
new file mode 100644
index 0000000..08c43b1
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/8.vim
@@ -0,0 +1,65 @@
+" Vim color file
+" Name: 8
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/8.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes frugal & sparse
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "8"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor term=reverse cterm=reverse
+:hi lCursor term=reverse cterm=reverse
+:hi StatusLine term=bold,reverse cterm=bold,reverse
+:hi StatusLineNC term=reverse cterm=reverse
+:hi Search term=reverse cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch term=reverse cterm=bold ctermbg=2 ctermfg=7
+:hi SpecialKey term=bold ctermfg=4
+:hi Visual term=reverse cterm=reverse
+:hi VisualNOS term=bold,underline cterm=bold,underline
+:hi MoreMsg term=bold ctermfg=2
+:hi ModeMsg term=bold cterm=bold
+:hi Question term=standout ctermfg=2
+:hi Title term=bold ctermfg=1
+:hi NonText term=bold cterm=bold ctermfg=4
+:hi LineNr term=underline cterm=none ctermbg=7 ctermfg=0
+:hi Directory term=bold ctermfg=4
+:hi WildMenu term=standout ctermbg=3 ctermfg=0
+:hi VertSplit term=reverse cterm=reverse
+:hi Folded term=standout cterm=none ctermbg=0 ctermfg=7
+:hi FoldColumn term=standout ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=4
+:hi Constant term=underline ctermfg=1
+:hi Special term=bold ctermfg=3
+:hi Identifier term=underline cterm=none ctermfg=6
+:hi Statement term=bold cterm=none ctermfg=2
+":hi Operator term=bold ctermfg=1
+:hi PreProc term=underline ctermfg=1
+:hi Type term=underline cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo term=standout ctermbg=3 ctermfg=0
+:hi Underlined term=underline cterm=underline
+":hi Include ctermfg=1
+":hi Define ctermfg=3
+":hi Function ctermfg=6
+":hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn cterm=bold
+:hi SpellBad term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg term=standout ctermfg=1
+:hi ErrorMsg term=standout cterm=bold ctermbg=1 ctermfg=7
+:hi Error term=reverse cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def-ruby.vim b/sundry/misc/editor-syntax-etc/vim/colors/def-ruby.vim
new file mode 100644
index 0000000..6578593
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def-ruby.vim
@@ -0,0 +1,223 @@
+" Vim color file
+" Name: def-ruby
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def-ruby.vim;hb=HEAD>
+" Note: primarily 16 color cterm improved by tweaking of .Xdefaults
+" (with occasional other colors selected from 256 color palate)
+" .Xdefaults tweaking to make identical to def (256) provided,
+" along with an alternative possibility using colors beyond
+" 256 color palate
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def-ruby"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=5 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title ctermfg=1
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=8
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=4
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier cterm=none ctermfg=6
+:hi Statement cterm=none ctermfg=2
+:hi Operator ctermfg=1
+:hi PreProc ctermfg=1
+:hi Type cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=5
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=5
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=5
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=5
+:hi TrailingWhitespace ctermbg=1
+:hi ExtraWhitespace ctermbg=1
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#D3D3D3
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#5971AD
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#4E9A06
+:hi ModeMsg gui=bold
+:hi Question guifg=#4E9A06
+:hi Title guifg=#CC0000
+:hi NonText gui=bold guifg=#5971AD
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#5971AD
+:hi WildMenu guibg=#C4A000 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#D3D3D3 guifg=#5971AD
+:hi DiffAdd gui=none guibg=#4E9A06 guifg=#000000
+:hi DiffChange gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffDelete gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffText gui=none guibg=#06989A guifg=#000000
+:hi String gui=none guifg=#C4A000
+:hi Comment gui=none guibg=#000000 guifg=#5971AD
+:hi Constant guifg=#CC0000
+:hi Special guifg=#06989A
+:hi Identifier gui=none guifg=#06989A
+:hi Statement gui=none guifg=#4E9A06
+:hi Operator guifg=#CC0000
+:hi PreProc guifg=#CC0000
+:hi Type gui=bold guifg=#C4A000
+:hi Delimiter gui=none guifg=#4E9A06
+:hi Ignore gui=bold guifg=#D3D3D3
+:hi Todo guibg=#C4A000 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#C4A000
+:hi Function guifg=#06989A
+:hi Structure guifg=#4E9A06
+:hi MatchParen gui=bold guibg=#5971AD guifg=#D3D3D3
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#75507B
+:hi SpellCap gui=underline guibg=#000000 guifg=#75507B
+:hi SpellLocal gui=underline guibg=#000000 guifg=#75507B
+:hi SpellRare gui=underline guibg=#000000 guifg=#75507B
+:hi TrailingWhitespace guibg=#CC0000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#D3D3D3
+:hi Error gui=bold guibg=#CC0000 guifg=#D3D3D3
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<http://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults (rxvt urxvt setting beyond 256 colors, vim colorscheme "def" gui settings)
+" (vim colorscheme "def" cterm matches "def" gui if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #555555
+" ! red
+" Rxvt.color1 : #CC0000
+" Rxvt.color9 : #EF2929
+" ! green
+" Rxvt.color2 : #4E9A06
+" Rxvt.color10 : #8AE234
+" ! yellow
+" Rxvt.color3 : #C4A000
+" Rxvt.color11 : #FCE94F
+" ! blue
+" Rxvt.color4 : #5971AD
+" Rxvt.color12 : #729FCF
+" ! magenta
+" Rxvt.color5 : #75507B
+" Rxvt.color13 : #AD7FA8
+" ! cyan
+" Rxvt.color6 : #06989A
+" Rxvt.color14 : #34E2E2
+" ! white
+" Rxvt.color7 : #D3D7CF
+" Rxvt.color15 : #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #808080
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def-sisu.vim b/sundry/misc/editor-syntax-etc/vim/colors/def-sisu.vim
new file mode 100644
index 0000000..ac3e0c3
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def-sisu.vim
@@ -0,0 +1,223 @@
+" Vim color file
+" Name: def-sisu
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <https://git.sisudoc.org/projects/?p=software/spine.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def-sisu.vim;hb=HEAD>
+" Note: primarily 16 color cterm improved by tweaking of .Xdefaults
+" (with occasional other colors selected from 256 color palate)
+" .Xdefaults tweaking to make identical to def (256) provided,
+" along with an alternative possibility using colors beyond
+" 256 color palate
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def-sisu"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=5 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title cterm=bold ctermfg=3
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=8
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=4
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier ctermfg=6
+:hi Statement ctermfg=6
+:hi Operator ctermfg=1
+:hi PreProc ctermbg=7 ctermfg=1
+:hi Type ctermfg=2
+:hi Delimiter cterm=none ctermfg=1
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=5
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=5
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=5
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=5
+:hi TrailingWhitespace ctermbg=1
+:hi ExtraWhitespace ctermbg=1
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#D3D3D3
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=bold,reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#5971AD
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#4E9A06
+:hi ModeMsg gui=bold
+:hi Question guifg=#4E9A06
+:hi Title gui=bold guifg=#C4A000
+:hi NonText gui=bold guifg=#5971AD
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#5971AD
+:hi WildMenu guibg=#C4A000 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#D3D3D3 guifg=#5971AD
+:hi DiffAdd gui=none guibg=#4E9A06 guifg=#000000
+:hi DiffChange gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffDelete gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffText gui=none guibg=#06989A guifg=#000000
+:hi String gui=none guifg=#C4A000
+:hi Comment gui=none guibg=#000000 guifg=#5971AD
+:hi Constant guifg=#CC0000
+:hi Special guifg=#06989A
+:hi Identifier guifg=#06989A
+:hi Statement guifg=#06989A
+:hi Operator guifg=#CC0000
+:hi PreProc guibg=#D3D3D3 guifg=#CC0000
+:hi Type guifg=#4E9A06
+:hi Delimiter gui=none guifg=#CC0000
+:hi Ignore gui=bold guifg=#D3D3D3
+:hi Todo guibg=#C4A000 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#C4A000
+:hi Function guifg=#06989A
+:hi Structure guifg=#4E9A06
+:hi MatchParen gui=bold guibg=#5971AD guifg=#D3D3D3
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#75507B
+:hi SpellCap gui=underline guibg=#000000 guifg=#75507B
+:hi SpellLocal gui=underline guibg=#000000 guifg=#75507B
+:hi SpellRare gui=underline guibg=#000000 guifg=#75507B
+:hi TrailingWhitespace guibg=#080000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#D3D3D3
+:hi Error gui=bold guibg=#CC0000 guifg=#D3D3D3
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<https://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<https://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults (rxvt urxvt setting beyond 256 colors, vim colorscheme "def" gui settings)
+" (vim colorscheme "def" cterm matches "def" gui if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #555555
+" ! red
+" Rxvt.color1 : #CC0000
+" Rxvt.color9 : #EF2929
+" ! green
+" Rxvt.color2 : #4E9A06
+" Rxvt.color10 : #8AE234
+" ! yellow
+" Rxvt.color3 : #C4A000
+" Rxvt.color11 : #FCE94F
+" ! blue
+" Rxvt.color4 : #5971AD
+" Rxvt.color12 : #729FCF
+" ! magenta
+" Rxvt.color5 : #75507B
+" Rxvt.color13 : #AD7FA8
+" ! cyan
+" Rxvt.color6 : #06989A
+" Rxvt.color14 : #34E2E2
+" ! white
+" Rxvt.color7 : #D3D7CF
+" Rxvt.color15 : #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #808080
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def.vim b/sundry/misc/editor-syntax-etc/vim/colors/def.vim
new file mode 100644
index 0000000..aca1975
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def.vim
@@ -0,0 +1,223 @@
+" Vim color file
+" Name: def
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <https://git.sisudoc.org/projects/?p=software/spine.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def.vim;hb=HEAD>
+" Note: primarily 16 color cterm improved by tweaking of .Xdefaults
+" (with occasional other colors selected from 256 color palate)
+" .Xdefaults tweaking to make identical to def (256) provided,
+" along with an alternative possibility using colors beyond
+" 256 color palate
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=5 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title cterm=bold ctermfg=3
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=8
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=4
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier ctermfg=6
+:hi Statement ctermfg=2
+:hi Operator ctermfg=2
+:hi PreProc ctermfg=1
+:hi Type cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=5
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=5
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=5
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=5
+:hi TrailingWhitespace ctermbg=1
+:hi ExtraWhitespace ctermbg=1
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#D3D3D3
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=bold,reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#5971AD
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#4E9A06
+:hi ModeMsg gui=bold
+:hi Question guifg=#4E9A06
+:hi Title gui=bold guifg=#C4A000
+:hi NonText gui=bold guifg=#5971AD
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#5971AD
+:hi WildMenu guibg=#C4A000 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#D3D3D3 guifg=#5971AD
+:hi DiffAdd gui=none guibg=#4E9A06 guifg=#000000
+:hi DiffChange gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffDelete gui=none guibg=#D3D3D3 guifg=#000000
+:hi DiffText gui=none guibg=#06989A guifg=#000000
+:hi String gui=none guifg=#C4A000
+:hi Comment gui=none guibg=#000000 guifg=#5971AD
+:hi Constant guifg=#CC0000
+:hi Special guifg=#06989A
+:hi Identifier guifg=#06989A
+:hi Statement guifg=#4E9A06
+:hi Operator guifg=#4E9A06
+:hi PreProc guifg=#CC0000
+:hi Type gui=bold guifg=#C4A000
+:hi Delimiter gui=none guifg=#4E9A06
+:hi Ignore gui=bold guifg=#D3D3D3
+:hi Todo guibg=#C4A000 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#C4A000
+:hi Function guifg=#06989A
+:hi Structure guifg=#4E9A06
+:hi MatchParen gui=bold guibg=#5971AD guifg=#D3D3D3
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#75507B
+:hi SpellCap gui=underline guibg=#000000 guifg=#75507B
+:hi SpellLocal gui=underline guibg=#000000 guifg=#75507B
+:hi SpellRare gui=underline guibg=#000000 guifg=#75507B
+:hi TrailingWhitespace guibg=#080000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#D3D3D3
+:hi Error gui=bold guibg=#CC0000 guifg=#D3D3D3
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<https://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<https://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults (rxvt urxvt setting beyond 256 colors, vim colorscheme "def" gui settings)
+" (vim colorscheme "def" cterm matches "def" gui if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #555555
+" ! red
+" Rxvt.color1 : #CC0000
+" Rxvt.color9 : #EF2929
+" ! green
+" Rxvt.color2 : #4E9A06
+" Rxvt.color10 : #8AE234
+" ! yellow
+" Rxvt.color3 : #C4A000
+" Rxvt.color11 : #FCE94F
+" ! blue
+" Rxvt.color4 : #5971AD
+" Rxvt.color12 : #729FCF
+" ! magenta
+" Rxvt.color5 : #75507B
+" Rxvt.color13 : #AD7FA8
+" ! cyan
+" Rxvt.color6 : #06989A
+" Rxvt.color14 : #34E2E2
+" ! white
+" Rxvt.color7 : #D3D7CF
+" Rxvt.color15 : #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #808080
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def256-ruby.vim b/sundry/misc/editor-syntax-etc/vim/colors/def256-ruby.vim
new file mode 100644
index 0000000..eb15db0
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def256-ruby.vim
@@ -0,0 +1,195 @@
+" Vim color file
+" Name: def256-ruby
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def256-ruby.vim;hb=HEAD>
+" Note: based on colorscheme defx, this scheme has 256 colors
+" selected primarily from 16 colors around the colors available
+" for 16 color cterm (with occasional other colors selected
+" from 256 color palate) .Xdefaults settings provided to make
+" term 16 colors match this, using 256 color palate.
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def256-ruby"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=5 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=24
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=112
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=112
+:hi Title ctermfg=160
+:hi NonText cterm=bold ctermfg=24
+:hi LineNr cterm=bold ctermbg=0 ctermfg=8
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=24
+:hi WildMenu ctermbg=178 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=59
+:hi FoldColumn ctermbg=7 ctermfg=24
+:hi DiffAdd cterm=none ctermbg=112 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=37 ctermfg=0
+:hi String cterm=none ctermfg=178
+:hi Comment cterm=none ctermbg=0 ctermfg=24
+:hi Constant ctermfg=160
+:hi Special ctermfg=37
+:hi Identifier cterm=none ctermfg=37
+:hi Statement cterm=none ctermfg=112
+:hi Operator ctermfg=160
+:hi PreProc ctermfg=160
+:hi Type cterm=bold ctermfg=178
+:hi Delimiter cterm=none ctermfg=112
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=178 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=160
+:hi Define ctermfg=178
+:hi Function ctermfg=37
+:hi Structure ctermfg=112
+:hi MatchParen cterm=bold ctermbg=24 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=90
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=90
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=90
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=90
+:hi TrailingWhitespace ctermbg=160
+:hi ExtraWhitespace ctermbg=160
+:hi WarningMsg ctermfg=160
+:hi ErrorMsg cterm=bold ctermbg=160 ctermfg=7
+:hi Error cterm=bold ctermbg=160 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#C0C0C0
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#005F87
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#87DF00
+:hi ModeMsg gui=bold
+:hi Question guifg=#87DF00
+:hi Title guifg=#CC0000
+:hi NonText gui=bold guifg=#005F87
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#005F87
+:hi WildMenu guibg=#DFAF00 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#C0C0C0 guifg=#005F87
+:hi DiffAdd gui=none guibg=#87DF00 guifg=#000000
+:hi DiffChange gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffDelete gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffText gui=none guibg=#00AFAF guifg=#000000
+:hi String gui=none guifg=#DFAF00
+:hi Comment gui=none guibg=#000000 guifg=#005F87
+:hi Constant guifg=#CC0000
+:hi Special guifg=#06989A
+:hi Identifier gui=none guifg=#06989A
+:hi Statement gui=none guifg=#87DF00
+:hi Operator guifg=#CC0000
+:hi PreProc guifg=#CC0000
+:hi Type gui=bold guifg=#DFAF00
+:hi Delimiter gui=none guifg=#87DF00
+:hi Ignore gui=bold guifg=#C0C0C0
+:hi Todo guibg=#DFAF00 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#DFAF00
+:hi Function guifg=#00AFAF
+:hi Structure guifg=#87DF00
+:hi MatchParen gui=bold guibg=#005F87 guifg=#C0C0C0
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#870087
+:hi SpellCap gui=underline guibg=#000000 guifg=#870087
+:hi SpellLocal gui=underline guibg=#000000 guifg=#870087
+:hi SpellRare gui=underline guibg=#000000 guifg=#870087
+:hi TrailingWhitespace guibg=#CC0000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#C0C0C0
+:hi Error gui=bold guibg=#CC0000 guifg=#C0C0C0
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<http://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #5F5F5F
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def256-sisu.vim b/sundry/misc/editor-syntax-etc/vim/colors/def256-sisu.vim
new file mode 100644
index 0000000..a7c3def
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def256-sisu.vim
@@ -0,0 +1,195 @@
+" Vim color file
+" Name: def256-sisu
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def256-sisu.vim;hb=HEAD>
+" Note: based on colorscheme defx, this scheme has 256 colors
+" selected primarily from 16 colors around the colors available
+" for 16 color cterm (with occasional other colors selected
+" from 256 color palate) .Xdefaults settings provided to make
+" term 16 colors match this, using 256 color palate.
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def256-sisu"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=90 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=24
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=112
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=112
+:hi Title cterm=bold ctermfg=178
+:hi NonText cterm=bold ctermfg=24
+:hi LineNr cterm=bold ctermbg=0 ctermfg=8
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=24
+:hi WildMenu ctermbg=178 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=8
+:hi FoldColumn ctermbg=7 ctermfg=24
+:hi DiffAdd cterm=none ctermbg=112 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=37 ctermfg=0
+:hi String cterm=none ctermfg=178
+:hi Comment cterm=none ctermbg=0 ctermfg=24
+:hi Constant ctermfg=160
+:hi Special ctermfg=37
+:hi Identifier ctermfg=37
+:hi Statement ctermfg=37
+:hi Operator ctermfg=160
+:hi PreProc ctermbg=7 ctermfg=160
+:hi Type ctermfg=112
+:hi Delimiter cterm=none ctermfg=160
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=178 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=160
+:hi Define ctermfg=178
+:hi Function ctermfg=37
+:hi Structure ctermfg=112
+:hi MatchParen cterm=bold ctermbg=24 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=90
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=90
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=90
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=90
+:hi TrailingWhitespace ctermbg=160
+:hi ExtraWhitespace ctermbg=160
+:hi WarningMsg ctermfg=160
+:hi ErrorMsg cterm=bold ctermbg=160 ctermfg=7
+:hi Error cterm=bold ctermbg=160 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#C0C0C0
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=bold,reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#005F87
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#87DF00
+:hi ModeMsg gui=bold
+:hi Question guifg=#87DF00
+:hi Title gui=bold guifg=#DFAF00
+:hi NonText gui=bold guifg=#005F87
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#005F87
+:hi WildMenu guibg=#DFAF00 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#C0C0C0 guifg=#005F87
+:hi DiffAdd gui=none guibg=#87DF00 guifg=#000000
+:hi DiffChange gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffDelete gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffText gui=none guibg=#00AFAF guifg=#000000
+:hi String gui=none guifg=#DFAF00
+:hi Comment gui=none guibg=#000000 guifg=#005F87
+:hi Constant guifg=#CC0000
+:hi Special guifg=#00AFAF
+:hi Identifier guifg=#00AFAF
+:hi Statement guifg=#00AFAF
+:hi Operator guifg=#CC0000
+:hi PreProc guibg=#C0C0C0 guifg=#CC0000
+:hi Type guifg=#87DF00
+:hi Delimiter gui=none guifg=#CC0000
+:hi Ignore gui=bold guifg=#C0C0C0
+:hi Todo guibg=#DFAF00 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#DFAF00
+:hi Function guifg=#00AFAF
+:hi Structure guifg=#87DF00
+:hi MatchParen gui=bold guibg=#005F87 guifg=#C0C0C0
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#870087
+:hi SpellCap gui=underline guibg=#000000 guifg=#870087
+:hi SpellLocal gui=underline guibg=#000000 guifg=#870087
+:hi SpellRare gui=underline guibg=#000000 guifg=#870087
+:hi TrailingWhitespace guibg=#CC0000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#C0C0C0
+:hi Error gui=bold guibg=#CC0000 guifg=#C0C0C0
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<http://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #5F5F5F
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/def256.vim b/sundry/misc/editor-syntax-etc/vim/colors/def256.vim
new file mode 100644
index 0000000..06faefa
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/def256.vim
@@ -0,0 +1,195 @@
+" Vim color file
+" Name: def256
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-14
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/def256.vim;hb=HEAD>
+" Note: based on colorscheme defx, this scheme has 256 colors
+" selected primarily from 16 colors around the colors available
+" for 16 color cterm (with occasional other colors selected
+" from 256 color palate) .Xdefaults settings provided to make
+" term 16 colors match this, using 256 color palate.
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:set t_Co=256
+:set background=dark
+:let colors_name = "def256"
+" -------
+" terminal def
+" -------
+:hi Normal ctermbg=0 ctermfg=7
+":hi Cursor ctermbg=90 ctermfg=0
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=57 ctermfg=0
+:hi IncSearch cterm=none ctermbg=154 ctermfg=0
+:hi SpecialKey ctermfg=24
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=112
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=112
+:hi Title cterm=bold ctermfg=178
+:hi NonText cterm=bold ctermfg=24
+:hi LineNr cterm=bold ctermbg=0 ctermfg=8
+:hi CursorLineNr cterm=bold ctermbg=166 ctermfg=0
+:hi Directory ctermfg=24
+:hi WildMenu ctermbg=178 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=8
+:hi FoldColumn ctermbg=7 ctermfg=24
+:hi DiffAdd cterm=none ctermbg=112 ctermfg=0
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=37 ctermfg=0
+:hi String cterm=none ctermfg=178
+:hi Comment cterm=none ctermbg=0 ctermfg=24
+:hi Constant ctermfg=160
+:hi Special ctermfg=37
+:hi Identifier ctermfg=37
+:hi Statement ctermfg=112
+:hi Operator ctermfg=112
+:hi PreProc ctermfg=160
+:hi Type cterm=bold ctermfg=178
+:hi Delimiter cterm=none ctermfg=112
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=178 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=160
+:hi Define ctermfg=178
+:hi Function ctermfg=37
+:hi Structure ctermfg=112
+:hi MatchParen cterm=bold ctermbg=24 ctermfg=7
+:hi CursorLine cterm=bold
+:hi CursorColumn ctermbg=17
+:hi ColorColumn ctermbg=17
+:hi SpellBad cterm=underline ctermbg=0 ctermfg=90
+:hi SpellCap cterm=underline ctermbg=0 ctermfg=90
+:hi SpellLocal cterm=underline ctermbg=0 ctermfg=90
+:hi SpellRare cterm=underline ctermbg=0 ctermfg=90
+:hi TrailingWhitespace ctermbg=160
+:hi ExtraWhitespace ctermbg=160
+:hi WarningMsg ctermfg=160
+:hi ErrorMsg cterm=bold ctermbg=160 ctermfg=7
+:hi Error cterm=bold ctermbg=160 ctermfg=7
+" -------
+" gui def
+" -------
+:hi Normal guibg=#000000 guifg=#C0C0C0
+:hi Cursor guibg=#CC9966 guifg=#000000
+:hi lCursor gui=reverse
+:hi StatusLine gui=bold,reverse
+:hi StatusLineNC gui=reverse
+:hi Search gui=none guibg=#5F00FF guifg=#000000
+:hi IncSearch gui=none guibg=#AFFF00 guifg=#000000
+:hi SpecialKey guifg=#005F87
+:hi Visual gui=reverse
+:hi VisualNOS gui=bold,underline
+:hi MoreMsg guifg=#87DF00
+:hi ModeMsg gui=bold
+:hi Question guifg=#87DF00
+:hi Title gui=bold guifg=#DFAF00
+:hi NonText gui=bold guifg=#005F87
+:hi LineNr gui=bold guibg=#000000 guifg=#808080
+:hi CursorLineNr gui=bold guibg=#D75F00 guifg=#000000
+:hi Directory guifg=#005F87
+:hi WildMenu guibg=#DFAF00 guifg=#000000
+:hi VertSplit gui=reverse
+:hi Folded gui=none guibg=#000000 guifg=#808080
+:hi FoldColumn guibg=#C0C0C0 guifg=#005F87
+:hi DiffAdd gui=none guibg=#87DF00 guifg=#000000
+:hi DiffChange gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffDelete gui=none guibg=#C0C0C0 guifg=#000000
+:hi DiffText gui=none guibg=#00AFAF guifg=#000000
+:hi String gui=none guifg=#DFAF00
+:hi Comment gui=none guibg=#000000 guifg=#005F87
+:hi Constant guifg=#CC0000
+:hi Special guifg=#00AFAF
+:hi Identifier guifg=#00AFAF
+:hi Statement guifg=#87DF00
+:hi Operator guifg=#87DF00
+:hi PreProc guifg=#CC0000
+:hi Type gui=bold guifg=#DFAF00
+:hi Delimiter gui=none guifg=#87DF00
+:hi Ignore gui=bold guifg=#C0C0C0
+:hi Todo guibg=#DFAF00 guifg=#000000
+:hi Underlined gui=underline
+:hi Include guifg=#CC0000
+:hi Define guifg=#DFAF00
+:hi Function guifg=#00AFAF
+:hi Structure guifg=#87DF00
+:hi MatchParen gui=bold guibg=#005F87 guifg=#C0C0C0
+:hi CursorLine gui=bold
+:hi CursorColumn guibg=#00005F
+:hi ColorColumn guibg=#00005F
+:hi SpellBad gui=underline guibg=#000000 guifg=#870087
+:hi SpellCap gui=underline guibg=#000000 guifg=#870087
+:hi SpellLocal gui=underline guibg=#000000 guifg=#870087
+:hi SpellRare gui=underline guibg=#000000 guifg=#870087
+:hi TrailingWhitespace guibg=#CC0000
+:hi ExtraWhitespace guibg=#CC0000
+:hi WarningMsg guifg=#CC0000
+:hi ErrorMsg gui=bold guibg=#CC0000 guifg=#C0C0C0
+:hi Error gui=bold guibg=#CC0000 guifg=#C0C0C0
+" -------
+"256 color .Xdefaults vim: cterm giu
+"<http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim>
+"<http://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png>
+" -------
+" 256 color .Xdefaults vim: cterm giu def
+" -------
+" 16 color standard altered 256 altered beyond 256
+" black/dark grey
+" 0 [ 0:#000000] #000000
+" 8 [ 8:#808080] [59:#5F5F5F] #555555
+" red
+" 1 [ 1:#800000] 160:#DF0000 #CC0000
+" 9 [ 9:#FF0000] #EF2929
+" green
+" 2 [ 2:#008000] 112:#87DF00 #4E9A06
+" 10 [10:#00FF00] 154:#AFFF00 #8AE234
+" yellow/orange
+" 3 [ 3:#808000] 178:#DFAF00 #C4A000
+" 11 [11:#FFFF00] 184:#DFDF00 #FC9E4F
+" blue
+" 4 [ 4:#000080] 24:#005F87 #5971AD
+" 12 [12:#0000FF] 73:#5FAFAF #729FCF
+" magenta
+" 5 [ 5:#800080] 90:#870087 #75507B
+" 13 [13:#FF00FF] 126:#AF0087 #AD7FA8
+" cyan
+" 6 [ 6:#008080] 37:#00AFAF #06989A
+" 14 [14:#00FFFF] 87:#5FFFFF #34E2E2
+" white
+" 7 [ 7:#C0C0C0] #D3D3D3
+" 15 [15:#FFFFFF] #EEEEEE
+" --------
+" .Xdefaults 256 (rxvt urxvt setting, vim colorscheme "def256" cterm & gui settings)
+" (vim colorscheme "def" cterm matches "def256" if .Xdefaults set thus)
+" --------
+" ! black
+" Rxvt.color0 : #000000
+" Rxvt.color8 : #5F5F5F
+" ! red
+" Rxvt.color1 : #DF0000
+" Rxvt.color9 : #FF0000
+" ! green
+" Rxvt.color2 : #87DF00
+" Rxvt.color10 : #AFFF00
+" ! yellow
+" Rxvt.color3 : #DFAF00
+" Rxvt.color11 : #FFFF00
+" ! blue
+" Rxvt.color4 : #5F87DF
+" Rxvt.color12 : #87DFFF
+" ! magenta
+" Rxvt.color5 : #8700DF
+" Rxvt.color13 : #87DFFF
+" ! cyan
+" Rxvt.color6 : #00DFDF
+" Rxvt.color14 : #5F5FDF
+" ! white
+" Rxvt.color7 : #C0C0C0
+" Rxvt.color15 : #FFFFFF
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-ruby.vim b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-ruby.vim
new file mode 100644
index 0000000..46938cf
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-ruby.vim
@@ -0,0 +1,66 @@
+" Vim color file
+" Name: frugal-cterm-ruby
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/frugal-cterm-ruby.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & sparse
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "frugal-cterm-ruby"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor cterm=reverse
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch cterm=bold ctermbg=2 ctermfg=7
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title ctermfg=1
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=7
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=bold ctermbg=0 ctermfg=4
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier cterm=none ctermfg=6
+:hi Statement cterm=none ctermfg=2
+":hi Operator ctermfg=1
+:hi PreProc ctermfg=1
+:hi Type cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+":hi Include ctermfg=1
+":hi Define ctermfg=3
+":hi Function ctermfg=6
+":hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-sisu.vim b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-sisu.vim
new file mode 100644
index 0000000..aae822d
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm-sisu.vim
@@ -0,0 +1,66 @@
+" Vim color file
+" Name: frugal-cterm-sisu
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <https://git.sisudoc.org/projects/?p=software/spine.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/frugal-cterm-sisu.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & sparse
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "frugal-cterm-sisu"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor cterm=reverse
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch cterm=bold ctermbg=2 ctermfg=7
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title cterm=bold ctermfg=3
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=7
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=7
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier ctermfg=6
+:hi Statement ctermfg=6
+:hi Operator ctermfg=1
+:hi PreProc ctermbg=7 ctermfg=1
+:hi Type ctermfg=2
+:hi Delimiter cterm=none ctermfg=1
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm.vim b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm.vim
new file mode 100644
index 0000000..9751538
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/frugal-cterm.vim
@@ -0,0 +1,69 @@
+" Vim color file
+" Name: frugal-cterm
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/frugal-cterm.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & sparse
+" http://vim.wikia.com/wiki/Xterm256_color_names_for_console_Vim
+" http://guns.github.com/xterm-color-table.vim/images/xterm-color-table-with-visible-rgb.png
+:set t_Co=256
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "frugal-cterm"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor cterm=reverse
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch cterm=bold ctermbg=2 ctermfg=0
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title cterm=bold ctermfg=3
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=none ctermbg=0 ctermfg=7
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=none ctermbg=0 ctermfg=7
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier ctermfg=6
+:hi Statement ctermfg=2
+:hi Operator ctermfg=2
+:hi PreProc ctermfg=1
+:hi Type cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/frugal.vim b/sundry/misc/editor-syntax-etc/vim/colors/frugal.vim
new file mode 100644
index 0000000..ee2f5ea
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/frugal.vim
@@ -0,0 +1,66 @@
+" Vim color file
+" Name: frugal
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/frugal.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & sparse
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "frugal"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7 guibg=black guifg=white
+:hi Cursor term=reverse cterm=reverse gui=bold guibg=white guifg=black
+:hi lCursor term=reverse cterm=reverse gui=bold guibg=white guifg=black
+:hi StatusLine term=bold,reverse cterm=bold,reverse gui=bold,reverse
+:hi StatusLineNC term=reverse cterm=reverse gui=reverse
+:hi Search term=reverse ctermbg=2 guifg=green
+:hi IncSearch term=reverse cterm=reverse gui=reverse
+:hi SpecialKey term=bold ctermfg=4 guifg=blue
+:hi Visual term=reverse cterm=reverse gui=reverse
+:hi VisualNOS term=bold,underline cterm=bold,underline gui=bold,underline
+:hi MoreMsg term=bold ctermfg=2 guifg=green
+:hi ModeMsg term=bold cterm=bold
+:hi Question term=standout ctermfg=2 guifg=green
+:hi Title term=bold ctermfg=1 guifg=red
+:hi NonText term=bold cterm=bold ctermfg=4 guifg=blue
+:hi LineNr term=underline cterm=bold ctermbg=0 ctermfg=0* gui=bold guibg=black guifg=grey
+:hi Directory term=bold ctermfg=4 guifg=blue
+:hi WildMenu term=standout ctermbg=3 ctermfg=0 guibg=yellow guifg=black
+:hi VertSplit term=reverse cterm=reverse gui=reverse
+:hi Folded term=standout cterm=none ctermbg=0 ctermfg=7 gui=none guibg=black guifg=white
+:hi FoldColumn term=standout ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0 gui=none guibg=white guifg=black
+:hi DiffText cterm=none ctermbg=6 ctermfg=0 gui=none guibg=magenta guifg=black
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0 gui=none guibg=green guifg=black
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0 gui=none guibg=white guifg=black
+:hi String cterm=none ctermfg=3 gui=none guifg=yellow
+:hi Comment cterm=none ctermbg=0 ctermfg=4 gui=bold guibg=black guifg=grey
+:hi Constant term=underline ctermfg=1 guifg=red
+:hi Special term=bold ctermfg=3 guifg=yellow
+:hi Identifier term=underline cterm=none ctermfg=6 gui=none guifg=magenta
+:hi Statement term=bold cterm=none ctermfg=2 gui=none guifg=green
+":hi Operator term=bold ctermfg=1 guifg=red
+:hi PreProc term=underline ctermfg=1 guifg=red
+:hi Type term=underline cterm=bold ctermfg=3 gui=bold guifg=yellow
+:hi Delimiter cterm=none ctermfg=2 gui=none guifg=green
+:hi Ignore cterm=bold ctermfg=7 gui=bold guifg=white
+:hi Todo term=standout ctermbg=3 ctermfg=0 guibg=yellow guifg=black
+:hi Underlined term=underline cterm=underline gui=underline
+":hi Include ctermfg=1 guifg=red
+":hi Define ctermfg=3 guifg=yellow
+":hi Function ctermfg=6 guifg=magenta
+":hi Structure ctermfg=2 guifg=green
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=white guifg=black
+:hi SpellCap term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=white guifg=black
+:hi SpellLocal term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=white guifg=black
+:hi SpellRare term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=white guifg=black
+:hi WarningMsg term=standout ctermfg=1 guifg=red
+:hi ErrorMsg term=standout cterm=bold ctermbg=1 ctermfg=7 gui=bold guibg=red guifg=white
+:hi Error term=reverse cterm=bold ctermbg=1 ctermfg=7 gui=bold guibg=red guifg=white
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/slate.vim b/sundry/misc/editor-syntax-etc/vim/colors/slate.vim
new file mode 100644
index 0000000..4d741e8
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/slate.vim
@@ -0,0 +1,75 @@
+"%% SiSU Vim color file
+" Name: Slate
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <https://git.sisudoc.org/projects/?p=software/spine.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/slate.vim;hb=HEAD>
+" Notes: cterm now uses frugal-sisu 8 colors for term
+" (for gui originally looked at desert Hans Fugal <hans@fugal.net>
+" <https://hans.fugal.net/vim/colors/desert.vim> (April/May 2003))
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "slate"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7 guibg=grey15 guifg=white
+:hi Cursor term=reverse cterm=reverse guibg=khaki guifg=slategrey
+:hi lCursor term=reverse cterm=reverse
+:hi StatusLine term=reverse cterm=bold,reverse gui=none guibg=#c2bfa5 guifg=black
+:hi StatusLineNC term=reverse cterm=reverse gui=none guibg=#c2bfa5 guifg=grey40
+:hi Search term=reverse cterm=none ctermbg=2 ctermfg=0 guibg=peru guifg=wheat
+:hi IncSearch term=reverse cterm=bold ctermbg=2 ctermfg=7 guibg=black guifg=green
+:hi SpecialKey term=bold ctermfg=4 guifg=yellowgreen
+:hi Visual term=reverse cterm=reverse gui=none guibg=olivedrab guifg=khaki
+:hi VisualNOS term=bold,underline cterm=bold,underline
+:hi MoreMsg term=bold ctermfg=2 guifg=SeaGreen
+:hi ModeMsg term=bold cterm=bold guifg=goldenrod
+:hi Question term=standout ctermfg=2 guifg=springgreen
+:hi Title term=bold cterm=bold ctermfg=3 gui=bold guifg=gold
+:hi NonText term=bold cterm=bold ctermfg=4 guibg=grey15 guifg=RoyalBlue
+:hi LineNr term=underline cterm=bold ctermbg=0 ctermfg=0* guifg=grey50
+:hi Directory term=bold ctermfg=4
+:hi WildMenu term=standout ctermbg=3 ctermfg=0 guibg=darkyellow guifg=black
+:hi VertSplit term=reverse cterm=reverse gui=none guibg=#c2bfa5 guifg=grey40
+:hi Folded term=standout cterm=none ctermbg=0 ctermfg=7 guibg=black guifg=grey40
+:hi FoldColumn term=standout ctermbg=7 ctermfg=4 guibg=black guifg=grey20
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0 guibg=darkgrey guifg=white
+:hi DiffText cterm=none ctermbg=6 ctermfg=0 guibg=darkcyan guifg=white
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0 guibg=darkgreen guifg=white
+:hi DiffDelete cterm=none ctermbg=7 ctermfg=0 guibg=darkgrey guifg=black
+:hi String cterm=none ctermfg=3 guifg=SkyBlue
+:hi Comment term=bold cterm=none ctermbg=0 ctermfg=7 guifg=grey40
+:hi Constant term=underline ctermfg=1 guifg=#ffa0a0
+:hi Special term=bold ctermfg=6 guifg=darkkhaki
+:hi Identifier term=underline ctermfg=6 guifg=salmon
+:hi Statement term=bold ctermfg=6 guifg=CornflowerBlue
+:hi Operator term=bold ctermfg=1 guifg=red
+:hi PreProc term=underline ctermbg=7 ctermfg=1 guibg=white guifg=red
+:hi Type term=underline ctermfg=2 guifg=CornflowerBlue
+:hi Delimiter term=none cterm=none ctermfg=1
+:hi Ignore cterm=bold ctermfg=7 guifg=grey40
+:hi Todo term=standout ctermbg=3 ctermfg=0 guibg=yellow2 guifg=orangered
+:hi Underlined term=underline cterm=underline
+:hi Include ctermfg=1 guifg=red
+:hi Define ctermfg=3 gui=bold guifg=gold
+:hi Function ctermfg=6 guifg=navajowhite
+:hi Structure ctermfg=2 guifg=green
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline guibg=black
+:hi CursorColumn cterm=bold guibg=black
+:hi SpellBad term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=darkmagenta guifg=white
+:hi SpellCap term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal term=underline,standout cterm=none ctermbg=7 ctermfg=0 guibg=darkmagenta guifg=white
+:hi SpellRare term=underline,standout cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg term=standout ctermfg=1 guibg=darkmagenta guifg=salmon
+:hi ErrorMsg term=standout cterm=bold ctermbg=1 ctermfg=7 guibg=darkmagenta guifg=white
+:hi Error term=reverse cterm=bold ctermbg=1 ctermfg=7 guibg=darkmagenta guifg=white
+:hi Black ctermbg=grey ctermfg=black guibg=grey guifg=black
+:hi Red ctermbg=black ctermfg=red guibg=black guifg=red
+:hi Magenta ctermbg=black ctermfg=magenta guibg=black guifg=magenta
+:hi Blue ctermbg=black ctermfg=blue guibg=black guifg=blue
+:hi Cyan ctermbg=black ctermfg=cyan guibg=black guifg=cyan
+:hi Green ctermbg=black ctermfg=green guibg=black guifg=green
+:hi Yellow ctermbg=black ctermfg=yellow guibg=black guifg=yellow
+:hi White ctermbg=black ctermfg=white guibg=black guifg=white
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-ruby.vim b/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-ruby.vim
new file mode 100644
index 0000000..53619b5
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-ruby.vim
@@ -0,0 +1,66 @@
+" Vim color file
+" Name: sparse-cterm-ruby
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/sparse-cterm-ruby.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & frugal
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "frugal-cterm-ruby"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor cterm=reverse
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch cterm=bold ctermbg=2 ctermfg=7
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title ctermfg=1
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=bold ctermbg=0 ctermfg=0*
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=bold ctermbg=0 ctermfg=0*
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=bold ctermbg=0 ctermfg=0*
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier cterm=none ctermfg=6
+:hi Statement cterm=none ctermfg=2
+":hi Operator ctermfg=1
+:hi PreProc ctermfg=1
+:hi Type cterm=bold ctermfg=3
+:hi Delimiter cterm=none ctermfg=2
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+":hi Include ctermfg=1
+":hi Define ctermfg=3
+":hi Function ctermfg=6
+":hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-sisu.vim b/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-sisu.vim
new file mode 100644
index 0000000..1410a69
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/colors/sparse-cterm-sisu.vim
@@ -0,0 +1,66 @@
+" Vim color file
+" Name: sparse-cterm-sisu
+" Maintainer: Ralph Amissah <ralph@amissah.com>
+" Last Change: 2013-02-09
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/colors/sparse-cterm-sisu.vim;hb=HEAD>
+" Note: 8 color cterm, related colorschemes 8 & frugal
+:set background=dark
+:hi clear
+if exists("syntax_on")
+ syntax reset
+endif
+:let colors_name = "sparse-cterm-sisu"
+" 0 = black, 1 = red, 2 = green, 3 = yellow/orange, 4 = blue, 5 = magenta, 6 = cyan, 7 = white
+:hi Normal ctermbg=0 ctermfg=7
+:hi Cursor cterm=reverse
+:hi lCursor cterm=reverse
+:hi StatusLine cterm=bold,reverse
+:hi StatusLineNC cterm=reverse
+:hi Search cterm=none ctermbg=2 ctermfg=0
+:hi IncSearch cterm=bold ctermbg=2 ctermfg=7
+:hi SpecialKey ctermfg=4
+:hi Visual cterm=reverse
+:hi VisualNOS cterm=bold,underline
+:hi MoreMsg ctermfg=2
+:hi ModeMsg cterm=bold
+:hi Question ctermfg=2
+:hi Title cterm=bold ctermfg=3
+:hi NonText cterm=bold ctermfg=4
+:hi LineNr cterm=bold ctermbg=0 ctermfg=0*
+:hi Directory ctermfg=4
+:hi WildMenu ctermbg=3 ctermfg=0
+:hi VertSplit cterm=reverse
+:hi Folded cterm=bold ctermbg=0 ctermfg=0*
+:hi FoldColumn ctermbg=7 ctermfg=4
+:hi DiffChange cterm=none ctermbg=7 ctermfg=0
+:hi DiffText cterm=none ctermbg=6 ctermfg=0
+:hi DiffAdd cterm=none ctermbg=2 ctermfg=0
+:hi DiffDelete cterm=bold ctermbg=0 ctermfg=0*
+:hi String cterm=none ctermfg=3
+:hi Comment cterm=bold ctermbg=0 ctermfg=0*
+:hi Constant ctermfg=1
+:hi Special ctermfg=6
+:hi Identifier ctermfg=6
+:hi Statement ctermfg=6
+:hi Operator ctermfg=1
+:hi PreProc ctermbg=7 ctermfg=1
+:hi Type ctermfg=2
+:hi Delimiter cterm=none ctermfg=1
+:hi Ignore cterm=bold ctermfg=7
+:hi Todo ctermbg=3 ctermfg=0
+:hi Underlined cterm=underline
+:hi Include ctermfg=1
+:hi Define ctermfg=3
+:hi Function ctermfg=6
+:hi Structure ctermfg=2
+:hi MatchParen cterm=bold ctermbg=4 ctermfg=7
+:hi CursorLine cterm=bold,underline
+:hi CursorColumn cterm=bold
+:hi ColorColumn ctermbg=8
+:hi SpellBad cterm=none ctermbg=7 ctermfg=0
+:hi SpellCap cterm=none ctermbg=7 ctermfg=0
+:hi SpellLocal cterm=none ctermbg=7 ctermfg=0
+:hi SpellRare cterm=none ctermbg=7 ctermfg=0
+:hi WarningMsg ctermfg=1
+:hi ErrorMsg cterm=bold ctermbg=1 ctermfg=7
+:hi Error cterm=bold ctermbg=1 ctermfg=7
diff --git a/sundry/misc/editor-syntax-etc/vim/filetype.vim b/sundry/misc/editor-syntax-etc/vim/filetype.vim
new file mode 100644
index 0000000..917c3e0
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/filetype.vim
@@ -0,0 +1,8 @@
+" SiSU filetype file
+if exists("did_load_filetypes")
+ finish
+endif
+augroup filetypedetect
+ au! BufNewFile,BufRead *.sst,*.ssm,*.ssi,*.-sst setf sisu
+ au! BufNewFile,BufRead *._sst,*.sst.meta,*.-sst.meta,*._sst.meta setf sisu
+augroup END
diff --git a/sundry/misc/editor-syntax-etc/vim/ftplugin/sisu.vim b/sundry/misc/editor-syntax-etc/vim/ftplugin/sisu.vim
new file mode 100644
index 0000000..43c7e53
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/ftplugin/sisu.vim
@@ -0,0 +1,224 @@
+"%% SiSU Vim ftplugin
+" SiSU Maintainer: Ralph Amissah <ralph@amissah.com>
+" SiSU Markup: SiSU (sisu-3.3) 2012-08-18
+" an ftplugin setting defaults for editing sisu markup files
+:syntax on
+:filetype off
+":filetype on
+:filetype indent on
+:autocmd FileType sisu :set nonumber
+:set encoding=utf-8 fileencodings=
+:set ff=unix
+:set autowrite " Automatically save before commands like :next and :make
+:set nocompatible
+:set tabstop=2
+:set expandtab
+:set shiftwidth=2
+:set autoindent
+:set showcmd " Show (partial) command in status line.
+:set showmatch " Show matching brackets.
+:set ignorecase " Do case insensitive matching
+:set smartcase
+:set incsearch
+:set hlsearch
+:set gdefault
+:set guioptions=agr " add 'm' for menu
+:map <silent> <C-m> :if &guioptions =~# 'm' <Bar>
+ \set guioptions-=m <Bar>
+ \set guioptions-=T <Bar>
+ \else <Bar>
+ \set guioptions+=m <Bar>
+ \set guioptions-=T <Bar>
+ \endif<CR>
+:set paste
+""% statusline
+"set statusline= "
+"set fillchars=stl:―,stlnc:—,vert:│,fold:۰,diff:·
+"" [ buffer number ]
+"set statusline +=%#Normal#[ "
+"set statusline +=%#Identifier#%n " buffer number
+"set statusline +=%#PreProc#%M " modified flag
+"set statusline +=%#Normal#] "
+"" [ file name (& modified?) ]
+"set statusline +=%#Normal#\ [ "
+"set statusline +=%#Statement#%<%F%* " full path
+""set statusline +=%#Statement#%<%t " full path
+"set statusline +=%#PreProc#%M " modified flag
+"set statusline +=%#Normal#] "
+"" [ column : line number / number of lines in file, percentage of file ] [%v:%l/%L\ %p%%]
+"set statusline +=%#Normal#\ [ "
+"set statusline +=%#Identifier#%v " column & line
+"set statusline +=%#Normal#: "
+"set statusline +=%#Identifier#%l " column & line
+"set statusline +=%#SpecialKey#/%L\ " total lines
+"set statusline +=%#Identifier#%p " percentage of file
+"set statusline +=%#SpecialKey#%% "
+"set statusline +=%#Normal#] " "
+"" [ file format : file type ]
+"set statusline +=%#Normal#\ [ "
+"set statusline +=%#SpecialKey#%{&fenc} " file format
+"set statusline +=%#Normal#: "
+"set statusline +=%#SpecialKey#%{&ff} " file format
+"set statusline +=%#Normal#: "
+"set statusline +=%#SpecialKey#%y " file type
+"set statusline +=%#Normal#] "
+"" [ character under cursor ]
+"set statusline +=%#Normal#\ [ "
+"set statusline +=%#String#0x%04B " character under cursor
+"set statusline +=%#Normal#]\ "
+"" [ syntastic ]
+"set statusline +=%#warningmsg#
+"set statusline +=%{SyntasticStatuslineFlag()}\ "
+""set statusline+=%*
+"" Status line background
+"set statusline +=%#Folded#\ "
+"" misc
+"set laststatus=2 " status line always on
+"% textwrap
+:set whichwrap=<,>,h,l,[,]
+:set nolinebreak " only affects display not buffer
+:set wrap
+:set wrapmargin=0
+"% map
+":let mapleader = "," " consider
+:map <leader>paste :set invpaste<cr>
+"% wrap/formatting paragraph according to the current 'textwidth' with ^\ (control-\):
+:imap <C-\> <C-O>gqap
+:nmap <C-\> gqap
+:vmap <C-\> gq
+"% save file, go to next file in buffer
+:map <leader>nf :w <enter> :n <enter>
+"% vimdiff q exits
+:if &diff
+: cmap q qa
+:endif
+"% directory files, placed in vertical split window
+:map <leader>ls :vs<cr> :Explore<cr>
+:map <leader>dir :vs<cr> :Explore<cr>
+"% remapping lines make cursor jump a line at a time within wrapped text
+:nnoremap j gj
+:nnoremap k gk
+:vnoremap j gj
+:vnoremap k gk
+:nnoremap <Down> gj
+:nnoremap <Up> gk
+:vnoremap <Down> gj
+:vnoremap <Up> gk
+:inoremap <Down> <C-o>gj
+:inoremap <Up> <C-o>gk
+"% search and replace
+:map <leader>rd :.,$s///c "search and replace down
+:map <leader>rg :%s///c "search and replace whole file
+:map <leader>rr :rubyd gsub!(//,"")
+"% pwd t64 working directory set to that of the file you're editing
+"changes pwd to directory of file in current buffer
+:function! CHANGE_CURR_DIR()
+: let _dir = expand("%:p:h")
+: exec "cd " . _dir
+: unlet _dir
+:endfunction
+"% Change to the directory the file in your current buffer is in
+:if has("autocmd")
+ autocmd BufEnter * :lcd %:p:h
+:endif
+"% autocompletefilenames To search for files in the current directory
+:set path=,,
+"auto-completion for file to edit in current dir, used in normal mode
+:map <leader>e :e <c-r>=expand("%:p:h") . "/" <cr>
+:map <leader>pwd :exe 'cd ' . expand ("%:p:h")<cr>
+"% searchhighlight t93: Toggle search highlight <C-n>
+:function! ToggleHLSearched()
+: if &hls
+: set nohls
+: else
+: set hls
+: endif
+:endfun
+:nmap <silent> <C-n> :silent call ToggleHLSearched()<cr>
+"%% SiSU vim folds
+"% foldsearchx FoldSearch (opens result of search all else closed) t77
+:map <leader>fs :set foldmethod=expr foldcolumn=2 foldlevel=0 <cr>
+:map <leader>ff :F<cr>
+:map <leader>fe :F<cr> zE
+"% foldtoggle Fold Toggle mapped to <space>
+:fun! ToggleFold()
+: if foldlevel('.') == 0
+: normal! l
+: else
+: if foldclosed('.') < 0
+: foldclose
+: else
+: foldopen
+: endif
+: endif
+" Clear status line
+: echo
+:endfun
+" Map this function to Space key.
+:noremap <space> :call ToggleFold()<cr>
+"% foldtype Fold? set foldtext
+:set foldtext=v:folddashes.substitute(getline(v:foldstart),'\\=','','g',)
+:set foldexpr=getline(v:lnum-1)!~@/&&getline(v:lnum)!~@/&&getline(v:lnum+1)!~@/
+"% foldsearch t77: Fold on search result
+:function! FoldMake(search)
+: set fdm=manual
+: normal zE
+: normal G$
+: let folded = 0 "flag to set when a fold is found
+: let flags = "w" "allow wrapping in the search
+: let line1 = 0 "set marker for beginning of fold
+: while search(a:search, flags) > 0
+: let line2 = line(".")
+: if (line2 -1 > line1)
+: "echo line1 . ":" . (line2-1)
+: "echo "a fold goes here."
+: execute ":" . line1 . "," . (line2-1) . "fold"
+: let folded = 1 "at least one fold has been found
+: endif
+: let line1 = line2 "update marker
+: let flags = "W" "turn off wrapping
+: endwhile
+" create the last fold which goes to the end of the file.
+: normal $G
+: let line2 = line(".")
+: if (line2 > line1 && folded == 1)
+: execute ":". line1 . "," . line2 . "fold"
+: endif
+: normal 1G
+:endfunction
+"% folds Fold Patterns
+:command! -nargs=+ -complete=command FMake call FoldMake(<q-args>)
+: if ( &filetype == "ruby" )
+: command! F FMake ^# ==\?\|^\s*\(\(def\|class\|module\)\s\|\(public\|protected\|private\|__END__\)\s*$\)\|\(^\s*\|\s\+\)#%\s
+: command! Fa FMake \(^# ==\?\|^\s*\(\(\(def\|class\|module\)\s\)\|\(\(public\|protected\|private\|__END__\)\(\s*$\)\)\)\)\|^[0-9]\~\|\([#%]\|^["]\)\{1,4\}\s*%\|{\({\|!!\)
+: command! FD FMake \(^# ==\?\|^\s*\(\(def\|class\|module\)\s\)\)\|^\s*\([#%"0-9]\{0,4\}\~\(%\+\s\|!!\)\|#\s\+=\+\s\+\)
+: else
+"% folds :F Fold Patterns SiSU Markup :F
+: command! F FMake ^\([1-8]\|:\?[A-C]\)\~\|\(^%\|\(^\|\s\+\)[#"]\)%\{1,2\}\(\s\|$\)\|^<<\s*|
+: command! Fa FMake ^\([1-8]\|:\?[A-C]\)\~\|\(^%\|\(^\|\s\+\)[#"]\)%\{1,2\}\(\s\|$\)\|^<<\s*|\|^\(Book\|Part\|Chapter\|Section\|Article\|BOOK\|PART\|CHAPTER\|SECTION\|ARTICLE\)\s
+: command! F0 FMake ^\(\s*0\~\|@\S\+:[+-]\?\s\+\)
+: command! FA FMake ^:\?A\~
+: command! FB FMake ^:\?[AB]\~
+: command! FC FMake ^:\?[A-C]\~
+: command! F1 FMake ^\(:\?[A-C]\|1\)\~
+: command! F2 FMake ^\(:\?[A-C]\|[12]\)\~
+: command! F3 FMake ^\(:\?[A-C]\|[1-3]\)\~
+: command! F4 FMake ^[1-4]\~
+: command! F5 FMake ^[4-5]\~
+: command! F6 FMake ^[4-6]\~
+: command! Fc FMake ^[%]\+\s\+
+: endif
+"% folds Fold Patterns misc
+":command! Fp FMake ^\s*[A-Za-z0-9#]
+:command! Fp FMake ^\s*\S
+:command! Fo FMake ^[%\"]\s*[{>]
+"% linenumbering, on, relative, off
+:map <Leader>nn :set <c-r>={'00':'','01':'r','10':'nor'}[&rnu.&nu]<CR>nu<CR>
+"% cursorline
+:map <leader>cu :if &cursorcolumn <Bar>
+ \set nocursorline nocursorcolumn <Bar>
+ \else <Bar>
+ \set cursorline cursorcolumn <Bar>
+ \endif<cr>
+:map <leader>cu- :set nocursorline nocursorcolumn<cr>
+:map <leader>cu+ :set cursorline cursorcolumn<cr>
diff --git a/sundry/misc/editor-syntax-etc/vim/ftplugin/sisurb.vim b/sundry/misc/editor-syntax-etc/vim/ftplugin/sisurb.vim
new file mode 100644
index 0000000..b959626
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/ftplugin/sisurb.vim
@@ -0,0 +1,7 @@
+:set number
+:set relativenumber
+:autocmd FileType ruby :set relativenumber
+:autocmd FocusLost *.rb :set norelativenumber
+:autocmd FocusGained *.rb :set relativenumber
+:autocmd InsertEnter *.rb :set norelativenumber
+:autocmd InsertLeave *.rb :set relativenumber
diff --git a/sundry/misc/editor-syntax-etc/vim/rc/vimrc_statusline b/sundry/misc/editor-syntax-etc/vim/rc/vimrc_statusline
new file mode 100644
index 0000000..e0ba493
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/rc/vimrc_statusline
@@ -0,0 +1,110 @@
+set statusline= "
+set fillchars=stl:―,stlnc:—,vert:│,fold:۰,diff:·
+"% [ buffer number ]
+set statusline +=%#Normal#[ "
+set statusline +=%#Identifier#%n " buffer number
+set statusline +=%#PreProc#%M " modified flag
+set statusline +=%#Normal#] "
+"% [ file name (& modified?) ]
+set statusline +=%#Normal#\ [ "
+set statusline +=%#Statement#%<%F%* " full path
+"set statusline +=%#Statement#%<%t " full path
+set statusline +=%#PreProc#%M " modified flag
+set statusline +=%#Normal#] "
+"% [ column : line number / number of lines in file, percentage of file ] [%v:%l/%L\ %p%%]
+set statusline +=%#Normal#\ [ "
+set statusline +=%#Identifier#%v " column & line
+set statusline +=%#Normal#: "
+set statusline +=%#Identifier#%l " column & line
+set statusline +=%#SpecialKey#/%L\ " total lines
+set statusline +=%#Identifier#%p " percentage of file
+set statusline +=%#SpecialKey#%% "
+set statusline +=%#Normal#] " "
+"% [ file format : file type ]
+set statusline +=%#Normal#\ [ "
+set statusline +=%#SpecialKey#%{&fenc} " file format
+set statusline +=%#Normal#: "
+set statusline +=%#SpecialKey#%{&ff} " file format
+set statusline +=%#Normal#: "
+set statusline +=%#SpecialKey#%y " file type
+set statusline +=%#Normal#] "
+"% [ character under cursor ]
+set statusline +=%#Normal#\ [ "
+set statusline +=%#String#0x%04B " character under cursor
+set statusline +=%#Normal#]\ "
+"% [ syntastic ]
+set statusline +=%#warningmsg#
+set statusline +=%{SyntasticStatuslineFlag()}\ "
+"set statusline+=%*
+"% Status line background
+set statusline +=%#Folded#\ "
+"set statusline +=%#Normal#\ "
+"set statusline +=%#MatchParen#\ "
+"set statusline +=%#ColorColumn#\ "
+"set statusline +=%#DiffDelete#\ "
+"set statusline +=%#VertSplit#\ "
+"% misc
+set laststatus=2 " status line always on
+"set statusline=%F%m%r%h%w\
+"set statusline=%f%m%r%h%w\ [type=%Y]\ [format=%{&ff}]\ [%v:%l/%L\ %p%%]
+"<http://www.reddit.com/r/vim/comments/1e6ph1/need_help_with_vim_statusline_customization/>
+"<http://stackoverflow.com/questions/5375240/a-more-useful-statusline-in-vim>
+"<http://got-ravings.blogspot.it/2008/08/vim-pr0n-making-statuslines-that-own.html>
+"! black
+hi User0 guifg=#000000 guibg=#222222
+"! red
+hi User1 guifg=#CC0000 guibg=#222222
+"! green
+hi User2 guifg=#4E9A06 guibg=#222222
+"! yellow
+hi User3 guifg=#C4A000 guibg=#222222
+"! blue
+hi User4 guifg=#5971AD guibg=#222222
+"! magenta
+hi User5 guifg=#75507B guibg=#222222
+"! cyan
+hi User6 guifg=#06989A guibg=#222222
+"! white
+hi User7 guifg=#D3D7CF guibg=#222222
+hi User8 guifg=#ffffff guibg=#222222
+hi User9 guifg=#ffffff guibg=#222222
+"! black
+"Rxvt.color0 : #000000
+"Rxvt.color8 : #555555
+"! red
+"Rxvt.color1 : #CC0000
+"Rxvt.color9 : #EF2929
+"! green
+"Rxvt.color2 : #4E9A06
+"Rxvt.color10 : #8AE234
+"! yellow
+"Rxvt.color3 : #C4A000
+"Rxvt.color11 : #FCE94F
+"! blue
+"Rxvt.color4 : #5971AD
+"Rxvt.color12 : #729FCF
+"! magenta
+"Rxvt.color5 : #75507B
+"Rxvt.color13 : #AD7FA8
+"! cyan
+"Rxvt.color6 : #06989A
+"Rxvt.color14 : #34E2E2
+"! white
+"Rxvt.color7 : #D3D7CF
+"Rxvt.color15 : #EEEEEE
+
+"hi User1 guifg=#eea040 guibg=#222222
+"hi User2 guifg=#dd3333 guibg=#222222
+"hi User3 guifg=#ff66ff guibg=#222222
+"hi User4 guifg=#a0ee40 guibg=#222222
+"hi User5 guifg=#eeee40 guibg=#222222
+"
+"hi User0 guifg=#ffffff guibg=#094afe
+"hi User1 guifg=#ffdad8 guibg=#880c0e
+"hi User2 guifg=#000000 guibg=#F4905C
+"hi User3 guifg=#292b00 guibg=#f4f597
+"hi User4 guifg=#112605 guibg=#aefe7B
+"hi User5 guifg=#051d00 guibg=#7dcc7d
+"hi User7 guifg=#ffffff guibg=#880c0e gui=bold
+"hi User8 guifg=#ffffff guibg=#5b7fbb
+"hi User9 guifg=#ffffff guibg=#810085
diff --git a/sundry/misc/editor-syntax-etc/vim/syntax/sisu.vim b/sundry/misc/editor-syntax-etc/vim/syntax/sisu.vim
new file mode 100644
index 0000000..effa9df
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/syntax/sisu.vim
@@ -0,0 +1,277 @@
+" SiSU Vim syntax file
+" SiSU Maintainer: Ralph Amissah <ralph.amissah@gmail.com>
+" SiSU Markup: SiSU (sisu-5.6.7)
+" Last Change: 2017-06-22
+" URL: <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob;f=data/sisu/conf/editor-syntax-etc/vim/syntax/sisu.vim;hb=HEAD>
+" <http://git.sisudoc.org/gitweb/?p=code/sisu.git;a=blob_plain;f=data/sisu/conf/editor-syntax-etc/vim/syntax/sisu.vim;hb=HEAD>
+"(originally looked at Ruby Vim by Mirko Nasato)
+
+if version < 600
+ syntax clear
+elseif exists("b:current_syntax")
+ finish
+else
+endif
+let s:cpo_save = &cpo
+set cpo&vim
+
+"% "Errors:
+syn match sisu_error contains=sisu_link,sisu_error_wspace "<![^ei]\S\+!>"
+
+"% "Markers Identifiers:
+if !exists("sisu_no_identifiers")
+ syn match sisu_mark_endnote "\~^"
+ syn match sisu_break contains=@NoSpell " \\\\\( \|$\)\|<br>\|<br />"
+ syn match sisu_control contains=@NoSpell "^\(-\\\\-\|=\\\\=\|-\.\.-\|<:p[bn]>\)\s*$"
+ syn match sisu_control contains=@NoSpell "^<:\(bo\|---\)>\s*$"
+ syn match sisu_marktail contains=@NoSpell "^--[+~-]#\s*$"
+ syn match sisu_marktail "[~-]#"
+ syn match sisu_control "\""
+ syn match sisu_underline "\(^\| \)_[a-zA-Z0-9]\+_\([ .,]\|$\)"
+ syn match sisu_number contains=@NoSpell "[0-9a-f]\{32\}\|[0-9a-f]\{64\}"
+ syn match sisu_link contains=@NoSpell "\(_\?https\?://\|\.\.\/\)\S\+"
+ syn match sisu_link " \*\~\S\+"
+ syn match sisu_require contains=@NoSpell "^<<\s*[a-zA-Z0-9^./_-]\+\.ss[it]$"
+ syn match sisu_structure "^:A\~$"
+
+"% "Document Sub Headers:
+ syn match sisu_sub_header_title "^\s\+:\(subtitle\|short\|edition\|language\|lang_char\|note\):\s" "group=sisu_header_content
+ syn match sisu_sub_header_creator "^\s\+:\(author\|editor\|contributor\|illustrator\|photographer\|translator\|digitized_by\|prepared_by\|audio\|video\):\s" " &hon &institution
+ syn match sisu_sub_header_rights "^\s\+:\(copyright\|text\|translation\|illustrations\|photographs\|preparation\|digitization\|audio\|video\|license\|all\):\s" " access_rights license
+ syn match sisu_sub_header_classify "^\s\+:\(topic_register\|keywords\|subject\|dewey\|loc\):\s"
+ syn match sisu_sub_header_identifier "^\s\+:\(oclc\|isbn\):\s"
+ syn match sisu_sub_header_date "^\s\+:\(added_to_site\|available\|created\|issued\|modified\|published\|valid\|translated\|original_publication\):\s"
+ syn match sisu_sub_header_original "^\s\+:\(publisher\|date\|language\|lang_char\|institution\|nationality\|source\):\s"
+ syn match sisu_sub_header_make "^\s\+:\(headings\|num_top\|breaks\|language\|italics\|bold\|emphasis\|substitute\|omit\|plaintext_wrap\|texpdf_font_mono\|texpdf_font\|stamp\|promo\|ad\|manpage\|home_button_text\|home_button_image\|cover_image\|footer\):\s"
+ syn match sisu_sub_header_notes "^\s\+:\(description\|abstract\|comment\|coverage\|relation\|source\|history\|type\|format\|prefix\|prefix_[ab]\|suffix\):\s"
+ syn match sisu_within_index_ignore "\S\+[:;]\(\s\+\|$\)"
+ syn match sisu_within_index "[:|;]\|+\d\+"
+
+"% "semantic markers: (ignore)
+ syn match sisu_sem_marker ";{\|};[a-z._]*[a-z]"
+ syn match sisu_sem_marker_block "\([a-z][a-z._]*\|\):{\|}:[a-z._]*[a-z]"
+ syn match sisu_sem_ex_marker ";\[\|\];[a-z._]*[a-z]"
+ syn match sisu_sem_ex_marker_block "\([a-z][a-z._]*\|\):\[\|\]:[a-z._]*[a-z]"
+ syn match sisu_sem_block contains=sisu_error,sisu_error_wspace,sisu_content_alt,sisu_link,sisu_linked,sisu_break,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_mark_endnote,sisu_content_endnote "\([a-z]*\):{[^}].\{-}}:\1"
+ syn match sisu_sem_content contains=sisu_error,sisu_error_wspace,sisu_content_alt,sisu_link,sisu_linked,sisu_break,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker ";{[^}].\{-}};[a-z]\+"
+ syn match sisu_sem_ex_block contains=sisu_error,sisu_error_wspace,sisu_content_alt,sisu_link,sisu_linked,sisu_break,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_mark_endnote,sisu_content_endnote "\([a-z]*\):\[[^}].\{-}\]:\1"
+ syn match sisu_sem_ex_content contains=sisu_error,sisu_error_wspace,sisu_content_alt,sisu_link,sisu_linked,sisu_break,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker ";\[[^}].\{-}\];[a-z]\+"
+endif
+
+"% "URLs Numbers And ASCII Codes:
+syn match sisu_number "\<\(0x\x\+\|0b[01]\+\|0\o\+\|0\.\d\+\|0\|[1-9][\.0-9_]*\)\>"
+syn match sisu_number "?\(\\M-\\C-\|\\c\|\\C-\|\\M-\)\=\(\\\o\{3}\|\\x\x\{2}\|\\\=\w\)"
+
+"% "Tuned Error: (is error if not already matched)
+syn match sisu_error contains=sisu_error "[\~/\*!_]{\|}[\~/\*!_]"
+syn match sisu_error contains=sisu_error "<a href\|</a>]"
+
+"% "Simple Paired Enclosed Markup:
+"url/link
+syn region sisu_link contains=sisu_error,sisu_error_wspace matchgroup=sisu_action start="^<<\s*|[a-zA-Z0-9^._-]\+|@|[a-zA-Z0-9^._-]\+|"rs=s+2 end="$"
+
+"% "Document Header:
+" title
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_title matchgroup=sisu_header start="^[@]title:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" creator
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_creator matchgroup=sisu_header start="^[@]creator:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" dates
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_date matchgroup=sisu_header start="^[@]date:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" publisher
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_publisher matchgroup=sisu_header start="^[@]publisher:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" rights
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_rights matchgroup=sisu_header start="^[@]rights:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" classify document
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_classify matchgroup=sisu_header start="^[@]classify:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" identifier document
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_identifier matchgroup=sisu_header start="^[@]identifier:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" original language (depreciated)
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_original matchgroup=sisu_header start="^[@]original:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" notes
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_notes matchgroup=sisu_header start="^[@]notes:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" links of interest
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_linked,sisu_sub_header_links matchgroup=sisu_header start="^[@]links:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+" make, processing instructions
+syn region sisu_header_content contains=sisu_error,sisu_comment,sisu_break,sisu_link,sisu_sub_header_make matchgroup=sisu_header start="^[@]make:[+-]\?\(\s\|\n\)"rs=e-1 end="\n$"
+
+"% "Headings:
+syn region sisu_heading contains=sisu_mark_endnote,sisu_content_endnote,sisu_marktail,sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_ocn,sisu_error,sisu_error_wspace matchgroup=sisu_structure start="^\([1-4]\|:\?[A-D]\)\~\(\S\+\|[^-]\)" end="$"
+
+"% "Block Group Text:
+" table
+syn region sisu_content_alt contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^table{.\+" end="}table"
+" table
+syn region sisu_content_alt contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^```\s\+table" end="^```\(\s\|$\)"
+syn region sisu_content_alt contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^{\(t\|table\)\(\~h\)\?\(\sc[0-9]\+;\)\?[0-9; ]*}" end="\n$"
+" block, group, poem, alt
+syn region sisu_content_alt contains=sisu_mark_endnote,sisu_content_endnote,sisu_link,sisu_mark,sisu_strikeout,sisu_number,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^\z(block\|group\|poem\|alt\){" end="^}\z1"
+syn region sisu_content_alt contains=sisu_mark_endnote,sisu_content_endnote,sisu_link,sisu_mark,sisu_strikeout,sisu_number,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^```\s\+\(block\|group\|poem\|alt\)" end="^```\(\s\|$\)"
+" box
+syn region sisu_content_alt contains=sisu_mark_endnote,sisu_content_endnote,sisu_link,sisu_mark,sisu_strikeout,sisu_number,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^box\(\.[a-z]\+\)\?{" end="^}box"
+syn region sisu_content_alt contains=sisu_mark_endnote,sisu_content_endnote,sisu_link,sisu_mark,sisu_strikeout,sisu_number,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_contain start="^```\s\+\box\(\.[a-z]\+\)\?" end="^```\(\s\|$\)"
+" code
+syn region sisu_content_alt contains=sisu_error,@NoSpell matchgroup=sisu_contain start="^code\(\.[a-z][0-9a-z_]\+\)\?{" end="^}code"
+syn region sisu_content_alt contains=sisu_error,@NoSpell matchgroup=sisu_contain start="^```\s\+code\(\.[a-z][0-9a-z_]\+\)\?" end="^```\(\s\|$\)"
+" quote
+syn region sisu_normal contains=sisu_fontface,sisu_bold,sisu_control,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_contain start="^```\s\+quote" end="^```\(\s\|$\)"
+
+"% "Endnotes:
+" regular endnote or asterisk or plus sign endnote
+syn region sisu_content_endnote contains=sisu_link,sisu_strikeout,sisu_underline,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error,sisu_error_wspace,sisu_mark,sisu_break,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker matchgroup=sisu_mark_endnote start="\~{[*+]*" end="}\~" skip="\n"
+" numbered asterisk or plus sign endnote
+syn region sisu_content_endnote contains=sisu_link,sisu_strikeout,sisu_underline,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error,sisu_error_wspace,sisu_mark,sisu_break,sisu_sem_block,sisu_sem_content,sisu_sem_marker matchgroup=sisu_mark_endnote start="\~\[[*+]*" end="\]\~" skip="\n"
+" endnote content marker (for binary content marking)
+syn region sisu_content_endnote contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_link,sisu_identifier,sisu_error,sisu_error_wspace,sisu_mark,sisu_break matchgroup=sisu_mark_endnote start="\^\~" end="\n$"
+
+"% "Links And Images:
+" image with url link (and possibly footnote of url)
+syn region sisu_linked contains=sisu_fontface,sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_sem_block,sisu_error matchgroup=sisu_link start="{\(\~^\s\)\?" end="}\(https\?:/\/\|:\|\.\.\/\|#\)\S\+" oneline
+" sisu outputs, short notation
+syn region sisu_linked contains=sisu_fontface,sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_sem_block,sisu_error matchgroup=sisu_link start="{\(\~^\s\)\?" end="\[[1-5][sS]*\]}\S\+\.ss[tm]" oneline
+" image
+syn region sisu_linked contains=sisu_fontface,sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_link start="{" end="}image" oneline
+
+"% "Some Line Operations:
+" bold line
+syn region sisu_bold contains=sisu_strikeout,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^!_ " end=" \\\\\|$"
+" indent and bullet paragraph
+syn region sisu_normal contains=sisu_fontface,sisu_bold,sisu_control,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^_\([1-9*]\|[1-9]\*\) " end="$"
+" indent and bullet (bold start) paragraph
+syn region sisu_bold contains=sisu_fontface,sisu_bold,sisu_control,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^_\([1-9*]\|[1-9]\*\)!_\? " end=" \\\\\|$"
+" hanging indent paragraph [proposed]
+syn region sisu_normal contains=sisu_fontface,sisu_bold,sisu_control,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^_[0-9]\?_[0-9] " end="$"
+" hanging indent (bold start/ definition) paragraph [proposed]
+syn region sisu_bold contains=sisu_fontface,sisu_bold,sisu_control,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_sem_block,sisu_sem_content,sisu_sem_marker_block,sisu_sem_marker,sisu_sem_ex_marker_block,sisu_sem_ex_marker,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^_[0-9]\?_[0-9]!_\? " end=" \\\\\|$"
+" list numbering
+syn region sisu_normal contains=sisu_strikeout,sisu_identifier,sisu_content_endnote,sisu_mark_endnote,sisu_link,sisu_linked,sisu_error,sisu_error_wspace matchgroup=sisu_markpara start="^\(#[ 1]\|_# \)" end="$"
+
+"% "Font Face Curly Brackets:
+"syn region sisu_identifier contains=sisu_strikeout,sisu_number,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_sem start="\S\+:{" end="}:[^<>,.!?:; ]\+" oneline
+" book index:
+syn region sisu_index contains=sisu_within_index_ignore,sisu_within_index matchgroup=sisu_index_block start="^={" end="}"
+" emphasis:
+syn region sisu_bold contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="\*{" end="}\*"
+" bold:
+syn region sisu_bold contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="!{" end="}!"
+" underscore:
+syn region sisu_underline contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="_{" end="}_"
+" italics:
+syn region sisu_identifier contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="/{" end="}/"
+" added:
+syn region sisu_underline contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="+{" end="}+"
+" superscript:
+syn region sisu_identifier contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="\^{" end="}\^"
+" subscript:
+syn region sisu_identifier contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start=",{" end="},"
+" monospace:
+syn region sisu_identifier contains=sisu_strikeout,sisu_number,sisu_bold,sisu_control,sisu_identifier,sisu_error matchgroup=sisu_fontface start="#{" end="}#"
+" strikethrough:
+syn region sisu_strikeout contains=sisu_error matchgroup=sisu_fontface start="-{" end="}-"
+
+"% "Single Words Bold Italicise Etc: (depreciated)
+syn region sisu_bold contains=sisu_error matchgroup=sisu_bold start="\([ (]\|^\)\*[^\|{\n\~\\]"hs=e-1 end="\*"he=e-0 skip="[a-zA-Z0-9']" oneline
+syn region sisu_identifier contains=sisu_error matchgroup=sisu_content_alt start="\([ ]\|^\)/[^{ \|\n\\]"hs=e-1 end="/\[ \.\]" skip="[a-zA-Z0-9']" oneline
+"misc
+syn region sisu_identifier contains=sisu_error matchgroup=sisu_fontface start="\^[^ {\|\n\\]"rs=s+1 end="\^[ ,.;:'})\\\n]" skip="[a-zA-Z0-9']" oneline
+
+"% "Expensive Mode:
+if !exists("sisu_no_expensive")
+else " not Expensive
+ syn region sisu_content_alt matchgroup=sisu_control start="^\s*def\s" matchgroup=NONE end="[?!]\|\>" skip="\.\|\(::\)" oneline
+endif " Expensive?
+
+"% "Headers And Headings: (Document Instructions)
+syn match sisu_control contains=sisu_error,sisu_error_wspace "4\~! \S\+"
+syn region sisu_markpara contains=sisu_error,sisu_error_wspace start="^=begin" end="^=end.*$"
+
+"% "Errors:
+syn match sisu_error_wspace contains=sisu_error_wspace "^\s\+[^:]"
+syn match sisu_error_wspace contains=sisu_error_wspace "\s\s\+"
+syn match sisu_error_wspace contains=sisu_error_wspace "\s\+$"
+syn match sisu_error contains=sisu_error_wspace "\t\+"
+syn match sisu_error contains=sisu_error,sisu_error_wspace "\([^ (][_\\]\||[^ (}]\)https\?:\S\+"
+syn match sisu_error contains=sisu_error "_\?https\?:\S\+[}><]"
+syn match sisu_error contains=sisu_error "\([!*/_\+,^]\){\([^(\}\1)]\)\{-}\n$"
+syn match sisu_error contains=sisu_error "^[\~]{[^{]\{-}\n$"
+syn match sisu_error contains=sisu_error "\s\+.{{"
+syn match sisu_error contains=sisu_error "^\~\s*$"
+syn match sisu_error contains=sisu_error "^0\~.*"
+syn match sisu_error contains=sisu_error "^[1-9]\~\s*$"
+syn match sisu_error contains=sisu_error "^[1-9]\~\S\+\s*$"
+syn match sisu_error contains=sisu_error "[^{]\~\^[^ \)]"
+syn match sisu_error contains=sisu_error "\~\^\s\+\.\s*"
+syn match sisu_error contains=sisu_error "{\~^\S\+"
+syn match sisu_error contains=sisu_error "[_/\*!^]{[ .,:;?><]*}[_/\*!^]"
+syn match sisu_error contains=sisu_error "[^ (\"'(\[][_/\*!]{\|}[_/\*!][a-zA-Z0-9)\]\"']"
+syn match sisu_error contains=sisu_error "<dir>"
+"errors for filetype sisu, though not error in 'metaverse':
+syn match sisu_error contains=sisu_error,sisu_match,sisu_strikeout,sisu_contain,sisu_content_alt,sisu_mark,sisu_break,sisu_number "<[a-zA-Z\/]\+>"
+syn match sisu_error "/\?<\([biu]\)>[^(</\1>)]\{-}\n$"
+
+"% "Error Exceptions:
+syn match sisu_control "\n$" "contains=ALL
+"syn match sisu_control " //"
+syn match sisu_error "%{"
+syn match sisu_error "<br>_\?https\?:\S\+\|_\?https\?:\S\+<br>"
+syn match sisu_error "[><]_\?https\?:\S\+\|_\?https\?:\S\+[><]"
+syn match sisu_comment "^%\{1,2\}.\+"
+
+"% "Definitions Default Highlighting:
+hi def link sisu_normal Normal
+hi def link sisu_bold Statement
+hi def link sisu_header PreProc
+hi def link sisu_header_content Normal
+hi def link sisu_sub_header_title Statement
+hi def link sisu_sub_header_creator Statement
+hi def link sisu_sub_header_date Statement
+hi def link sisu_sub_header_publisher Statement
+hi def link sisu_sub_header_rights Statement
+hi def link sisu_sub_header_classify Statement
+hi def link sisu_sub_header_identifier Statement
+hi def link sisu_sub_header_original Statement
+hi def link sisu_sub_header_links Statement
+hi def link sisu_sub_header_notes Statement
+hi def link sisu_sub_header_make Statement
+hi def link sisu_heading Title
+hi def link sisu_structure Operator
+hi def link sisu_contain Include
+hi def link sisu_mark_endnote Delimiter
+hi def link sisu_require NonText
+hi def link sisu_link NonText
+hi def link sisu_linked String
+hi def link sisu_fontface Delimiter
+hi def link sisu_strikeout DiffDelete
+hi def link sisu_content_alt Special
+hi def link sisu_sem_content SpecialKey
+hi def link sisu_sem_block Special
+hi def link sisu_sem_marker Visual
+"hi def link sisu_sem_marker Structure
+hi def link sisu_sem_marker_block MatchParen
+hi def link sisu_sem_ex_marker FoldColumn
+hi def link sisu_sem_ex_marker_block Folded
+hi def link sisu_sem_ex_content Comment
+"hi def link sisu_sem_ex_content SpecialKey
+hi def link sisu_sem_ex_block Comment
+hi def link sisu_index SpecialKey
+hi def link sisu_index_block Visual
+hi def link sisu_content_endnote Special
+hi def link sisu_control Delimiter
+hi def link sisu_within_index Delimiter
+hi def link sisu_within_index_ignore SpecialKey
+hi def link sisu_ocn Include
+hi def link sisu_number Number
+hi def link sisu_identifier Function
+hi def link sisu_underline Underlined
+hi def link sisu_markpara Include
+hi def link sisu_marktail Include
+hi def link sisu_mark Identifier
+hi def link sisu_break Structure
+hi def link sisu_html Type
+hi def link sisu_action Identifier
+hi def link sisu_comment Comment
+hi def link sisu_error_sem_marker Error
+hi def link sisu_error_wspace Error
+hi def link sisu_error Error
+let b:current_syntax = "sisu"
+let &cpo = s:cpo_save
+unlet s:cpo_save
diff --git a/sundry/misc/editor-syntax-etc/vim/templates/ssi.tpl b/sundry/misc/editor-syntax-etc/vim/templates/ssi.tpl
new file mode 100644
index 0000000..28e8101
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/templates/ssi.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0 insert
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#
diff --git a/sundry/misc/editor-syntax-etc/vim/templates/ssm.tpl b/sundry/misc/editor-syntax-etc/vim/templates/ssm.tpl
new file mode 100644
index 0000000..579375f
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/templates/ssm.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0 master
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#
diff --git a/sundry/misc/editor-syntax-etc/vim/templates/sst.tpl b/sundry/misc/editor-syntax-etc/vim/templates/sst.tpl
new file mode 100644
index 0000000..069d498
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/templates/sst.tpl
@@ -0,0 +1,30 @@
+# SiSU 8.0
+
+title:
+ main: "#___#"
+ sub: "#___#"
+ language: "#___#"
+
+creator:
+ author: "#___#"
+
+date:
+ :published: "YYYY-MM-DD"
+
+rights:
+ copyright: "#___#"
+ license: "#___#"
+
+classify:
+ topic_register: "#___#"
+
+make:
+ breaks: "new=:B; break=1"
+# home_button_text: "#___#"
+# footer: "#___#"
+
+#% -- body ---
+
+:A~ @title @author
+
+1~ #___#
diff --git a/sundry/misc/editor-syntax-etc/vim/vim-sisu.yaml b/sundry/misc/editor-syntax-etc/vim/vim-sisu.yaml
new file mode 100644
index 0000000..ca665bc
--- /dev/null
+++ b/sundry/misc/editor-syntax-etc/vim/vim-sisu.yaml
@@ -0,0 +1,7 @@
+#vim-addons: debian vim-addon-manager
+addon: sisu
+description: SiSU documents - structuring, publishing in multiple formats and search
+basedir: /usr/share/vim-scripts/
+files:
+ - ftplugin/sisu.vim
+ - syntax/sisu.vim
diff --git a/sundry/misc/ext_lib/src/arsd/README b/sundry/misc/ext_lib/src/arsd/README
new file mode 100644
index 0000000..792a2bb
--- /dev/null
+++ b/sundry/misc/ext_lib/src/arsd/README
@@ -0,0 +1,2 @@
+aria2c https://raw.githubusercontent.com/adamdruppe/arsd/master/cgi.d --allow-overwrite=true
+aria2c https://raw.githubusercontent.com/adamdruppe/arsd/master/cgi.d -o"misc/ext_lib/src/arsd/cgi.d" --allow-overwrite=true
diff --git a/sundry/misc/ext_lib/src/arsd/cgi.d b/sundry/misc/ext_lib/src/arsd/cgi.d
new file mode 100644
index 0000000..a0249ee
--- /dev/null
+++ b/sundry/misc/ext_lib/src/arsd/cgi.d
@@ -0,0 +1,10315 @@
+// FIXME: if an exception is thrown, we shouldn't necessarily cache...
+// FIXME: there's some annoying duplication of code in the various versioned mains
+
+// add the Range header in there too. should return 206
+
+// FIXME: cgi per-request arena allocator
+
+// i need to add a bunch of type templates for validations... mayne @NotNull or NotNull!
+
+// FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable
+// but the later one can edit and simplify the api. You'd have to use the subclass tho!
+
+/*
+void foo(int f, @("test") string s) {}
+
+void main() {
+ static if(is(typeof(foo) Params == __parameters))
+ //pragma(msg, __traits(getAttributes, Params[0]));
+ pragma(msg, __traits(getAttributes, Params[1..2]));
+ else
+ pragma(msg, "fail");
+}
+*/
+
+// Note: spawn-fcgi can help with fastcgi on nginx
+
+// FIXME: to do: add openssl optionally
+// make sure embedded_httpd doesn't send two answers if one writes() then dies
+
+// future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections
+
+/*
+ Session manager process: it spawns a new process, passing a
+ command line argument, to just be a little key/value store
+ of some serializable struct. On Windows, it CreateProcess.
+ On Linux, it can just fork or maybe fork/exec. The session
+ key is in a cookie.
+
+ Server-side event process: spawns an async manager. You can
+ push stuff out to channel ids and the clients listen to it.
+
+ websocket process: spawns an async handler. They can talk to
+ each other or get info from a cgi request.
+
+ Tempting to put web.d 2.0 in here. It would:
+ * map urls and form generation to functions
+ * have data presentation magic
+ * do the skeleton stuff like 1.0
+ * auto-cache generated stuff in files (at least if pure?)
+ * introspect functions in json for consumers
+
+
+ https://linux.die.net/man/3/posix_spawn
+*/
+
+/++
+ Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications.
+
+ ---
+ import arsd.cgi;
+
+ // Instead of writing your own main(), you should write a function
+ // that takes a Cgi param, and use mixin GenericMain
+ // for maximum compatibility with different web servers.
+ void hello(Cgi cgi) {
+ cgi.setResponseContentType("text/plain");
+
+ if("name" in cgi.get)
+ cgi.write("Hello, " ~ cgi.get["name"]);
+ else
+ cgi.write("Hello, world!");
+ }
+
+ mixin GenericMain!hello;
+ ---
+
+ Test on console (works in any interface mode):
+ $(CONSOLE
+ $ ./cgi_hello GET / name=whatever
+ )
+
+ If using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd):
+ $(CONSOLE
+ $ ./cgi_hello --port 8080
+ # now you can go to http://localhost:8080/?name=whatever
+ )
+
+
+ Compile_versions:
+
+ If you are using `dub`, use:
+
+ ```sdlang
+ subConfiguration "arsd-official:cgi" "VALUE_HERE"
+ ```
+
+ or to dub.json:
+
+ ```json
+ "subConfigurations": {"arsd-official:cgi": "VALUE_HERE"}
+ ```
+
+ to change versions. The possible options for `VALUE_HERE` are:
+
+ $(LIST
+ * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default.
+ * `cgi` for traditional cgi binaries.
+ * `fastcgi` for FastCGI builds.
+ * `scgi` for SCGI builds.
+ )
+
+ With dmd, use:
+
+ $(TABLE_ROWS
+
+ * + Interfaces
+ + (mutually exclusive)
+
+ * - `-version=plain_cgi`
+ - The default building the module alone without dub - a traditional, plain CGI executable will be generated.
+ * - `-version=embedded_httpd`
+ - A HTTP server will be embedded in the generated executable. This is default when building with dub.
+ * - `-version=fastcgi`
+ - A FastCGI executable will be generated.
+ * - `-version=scgi`
+ - A SCGI (SimpleCGI) executable will be generated.
+
+ * - `-version=embedded_httpd_threads`
+ - The embedded HTTP server will use a single process with a thread pool. (use instead of plain `embedded_httpd` if you want this specific implementation)
+ * - `-version=embedded_httpd_processes`
+ - The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation)
+ * - `-version=embedded_httpd_processes_accept_after_fork`
+ - It will call accept() in each child process, after forking. This is currently the only option, though I am experimenting with other ideas. You probably should NOT specify this right now.
+
+ * + Tweaks
+ + (can be used together with others)
+
+ * - `-version=cgi_with_websocket`
+ - The CGI class has websocket server support.
+
+ * - `-version=with_openssl`
+ - not currently used
+ * - `-version=cgi_embedded_sessions`
+ - The session server will be embedded in the cgi.d server process
+ * - `-version=cgi_session_server_process`
+ - The session will be provided in a separate process, provided by cgi.d.
+ )
+
+ Compile_and_run:
+
+ For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory.
+
+ For FastCGI: `dmd yourfile.d cgi.d -version=fastcgi` and run it. spawn-fcgi helps on nginx. You can put the file in the directory for Apache. On IIS, run it with a port on the command line (this causes it to call FCGX_OpenSocket, which can work on nginx too).
+
+ For SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line.
+
+ For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program.
+
+ You can also simulate a request by passing parameters on the command line, like:
+
+ $(CONSOLE
+ ./yourprogram GET / name=adr
+ )
+
+ And it will print the result to stdout.
+
+ CGI_Setup_tips:
+
+ On Apache, you may do `SetHandler cgi-script` in your `.htaccess` file.
+
+ Integration_tips:
+
+ cgi.d works well with dom.d for generating html. You may also use web.d for other utilities and automatic api wrapping.
+
+ dom.d usage:
+
+ ---
+ import arsd.cgi;
+ import arsd.dom;
+
+ void hello_dom(Cgi cgi) {
+ auto document = new Document();
+
+ static import std.file;
+ // parse the file in strict mode, requiring it to be well-formed UTF-8 XHTML
+ // (You'll appreciate this if you've ever had to deal with a missing </div>
+ // or something in a php or erb template before that would randomly mess up
+ // the output in your browser. Just check it and throw an exception early!)
+ //
+ // You could also hard-code a template or load one at compile time with an
+ // import expression, but you might appreciate making it a regular file
+ // because that means it can be more easily edited by the frontend team and
+ // they can see their changes without needing to recompile the program.
+ //
+ // Note on CTFE: if you do choose to load a static file at compile time,
+ // you *can* parse it in CTFE using enum, which will cause it to throw at
+ // compile time, which is kinda cool too. Be careful in modifying that document,
+ // though, as it will be a static instance. You might want to clone on on demand,
+ // or perhaps modify it lazily as you print it out. (Try element.tree, it returns
+ // a range of elements which you could send through std.algorithm functions. But
+ // since my selector implementation doesn't work on that level yet, you'll find that
+ // harder to use. Of course, you could make a static list of matching elements and
+ // then use a simple e is e2 predicate... :) )
+ document.parseUtf8(std.file.read("your_template.html"), true, true);
+
+ // fill in data using DOM functions, so placing it is in the hands of HTML
+ // and it will be properly encoded as text too.
+ //
+ // Plain html templates can't run server side logic, but I think that's a
+ // good thing - it keeps them simple. You may choose to extend the html,
+ // but I think it is best to try to stick to standard elements and fill them
+ // in with requested data with IDs or class names. A further benefit of
+ // this is the designer can also highlight data based on sources in the CSS.
+ //
+ // However, all of dom.d is available, so you can format your data however
+ // you like. You can do partial templates with innerHTML too, or perhaps better,
+ // injecting cloned nodes from a partial document.
+ //
+ // There's a lot of possibilities.
+ document["#name"].innerText = cgi.request("name", "default name");
+
+ // send the document to the browser. The second argument to `cgi.write`
+ // indicates that this is all the data at once, enabling a few small
+ // optimizations.
+ cgi.write(document.toString(), true);
+ }
+ ---
+
+ Concepts:
+ Input: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod],
+ and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId])
+
+ Output: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse]
+
+ Cookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies]
+
+ Caching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache]
+
+ Redirections: [Cgi.setResponseLocation]
+
+ Other Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived]
+
+ Overriding behavior: [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState]
+
+ Installing: Apache, IIS, CGI, FastCGI, SCGI, embedded HTTPD (not recommended for production use)
+
+ Guide_for_PHP_users:
+ If you are coming from PHP, here's a quick guide to help you get started:
+
+ $(SIDE_BY_SIDE
+ $(COLUMN
+ ```php
+ <?php
+ $foo = $_POST["foo"];
+ $bar = $_GET["bar"];
+ $baz = $_COOKIE["baz"];
+
+ $user_ip = $_SERVER["REMOTE_ADDR"];
+ $host = $_SERVER["HTTP_HOST"];
+ $path = $_SERVER["PATH_INFO"];
+
+ setcookie("baz", "some value");
+
+ echo "hello!";
+ ?>
+ ```
+ )
+ $(COLUMN
+ ---
+ import arsd.cgi;
+ void app(Cgi cgi) {
+ string foo = cgi.post["foo"];
+ string bar = cgi.get["bar"];
+ string baz = cgi.cookies["baz"];
+
+ string user_ip = cgi.remoteAddress;
+ string host = cgi.host;
+ string path = cgi.pathInfo;
+
+ cgi.setCookie("baz", "some value");
+
+ cgi.write("hello!");
+ }
+
+ mixin GenericMain!app
+ ---
+ )
+ )
+
+ $(H3 Array elements)
+
+
+ In PHP, you can give a form element a name like `"something[]"`, and then
+ `$_POST["something"]` gives an array. In D, you can use whatever name
+ you want, and access an array of values with the `cgi.getArray["name"]` and
+ `cgi.postArray["name"]` members.
+
+ $(H3 Databases)
+
+ PHP has a lot of stuff in its standard library. cgi.d doesn't include most
+ of these, but the rest of my arsd repository has much of it. For example,
+ to access a MySQL database, download `database.d` and `mysql.d` from my
+ github repo, and try this code (assuming, of course, your database is
+ set up):
+
+ ---
+ import arsd.cgi;
+ import arsd.mysql;
+
+ void app(Cgi cgi) {
+ auto database = new MySql("localhost", "username", "password", "database_name");
+ foreach(row; mysql.query("SELECT count(id) FROM people"))
+ cgi.write(row[0] ~ " people in database");
+ }
+
+ mixin GenericMain!app;
+ ---
+
+ Similar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases,
+ implementing the same basic interface.
+
+ See_Also:
+
+ You may also want to see [arsd.dom], [arsd.web], and [arsd.html] for more code for making
+ web applications.
+
+ For working with json, try [arsd.jsvar].
+
+ [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in
+ accessing databases.
+
+ If you are looking to access a web application via HTTP, try [std.net.curl], [arsd.curl], or [arsd.http2].
+
+ Copyright:
+
+ cgi.d copyright 2008-2021, Adam D. Ruppe. Provided under the Boost Software License.
+
+ Yes, this file is old, and yes, it is still actively maintained and used.
++/
+module arsd.cgi;
+
+version(Demo)
+unittest {
+
+}
+
+static import std.file;
+
+// for a single thread, linear request thing, use:
+// -version=embedded_httpd_threads -version=cgi_no_threads
+
+version(Posix) {
+ version(CRuntime_Musl) {
+
+ } else version(minimal) {
+
+ } else {
+ version(GNU) {
+ // GDC doesn't support static foreach so I had to cheat on it :(
+ } else version(FreeBSD) {
+ // I never implemented the fancy stuff there either
+ } else {
+ version=with_breaking_cgi_features;
+ version=with_sendfd;
+ version=with_addon_servers;
+ }
+ }
+}
+
+void cloexec(int fd) {
+ version(Posix) {
+ import core.sys.posix.fcntl;
+ fcntl(fd, F_SETFD, FD_CLOEXEC);
+ }
+}
+
+void cloexec(Socket s) {
+ version(Posix) {
+ import core.sys.posix.fcntl;
+ fcntl(s.handle, F_SETFD, FD_CLOEXEC);
+ }
+}
+
+version(embedded_httpd_hybrid) {
+ version=embedded_httpd_threads;
+ version(cgi_no_fork) {} else
+ version=cgi_use_fork;
+ version=cgi_use_fiber;
+}
+
+// the servers must know about the connections to talk to them; the interfaces are vital
+version(with_addon_servers)
+ version=with_addon_servers_connections;
+
+version(embedded_httpd) {
+ version(linux)
+ version=embedded_httpd_processes;
+ else {
+ version=embedded_httpd_threads;
+ }
+
+ /*
+ version(with_openssl) {
+ pragma(lib, "crypto");
+ pragma(lib, "ssl");
+ }
+ */
+}
+
+version(embedded_httpd_processes)
+ version=embedded_httpd_processes_accept_after_fork; // I am getting much better average performance on this, so just keeping it. But the other way MIGHT help keep the variation down so i wanna keep the code to play with later
+
+version(embedded_httpd_threads) {
+ // unless the user overrides the default..
+ version(cgi_session_server_process)
+ {}
+ else
+ version=cgi_embedded_sessions;
+}
+version(scgi) {
+ // unless the user overrides the default..
+ version(cgi_session_server_process)
+ {}
+ else
+ version=cgi_embedded_sessions;
+}
+
+// fall back if the other is not defined so we can cleanly version it below
+version(cgi_embedded_sessions) {}
+else version=cgi_session_server_process;
+
+
+version=cgi_with_websocket;
+
+enum long defaultMaxContentLength = 5_000_000;
+
+/*
+
+ To do a file download offer in the browser:
+
+ cgi.setResponseContentType("text/csv");
+ cgi.header("Content-Disposition: attachment; filename=\"customers.csv\"");
+*/
+
+// FIXME: the location header is supposed to be an absolute url I guess.
+
+// FIXME: would be cool to flush part of a dom document before complete
+// somehow in here and dom.d.
+
+
+// these are public so you can mixin GenericMain.
+// FIXME: use a function level import instead!
+public import std.string;
+public import std.stdio;
+public import std.conv;
+import std.uri;
+import std.uni;
+import std.algorithm.comparison;
+import std.algorithm.searching;
+import std.exception;
+import std.base64;
+static import std.algorithm;
+import std.datetime;
+import std.range;
+
+import std.process;
+
+import std.zlib;
+
+
+T[] consume(T)(T[] range, int count) {
+ if(count > range.length)
+ count = range.length;
+ return range[count..$];
+}
+
+int locationOf(T)(T[] data, string item) {
+ const(ubyte[]) d = cast(const(ubyte[])) data;
+ const(ubyte[]) i = cast(const(ubyte[])) item;
+
+ // this is a vague sanity check to ensure we aren't getting insanely
+ // sized input that will infinite loop below. it should never happen;
+ // even huge file uploads ought to come in smaller individual pieces.
+ if(d.length > (int.max/2))
+ throw new Exception("excessive block of input");
+
+ for(int a = 0; a < d.length; a++) {
+ if(a + i.length > d.length)
+ return -1;
+ if(d[a..a+i.length] == i)
+ return a;
+ }
+
+ return -1;
+}
+
+/// If you are doing a custom cgi class, mixing this in can take care of
+/// the required constructors for you
+mixin template ForwardCgiConstructors() {
+ this(long maxContentLength = defaultMaxContentLength,
+ string[string] env = null,
+ const(ubyte)[] delegate() readdata = null,
+ void delegate(const(ubyte)[]) _rawDataOutput = null,
+ void delegate() _flush = null
+ ) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); }
+
+ this(string[] args) { super(args); }
+
+ this(
+ BufferedInputRange inputData,
+ string address, ushort _port,
+ int pathInfoStarts = 0,
+ bool _https = false,
+ void delegate(const(ubyte)[]) _rawDataOutput = null,
+ void delegate() _flush = null,
+ // this pointer tells if the connection is supposed to be closed after we handle this
+ bool* closeConnection = null)
+ {
+ super(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection);
+ }
+
+ this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); }
+}
+
+/// thrown when a connection is closed remotely while we waiting on data from it
+class ConnectionClosedException : Exception {
+ this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ super(message, file, line, next);
+ }
+}
+
+
+version(Windows) {
+// FIXME: ugly hack to solve stdin exception problems on Windows:
+// reading stdin results in StdioException (Bad file descriptor)
+// this is probably due to http://d.puremagic.com/issues/show_bug.cgi?id=3425
+private struct stdin {
+ struct ByChunk { // Replicates std.stdio.ByChunk
+ private:
+ ubyte[] chunk_;
+ public:
+ this(size_t size)
+ in {
+ assert(size, "size must be larger than 0");
+ }
+ body {
+ chunk_ = new ubyte[](size);
+ popFront();
+ }
+
+ @property bool empty() const {
+ return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job
+ }
+ @property nothrow ubyte[] front() { return chunk_; }
+ void popFront() {
+ enforce(!empty, "Cannot call popFront on empty range");
+ chunk_ = stdin.rawRead(chunk_);
+ }
+ }
+
+ import core.sys.windows.windows;
+static:
+
+ static this() {
+ // Set stdin to binary mode
+ version(Win64)
+ _setmode(std.stdio.stdin.fileno(), 0x8000);
+ else
+ setmode(std.stdio.stdin.fileno(), 0x8000);
+ }
+
+ T[] rawRead(T)(T[] buf) {
+ uint bytesRead;
+ auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null);
+
+ if (!result) {
+ auto err = GetLastError();
+ if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input
+ return buf[0..0];
+ // Some other error, throw it
+
+ char* buffer;
+ scope(exit) LocalFree(buffer);
+
+ // FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
+ // FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
+ FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null);
+ throw new Exception(to!string(buffer));
+ }
+ enforce(!(bytesRead % T.sizeof), "I/O error");
+ return buf[0..bytesRead / T.sizeof];
+ }
+
+ auto byChunk(size_t sz) { return ByChunk(sz); }
+}
+}
+
+/// The main interface with the web request
+class Cgi {
+ public:
+ /// the methods a request can be
+ enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work
+ // these are defined in the standard, but idk if they are useful for anything
+ OPTIONS, TRACE, CONNECT,
+ // These seem new, I have only recently seen them
+ PATCH, MERGE,
+ // this is an extension for when the method is not specified and you want to assume
+ CommandLine }
+
+
+ /+
+ /++
+ Cgi provides a per-request memory pool
+
+ +/
+ void[] allocateMemory(size_t nBytes) {
+
+ }
+
+ /// ditto
+ void[] reallocateMemory(void[] old, size_t nBytes) {
+
+ }
+
+ /// ditto
+ void freeMemory(void[] memory) {
+
+ }
+ +/
+
+
+/*
+ import core.runtime;
+ auto args = Runtime.args();
+
+ we can call the app a few ways:
+
+ 1) set up the environment variables and call the app (manually simulating CGI)
+ 2) simulate a call automatically:
+ ./app method 'uri'
+
+ for example:
+ ./app get /path?arg arg2=something
+
+ Anything on the uri is treated as query string etc
+
+ on get method, further args are appended to the query string (encoded automatically)
+ on post method, further args are done as post
+
+
+ @name means import from file "name". if name == -, it uses stdin
+ (so info=@- means set info to the value of stdin)
+
+
+ Other arguments include:
+ --cookie name=value (these are all concated together)
+ --header 'X-Something: cool'
+ --referrer 'something'
+ --port 80
+ --remote-address some.ip.address.here
+ --https yes
+ --user-agent 'something'
+ --userpass 'user:pass'
+ --authorization 'Basic base64encoded_user:pass'
+ --accept 'content' // FIXME: better example
+ --last-event-id 'something'
+ --host 'something.com'
+
+ Non-simulation arguments:
+ --port xxx listening port for non-cgi things (valid for the cgi interfaces)
+ --listening-host the ip address the application should listen on, or if you want to use unix domain sockets, it is here you can set them: `--listening-host unix:filename` or, on Linux, `--listening-host abstract:name`.
+
+*/
+
+ /** Initializes it with command line arguments (for easy testing) */
+ this(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) {
+ rawDataOutput = _rawDataOutput;
+ // these are all set locally so the loop works
+ // without triggering errors in dmd 2.064
+ // we go ahead and set them at the end of it to the this version
+ int port;
+ string referrer;
+ string remoteAddress;
+ string userAgent;
+ string authorization;
+ string origin;
+ string accept;
+ string lastEventId;
+ bool https;
+ string host;
+ RequestMethod requestMethod;
+ string requestUri;
+ string pathInfo;
+ string queryString;
+
+ bool lookingForMethod;
+ bool lookingForUri;
+ string nextArgIs;
+
+ string _cookie;
+ string _queryString;
+ string[][string] _post;
+ string[string] _headers;
+
+ string[] breakUp(string s) {
+ string k, v;
+ auto idx = s.indexOf("=");
+ if(idx == -1) {
+ k = s;
+ } else {
+ k = s[0 .. idx];
+ v = s[idx + 1 .. $];
+ }
+
+ return [k, v];
+ }
+
+ lookingForMethod = true;
+
+ scriptName = args[0];
+ scriptFileName = args[0];
+
+ environmentVariables = cast(const) environment.toAA;
+
+ foreach(arg; args[1 .. $]) {
+ if(arg.startsWith("--")) {
+ nextArgIs = arg[2 .. $];
+ } else if(nextArgIs.length) {
+ if (nextArgIs == "cookie") {
+ auto info = breakUp(arg);
+ if(_cookie.length)
+ _cookie ~= "; ";
+ _cookie ~= std.uri.encodeComponent(info[0]) ~ "=" ~ std.uri.encodeComponent(info[1]);
+ }
+ else if (nextArgIs == "port") {
+ port = to!int(arg);
+ }
+ else if (nextArgIs == "referrer") {
+ referrer = arg;
+ }
+ else if (nextArgIs == "remote-address") {
+ remoteAddress = arg;
+ }
+ else if (nextArgIs == "user-agent") {
+ userAgent = arg;
+ }
+ else if (nextArgIs == "authorization") {
+ authorization = arg;
+ }
+ else if (nextArgIs == "userpass") {
+ authorization = "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup;
+ }
+ else if (nextArgIs == "origin") {
+ origin = arg;
+ }
+ else if (nextArgIs == "accept") {
+ accept = arg;
+ }
+ else if (nextArgIs == "last-event-id") {
+ lastEventId = arg;
+ }
+ else if (nextArgIs == "https") {
+ if(arg == "yes")
+ https = true;
+ }
+ else if (nextArgIs == "header") {
+ string thing, other;
+ auto idx = arg.indexOf(":");
+ if(idx == -1)
+ throw new Exception("need a colon in a http header");
+ thing = arg[0 .. idx];
+ other = arg[idx + 1.. $];
+ _headers[thing.strip.toLower()] = other.strip;
+ }
+ else if (nextArgIs == "host") {
+ host = arg;
+ }
+ // else
+ // skip, we don't know it but that's ok, it might be used elsewhere so no error
+
+ nextArgIs = null;
+ } else if(lookingForMethod) {
+ lookingForMethod = false;
+ lookingForUri = true;
+
+ if(arg.asLowerCase().equal("commandline"))
+ requestMethod = RequestMethod.CommandLine;
+ else
+ requestMethod = to!RequestMethod(arg.toUpper());
+ } else if(lookingForUri) {
+ lookingForUri = false;
+
+ requestUri = arg;
+
+ auto idx = arg.indexOf("?");
+ if(idx == -1)
+ pathInfo = arg;
+ else {
+ pathInfo = arg[0 .. idx];
+ _queryString = arg[idx + 1 .. $];
+ }
+ } else {
+ // it is an argument of some sort
+ if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) {
+ auto parts = breakUp(arg);
+ _post[parts[0]] ~= parts[1];
+ allPostNamesInOrder ~= parts[0];
+ allPostValuesInOrder ~= parts[1];
+ } else {
+ if(_queryString.length)
+ _queryString ~= "&";
+ auto parts = breakUp(arg);
+ _queryString ~= std.uri.encodeComponent(parts[0]) ~ "=" ~ std.uri.encodeComponent(parts[1]);
+ }
+ }
+ }
+
+ acceptsGzip = false;
+ keepAliveRequested = false;
+ requestHeaders = cast(immutable) _headers;
+
+ cookie = _cookie;
+ cookiesArray = getCookieArray();
+ cookies = keepLastOf(cookiesArray);
+
+ queryString = _queryString;
+ getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
+ get = keepLastOf(getArray);
+
+ postArray = cast(immutable) _post;
+ post = keepLastOf(_post);
+
+ // FIXME
+ filesArray = null;
+ files = null;
+
+ isCalledWithCommandLineArguments = true;
+
+ this.port = port;
+ this.referrer = referrer;
+ this.remoteAddress = remoteAddress;
+ this.userAgent = userAgent;
+ this.authorization = authorization;
+ this.origin = origin;
+ this.accept = accept;
+ this.lastEventId = lastEventId;
+ this.https = https;
+ this.host = host;
+ this.requestMethod = requestMethod;
+ this.requestUri = requestUri;
+ this.pathInfo = pathInfo;
+ this.queryString = queryString;
+ this.postBody = null;
+ }
+
+ private {
+ string[] allPostNamesInOrder;
+ string[] allPostValuesInOrder;
+ string[] allGetNamesInOrder;
+ string[] allGetValuesInOrder;
+ }
+
+ CgiConnectionHandle getOutputFileHandle() {
+ return _outputFileHandle;
+ }
+
+ CgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE;
+
+ /** Initializes it using a CGI or CGI-like interface */
+ this(long maxContentLength = defaultMaxContentLength,
+ // use this to override the environment variable listing
+ in string[string] env = null,
+ // and this should return a chunk of data. return empty when done
+ const(ubyte)[] delegate() readdata = null,
+ // finally, use this to do custom output if needed
+ void delegate(const(ubyte)[]) _rawDataOutput = null,
+ // to flush teh custom output
+ void delegate() _flush = null
+ )
+ {
+
+ // these are all set locally so the loop works
+ // without triggering errors in dmd 2.064
+ // we go ahead and set them at the end of it to the this version
+ int port;
+ string referrer;
+ string remoteAddress;
+ string userAgent;
+ string authorization;
+ string origin;
+ string accept;
+ string lastEventId;
+ bool https;
+ string host;
+ RequestMethod requestMethod;
+ string requestUri;
+ string pathInfo;
+ string queryString;
+
+
+
+ isCalledWithCommandLineArguments = false;
+ rawDataOutput = _rawDataOutput;
+ flushDelegate = _flush;
+ auto getenv = delegate string(string var) {
+ if(env is null)
+ return std.process.environment.get(var);
+ auto e = var in env;
+ if(e is null)
+ return null;
+ return *e;
+ };
+
+ environmentVariables = env is null ?
+ cast(const) environment.toAA :
+ env;
+
+ // fetching all the request headers
+ string[string] requestHeadersHere;
+ foreach(k, v; env is null ? cast(const) environment.toAA() : env) {
+ if(k.startsWith("HTTP_")) {
+ requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v;
+ }
+ }
+
+ this.requestHeaders = assumeUnique(requestHeadersHere);
+
+ requestUri = getenv("REQUEST_URI");
+
+ cookie = getenv("HTTP_COOKIE");
+ cookiesArray = getCookieArray();
+ cookies = keepLastOf(cookiesArray);
+
+ referrer = getenv("HTTP_REFERER");
+ userAgent = getenv("HTTP_USER_AGENT");
+ remoteAddress = getenv("REMOTE_ADDR");
+ host = getenv("HTTP_HOST");
+ pathInfo = getenv("PATH_INFO");
+
+ queryString = getenv("QUERY_STRING");
+ scriptName = getenv("SCRIPT_NAME");
+ {
+ import core.runtime;
+ auto sfn = getenv("SCRIPT_FILENAME");
+ scriptFileName = sfn.length ? sfn : Runtime.args[0];
+ }
+
+ bool iis = false;
+
+ // Because IIS doesn't pass requestUri, we simulate it here if it's empty.
+ if(requestUri.length == 0) {
+ // IIS sometimes includes the script name as part of the path info - we don't want that
+ if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName))
+ pathInfo = pathInfo[scriptName.length .. $];
+
+ requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : "");
+
+ iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339
+
+ // FIXME: this works for apache and iis... but what about others?
+ }
+
+
+ auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
+ getArray = assumeUnique(ugh);
+ get = keepLastOf(getArray);
+
+
+ // NOTE: on shitpache, you need to specifically forward this
+ authorization = getenv("HTTP_AUTHORIZATION");
+ // this is a hack because Apache is a shitload of fuck and
+ // refuses to send the real header to us. Compatible
+ // programs should send both the standard and X- versions
+
+ // NOTE: if you have access to .htaccess or httpd.conf, you can make this
+ // unnecessary with mod_rewrite, so it is commented
+
+ //if(authorization.length == 0) // if the std is there, use it
+ // authorization = getenv("HTTP_X_AUTHORIZATION");
+
+ // the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong
+ if(getenv("SERVER_PORT").length && getenv("REDIRECT_HTTPS") != "on")
+ port = to!int(getenv("SERVER_PORT"));
+ else
+ port = 0; // this was probably called from the command line
+
+ auto ae = getenv("HTTP_ACCEPT_ENCODING");
+ if(ae.length && ae.indexOf("gzip") != -1)
+ acceptsGzip = true;
+
+ accept = getenv("HTTP_ACCEPT");
+ lastEventId = getenv("HTTP_LAST_EVENT_ID");
+
+ auto ka = getenv("HTTP_CONNECTION");
+ if(ka.length && ka.asLowerCase().canFind("keep-alive"))
+ keepAliveRequested = true;
+
+ auto or = getenv("HTTP_ORIGIN");
+ origin = or;
+
+ auto rm = getenv("REQUEST_METHOD");
+ if(rm.length)
+ requestMethod = to!RequestMethod(getenv("REQUEST_METHOD"));
+ else
+ requestMethod = RequestMethod.CommandLine;
+
+ // FIXME: hack on REDIRECT_HTTPS; this is there because the work app uses mod_rewrite which loses the https flag! So I set it with [E=HTTPS=%HTTPS] or whatever but then it gets translated to here so i want it to still work. This is arguably wrong but meh.
+ https = (getenv("HTTPS") == "on" || getenv("REDIRECT_HTTPS") == "on");
+
+ // FIXME: DOCUMENT_ROOT?
+
+ // FIXME: what about PUT?
+ if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) {
+ version(preserveData) // a hack to make forwarding simpler
+ immutable(ubyte)[] data;
+ size_t amountReceived = 0;
+ auto contentType = getenv("CONTENT_TYPE");
+
+ // FIXME: is this ever not going to be set? I guess it depends
+ // on if the server de-chunks and buffers... seems like it has potential
+ // to be slow if they did that. The spec says it is always there though.
+ // And it has worked reliably for me all year in the live environment,
+ // but some servers might be different.
+ auto contentLength = to!size_t(getenv("CONTENT_LENGTH"));
+
+ immutable originalContentLength = contentLength;
+ if(contentLength) {
+ if(maxContentLength > 0 && contentLength > maxContentLength) {
+ setResponseStatus("413 Request entity too large");
+ write("You tried to upload a file that is too large.");
+ close();
+ throw new Exception("POST too large");
+ }
+ prepareForIncomingDataChunks(contentType, contentLength);
+
+
+ int processChunk(in ubyte[] chunk) {
+ if(chunk.length > contentLength) {
+ handleIncomingDataChunk(chunk[0..contentLength]);
+ amountReceived += contentLength;
+ contentLength = 0;
+ return 1;
+ } else {
+ handleIncomingDataChunk(chunk);
+ contentLength -= chunk.length;
+ amountReceived += chunk.length;
+ }
+ if(contentLength == 0)
+ return 1;
+
+ onRequestBodyDataReceived(amountReceived, originalContentLength);
+ return 0;
+ }
+
+
+ if(readdata is null) {
+ foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096))
+ if(processChunk(chunk))
+ break;
+ } else {
+ // we have a custom data source..
+ auto chunk = readdata();
+ while(chunk.length) {
+ if(processChunk(chunk))
+ break;
+ chunk = readdata();
+ }
+ }
+
+ onRequestBodyDataReceived(amountReceived, originalContentLength);
+ postArray = assumeUnique(pps._post);
+ filesArray = assumeUnique(pps._files);
+ files = keepLastOf(filesArray);
+ post = keepLastOf(postArray);
+ this.postBody = pps.postBody;
+ cleanUpPostDataState();
+ }
+
+ version(preserveData)
+ originalPostData = data;
+ }
+ // fixme: remote_user script name
+
+
+ this.port = port;
+ this.referrer = referrer;
+ this.remoteAddress = remoteAddress;
+ this.userAgent = userAgent;
+ this.authorization = authorization;
+ this.origin = origin;
+ this.accept = accept;
+ this.lastEventId = lastEventId;
+ this.https = https;
+ this.host = host;
+ this.requestMethod = requestMethod;
+ this.requestUri = requestUri;
+ this.pathInfo = pathInfo;
+ this.queryString = queryString;
+ }
+
+ /// Cleans up any temporary files. Do not use the object
+ /// after calling this.
+ ///
+ /// NOTE: it is called automatically by GenericMain
+ // FIXME: this should be called if the constructor fails too, if it has created some garbage...
+ void dispose() {
+ foreach(file; files) {
+ if(!file.contentInMemory)
+ if(std.file.exists(file.contentFilename))
+ std.file.remove(file.contentFilename);
+ }
+ }
+
+ private {
+ struct PostParserState {
+ string contentType;
+ string boundary;
+ string localBoundary; // the ones used at the end or something lol
+ bool isMultipart;
+ bool needsSavedBody;
+
+ ulong expectedLength;
+ ulong contentConsumed;
+ immutable(ubyte)[] buffer;
+
+ // multipart parsing state
+ int whatDoWeWant;
+ bool weHaveAPart;
+ string[] thisOnesHeaders;
+ immutable(ubyte)[] thisOnesData;
+
+ string postBody;
+
+ UploadedFile piece;
+ bool isFile = false;
+
+ size_t memoryCommitted;
+
+ // do NOT keep mutable references to these anywhere!
+ // I assume they are unique in the constructor once we're all done getting data.
+ string[][string] _post;
+ UploadedFile[][string] _files;
+ }
+
+ PostParserState pps;
+ }
+
+ /// This represents a file the user uploaded via a POST request.
+ static struct UploadedFile {
+ /// If you want to create one of these structs for yourself from some data,
+ /// use this function.
+ static UploadedFile fromData(immutable(void)[] data, string name = null) {
+ Cgi.UploadedFile f;
+ f.filename = name;
+ f.content = cast(immutable(ubyte)[]) data;
+ f.contentInMemory = true;
+ return f;
+ }
+
+ string name; /// The name of the form element.
+ string filename; /// The filename the user set.
+ string contentType; /// The MIME type the user's browser reported. (Not reliable.)
+
+ /**
+ For small files, cgi.d will buffer the uploaded file in memory, and make it
+ directly accessible to you through the content member. I find this very convenient
+ and somewhat efficient, since it can avoid hitting the disk entirely. (I
+ often want to inspect and modify the file anyway!)
+
+ I find the file is very large, it is undesirable to eat that much memory just
+ for a file buffer. In those cases, if you pass a large enough value for maxContentLength
+ to the constructor so they are accepted, cgi.d will write the content to a temporary
+ file that you can re-read later.
+
+ You can override this behavior by subclassing Cgi and overriding the protected
+ handlePostChunk method. Note that the object is not initialized when you
+ write that method - the http headers are available, but the cgi.post method
+ is not. You may parse the file as it streams in using this method.
+
+
+ Anyway, if the file is small enough to be in memory, contentInMemory will be
+ set to true, and the content is available in the content member.
+
+ If not, contentInMemory will be set to false, and the content saved in a file,
+ whose name will be available in the contentFilename member.
+
+
+ Tip: if you know you are always dealing with small files, and want the convenience
+ of ignoring this member, construct Cgi with a small maxContentLength. Then, if
+ a large file comes in, it simply throws an exception (and HTTP error response)
+ instead of trying to handle it.
+
+ The default value of maxContentLength in the constructor is for small files.
+ */
+ bool contentInMemory = true; // the default ought to always be true
+ immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true
+ string contentFilename; /// the file where we dumped the content, if contentInMemory == false. Note that if you want to keep it, you MUST move the file, since otherwise it is considered garbage when cgi is disposed.
+
+ ///
+ ulong fileSize() {
+ if(contentInMemory)
+ return content.length;
+ import std.file;
+ return std.file.getSize(contentFilename);
+
+ }
+
+ ///
+ void writeToFile(string filenameToSaveTo) const {
+ import std.file;
+ if(contentInMemory)
+ std.file.write(filenameToSaveTo, content);
+ else
+ std.file.rename(contentFilename, filenameToSaveTo);
+ }
+ }
+
+ // given a content type and length, decide what we're going to do with the data..
+ protected void prepareForIncomingDataChunks(string contentType, ulong contentLength) {
+ pps.expectedLength = contentLength;
+
+ auto terminator = contentType.indexOf(";");
+ if(terminator == -1)
+ terminator = contentType.length;
+
+ pps.contentType = contentType[0 .. terminator];
+ auto b = contentType[terminator .. $];
+ if(b.length) {
+ auto idx = b.indexOf("boundary=");
+ if(idx != -1) {
+ pps.boundary = b[idx + "boundary=".length .. $];
+ pps.localBoundary = "\r\n--" ~ pps.boundary;
+ }
+ }
+
+ // while a content type SHOULD be sent according to the RFC, it is
+ // not required. We're told we SHOULD guess by looking at the content
+ // but it seems to me that this only happens when it is urlencoded.
+ if(pps.contentType == "application/x-www-form-urlencoded" || pps.contentType == "") {
+ pps.isMultipart = false;
+ pps.needsSavedBody = false;
+ } else if(pps.contentType == "multipart/form-data") {
+ pps.isMultipart = true;
+ enforce(pps.boundary.length, "no boundary");
+ } else if(pps.contentType == "text/xml") { // FIXME: could this be special and load the post params
+ // save the body so the application can handle it
+ pps.isMultipart = false;
+ pps.needsSavedBody = true;
+ } else if(pps.contentType == "application/json") { // FIXME: this could prolly try to load post params too
+ // save the body so the application can handle it
+ pps.needsSavedBody = true;
+ pps.isMultipart = false;
+ } else {
+ // the rest is 100% handled by the application. just save the body and send it to them
+ pps.needsSavedBody = true;
+ pps.isMultipart = false;
+ }
+ }
+
+ // handles streaming POST data. If you handle some other content type, you should
+ // override this. If the data isn't the content type you want, you ought to call
+ // super.handleIncomingDataChunk so regular forms and files still work.
+
+ // FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the
+ // file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network
+ // input anyway, so I'm not going to get too worked up about it right now.
+ protected void handleIncomingDataChunk(const(ubyte)[] chunk) {
+ if(chunk.length == 0)
+ return;
+ assert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so
+ // if we're passed big chunks, it might throw unnecessarily.
+ // just pass it smaller chunks at a time.
+ if(pps.isMultipart) {
+ // multipart/form-data
+
+
+ // FIXME: this might want to be factored out and factorized
+ // need to make sure the stream hooks actually work.
+ void pieceHasNewContent() {
+ // we just grew the piece's buffer. Do we have to switch to file backing?
+ if(pps.piece.contentInMemory) {
+ if(pps.piece.content.length <= 10 * 1024 * 1024)
+ // meh, I'm ok with it.
+ return;
+ else {
+ // this is too big.
+ if(!pps.isFile)
+ throw new Exception("Request entity too large"); // a variable this big is kinda ridiculous, just reject it.
+ else {
+ // a file this large is probably acceptable though... let's use a backing file.
+ pps.piece.contentInMemory = false;
+ // FIXME: say... how do we intend to delete these things? cgi.dispose perhaps.
+
+ int count = 0;
+ pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count);
+ // odds are this loop will never be entered, but we want it just in case.
+ while(std.file.exists(pps.piece.contentFilename)) {
+ count++;
+ pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count);
+ }
+ // I hope this creates the file pretty quickly, or the loop might be useless...
+ // FIXME: maybe I should write some kind of custom transaction here.
+ std.file.write(pps.piece.contentFilename, pps.piece.content);
+
+ pps.piece.content = null;
+ }
+ }
+ } else {
+ // it's already in a file, so just append it to what we have
+ if(pps.piece.content.length) {
+ // FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk...
+ std.file.append(pps.piece.contentFilename, pps.piece.content);
+ pps.piece.content = null;
+ }
+ }
+ }
+
+
+ void commitPart() {
+ if(!pps.weHaveAPart)
+ return;
+
+ pieceHasNewContent(); // be sure the new content is handled every time
+
+ if(pps.isFile) {
+ // I'm not sure if other environments put files in post or not...
+ // I used to not do it, but I think I should, since it is there...
+ pps._post[pps.piece.name] ~= pps.piece.filename;
+ pps._files[pps.piece.name] ~= pps.piece;
+
+ allPostNamesInOrder ~= pps.piece.name;
+ allPostValuesInOrder ~= pps.piece.filename;
+ } else {
+ pps._post[pps.piece.name] ~= cast(string) pps.piece.content;
+
+ allPostNamesInOrder ~= pps.piece.name;
+ allPostValuesInOrder ~= cast(string) pps.piece.content;
+ }
+
+ /*
+ stderr.writeln("RECEIVED: ", pps.piece.name, "=",
+ pps.piece.content.length < 1000
+ ?
+ to!string(pps.piece.content)
+ :
+ "too long");
+ */
+
+ // FIXME: the limit here
+ pps.memoryCommitted += pps.piece.content.length;
+
+ pps.weHaveAPart = false;
+ pps.whatDoWeWant = 1;
+ pps.thisOnesHeaders = null;
+ pps.thisOnesData = null;
+
+ pps.piece = UploadedFile.init;
+ pps.isFile = false;
+ }
+
+ void acceptChunk() {
+ pps.buffer ~= chunk;
+ chunk = null; // we've consumed it into the buffer, so keeping it just brings confusion
+ }
+
+ immutable(ubyte)[] consume(size_t howMuch) {
+ pps.contentConsumed += howMuch;
+ auto ret = pps.buffer[0 .. howMuch];
+ pps.buffer = pps.buffer[howMuch .. $];
+ return ret;
+ }
+
+ dataConsumptionLoop: do {
+ switch(pps.whatDoWeWant) {
+ default: assert(0);
+ case 0:
+ acceptChunk();
+ // the format begins with two extra leading dashes, then we should be at the boundary
+ if(pps.buffer.length < 2)
+ return;
+ assert(pps.buffer[0] == '-', "no leading dash");
+ consume(1);
+ assert(pps.buffer[0] == '-', "no second leading dash");
+ consume(1);
+
+ pps.whatDoWeWant = 1;
+ goto case 1;
+ /* fallthrough */
+ case 1: // looking for headers
+ // here, we should be lined up right at the boundary, which is followed by a \r\n
+
+ // want to keep the buffer under control in case we're under attack
+ //stderr.writeln("here once");
+ //if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really....
+ // throw new Exception("wtf is up with the huge mime part headers");
+
+ acceptChunk();
+
+ if(pps.buffer.length < pps.boundary.length)
+ return; // not enough data, since there should always be a boundary here at least
+
+ if(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) {
+ assert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \r\n
+ // we *should* be at the end here!
+ assert(pps.buffer[0] == '-');
+ consume(1);
+ assert(pps.buffer[0] == '-');
+ consume(1);
+
+ // the message is terminated by --BOUNDARY--\r\n (after a \r\n leading to the boundary)
+ assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary,
+ "not lined up on boundary " ~ pps.boundary);
+ consume(pps.boundary.length);
+
+ assert(pps.buffer[0] == '-');
+ consume(1);
+ assert(pps.buffer[0] == '-');
+ consume(1);
+
+ assert(pps.buffer[0] == '\r');
+ consume(1);
+ assert(pps.buffer[0] == '\n');
+ consume(1);
+
+ assert(pps.buffer.length == 0);
+ assert(pps.contentConsumed == pps.expectedLength);
+ break dataConsumptionLoop; // we're done!
+ } else {
+ // we're not done yet. We should be lined up on a boundary.
+
+ // But, we want to ensure the headers are here before we consume anything!
+ auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n");
+ if(headerEndLocation == -1)
+ return; // they *should* all be here, so we can handle them all at once.
+
+ assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary,
+ "not lined up on boundary " ~ pps.boundary);
+
+ consume(pps.boundary.length);
+ // the boundary is always followed by a \r\n
+ assert(pps.buffer[0] == '\r');
+ consume(1);
+ assert(pps.buffer[0] == '\n');
+ consume(1);
+ }
+
+ // re-running since by consuming the boundary, we invalidate the old index.
+ auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n");
+ assert(headerEndLocation >= 0, "no header");
+ auto thisOnesHeaders = pps.buffer[0..headerEndLocation];
+
+ consume(headerEndLocation + 4); // The +4 is the \r\n\r\n that caps it off
+
+ pps.thisOnesHeaders = split(cast(string) thisOnesHeaders, "\r\n");
+
+ // now we'll parse the headers
+ foreach(h; pps.thisOnesHeaders) {
+ auto p = h.indexOf(":");
+ assert(p != -1, "no colon in header, got " ~ to!string(pps.thisOnesHeaders));
+ string hn = h[0..p];
+ string hv = h[p+2..$];
+
+ switch(hn.toLower) {
+ default: assert(0);
+ case "content-disposition":
+ auto info = hv.split("; ");
+ foreach(i; info[1..$]) { // skipping the form-data
+ auto o = i.split("="); // FIXME
+ string pn = o[0];
+ string pv = o[1][1..$-1];
+
+ if(pn == "name") {
+ pps.piece.name = pv;
+ } else if (pn == "filename") {
+ pps.piece.filename = pv;
+ pps.isFile = true;
+ }
+ }
+ break;
+ case "content-type":
+ pps.piece.contentType = hv;
+ break;
+ }
+ }
+
+ pps.whatDoWeWant++; // move to the next step - the data
+ break;
+ case 2:
+ // when we get here, pps.buffer should contain our first chunk of data
+
+ if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much
+ throw new Exception("wtf is up with the huge mime part buffer");
+
+ acceptChunk();
+
+ // so the trick is, we want to process all the data up to the boundary,
+ // but what if the chunk's end cuts the boundary off? If we're unsure, we
+ // want to wait for the next chunk. We start by looking for the whole boundary
+ // in the buffer somewhere.
+
+ auto boundaryLocation = locationOf(pps.buffer, pps.localBoundary);
+ // assert(boundaryLocation != -1, "should have seen "~to!string(cast(ubyte[]) pps.localBoundary)~" in " ~ to!string(pps.buffer));
+ if(boundaryLocation != -1) {
+ // this is easy - we can see it in it's entirety!
+
+ pps.piece.content ~= consume(boundaryLocation);
+
+ assert(pps.buffer[0] == '\r');
+ consume(1);
+ assert(pps.buffer[0] == '\n');
+ consume(1);
+ assert(pps.buffer[0] == '-');
+ consume(1);
+ assert(pps.buffer[0] == '-');
+ consume(1);
+ // the boundary here is always preceded by \r\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off.
+ pps.weHaveAPart = true;
+ pps.whatDoWeWant = 1; // back to getting headers for the next part
+
+ commitPart(); // we're done here
+ } else {
+ // we can't see the whole thing, but what if there's a partial boundary?
+
+ enforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line...
+ assert(pps.localBoundary.length > 1); // should already be sane but just in case
+ bool potentialBoundaryFound = false;
+
+ boundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) {
+ // we grow the boundary a bit each time. If we think it looks the
+ // same, better pull another chunk to be sure it's not the end.
+ // Starting small because exiting the loop early is desirable, since
+ // we're not keeping any ambiguity and 1 / 256 chance of exiting is
+ // the best we can do.
+ if(a > pps.buffer.length)
+ break; // FIXME: is this right?
+ assert(a <= pps.buffer.length);
+ assert(a > 0);
+ if(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) {
+ // ok, there *might* be a boundary here, so let's
+ // not treat the end as data yet. The rest is good to
+ // use though, since if there was a boundary there, we'd
+ // have handled it up above after locationOf.
+
+ pps.piece.content ~= pps.buffer[0 .. $ - a];
+ consume(pps.buffer.length - a);
+ pieceHasNewContent();
+ potentialBoundaryFound = true;
+ break boundaryCheck;
+ }
+ }
+
+ if(!potentialBoundaryFound) {
+ // we can consume the whole thing
+ pps.piece.content ~= pps.buffer;
+ pieceHasNewContent();
+ consume(pps.buffer.length);
+ } else {
+ // we found a possible boundary, but there was
+ // insufficient data to be sure.
+ assert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]);
+
+ return; // wait for the next chunk.
+ }
+ }
+ }
+ } while(pps.buffer.length);
+
+ // btw all boundaries except the first should have a \r\n before them
+ } else {
+ // application/x-www-form-urlencoded and application/json
+
+ // not using maxContentLength because that might be cranked up to allow
+ // large file uploads. We can handle them, but a huge post[] isn't any good.
+ if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough
+ throw new Exception("wtf is up with such a gigantic form submission????");
+
+ pps.buffer ~= chunk;
+
+ // simple handling, but it works... until someone bombs us with gigabytes of crap at least...
+ if(pps.buffer.length == pps.expectedLength) {
+ if(pps.needsSavedBody)
+ pps.postBody = cast(string) pps.buffer;
+ else
+ pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder);
+ version(preserveData)
+ originalPostData = pps.buffer;
+ } else {
+ // just for debugging
+ }
+ }
+ }
+
+ protected void cleanUpPostDataState() {
+ pps = PostParserState.init;
+ }
+
+ /// you can override this function to somehow react
+ /// to an upload in progress.
+ ///
+ /// Take note that parts of the CGI object is not yet
+ /// initialized! Stuff from HTTP headers, including get[], is usable.
+ /// But, none of post[] is usable, and you cannot write here. That's
+ /// why this method is const - mutating the object won't do much anyway.
+ ///
+ /// My idea here was so you can output a progress bar or
+ /// something to a cooperative client (see arsd.rtud for a potential helper)
+ ///
+ /// The default is to do nothing. Subclass cgi and use the
+ /// CustomCgiMain mixin to do something here.
+ void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const {
+ // This space intentionally left blank.
+ }
+
+ /// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source.
+ /// *closeConnection will be set to true if you should close the connection after handling this request
+ this(BufferedInputRange ir, bool* closeConnection) {
+ isCalledWithCommandLineArguments = false;
+ import al = std.algorithm;
+
+ immutable(ubyte)[] data;
+
+ void rdo(const(ubyte)[] d) {
+ //import std.stdio; writeln(d);
+ sendAll(ir.source, d);
+ }
+
+ auto ira = ir.source.remoteAddress();
+ auto irLocalAddress = ir.source.localAddress();
+
+ ushort port = 80;
+ if(auto ia = cast(InternetAddress) irLocalAddress) {
+ port = ia.port;
+ } else if(auto ia = cast(Internet6Address) irLocalAddress) {
+ port = ia.port;
+ }
+
+ // that check for UnixAddress is to work around a Phobos bug
+ // see: https://github.com/dlang/phobos/pull/7383
+ // but this might be more useful anyway tbh for this case
+ version(Posix)
+ this(ir, cast(UnixAddress) ira ? "unix:" : ira.toString(), port, 0, false, &rdo, null, closeConnection);
+ else
+ this(ir, ira.toString(), port, 0, false, &rdo, null, closeConnection);
+ }
+
+ /**
+ Initializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd.
+
+ NOTE: If you are behind a reverse proxy, the values here might not be what you expect.... it will use X-Forwarded-For for remote IP and X-Forwarded-Host for host
+
+ Params:
+ inputData = the incoming data, including headers and other raw http data.
+ When the constructor exits, it will leave this range exactly at the start of
+ the next request on the connection (if there is one).
+
+ address = the IP address of the remote user
+ _port = the port number of the connection
+ pathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins.
+ _https = if this connection is encrypted (note that the input data must not actually be encrypted)
+ _rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http.
+ _flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire
+ closeConnection = if the request asks to close the connection, *closeConnection == true.
+ */
+ this(
+ BufferedInputRange inputData,
+// string[] headers, immutable(ubyte)[] data,
+ string address, ushort _port,
+ int pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment
+ bool _https = false,
+ void delegate(const(ubyte)[]) _rawDataOutput = null,
+ void delegate() _flush = null,
+ // this pointer tells if the connection is supposed to be closed after we handle this
+ bool* closeConnection = null)
+ {
+ // these are all set locally so the loop works
+ // without triggering errors in dmd 2.064
+ // we go ahead and set them at the end of it to the this version
+ int port;
+ string referrer;
+ string remoteAddress;
+ string userAgent;
+ string authorization;
+ string origin;
+ string accept;
+ string lastEventId;
+ bool https;
+ string host;
+ RequestMethod requestMethod;
+ string requestUri;
+ string pathInfo;
+ string queryString;
+ string scriptName;
+ string[string] get;
+ string[][string] getArray;
+ bool keepAliveRequested;
+ bool acceptsGzip;
+ string cookie;
+
+
+
+ environmentVariables = cast(const) environment.toAA;
+
+ idlol = inputData;
+
+ isCalledWithCommandLineArguments = false;
+
+ https = _https;
+ port = _port;
+
+ rawDataOutput = _rawDataOutput;
+ flushDelegate = _flush;
+ nph = true;
+
+ remoteAddress = address;
+
+ // streaming parser
+ import al = std.algorithm;
+
+ // FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason.
+ auto idx = indexOf(cast(string) inputData.front(), "\r\n\r\n");
+ while(idx == -1) {
+ inputData.popFront(0);
+ idx = indexOf(cast(string) inputData.front(), "\r\n\r\n");
+ }
+
+ assert(idx != -1);
+
+
+ string contentType = "";
+ string[string] requestHeadersHere;
+
+ size_t contentLength;
+
+ bool isChunked;
+
+ {
+ import core.runtime;
+ scriptFileName = Runtime.args[0];
+ }
+
+
+ int headerNumber = 0;
+ foreach(line; al.splitter(inputData.front()[0 .. idx], "\r\n"))
+ if(line.length) {
+ headerNumber++;
+ auto header = cast(string) line.idup;
+ if(headerNumber == 1) {
+ // request line
+ auto parts = al.splitter(header, " ");
+ requestMethod = to!RequestMethod(parts.front);
+ parts.popFront();
+ requestUri = parts.front;
+
+ // FIXME: the requestUri could be an absolute path!!! should I rename it or something?
+ scriptName = requestUri[0 .. pathInfoStarts];
+
+ auto question = requestUri.indexOf("?");
+ if(question == -1) {
+ queryString = "";
+ // FIXME: double check, this might be wrong since it could be url encoded
+ pathInfo = requestUri[pathInfoStarts..$];
+ } else {
+ queryString = requestUri[question+1..$];
+ pathInfo = requestUri[pathInfoStarts..question];
+ }
+
+ auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
+ getArray = cast(string[][string]) assumeUnique(ugh);
+
+ if(header.indexOf("HTTP/1.0") != -1) {
+ http10 = true;
+ autoBuffer = true;
+ if(closeConnection) {
+ // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive)
+ *closeConnection = true;
+ }
+ }
+ } else {
+ // other header
+ auto colon = header.indexOf(":");
+ if(colon == -1)
+ throw new Exception("HTTP headers should have a colon!");
+ string name = header[0..colon].toLower;
+ string value = header[colon+2..$]; // skip the colon and the space
+
+ requestHeadersHere[name] = value;
+
+ if (name == "accept") {
+ accept = value;
+ }
+ else if (name == "origin") {
+ origin = value;
+ }
+ else if (name == "connection") {
+ if(value == "close" && closeConnection)
+ *closeConnection = true;
+ if(value.asLowerCase().canFind("keep-alive")) {
+ keepAliveRequested = true;
+
+ // on http 1.0, the connection is closed by default,
+ // but not if they request keep-alive. then we don't close
+ // anymore - undoing the set above
+ if(http10 && closeConnection) {
+ *closeConnection = false;
+ }
+ }
+ }
+ else if (name == "transfer-encoding") {
+ if(value == "chunked")
+ isChunked = true;
+ }
+ else if (name == "last-event-id") {
+ lastEventId = value;
+ }
+ else if (name == "authorization") {
+ authorization = value;
+ }
+ else if (name == "content-type") {
+ contentType = value;
+ }
+ else if (name == "content-length") {
+ contentLength = to!size_t(value);
+ }
+ else if (name == "x-forwarded-for") {
+ remoteAddress = value;
+ }
+ else if (name == "x-forwarded-host" || name == "host") {
+ if(name != "host" || host is null)
+ host = value;
+ }
+ // FIXME: https://tools.ietf.org/html/rfc7239
+ else if (name == "accept-encoding") {
+ if(value.indexOf("gzip") != -1)
+ acceptsGzip = true;
+ }
+ else if (name == "user-agent") {
+ userAgent = value;
+ }
+ else if (name == "referer") {
+ referrer = value;
+ }
+ else if (name == "cookie") {
+ cookie ~= value;
+ } else if(name == "expect") {
+ if(value == "100-continue") {
+ // FIXME we should probably give user code a chance
+ // to process and reject but that needs to be virtual,
+ // perhaps part of the CGI redesign.
+
+ // FIXME: if size is > max content length it should
+ // also fail at this point.
+ _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n");
+ }
+ }
+ // else
+ // ignore it
+
+ }
+ }
+
+ inputData.consume(idx + 4);
+ // done
+
+ requestHeaders = assumeUnique(requestHeadersHere);
+
+ ByChunkRange dataByChunk;
+
+ // reading Content-Length type data
+ // We need to read up the data we have, and write it out as a chunk.
+ if(!isChunked) {
+ dataByChunk = byChunk(inputData, contentLength);
+ } else {
+ // chunked requests happen, but not every day. Since we need to know
+ // the content length (for now, maybe that should change), we'll buffer
+ // the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes)
+ auto data = dechunk(inputData);
+
+ // set the range here
+ dataByChunk = byChunk(data);
+ contentLength = data.length;
+ }
+
+ assert(dataByChunk !is null);
+
+ if(contentLength) {
+ prepareForIncomingDataChunks(contentType, contentLength);
+ foreach(dataChunk; dataByChunk) {
+ handleIncomingDataChunk(dataChunk);
+ }
+ postArray = assumeUnique(pps._post);
+ filesArray = assumeUnique(pps._files);
+ files = keepLastOf(filesArray);
+ post = keepLastOf(postArray);
+ postBody = pps.postBody;
+ cleanUpPostDataState();
+ }
+
+ this.port = port;
+ this.referrer = referrer;
+ this.remoteAddress = remoteAddress;
+ this.userAgent = userAgent;
+ this.authorization = authorization;
+ this.origin = origin;
+ this.accept = accept;
+ this.lastEventId = lastEventId;
+ this.https = https;
+ this.host = host;
+ this.requestMethod = requestMethod;
+ this.requestUri = requestUri;
+ this.pathInfo = pathInfo;
+ this.queryString = queryString;
+
+ this.scriptName = scriptName;
+ this.get = keepLastOf(getArray);
+ this.getArray = cast(immutable) getArray;
+ this.keepAliveRequested = keepAliveRequested;
+ this.acceptsGzip = acceptsGzip;
+ this.cookie = cookie;
+
+ cookiesArray = getCookieArray();
+ cookies = keepLastOf(cookiesArray);
+
+ }
+ BufferedInputRange idlol;
+
+ private immutable(string[string]) keepLastOf(in string[][string] arr) {
+ string[string] ca;
+ foreach(k, v; arr)
+ ca[k] = v[$-1];
+
+ return assumeUnique(ca);
+ }
+
+ // FIXME duplication
+ private immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) {
+ UploadedFile[string] ca;
+ foreach(k, v; arr)
+ ca[k] = v[$-1];
+
+ return assumeUnique(ca);
+ }
+
+
+ private immutable(string[][string]) getCookieArray() {
+ auto forTheLoveOfGod = decodeVariables(cookie, "; ");
+ return assumeUnique(forTheLoveOfGod);
+ }
+
+ /// Very simple method to require a basic auth username and password.
+ /// If the http request doesn't include the required credentials, it throws a
+ /// HTTP 401 error, and an exception.
+ ///
+ /// Note: basic auth does not provide great security, especially over unencrypted HTTP;
+ /// the user's credentials are sent in plain text on every request.
+ ///
+ /// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the
+ /// application. Either use Apache's built in methods for basic authentication, or add
+ /// something along these lines to your server configuration:
+ ///
+ /// RewriteEngine On
+ /// RewriteCond %{HTTP:Authorization} ^(.*)
+ /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
+ ///
+ /// To ensure the necessary data is available to cgi.d.
+ void requireBasicAuth(string user, string pass, string message = null) {
+ if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) {
+ setResponseStatus("401 Authorization Required");
+ header ("WWW-Authenticate: Basic realm=\""~message~"\"");
+ close();
+ throw new Exception("Not authorized; got " ~ authorization);
+ }
+ }
+
+ /// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites.
+ /// setCache(true) means it will always be cached for as long as possible. Best for static content.
+ /// Use setResponseExpires and updateResponseExpires for more control
+ void setCache(bool allowCaching) {
+ noCache = !allowCaching;
+ }
+
+ /// Set to true and use cgi.write(data, true); to send a gzipped response to browsers
+ /// who can accept it
+ bool gzipResponse;
+
+ immutable bool acceptsGzip;
+ immutable bool keepAliveRequested;
+
+ /// Set to true if and only if this was initialized with command line arguments
+ immutable bool isCalledWithCommandLineArguments;
+
+ /// This gets a full url for the current request, including port, protocol, host, path, and query
+ string getCurrentCompleteUri() const {
+ ushort defaultPort = https ? 443 : 80;
+
+ string uri = "http";
+ if(https)
+ uri ~= "s";
+ uri ~= "://";
+ uri ~= host;
+ if(!(!port || port == defaultPort)) {
+ uri ~= ":";
+ uri ~= to!string(port);
+ }
+ uri ~= requestUri;
+ return uri;
+ }
+
+ /// You can override this if your site base url isn't the same as the script name
+ string logicalScriptName() const {
+ return scriptName;
+ }
+
+ /++
+ Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error".
+ It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation().
+ Note setResponseStatus() must be called *before* you write() any data to the output.
+
+ History:
+ The `int` overload was added on January 11, 2021.
+ +/
+ void setResponseStatus(string status) {
+ assert(!outputtedResponseData);
+ responseStatus = status;
+ }
+ /// ditto
+ void setResponseStatus(int statusCode) {
+ setResponseStatus(getHttpCodeText(statusCode));
+ }
+ private string responseStatus = null;
+
+ /// Returns true if it is still possible to output headers
+ bool canOutputHeaders() {
+ return !isClosed && !outputtedResponseData;
+ }
+
+ /// Sets the location header, which the browser will redirect the user to automatically.
+ /// Note setResponseLocation() must be called *before* you write() any data to the output.
+ /// The optional important argument is used if it's a default suggestion rather than something to insist upon.
+ void setResponseLocation(string uri, bool important = true, string status = null) {
+ if(!important && isCurrentResponseLocationImportant)
+ return; // important redirects always override unimportant ones
+
+ if(uri is null) {
+ responseStatus = "200 OK";
+ responseLocation = null;
+ isCurrentResponseLocationImportant = important;
+ return; // this just cancels the redirect
+ }
+
+ assert(!outputtedResponseData);
+ if(status is null)
+ responseStatus = "302 Found";
+ else
+ responseStatus = status;
+
+ responseLocation = uri.strip;
+ isCurrentResponseLocationImportant = important;
+ }
+ protected string responseLocation = null;
+ private bool isCurrentResponseLocationImportant = false;
+
+ /// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching
+ /// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use.
+ /// Note: the when parameter is different than setCookie's expire parameter.
+ void setResponseExpires(long when, bool isPublic = false) {
+ responseExpires = when;
+ setCache(true); // need to enable caching so the date has meaning
+
+ responseIsPublic = isPublic;
+ responseExpiresRelative = false;
+ }
+
+ /// Sets a cache-control max-age header for whenFromNow, in seconds.
+ void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) {
+ responseExpires = whenFromNow;
+ setCache(true); // need to enable caching so the date has meaning
+
+ responseIsPublic = isPublic;
+ responseExpiresRelative = true;
+ }
+ private long responseExpires = long.min;
+ private bool responseIsPublic = false;
+ private bool responseExpiresRelative = false;
+
+ /// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept.
+ /// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program
+ /// output as a whole is as cacheable as the least cachable part in the chain.
+
+ /// setCache(false) always overrides this - it is, by definition, the strictest anti-cache statement available. If your site outputs sensitive user data, you should probably call setCache(false) when you do, to ensure no other functions will cache the content, as it may be a privacy risk.
+ /// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity.
+ void updateResponseExpires(long when, bool isPublic) {
+ if(responseExpires == long.min)
+ setResponseExpires(when, isPublic);
+ else if(when < responseExpires)
+ setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is
+ }
+
+ /*
+ /// Set to true if you want the result to be cached publically - that is, is the content shared?
+ /// Should generally be false if the user is logged in. It assumes private cache only.
+ /// setCache(true) also turns on public caching, and setCache(false) sets to private.
+ void setPublicCaching(bool allowPublicCaches) {
+ publicCaching = allowPublicCaches;
+ }
+ private bool publicCaching = false;
+ */
+
+ /++
+ History:
+ Added January 11, 2021
+ +/
+ enum SameSitePolicy {
+ Lax,
+ Strict,
+ None
+ }
+
+ /++
+ Sets an HTTP cookie, automatically encoding the data to the correct string.
+ expiresIn is how many milliseconds in the future the cookie will expire.
+ TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com.
+ Note setCookie() must be called *before* you write() any data to the output.
+
+ History:
+ Parameter `sameSitePolicy` was added on January 11, 2021.
+ +/
+ void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) {
+ assert(!outputtedResponseData);
+ string cookie = std.uri.encodeComponent(name) ~ "=";
+ cookie ~= std.uri.encodeComponent(data);
+ if(path !is null)
+ cookie ~= "; path=" ~ path;
+ // FIXME: should I just be using max-age here? (also in cache below)
+ if(expiresIn != 0)
+ cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!"msecs"(expiresIn));
+ if(domain !is null)
+ cookie ~= "; domain=" ~ domain;
+ if(secure == true)
+ cookie ~= "; Secure";
+ if(httpOnly == true )
+ cookie ~= "; HttpOnly";
+ final switch(sameSitePolicy) {
+ case SameSitePolicy.Lax:
+ cookie ~= "; SameSite=Lax";
+ break;
+ case SameSitePolicy.Strict:
+ cookie ~= "; SameSite=Strict";
+ break;
+ case SameSitePolicy.None:
+ cookie ~= "; SameSite=None";
+ assert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
+ break;
+ }
+
+ if(auto idx = name in cookieIndexes) {
+ responseCookies[*idx] = cookie;
+ } else {
+ cookieIndexes[name] = responseCookies.length;
+ responseCookies ~= cookie;
+ }
+ }
+ private string[] responseCookies;
+ private size_t[string] cookieIndexes;
+
+ /// Clears a previously set cookie with the given name, path, and domain.
+ void clearCookie(string name, string path = null, string domain = null) {
+ assert(!outputtedResponseData);
+ setCookie(name, "", 1, path, domain);
+ }
+
+ /// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image
+ void setResponseContentType(string ct) {
+ assert(!outputtedResponseData);
+ responseContentType = ct;
+ }
+ private string responseContentType = null;
+
+ /// Adds a custom header. It should be the name: value, but without any line terminator.
+ /// For example: header("X-My-Header: Some value");
+ /// Note you should use the specialized functions in this object if possible to avoid
+ /// duplicates in the output.
+ void header(string h) {
+ customHeaders ~= h;
+ }
+
+ private string[] customHeaders;
+ private bool websocketMode;
+
+ void flushHeaders(const(void)[] t, bool isAll = false) {
+ StackBuffer buffer = StackBuffer(0);
+
+ prepHeaders(t, isAll, &buffer);
+
+ if(rawDataOutput !is null)
+ rawDataOutput(cast(const(ubyte)[]) buffer.get());
+ else {
+ stdout.rawWrite(buffer.get());
+ }
+ }
+
+ private void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) {
+ string terminator = "\n";
+ if(rawDataOutput !is null)
+ terminator = "\r\n";
+
+ if(responseStatus !is null) {
+ if(nph) {
+ if(http10)
+ buffer.add("HTTP/1.0 ", responseStatus, terminator);
+ else
+ buffer.add("HTTP/1.1 ", responseStatus, terminator);
+ } else
+ buffer.add("Status: ", responseStatus, terminator);
+ } else if (nph) {
+ if(http10)
+ buffer.add("HTTP/1.0 200 OK", terminator);
+ else
+ buffer.add("HTTP/1.1 200 OK", terminator);
+ }
+
+ if(websocketMode)
+ goto websocket;
+
+ if(nph) { // we're responsible for setting the date too according to http 1.1
+ char[29] db = void;
+ printDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]);
+ buffer.add("Date: ", db[], terminator);
+ }
+
+ // FIXME: what if the user wants to set his own content-length?
+ // The custom header function can do it, so maybe that's best.
+ // Or we could reuse the isAll param.
+ if(responseLocation !is null) {
+ buffer.add("Location: ", responseLocation, terminator);
+ }
+ if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
+ if(responseExpiresRelative) {
+ buffer.add("Cache-Control: ", responseIsPublic ? "public" : "private", ", max-age=");
+ buffer.add(responseExpires);
+ buffer.add(", no-cache=\"set-cookie, set-cookie2\"", terminator);
+ } else {
+ auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
+ char[29] db = void;
+ printDateToBuffer(cast(DateTime) expires, db[]);
+ buffer.add("Expires: ", db[], terminator);
+ // FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
+ buffer.add("Cache-Control: ", (responseIsPublic ? "public" : "private"), ", no-cache=\"set-cookie, set-cookie2\"");
+ buffer.add(terminator);
+ }
+ }
+ if(responseCookies !is null && responseCookies.length > 0) {
+ foreach(c; responseCookies)
+ buffer.add("Set-Cookie: ", c, terminator);
+ }
+ if(noCache) { // we specifically do not want caching (this is actually the default)
+ buffer.add("Cache-Control: private, no-cache=\"set-cookie\"", terminator);
+ buffer.add("Expires: 0", terminator);
+ buffer.add("Pragma: no-cache", terminator);
+ } else {
+ if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever
+ buffer.add("Cache-Control: public", terminator);
+ buffer.add("Expires: Tue, 31 Dec 2030 14:00:00 GMT", terminator); // FIXME: should not be more than one year in the future
+ }
+ }
+ if(responseContentType !is null) {
+ buffer.add("Content-Type: ", responseContentType, terminator);
+ } else
+ buffer.add("Content-Type: text/html; charset=utf-8", terminator);
+
+ if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary
+ buffer.add("Content-Encoding: gzip", terminator);
+ }
+
+
+ if(!isAll) {
+ if(nph && !http10) {
+ buffer.add("Transfer-Encoding: chunked", terminator);
+ responseChunked = true;
+ }
+ } else {
+ buffer.add("Content-Length: ");
+ buffer.add(t.length);
+ buffer.add(terminator);
+ if(nph && keepAliveRequested) {
+ buffer.add("Connection: Keep-Alive", terminator);
+ }
+ }
+
+ websocket:
+
+ foreach(hd; customHeaders)
+ buffer.add(hd, terminator);
+
+ // FIXME: what about duplicated headers?
+
+ // end of header indicator
+ buffer.add(terminator);
+
+ outputtedResponseData = true;
+ }
+
+ /// Writes the data to the output, flushing headers if they have not yet been sent.
+ void write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) {
+ assert(!closed, "Output has already been closed");
+
+ StackBuffer buffer = StackBuffer(0);
+
+ if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary
+ // actually gzip the data here
+
+ auto c = new Compress(HeaderFormat.gzip); // want gzip
+
+ auto data = c.compress(t);
+ data ~= c.flush();
+
+ // std.file.write("/tmp/last-item", data);
+
+ t = data;
+ }
+
+ if(!outputtedResponseData && (!autoBuffer || isAll)) {
+ prepHeaders(t, isAll, &buffer);
+ }
+
+ if(requestMethod != RequestMethod.HEAD && t.length > 0) {
+ if (autoBuffer && !isAll) {
+ outputBuffer ~= cast(ubyte[]) t;
+ }
+ if(!autoBuffer || isAll) {
+ if(rawDataOutput !is null)
+ if(nph && responseChunked) {
+ //rawDataOutput(makeChunk(cast(const(ubyte)[]) t));
+ // we're making the chunk here instead of in a function
+ // to avoid unneeded gc pressure
+ buffer.add(toHex(t.length));
+ buffer.add("\r\n");
+ buffer.add(cast(char[]) t, "\r\n");
+ } else {
+ buffer.add(cast(char[]) t);
+ }
+ else
+ buffer.add(cast(char[]) t);
+ }
+ }
+
+ if(rawDataOutput !is null)
+ rawDataOutput(cast(const(ubyte)[]) buffer.get());
+ else
+ stdout.rawWrite(buffer.get());
+
+ if(maybeAutoClose && isAll)
+ close(); // if you say it is all, that means we're definitely done
+ // maybeAutoClose can be false though to avoid this (important if you call from inside close()!
+ }
+
+ /++
+ Convenience method to set content type to json and write the string as the complete response.
+
+ History:
+ Added January 16, 2020
+ +/
+ void writeJson(string json) {
+ this.setResponseContentType("application/json");
+ this.write(json, true);
+ }
+
+ /// Flushes the pending buffer, leaving the connection open so you can send more.
+ void flush() {
+ if(rawDataOutput is null)
+ stdout.flush();
+ else if(flushDelegate !is null)
+ flushDelegate();
+ }
+
+ version(autoBuffer)
+ bool autoBuffer = true;
+ else
+ bool autoBuffer = false;
+ ubyte[] outputBuffer;
+
+ /// Flushes the buffers to the network, signifying that you are done.
+ /// You should always call this explicitly when you are done outputting data.
+ void close() {
+ if(closed)
+ return; // don't double close
+
+ if(!outputtedResponseData)
+ write("", false, false);
+
+ // writing auto buffered data
+ if(requestMethod != RequestMethod.HEAD && autoBuffer) {
+ if(!nph)
+ stdout.rawWrite(outputBuffer);
+ else
+ write(outputBuffer, true, false); // tell it this is everything
+ }
+
+ // closing the last chunk...
+ if(nph && rawDataOutput !is null && responseChunked)
+ rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n");
+
+ if(flushDelegate)
+ flushDelegate();
+
+ closed = true;
+ }
+
+ // Closes without doing anything, shouldn't be used often
+ void rawClose() {
+ closed = true;
+ }
+
+ /++
+ Gets a request variable as a specific type, or the default value of it isn't there
+ or isn't convertible to the request type.
+
+ Checks both GET and POST variables, preferring the POST variable, if available.
+
+ A nice trick is using the default value to choose the type:
+
+ ---
+ /*
+ The return value will match the type of the default.
+ Here, I gave 10 as a default, so the return value will
+ be an int.
+
+ If the user-supplied value cannot be converted to the
+ requested type, you will get the default value back.
+ */
+ int a = cgi.request("number", 10);
+
+ if(cgi.get["number"] == "11")
+ assert(a == 11); // conversion succeeds
+
+ if("number" !in cgi.get)
+ assert(a == 10); // no value means you can't convert - give the default
+
+ if(cgi.get["number"] == "twelve")
+ assert(a == 10); // conversion from string to int would fail, so we get the default
+ ---
+
+ You can use an enum as an easy whitelist, too:
+
+ ---
+ enum Operations {
+ add, remove, query
+ }
+
+ auto op = cgi.request("op", Operations.query);
+
+ if(cgi.get["op"] == "add")
+ assert(op == Operations.add);
+ if(cgi.get["op"] == "remove")
+ assert(op == Operations.remove);
+ if(cgi.get["op"] == "query")
+ assert(op == Operations.query);
+
+ if(cgi.get["op"] == "random string")
+ assert(op == Operations.query); // the value can't be converted to the enum, so we get the default
+ ---
+ +/
+ T request(T = string)(in string name, in T def = T.init) const nothrow {
+ try {
+ return
+ (name in post) ? to!T(post[name]) :
+ (name in get) ? to!T(get[name]) :
+ def;
+ } catch(Exception e) { return def; }
+ }
+
+ /// Is the output already closed?
+ bool isClosed() const {
+ return closed;
+ }
+
+ /++
+ Gets a session object associated with the `cgi` request. You can use different type throughout your application.
+ +/
+ Session!Data getSessionObject(Data)() {
+ if(testInProcess !is null) {
+ // test mode
+ auto obj = testInProcess.getSessionOverride(typeid(typeof(return)));
+ if(obj !is null)
+ return cast(typeof(return)) obj;
+ else {
+ auto o = new MockSession!Data();
+ testInProcess.setSessionOverride(typeid(typeof(return)), o);
+ return o;
+ }
+ } else {
+ // normal operation
+ return new BasicDataServerSession!Data(this);
+ }
+ }
+
+ // if it is in test mode; triggers mock sessions. Used by CgiTester
+ version(with_breaking_cgi_features)
+ private CgiTester testInProcess;
+
+ /* Hooks for redirecting input and output */
+ private void delegate(const(ubyte)[]) rawDataOutput = null;
+ private void delegate() flushDelegate = null;
+
+ /* This info is used when handling a more raw HTTP protocol */
+ private bool nph;
+ private bool http10;
+ private bool closed;
+ private bool responseChunked = false;
+
+ version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it.
+ immutable(ubyte)[] originalPostData;
+
+ public immutable string postBody;
+ alias postJson = postBody; // old name
+
+ /* Internal state flags */
+ private bool outputtedResponseData;
+ private bool noCache = true;
+
+ const(string[string]) environmentVariables;
+
+ /** What follows is data gotten from the HTTP request. It is all fully immutable,
+ partially because it logically is (your code doesn't change what the user requested...)
+ and partially because I hate how bad programs in PHP change those superglobals to do
+ all kinds of hard to follow ugliness. I don't want that to ever happen in D.
+
+ For some of these, you'll want to refer to the http or cgi specs for more details.
+ */
+ immutable(string[string]) requestHeaders; /// All the raw headers in the request as name/value pairs. The name is stored as all lower case, but otherwise the same as it is in HTTP; words separated by dashes. For example, "cookie" or "accept-encoding". Many HTTP headers have specialized variables below for more convenience and static name checking; you should generally try to use them.
+
+ immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them.
+ immutable(char[]) origin; /// The origin header in the request, if present. Some HTML5 cross-domain apis set this and you should check it on those cross domain requests and websockets.
+ immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser.
+ immutable(char[]) pathInfo; /// This is any stuff sent after your program's name on the url, but before the query string. For example, suppose your program is named "app". If the user goes to site.com/app, pathInfo is empty. But, he can also go to site.com/app/some/sub/path; treating your program like a virtual folder. In this case, pathInfo == "/some/sub/path".
+ immutable(char[]) scriptName; /// The full base path of your program, as seen by the user. If your program is located at site.com/programs/apps, scriptName == "/programs/apps".
+ immutable(char[]) scriptFileName; /// The physical filename of your script
+ immutable(char[]) authorization; /// The full authorization string from the header, undigested. Useful for implementing auth schemes such as OAuth 1.0. Note that some web servers do not forward this to the app without taking extra steps. See requireBasicAuth's comment for more info.
+ immutable(char[]) accept; /// The HTTP accept header is the user agent telling what content types it is willing to accept. This is often */*; they accept everything, so it's not terribly useful. (The similar sounding Accept-Encoding header is handled automatically for chunking and gzipping. Simply set gzipResponse = true and cgi.d handles the details, zipping if the user's browser is willing to accept it.)
+ immutable(char[]) lastEventId; /// The HTML 5 draft includes an EventSource() object that connects to the server, and remains open to take a stream of events. My arsd.rtud module can help with the server side part of that. The Last-Event-Id http header is defined in the draft to help handle loss of connection. When the browser reconnects to you, it sets this header to the last event id it saw, so you can catch it up. This member has the contents of that header.
+
+ immutable(RequestMethod) requestMethod; /// The HTTP request verb: GET, POST, etc. It is represented as an enum in cgi.d (which, like many enums, you can convert back to string with std.conv.to()). A HTTP GET is supposed to, according to the spec, not have side effects; a user can GET something over and over again and always have the same result. On all requests, the get[] and getArray[] members may be filled in. The post[] and postArray[] members are only filled in on POST methods.
+ immutable(char[]) queryString; /// The unparsed content of the request query string - the stuff after the ? in your URL. See get[] and getArray[] for a parse view of it. Sometimes, the unparsed string is useful though if you want a custom format of data up there (probably not a good idea, unless it is really simple, like "?username" perhaps.)
+ immutable(char[]) cookie; /// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data.
+ /** The Referer header from the request. (It is misspelled in the HTTP spec, and thus the actual request and cgi specs too, but I spelled the word correctly here because that's sane. The spec's misspelling is an implementation detail.) It contains the site url that referred the user to your program; the site that linked to you, or if you're serving images, the site that has you as an image. Also, if you're in an iframe, the referrer is the site that is framing you.
+
+ Important note: if the user copy/pastes your url, this is blank, and, just like with all other user data, their browsers can also lie to you. Don't rely on it for real security.
+ */
+ immutable(char[]) referrer;
+ immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : "");
+
+ immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.)
+
+ immutable bool https; /// Was the request encrypted via https?
+ immutable int port; /// On what TCP port number did the server receive the request?
+
+ /** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */
+
+ immutable(string[string]) get; /// The data from your query string in the url, only showing the last string of each name. If you want to handle multiple values with the same name, use getArray. This only works right if the query string is x-www-form-urlencoded; the default you see on the web with name=value pairs separated by the & character.
+ immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself.
+ immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!)
+
+ /**
+ Represents user uploaded files.
+
+ When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html <form> tag attributes. The key into this array is the name attribute on your input tag, just like with other post variables. See the comments on the UploadedFile struct for more information about the data inside, including important notes on max size and content location.
+ */
+ immutable(UploadedFile[][string]) filesArray;
+ immutable(UploadedFile[string]) files;
+
+ /// Use these if you expect multiple items submitted with the same name. btw, assert(get[name] is getArray[name][$-1); should pass. Same for post and cookies.
+ /// the order of the arrays is the order the data arrives
+ immutable(string[][string]) getArray; /// like get, but an array of values per name
+ immutable(string[][string]) postArray; /// ditto for post
+ immutable(string[][string]) cookiesArray; /// ditto for cookies
+
+ // convenience function for appending to a uri without extra ?
+ // matches the name and effect of javascript's location.search property
+ string search() const {
+ if(queryString.length)
+ return "?" ~ queryString;
+ return "";
+ }
+
+ // FIXME: what about multiple files with the same name?
+ private:
+ //RequestMethod _requestMethod;
+}
+
+/// use this for testing or other isolated things when you want it to be no-ops
+Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) {
+ // we want to ignore, not use stdout
+ if(outputSink is null)
+ outputSink = delegate void(const(ubyte)[]) { };
+
+ string[string] env;
+ env["REQUEST_METHOD"] = to!string(method);
+ env["CONTENT_LENGTH"] = to!string(data.length);
+
+ auto cgi = new Cgi(
+ 0,
+ env,
+ { return data; },
+ outputSink,
+ null);
+
+ return cgi;
+}
+
+/++
+ A helper test class for request handler unittests.
++/
+version(with_breaking_cgi_features)
+class CgiTester {
+ private {
+ SessionObject[TypeInfo] mockSessions;
+ SessionObject getSessionOverride(TypeInfo ti) {
+ if(auto o = ti in mockSessions)
+ return *o;
+ else
+ return null;
+ }
+ void setSessionOverride(TypeInfo ti, SessionObject so) {
+ mockSessions[ti] = so;
+ }
+ }
+
+ /++
+ Gets (and creates if necessary) a mock session object for this test. Note
+ it will be the same one used for any test operations through this CgiTester instance.
+ +/
+ Session!Data getSessionObject(Data)() {
+ auto obj = getSessionOverride(typeid(typeof(return)));
+ if(obj !is null)
+ return cast(typeof(return)) obj;
+ else {
+ auto o = new MockSession!Data();
+ setSessionOverride(typeid(typeof(return)), o);
+ return o;
+ }
+ }
+
+ /++
+ Pass a reference to your request handler when creating the tester.
+ +/
+ this(void function(Cgi) requestHandler) {
+ this.requestHandler = requestHandler;
+ }
+
+ /++
+ You can check response information with these methods after you call the request handler.
+ +/
+ struct Response {
+ int code;
+ string[string] headers;
+ string responseText;
+ ubyte[] responseBody;
+ }
+
+ /++
+ Executes a test request on your request handler, and returns the response.
+
+ Params:
+ url = The URL to test. Should be an absolute path, but excluding domain. e.g. `"/test"`.
+ args = additional arguments. Same format as cgi's command line handler.
+ +/
+ Response GET(string url, string[] args = null) {
+ return executeTest("GET", url, args);
+ }
+ /// ditto
+ Response POST(string url, string[] args = null) {
+ return executeTest("POST", url, args);
+ }
+
+ /// ditto
+ Response executeTest(string method, string url, string[] args) {
+ ubyte[] outputtedRawData;
+ void outputSink(const(ubyte)[] data) {
+ outputtedRawData ~= data;
+ }
+ auto cgi = new Cgi(["test", method, url] ~ args, &outputSink);
+ cgi.testInProcess = this;
+ scope(exit) cgi.dispose();
+
+ requestHandler(cgi);
+
+ cgi.close();
+
+ Response response;
+
+ if(outputtedRawData.length) {
+ enum LINE = "\r\n";
+
+ auto idx = outputtedRawData.locationOf(LINE ~ LINE);
+ assert(idx != -1, to!string(outputtedRawData));
+ auto headers = cast(string) outputtedRawData[0 .. idx];
+ response.code = 200;
+ while(headers.length) {
+ auto i = headers.locationOf(LINE);
+ if(i == -1) i = cast(int) headers.length;
+
+ auto header = headers[0 .. i];
+
+ auto c = header.locationOf(":");
+ if(c != -1) {
+ auto name = header[0 .. c];
+ auto value = header[c + 2 ..$];
+
+ if(name == "Status")
+ response.code = value[0 .. value.locationOf(" ")].to!int;
+
+ response.headers[name] = value;
+ } else {
+ assert(0);
+ }
+
+ if(i != headers.length)
+ i += 2;
+ headers = headers[i .. $];
+ }
+ response.responseBody = outputtedRawData[idx + 4 .. $];
+ response.responseText = cast(string) response.responseBody;
+ }
+
+ return response;
+ }
+
+ private void function(Cgi) requestHandler;
+}
+
+
+// should this be a separate module? Probably, but that's a hassle.
+
+/// Makes a data:// uri that can be used as links in most newer browsers (IE8+).
+string makeDataUrl(string mimeType, in void[] data) {
+ auto data64 = Base64.encode(cast(const(ubyte[])) data);
+ return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64);
+}
+
+// FIXME: I don't think this class correctly decodes/encodes the individual parts
+/// Represents a url that can be broken down or built up through properties
+struct Uri {
+ alias toString this; // blargh idk a url really is a string, but should it be implicit?
+
+ // scheme//userinfo@host:port/path?query#fragment
+
+ string scheme; /// e.g. "http" in "http://example.com/"
+ string userinfo; /// the username (and possibly a password) in the uri
+ string host; /// the domain name
+ int port; /// port number, if given. Will be zero if a port was not explicitly given
+ string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html"
+ string query; /// the stuff after the ? in a uri
+ string fragment; /// the stuff after the # in a uri.
+
+ // idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility
+ // the decode ones need to keep different names anyway because we can't overload on return values...
+ static string encode(string s) { return std.uri.encodeComponent(s); }
+ static string encode(string[string] s) { return encodeVariables(s); }
+ static string encode(string[][string] s) { return encodeVariables(s); }
+
+ /// Breaks down a uri string to its components
+ this(string uri) {
+ reparse(uri);
+ }
+
+ private void reparse(string uri) {
+ // from RFC 3986
+ // the ctRegex triples the compile time and makes ugly errors for no real benefit
+ // it was a nice experiment but just not worth it.
+ // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?";
+ /*
+ Captures:
+ 0 = whole url
+ 1 = scheme, with :
+ 2 = scheme, no :
+ 3 = authority, with //
+ 4 = authority, no //
+ 5 = path
+ 6 = query string, with ?
+ 7 = query string, no ?
+ 8 = anchor, with #
+ 9 = anchor, no #
+ */
+ // Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer!
+ // instead, I will DIY and cut that down to 0.6s on the same computer.
+ /*
+
+ Note that authority is
+ user:password@domain:port
+ where the user:password@ part is optional, and the :port is optional.
+
+ Regex translation:
+
+ Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first.
+ Authority must start with //, but cannot have any other /, ?, or # in it. It is optional.
+ Path cannot have any ? or # in it. It is optional.
+ Query must start with ? and must not have # in it. It is optional.
+ Anchor must start with # and can have anything else in it to end of string. It is optional.
+ */
+
+ this = Uri.init; // reset all state
+
+ // empty uri = nothing special
+ if(uri.length == 0) {
+ return;
+ }
+
+ size_t idx;
+
+ scheme_loop: foreach(char c; uri[idx .. $]) {
+ switch(c) {
+ case ':':
+ case '/':
+ case '?':
+ case '#':
+ break scheme_loop;
+ default:
+ }
+ idx++;
+ }
+
+ if(idx == 0 && uri[idx] == ':') {
+ // this is actually a path! we skip way ahead
+ goto path_loop;
+ }
+
+ if(idx == uri.length) {
+ // the whole thing is a path, apparently
+ path = uri;
+ return;
+ }
+
+ if(idx > 0 && uri[idx] == ':') {
+ scheme = uri[0 .. idx];
+ idx++;
+ } else {
+ // we need to rewind; it found a / but no :, so the whole thing is prolly a path...
+ idx = 0;
+ }
+
+ if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") {
+ // we have an authority....
+ idx += 2;
+
+ auto authority_start = idx;
+ authority_loop: foreach(char c; uri[idx .. $]) {
+ switch(c) {
+ case '/':
+ case '?':
+ case '#':
+ break authority_loop;
+ default:
+ }
+ idx++;
+ }
+
+ auto authority = uri[authority_start .. idx];
+
+ auto idx2 = authority.indexOf("@");
+ if(idx2 != -1) {
+ userinfo = authority[0 .. idx2];
+ authority = authority[idx2 + 1 .. $];
+ }
+
+ if(authority.length && authority[0] == '[') {
+ // ipv6 address special casing
+ idx2 = authority.indexOf(']');
+ if(idx2 != -1) {
+ auto end = authority[idx2 + 1 .. $];
+ if(end.length && end[0] == ':')
+ idx2 = idx2 + 1;
+ else
+ idx2 = -1;
+ }
+ } else {
+ idx2 = authority.indexOf(":");
+ }
+
+ if(idx2 == -1) {
+ port = 0; // 0 means not specified; we should use the default for the scheme
+ host = authority;
+ } else {
+ host = authority[0 .. idx2];
+ port = to!int(authority[idx2 + 1 .. $]);
+ }
+ }
+
+ path_loop:
+ auto path_start = idx;
+
+ foreach(char c; uri[idx .. $]) {
+ if(c == '?' || c == '#')
+ break;
+ idx++;
+ }
+
+ path = uri[path_start .. idx];
+
+ if(idx == uri.length)
+ return; // nothing more to examine...
+
+ if(uri[idx] == '?') {
+ idx++;
+ auto query_start = idx;
+ foreach(char c; uri[idx .. $]) {
+ if(c == '#')
+ break;
+ idx++;
+ }
+ query = uri[query_start .. idx];
+ }
+
+ if(idx < uri.length && uri[idx] == '#') {
+ idx++;
+ fragment = uri[idx .. $];
+ }
+
+ // uriInvalidated = false;
+ }
+
+ private string rebuildUri() const {
+ string ret;
+ if(scheme.length)
+ ret ~= scheme ~ ":";
+ if(userinfo.length || host.length)
+ ret ~= "//";
+ if(userinfo.length)
+ ret ~= userinfo ~ "@";
+ if(host.length)
+ ret ~= host;
+ if(port)
+ ret ~= ":" ~ to!string(port);
+
+ ret ~= path;
+
+ if(query.length)
+ ret ~= "?" ~ query;
+
+ if(fragment.length)
+ ret ~= "#" ~ fragment;
+
+ // uri = ret;
+ // uriInvalidated = false;
+ return ret;
+ }
+
+ /// Converts the broken down parts back into a complete string
+ string toString() const {
+ // if(uriInvalidated)
+ return rebuildUri();
+ }
+
+ /// Returns a new absolute Uri given a base. It treats this one as
+ /// relative where possible, but absolute if not. (If protocol, domain, or
+ /// other info is not set, the new one inherits it from the base.)
+ ///
+ /// Browsers use a function like this to figure out links in html.
+ Uri basedOn(in Uri baseUrl) const {
+ Uri n = this; // copies
+ // n.uriInvalidated = true; // make sure we regenerate...
+
+ // userinfo is not inherited... is this wrong?
+
+ // if anything is given in the existing url, we don't use the base anymore.
+ if(n.scheme.empty) {
+ n.scheme = baseUrl.scheme;
+ if(n.host.empty) {
+ n.host = baseUrl.host;
+ if(n.port == 0) {
+ n.port = baseUrl.port;
+ if(n.path.length > 0 && n.path[0] != '/') {
+ auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1];
+ if(b.length == 0)
+ b = "/";
+ n.path = b ~ n.path;
+ } else if(n.path.length == 0) {
+ n.path = baseUrl.path;
+ }
+ }
+ }
+ }
+
+ n.removeDots();
+
+ return n;
+ }
+
+ void removeDots() {
+ auto parts = this.path.split("/");
+ string[] toKeep;
+ foreach(part; parts) {
+ if(part == ".") {
+ continue;
+ } else if(part == "..") {
+ toKeep = toKeep[0 .. $-1];
+ continue;
+ } else {
+ toKeep ~= part;
+ }
+ }
+
+ this.path = toKeep.join("/");
+ }
+
+ unittest {
+ auto uri = Uri("test.html");
+ assert(uri.path == "test.html");
+ uri = Uri("path/1/lol");
+ assert(uri.path == "path/1/lol");
+ uri = Uri("http://me@example.com");
+ assert(uri.scheme == "http");
+ assert(uri.userinfo == "me");
+ assert(uri.host == "example.com");
+ uri = Uri("http://example.com/#a");
+ assert(uri.scheme == "http");
+ assert(uri.host == "example.com");
+ assert(uri.fragment == "a");
+ uri = Uri("#foo");
+ assert(uri.fragment == "foo");
+ uri = Uri("?lol");
+ assert(uri.query == "lol");
+ uri = Uri("#foo?lol");
+ assert(uri.fragment == "foo?lol");
+ uri = Uri("?lol#foo");
+ assert(uri.fragment == "foo");
+ assert(uri.query == "lol");
+
+ uri = Uri("http://127.0.0.1/");
+ assert(uri.host == "127.0.0.1");
+ assert(uri.port == 0);
+
+ uri = Uri("http://127.0.0.1:123/");
+ assert(uri.host == "127.0.0.1");
+ assert(uri.port == 123);
+
+ uri = Uri("http://[ff:ff::0]/");
+ assert(uri.host == "[ff:ff::0]");
+
+ uri = Uri("http://[ff:ff::0]:123/");
+ assert(uri.host == "[ff:ff::0]");
+ assert(uri.port == 123);
+ }
+
+ // This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover
+ // the possibilities.
+ unittest {
+ auto url = Uri("cool.html"); // checking relative links
+
+ assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html");
+ assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html");
+ assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
+
+ url = Uri("/something/cool.html"); // same server, different path
+ assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
+
+ url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment
+ assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer");
+ assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer");
+ assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer");
+ assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer");
+ assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer");
+ assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
+
+ url = Uri("/test/bar");
+ assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url));
+ assert(Uri("../").basedOn(url) == "/");
+
+ //auto uriBefore = url;
+ url = Uri("#anchor"); // everything should remain the same except the anchor
+ //uriBefore.anchor = "anchor");
+ //assert(url == uriBefore);
+
+ url = Uri("//example.com"); // same protocol, but different server. the path here should be blank.
+
+ url = Uri("//example.com/example.html"); // same protocol, but different server and path
+
+ url = Uri("http://example.com/test.html"); // completely absolute link should never be modified
+
+ url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path
+
+ // FIXME: add something for port too
+ }
+
+ // these are like javascript's location.search and location.hash
+ string search() const {
+ return query.length ? ("?" ~ query) : "";
+ }
+ string hash() const {
+ return fragment.length ? ("#" ~ fragment) : "";
+ }
+}
+
+
+/*
+ for session, see web.d
+*/
+
+/// breaks down a url encoded string
+string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) {
+ auto vars = data.split(separator);
+ string[][string] _get;
+ foreach(var; vars) {
+ auto equal = var.indexOf("=");
+ string name;
+ string value;
+ if(equal == -1) {
+ name = decodeComponent(var);
+ value = "";
+ } else {
+ //_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
+ // stupid + -> space conversion.
+ name = decodeComponent(var[0..equal].replace("+", " "));
+ value = decodeComponent(var[equal + 1 .. $].replace("+", " "));
+ }
+
+ _get[name] ~= value;
+ if(namesInOrder)
+ (*namesInOrder) ~= name;
+ if(valuesInOrder)
+ (*valuesInOrder) ~= value;
+ }
+ return _get;
+}
+
+/// breaks down a url encoded string, but only returns the last value of any array
+string[string] decodeVariablesSingle(string data) {
+ string[string] va;
+ auto varArray = decodeVariables(data);
+ foreach(k, v; varArray)
+ va[k] = v[$-1];
+
+ return va;
+}
+
+/// url encodes the whole string
+string encodeVariables(in string[string] data) {
+ string ret;
+
+ bool outputted = false;
+ foreach(k, v; data) {
+ if(outputted)
+ ret ~= "&";
+ else
+ outputted = true;
+
+ ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
+ }
+
+ return ret;
+}
+
+/// url encodes a whole string
+string encodeVariables(in string[][string] data) {
+ string ret;
+
+ bool outputted = false;
+ foreach(k, arr; data) {
+ foreach(v; arr) {
+ if(outputted)
+ ret ~= "&";
+ else
+ outputted = true;
+ ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
+ }
+ }
+
+ return ret;
+}
+
+/// Encodes all but the explicitly unreserved characters per rfc 3986
+/// Alphanumeric and -_.~ are the only ones left unencoded
+/// name is borrowed from php
+string rawurlencode(in char[] data) {
+ string ret;
+ ret.reserve(data.length * 2);
+ foreach(char c; data) {
+ if(
+ (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ c == '-' || c == '_' || c == '.' || c == '~')
+ {
+ ret ~= c;
+ } else {
+ ret ~= '%';
+ // since we iterate on char, this should give us the octets of the full utf8 string
+ ret ~= toHexUpper(c);
+ }
+ }
+
+ return ret;
+}
+
+
+// http helper functions
+
+// for chunked responses (which embedded http does whenever possible)
+version(none) // this is moved up above to avoid making a copy of the data
+const(ubyte)[] makeChunk(const(ubyte)[] data) {
+ const(ubyte)[] ret;
+
+ ret = cast(const(ubyte)[]) toHex(data.length);
+ ret ~= cast(const(ubyte)[]) "\r\n";
+ ret ~= data;
+ ret ~= cast(const(ubyte)[]) "\r\n";
+
+ return ret;
+}
+
+string toHex(long num) {
+ string ret;
+ while(num) {
+ int v = num % 16;
+ num /= 16;
+ char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a');
+ ret ~= d;
+ }
+
+ return to!string(array(ret.retro));
+}
+
+string toHexUpper(long num) {
+ string ret;
+ while(num) {
+ int v = num % 16;
+ num /= 16;
+ char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A');
+ ret ~= d;
+ }
+
+ if(ret.length == 1)
+ ret ~= "0"; // url encoding requires two digits and that's what this function is used for...
+
+ return to!string(array(ret.retro));
+}
+
+
+// the generic mixins
+
+/// Use this instead of writing your own main
+mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) {
+ mixin CustomCgiMain!(Cgi, fun, maxContentLength);
+}
+
+private string simpleHtmlEncode(string s) {
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br />\n");
+}
+
+string messageFromException(Throwable t) {
+ string message;
+ if(t !is null) {
+ debug message = t.toString();
+ else message = "An unexpected error has occurred.";
+ } else {
+ message = "Unknown error";
+ }
+ return message;
+}
+
+string plainHttpError(bool isCgi, string type, Throwable t) {
+ auto message = messageFromException(t);
+ message = simpleHtmlEncode(message);
+
+ return format("%s %s\r\nContent-Length: %s\r\n\r\n%s",
+ isCgi ? "Status:" : "HTTP/1.0",
+ type, message.length, message);
+}
+
+// returns true if we were able to recover reasonably
+bool handleException(Cgi cgi, Throwable t) {
+ if(cgi.isClosed) {
+ // if the channel has been explicitly closed, we can't handle it here
+ return true;
+ }
+
+ if(cgi.outputtedResponseData) {
+ // the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here.
+ return false; // but I don't want to, since I don't know what condition the output is in; I don't want to inject something (nor check the content-type for that matter. So we say it was not a clean handling.
+ } else {
+ // no headers are sent, we can send a full blown error and recover
+ cgi.setCache(false);
+ cgi.setResponseContentType("text/html");
+ cgi.setResponseLocation(null); // cancel the redirect
+ cgi.setResponseStatus("500 Internal Server Error");
+ cgi.write(simpleHtmlEncode(messageFromException(t)));
+ cgi.close();
+ return true;
+ }
+}
+
+bool isCgiRequestMethod(string s) {
+ s = s.toUpper();
+ if(s == "COMMANDLINE")
+ return true;
+ foreach(member; __traits(allMembers, Cgi.RequestMethod))
+ if(s == member)
+ return true;
+ return false;
+}
+
+/// If you want to use a subclass of Cgi with generic main, use this mixin.
+mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) {
+ // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere
+ void main(string[] args) {
+ cgiMainImpl!(fun, CustomCgi, maxContentLength)(args);
+ }
+}
+
+version(embedded_httpd_processes)
+ __gshared int processPoolSize = 8;
+
+// Returns true if run. You should exit the program after that.
+bool tryAddonServers(string[] args) {
+ if(args.length > 1) {
+ // run the special separate processes if needed
+ switch(args[1]) {
+ case "--websocket-server":
+ version(with_addon_servers)
+ websocketServers[args[2]](args[3 .. $]);
+ else
+ printf("Add-on servers not compiled in.\n");
+ return true;
+ case "--websocket-servers":
+ import core.demangle;
+ version(with_addon_servers_connections)
+ foreach(k, v; websocketServers)
+ writeln(k, "\t", demangle(k));
+ return true;
+ case "--session-server":
+ version(with_addon_servers)
+ runSessionServer();
+ else
+ printf("Add-on servers not compiled in.\n");
+ return true;
+ case "--event-server":
+ version(with_addon_servers)
+ runEventServer();
+ else
+ printf("Add-on servers not compiled in.\n");
+ return true;
+ case "--timer-server":
+ version(with_addon_servers)
+ runTimerServer();
+ else
+ printf("Add-on servers not compiled in.\n");
+ return true;
+ case "--timed-jobs":
+ import core.demangle;
+ version(with_addon_servers_connections)
+ foreach(k, v; scheduledJobHandlers)
+ writeln(k, "\t", demangle(k));
+ return true;
+ case "--timed-job":
+ scheduledJobHandlers[args[2]](args[3 .. $]);
+ return true;
+ default:
+ // intentionally blank - do nothing and carry on to run normally
+ }
+ }
+ return false;
+}
+
+/// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args.
+bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) {
+ // we support command line thing for easy testing everywhere
+ // it needs to be called ./app method uri [other args...]
+ if(args.length >= 3 && isCgiRequestMethod(args[1])) {
+ Cgi cgi = new CustomCgi(args);
+ scope(exit) cgi.dispose();
+ fun(cgi);
+ cgi.close();
+ return true;
+ }
+ return false;
+}
+
+/++
+ A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] to an example of how you can use it.
+
+ History:
+ Added Sept 26, 2020 (release version 8.5).
++/
+struct RequestServer {
+ ///
+ string listeningHost = defaultListeningHost();
+ ///
+ ushort listeningPort = defaultListeningPort();
+
+ ///
+ this(string defaultHost, ushort defaultPort) {
+ this.listeningHost = defaultHost;
+ this.listeningPort = defaultPort;
+ }
+
+ ///
+ this(ushort defaultPort) {
+ listeningPort = defaultPort;
+ }
+
+ /// Reads the args into the other values.
+ void configureFromCommandLine(string[] args) {
+ bool foundPort = false;
+ bool foundHost = false;
+ bool foundUid = false;
+ bool foundGid = false;
+ foreach(arg; args) {
+ if(foundPort) {
+ listeningPort = to!ushort(arg);
+ foundPort = false;
+ }
+ if(foundHost) {
+ listeningHost = arg;
+ foundHost = false;
+ }
+ if(foundUid) {
+ privDropUserId = to!int(arg);
+ foundUid = false;
+ }
+ if(foundGid) {
+ privDropGroupId = to!int(arg);
+ foundGid = false;
+ }
+ if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host")
+ foundHost = true;
+ else if(arg == "--port" || arg == "-p" || arg == "/port" || arg == "--listening-port")
+ foundPort = true;
+ else if(arg == "--uid")
+ foundUid = true;
+ else if(arg == "--gid")
+ foundGid = true;
+ }
+ }
+
+ // FIXME: the privDropUserId/group id need to be set in here instead of global
+
+ /++
+ Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders
+
+ History:
+ Added Oct 10, 2020.
+ Example:
+
+ ---
+ import arsd.cgi;
+ void main() {
+ RequestServer server = RequestServer("127.0.0.1", 6789);
+ string oauthCode;
+ string oauthScope;
+ server.serveHttpOnce!((cgi) {
+ oauthCode = cgi.request("code");
+ oauthScope = cgi.request("scope");
+ cgi.write("Thank you, please return to the application.");
+ });
+ // use the code and scope given
+ }
+ ---
+ +/
+ void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {
+ import std.socket;
+
+ bool tcp;
+ void delegate() cleanup;
+ auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1);
+ auto connection = socket.accept();
+ doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection);
+
+ if(cleanup)
+ cleanup();
+ }
+
+ /++
+ Starts serving requests according to the current configuration.
+ +/
+ void serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {
+ version(netman_httpd) {
+ // Obsolete!
+
+ import arsd.httpd;
+ // what about forwarding the other constructor args?
+ // this probably needs a whole redoing...
+ serveHttp!CustomCgi(&fun, listeningPort);//5005);
+ return;
+ } else
+ version(embedded_httpd_processes) {
+ serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this);
+ } else
+ version(embedded_httpd_threads) {
+ serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)();
+ } else
+ version(scgi) {
+ serveScgi!(fun, CustomCgi, maxContentLength)();
+ } else
+ version(fastcgi) {
+ serveFastCgi!(fun, CustomCgi, maxContentLength)(this);
+ } else {
+ //version=plain_cgi;
+ handleCgiRequest!(fun, CustomCgi, maxContentLength)();
+ }
+ }
+
+ void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {
+ auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, fun));
+ manager.listen();
+ }
+ void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {
+ auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength));
+ manager.listen();
+ }
+
+ void stop() {
+ // FIXME
+ }
+}
+
+private int privDropUserId;
+private int privDropGroupId;
+
+// Added Jan 11, 2021
+private void dropPrivs() {
+ version(Posix) {
+ import core.sys.posix.unistd;
+
+ auto userId = privDropUserId;
+ auto groupId = privDropGroupId;
+
+ if((userId != 0 || groupId != 0) && getuid() == 0) {
+ if(groupId)
+ setgid(groupId);
+ if(userId)
+ setuid(userId);
+ }
+
+ }
+ // FIXME: Windows?
+}
+
+version(embedded_httpd_processes)
+void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) {
+ import core.sys.posix.unistd;
+ import core.sys.posix.sys.socket;
+ import core.sys.posix.netinet.in_;
+ //import std.c.linux.socket;
+
+ int sock = socket(AF_INET, SOCK_STREAM, 0);
+ if(sock == -1)
+ throw new Exception("socket");
+
+ cloexec(sock);
+
+ {
+
+ sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(params.listeningPort);
+ auto lh = params.listeningHost;
+ if(lh.length) {
+ if(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1)
+ throw new Exception("bad listening host given, please use an IP address.\nExample: --listening-host 127.0.0.1 means listen only on Localhost.\nExample: --listening-host 0.0.0.0 means listen on all interfaces.\nOr you can pass any other single numeric IPv4 address.");
+ } else
+ addr.sin_addr.s_addr = INADDR_ANY;
+
+ // HACKISH
+ int on = 1;
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof);
+ // end hack
+
+
+ if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) {
+ close(sock);
+ throw new Exception("bind");
+ }
+
+ // FIXME: if this queue is full, it will just ignore it
+ // and wait for the client to retransmit it. This is an
+ // obnoxious timeout condition there.
+ if(sock.listen(128) == -1) {
+ close(sock);
+ throw new Exception("listen");
+ }
+ dropPrivs();
+ }
+
+ version(embedded_httpd_processes_accept_after_fork) {} else {
+ int pipeReadFd;
+ int pipeWriteFd;
+
+ {
+ int[2] pipeFd;
+ if(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) {
+ import core.stdc.errno;
+ throw new Exception("pipe failed " ~ to!string(errno));
+ }
+
+ pipeReadFd = pipeFd[0];
+ pipeWriteFd = pipeFd[1];
+ }
+ }
+
+
+ int processCount;
+ pid_t newPid;
+ reopen:
+ while(processCount < processPoolSize) {
+ newPid = fork();
+ if(newPid == 0) {
+ // start serving on the socket
+ //ubyte[4096] backingBuffer;
+ for(;;) {
+ bool closeConnection;
+ uint i;
+ sockaddr addr;
+ i = addr.sizeof;
+ version(embedded_httpd_processes_accept_after_fork) {
+ int s = accept(sock, &addr, &i);
+ int opt = 1;
+ import core.sys.posix.netinet.tcp;
+ // the Cgi class does internal buffering, so disabling this
+ // helps with latency in many cases...
+ setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
+ cloexec(s);
+ } else {
+ int s;
+ auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s);
+ if(readret != s.sizeof) {
+ import core.stdc.errno;
+ throw new Exception("pipe read failed " ~ to!string(errno));
+ }
+
+ //writeln("process ", getpid(), " got socket ", s);
+ }
+
+ try {
+
+ if(s == -1)
+ throw new Exception("accept");
+
+ scope(failure) close(s);
+ //ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer;
+ auto ir = new BufferedInputRange(s);
+ //auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer);
+
+ while(!ir.empty) {
+ //ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer;
+
+ Cgi cgi;
+ try {
+ cgi = new CustomCgi(ir, &closeConnection);
+ cgi._outputFileHandle = s;
+ // if we have a single process and the browser tries to leave the connection open while concurrently requesting another, it will block everything an deadlock since there's no other server to accept it. By closing after each request in this situation, it tells the browser to serialize for us.
+ if(processPoolSize <= 1)
+ closeConnection = true;
+ //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection);
+ } catch(Throwable t) {
+ // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P
+ // anyway let's kill the connection
+ version(CRuntime_Musl) {
+ // LockingTextWriter fails here
+ // so working around it
+ auto estr = t.toString();
+ stderr.rawWrite(estr);
+ stderr.rawWrite("\n");
+ } else
+ stderr.writeln(t.toString());
+ sendAll(ir.source, plainHttpError(false, "400 Bad Request", t));
+ closeConnection = true;
+ break;
+ }
+ assert(cgi !is null);
+ scope(exit)
+ cgi.dispose();
+
+ try {
+ fun(cgi);
+ cgi.close();
+ if(cgi.websocketMode)
+ closeConnection = true;
+ } catch(ConnectionException ce) {
+ closeConnection = true;
+ } catch(Throwable t) {
+ // a processing error can be recovered from
+ version(CRuntime_Musl) {
+ // LockingTextWriter fails here
+ // so working around it
+ auto estr = t.toString();
+ stderr.rawWrite(estr);
+ } else {
+ stderr.writeln(t.toString);
+ }
+ if(!handleException(cgi, t))
+ closeConnection = true;
+ }
+
+ if(closeConnection) {
+ ir.source.close();
+ break;
+ } else {
+ if(!ir.empty)
+ ir.popFront(); // get the next
+ else if(ir.sourceClosed) {
+ ir.source.close();
+ }
+ }
+ }
+
+ ir.source.close();
+ } catch(Throwable t) {
+ version(CRuntime_Musl) {} else
+ debug writeln(t);
+ // most likely cause is a timeout
+ }
+ }
+ } else if(newPid < 0) {
+ throw new Exception("fork failed");
+ } else {
+ processCount++;
+ }
+ }
+
+ // the parent should wait for its children...
+ if(newPid) {
+ import core.sys.posix.sys.wait;
+
+ version(embedded_httpd_processes_accept_after_fork) {} else {
+ import core.sys.posix.sys.select;
+ int[] fdQueue;
+ while(true) {
+ // writeln("select call");
+ int nfds = pipeWriteFd;
+ if(sock > pipeWriteFd)
+ nfds = sock;
+ nfds += 1;
+ fd_set read_fds;
+ fd_set write_fds;
+ FD_ZERO(&read_fds);
+ FD_ZERO(&write_fds);
+ FD_SET(sock, &read_fds);
+ if(fdQueue.length)
+ FD_SET(pipeWriteFd, &write_fds);
+ auto ret = select(nfds, &read_fds, &write_fds, null, null);
+ if(ret == -1) {
+ import core.stdc.errno;
+ if(errno == EINTR)
+ goto try_wait;
+ else
+ throw new Exception("wtf select");
+ }
+
+ int s = -1;
+ if(FD_ISSET(sock, &read_fds)) {
+ uint i;
+ sockaddr addr;
+ i = addr.sizeof;
+ s = accept(sock, &addr, &i);
+ cloexec(s);
+ import core.sys.posix.netinet.tcp;
+ int opt = 1;
+ setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
+ }
+
+ if(FD_ISSET(pipeWriteFd, &write_fds)) {
+ if(s == -1 && fdQueue.length) {
+ s = fdQueue[0];
+ fdQueue = fdQueue[1 .. $]; // FIXME reuse buffer
+ }
+ write_fd(pipeWriteFd, &s, s.sizeof, s);
+ close(s); // we are done with it, let the other process take ownership
+ } else
+ fdQueue ~= s;
+ }
+ }
+
+ try_wait:
+
+ int status;
+ while(-1 != wait(&status)) {
+ version(CRuntime_Musl) {} else {
+ import std.stdio; writeln("Process died ", status);
+ }
+ processCount--;
+ goto reopen;
+ }
+ close(sock);
+ }
+}
+
+version(fastcgi)
+void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) {
+ // SetHandler fcgid-script
+ FCGX_Stream* input, output, error;
+ FCGX_ParamArray env;
+
+
+
+ const(ubyte)[] getFcgiChunk() {
+ const(ubyte)[] ret;
+ while(FCGX_HasSeenEOF(input) != -1)
+ ret ~= cast(ubyte) FCGX_GetChar(input);
+ return ret;
+ }
+
+ void writeFcgi(const(ubyte)[] data) {
+ FCGX_PutStr(data.ptr, data.length, output);
+ }
+
+ void doARequest() {
+ string[string] fcgienv;
+
+ for(auto e = env; e !is null && *e !is null; e++) {
+ string cur = to!string(*e);
+ auto idx = cur.indexOf("=");
+ string name, value;
+ if(idx == -1)
+ name = cur;
+ else {
+ name = cur[0 .. idx];
+ value = cur[idx + 1 .. $];
+ }
+
+ fcgienv[name] = value;
+ }
+
+ void flushFcgi() {
+ FCGX_FFlush(output);
+ }
+
+ Cgi cgi;
+ try {
+ cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi);
+ } catch(Throwable t) {
+ FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error);
+ writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t));
+ return; //continue;
+ }
+ assert(cgi !is null);
+ scope(exit) cgi.dispose();
+ try {
+ fun(cgi);
+ cgi.close();
+ } catch(Throwable t) {
+ // log it to the error stream
+ FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error);
+ // handle it for the user, if we can
+ if(!handleException(cgi, t))
+ return; // continue;
+ }
+ }
+
+ auto lp = params.listeningPort;
+
+ FCGX_Request request;
+ if(lp) {
+ // if a listening port was specified on the command line, we want to spawn ourself
+ // (needed for nginx without spawn-fcgi, e.g. on Windows)
+ FCGX_Init();
+ auto sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12);
+ if(sock < 0)
+ throw new Exception("Couldn't listen on the port");
+ FCGX_InitRequest(&request, sock, 0);
+ while(FCGX_Accept_r(&request) >= 0) {
+ input = request.inStream;
+ output = request.outStream;
+ error = request.errStream;
+ env = request.envp;
+ doARequest();
+ }
+ } else {
+ // otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd)
+ // using the version with a global variable since we are separate processes anyway
+ while(FCGX_Accept(&input, &output, &error, &env) >= 0) {
+ doARequest();
+ }
+ }
+}
+
+/// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others.
+ushort defaultListeningPort() {
+ version(netman_httpd)
+ return 8080;
+ else version(embedded_httpd_processes)
+ return 8085;
+ else version(embedded_httpd_threads)
+ return 8085;
+ else version(scgi)
+ return 4000;
+ else
+ return 0;
+}
+
+/// Default host for listening. 127.0.0.1 for scgi, null (aka all interfaces) for all others. If you want the server directly accessible from other computers on the network, normally use null. If not, 127.0.0.1 is a bit better. Settable with default handlers with --listening-host command line argument.
+string defaultListeningHost() {
+ version(netman_httpd)
+ return null;
+ else version(embedded_httpd_processes)
+ return null;
+ else version(embedded_httpd_threads)
+ return null;
+ else version(scgi)
+ return "127.0.0.1";
+ else
+ return null;
+
+}
+
+/++
+ This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`.
+
+ Params:
+ fun = Your request handler
+ CustomCgi = a subclass of Cgi, if you wise to customize it further
+ maxContentLength = max POST size you want to allow
+ args = command-line arguments
+
+ History:
+ Documented Sept 26, 2020.
++/
+void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) {
+ if(tryAddonServers(args))
+ return;
+
+ if(trySimulatedRequest!(fun, CustomCgi)(args))
+ return;
+
+ RequestServer server;
+ // you can change the port here if you like
+ // server.listeningPort = 9000;
+
+ // then call this to let the command line args override your default
+ server.configureFromCommandLine(args);
+
+ // and serve the request(s).
+ server.serve!(fun, CustomCgi, maxContentLength)();
+}
+
+//version(plain_cgi)
+void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {
+ // standard CGI is the default version
+ Cgi cgi;
+ try {
+ cgi = new CustomCgi(maxContentLength);
+ version(Posix)
+ cgi._outputFileHandle = 1; // stdout
+ else version(Windows)
+ cgi._outputFileHandle = GetStdHandle(STD_OUTPUT_HANDLE);
+ else static assert(0);
+ } catch(Throwable t) {
+ version(CRuntime_Musl) {
+ // LockingTextWriter fails here
+ // so working around it
+ auto s = t.toString();
+ stderr.rawWrite(s);
+ stdout.rawWrite(plainHttpError(true, "400 Bad Request", t));
+ } else {
+ stderr.writeln(t.msg);
+ // the real http server will probably handle this;
+ // most likely, this is a bug in Cgi. But, oh well.
+ stdout.write(plainHttpError(true, "400 Bad Request", t));
+ }
+ return;
+ }
+ assert(cgi !is null);
+ scope(exit) cgi.dispose();
+
+ try {
+ fun(cgi);
+ cgi.close();
+ } catch (Throwable t) {
+ version(CRuntime_Musl) {
+ // LockingTextWriter fails here
+ // so working around it
+ auto s = t.msg;
+ stderr.rawWrite(s);
+ } else {
+ stderr.writeln(t.msg);
+ }
+ if(!handleException(cgi, t))
+ return;
+ }
+}
+
+/+
+ The event loop for embedded_httpd_threads will prolly fiber dispatch
+ cgi constructors too, so slow posts will not monopolize a worker thread.
+
+ May want to provide the worker task system just need to ensure all the fibers
+ has a big enough stack for real work... would also ideally like to reuse them.
+
+
+ So prolly bir would switch it to nonblocking. If it would block, it epoll
+ registers one shot with this existing fiber to take it over.
+
+ new connection comes in. it picks a fiber off the free list,
+ or if there is none, it creates a new one. this fiber handles
+ this connection the whole time.
+
+ epoll triggers the fiber when something comes in. it is called by
+ a random worker thread, it might change at any time. at least during
+ the constructor. maybe into the main body it will stay tied to a thread
+ just so TLS stuff doesn't randomly change in the middle. but I could
+ specify if you yield all bets are off.
+
+ when the request is finished, if there's more data buffered, it just
+ keeps going. if there is no more data buffered, it epoll ctls to
+ get triggered when more data comes in. all one shot.
+
+ when a connection is closed, the fiber returns and is then reset
+ and added to the free list. if the free list is full, the fiber is
+ just freed, this means it will balloon to a certain size but not generally
+ grow beyond that unless the activity keeps going.
+
+ 256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory.
+
+ So the fiber has its own magic methods to read and write. if they would block, it registers
+ for epoll and yields. when it returns, it read/writes and then returns back normal control.
+
+ basically you issue the command and it tells you when it is done
+
+ it needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued
+
++/
+
+version(cgi_use_fiber)
+class CgiFiber : Fiber {
+ private void function(Socket) f_handler;
+ private void f_handler_dg(Socket s) { // to avoid extra allocation w/ function
+ f_handler(s);
+ }
+ this(void function(Socket) handler) {
+ this.f_handler = handler;
+ this(&f_handler_dg);
+ }
+
+ this(void delegate(Socket) handler) {
+ this.handler = handler;
+ // FIXME: stack size
+ super(&run);
+ }
+
+ Socket connection;
+ void delegate(Socket) handler;
+
+ void run() {
+ handler(connection);
+ }
+
+ void delegate() postYield;
+
+ private void setPostYield(scope void delegate() py) @nogc {
+ postYield = cast(void delegate()) py;
+ }
+
+ void proceed() {
+ call();
+ auto py = postYield;
+ postYield = null;
+ if(py !is null)
+ py();
+ if(state == State.TERM) {
+ import core.memory;
+ GC.removeRoot(cast(void*) this);
+ }
+ }
+}
+
+void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) {
+ assert(connection !is null);
+ version(cgi_use_fiber) {
+ auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun));
+ import core.memory;
+ GC.addRoot(cast(void*) fiber);
+ fiber.connection = connection;
+ fiber.proceed();
+ } else {
+ doThreadHttpConnectionGuts!(CustomCgi, fun)(connection);
+ }
+}
+
+void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) {
+ scope(failure) {
+ // catch all for other errors
+ sendAll(connection, plainHttpError(false, "500 Internal Server Error", null));
+ connection.close();
+ }
+
+ bool closeConnection = alwaysCloseConnection;
+
+ /+
+ ubyte[4096] inputBuffer = void;
+ ubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void;
+ ubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void;
+
+ birBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[];
+ BufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr;
+ ir.__ctor(connection, inputBuffer[], true);
+ +/
+
+ auto ir = new BufferedInputRange(connection);
+
+ while(!ir.empty) {
+
+ if(ir.view.length == 0) {
+ ir.popFront();
+ if(ir.sourceClosed) {
+ connection.close();
+ closeConnection = true;
+ break;
+ }
+ }
+
+ Cgi cgi;
+ try {
+ cgi = new CustomCgi(ir, &closeConnection);
+ cgi._outputFileHandle = connection.handle;
+ } catch(ConnectionClosedException ce) {
+ closeConnection = true;
+ break;
+ } catch(ConnectionException ce) {
+ // broken pipe or something, just abort the connection
+ closeConnection = true;
+ break;
+ } catch(Throwable t) {
+ // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P
+ // anyway let's kill the connection
+ version(CRuntime_Musl) {
+ stderr.rawWrite(t.toString());
+ stderr.rawWrite("\n");
+ } else {
+ stderr.writeln(t.toString());
+ }
+ sendAll(connection, plainHttpError(false, "400 Bad Request", t));
+ closeConnection = true;
+ break;
+ }
+ assert(cgi !is null);
+ scope(exit)
+ cgi.dispose();
+
+ try {
+ fun(cgi);
+ cgi.close();
+ if(cgi.websocketMode)
+ closeConnection = true;
+ } catch(ConnectionException ce) {
+ // broken pipe or something, just abort the connection
+ closeConnection = true;
+ } catch(ConnectionClosedException ce) {
+ // broken pipe or something, just abort the connection
+ closeConnection = true;
+ } catch(Throwable t) {
+ // a processing error can be recovered from
+ version(CRuntime_Musl) {} else
+ stderr.writeln(t.toString);
+ if(!handleException(cgi, t))
+ closeConnection = true;
+ }
+
+ if(closeConnection || alwaysCloseConnection) {
+ connection.shutdown(SocketShutdown.BOTH);
+ connection.close();
+ ir.dispose();
+ closeConnection = false; // don't reclose after loop
+ break;
+ } else {
+ if(ir.front.length) {
+ ir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along
+ } else if(ir.sourceClosed) {
+ ir.source.shutdown(SocketShutdown.BOTH);
+ ir.source.close();
+ ir.dispose();
+ closeConnection = false;
+ } else {
+ continue;
+ // break; // this was for a keepalive experiment
+ }
+ }
+ }
+
+ if(closeConnection) {
+ connection.shutdown(SocketShutdown.BOTH);
+ connection.close();
+ ir.dispose();
+ }
+
+ // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection!
+}
+
+void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) {
+ // and now we can buffer
+ scope(failure)
+ connection.close();
+
+ import al = std.algorithm;
+
+ size_t size;
+
+ string[string] headers;
+
+ auto range = new BufferedInputRange(connection);
+ more_data:
+ auto chunk = range.front();
+ // waiting for colon for header length
+ auto idx = indexOf(cast(string) chunk, ':');
+ if(idx == -1) {
+ try {
+ range.popFront();
+ } catch(Exception e) {
+ // it is just closed, no big deal
+ connection.close();
+ return;
+ }
+ goto more_data;
+ }
+
+ size = to!size_t(cast(string) chunk[0 .. idx]);
+ chunk = range.consume(idx + 1);
+ // reading headers
+ if(chunk.length < size)
+ range.popFront(0, size + 1);
+ // we are now guaranteed to have enough
+ chunk = range.front();
+ assert(chunk.length > size);
+
+ idx = 0;
+ string key;
+ string value;
+ foreach(part; al.splitter(chunk, '\0')) {
+ if(idx & 1) { // odd is value
+ value = cast(string)(part.idup);
+ headers[key] = value; // commit
+ } else
+ key = cast(string)(part.idup);
+ idx++;
+ }
+
+ enforce(chunk[size] == ','); // the terminator
+
+ range.consume(size + 1);
+ // reading data
+ // this will be done by Cgi
+
+ const(ubyte)[] getScgiChunk() {
+ // we are already primed
+ auto data = range.front();
+ if(data.length == 0 && !range.sourceClosed) {
+ range.popFront(0);
+ data = range.front();
+ } else if (range.sourceClosed)
+ range.source.close();
+
+ return data;
+ }
+
+ void writeScgi(const(ubyte)[] data) {
+ sendAll(connection, data);
+ }
+
+ void flushScgi() {
+ // I don't *think* I have to do anything....
+ }
+
+ Cgi cgi;
+ try {
+ cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi);
+ cgi._outputFileHandle = connection.handle;
+ } catch(Throwable t) {
+ sendAll(connection, plainHttpError(true, "400 Bad Request", t));
+ connection.close();
+ return; // this connection is dead
+ }
+ assert(cgi !is null);
+ scope(exit) cgi.dispose();
+ try {
+ fun(cgi);
+ cgi.close();
+ connection.close();
+ } catch(Throwable t) {
+ // no std err
+ if(!handleException(cgi, t)) {
+ connection.close();
+ return;
+ } else {
+ connection.close();
+ return;
+ }
+ }
+}
+
+string printDate(DateTime date) {
+ char[29] buffer = void;
+ printDateToBuffer(date, buffer[]);
+ return buffer.idup;
+}
+
+int printDateToBuffer(DateTime date, char[] buffer) @nogc {
+ assert(buffer.length >= 29);
+ // 29 static length ?
+
+ static immutable daysOfWeek = [
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+ ];
+
+ static immutable months = [
+ null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+ ];
+
+ buffer[0 .. 3] = daysOfWeek[date.dayOfWeek];
+ buffer[3 .. 5] = ", ";
+ buffer[5] = date.day / 10 + '0';
+ buffer[6] = date.day % 10 + '0';
+ buffer[7] = ' ';
+ buffer[8 .. 11] = months[date.month];
+ buffer[11] = ' ';
+ auto y = date.year;
+ buffer[12] = cast(char) (y / 1000 + '0'); y %= 1000;
+ buffer[13] = cast(char) (y / 100 + '0'); y %= 100;
+ buffer[14] = cast(char) (y / 10 + '0'); y %= 10;
+ buffer[15] = cast(char) (y + '0');
+ buffer[16] = ' ';
+ buffer[17] = date.hour / 10 + '0';
+ buffer[18] = date.hour % 10 + '0';
+ buffer[19] = ':';
+ buffer[20] = date.minute / 10 + '0';
+ buffer[21] = date.minute % 10 + '0';
+ buffer[22] = ':';
+ buffer[23] = date.second / 10 + '0';
+ buffer[24] = date.second % 10 + '0';
+ buffer[25 .. $] = " GMT";
+
+ return 29;
+}
+
+
+// Referencing this gigantic typeid seems to remind the compiler
+// to actually put the symbol in the object file. I guess the immutable
+// assoc array array isn't actually included in druntime
+void hackAroundLinkerError() {
+ stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString());
+ stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString());
+ // this is getting kinda ridiculous btw. Moving assoc arrays
+ // to the library is the pain that keeps on coming.
+
+ // eh this broke the build on the work server
+ // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])]));
+ stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString());
+}
+
+
+
+
+
+version(fastcgi) {
+ pragma(lib, "fcgi");
+
+ static if(size_t.sizeof == 8) // 64 bit
+ alias long c_int;
+ else
+ alias int c_int;
+
+ extern(C) {
+ struct FCGX_Stream {
+ ubyte* rdNext;
+ ubyte* wrNext;
+ ubyte* stop;
+ ubyte* stopUnget;
+ c_int isReader;
+ c_int isClosed;
+ c_int wasFCloseCalled;
+ c_int FCGI_errno;
+ void* function(FCGX_Stream* stream) fillBuffProc;
+ void* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc;
+ void* data;
+ }
+
+ // note: this is meant to be opaque, so don't access it directly
+ struct FCGX_Request {
+ int requestId;
+ int role;
+ FCGX_Stream* inStream;
+ FCGX_Stream* outStream;
+ FCGX_Stream* errStream;
+ char** envp;
+ void* paramsPtr;
+ int ipcFd;
+ int isBeginProcessed;
+ int keepConnection;
+ int appStatus;
+ int nWriters;
+ int flags;
+ int listen_sock;
+ }
+
+ int FCGX_InitRequest(FCGX_Request *request, int sock, int flags);
+ void FCGX_Init();
+
+ int FCGX_Accept_r(FCGX_Request *request);
+
+
+ alias char** FCGX_ParamArray;
+
+ c_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp);
+ c_int FCGX_GetChar(FCGX_Stream* stream);
+ c_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream);
+ int FCGX_HasSeenEOF(FCGX_Stream* stream);
+ c_int FCGX_FFlush(FCGX_Stream *stream);
+
+ int FCGX_OpenSocket(in char*, int);
+ }
+}
+
+
+/* This might go int a separate module eventually. It is a network input helper class. */
+
+import std.socket;
+
+version(cgi_use_fiber) {
+ import core.thread;
+ import core.sys.linux.epoll;
+
+ __gshared int epfd;
+}
+
+
+version(cgi_use_fiber)
+private enum WakeupEvent {
+ Read = EPOLLIN,
+ Write = EPOLLOUT
+}
+
+version(cgi_use_fiber)
+private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc {
+
+ // static cast since I know what i have in here and don't want to pay for dynamic cast
+ auto f = cast(CgiFiber) cast(void*) Fiber.getThis();
+
+ f.setPostYield = () {
+ if(*registered) {
+ // rearm
+ epoll_event evt;
+ evt.events = e | EPOLLONESHOT;
+ evt.data.ptr = cast(void*) f;
+ if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1)
+ throw new Exception("epoll_ctl");
+ } else {
+ // initial registration
+ *registered = true ;
+ int fd = source.handle;
+ epoll_event evt;
+ evt.events = e | EPOLLONESHOT;
+ evt.data.ptr = cast(void*) f;
+ if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1)
+ throw new Exception("epoll_ctl");
+ }
+ };
+
+ Fiber.yield();
+
+ f.setPostYield(null);
+}
+
+version(cgi_use_fiber)
+void unregisterSource(Socket s) {
+ epoll_event evt;
+ epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt);
+}
+
+// it is a class primarily for reference semantics
+// I might change this interface
+/// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda.
+class BufferedInputRange {
+ version(Posix)
+ this(int source, ubyte[] buffer = null) {
+ this(new Socket(cast(socket_t) source, AddressFamily.INET), buffer);
+ }
+
+ this(Socket source, ubyte[] buffer = null, bool allowGrowth = true) {
+ // if they connect but never send stuff to us, we don't want it wasting the process
+ // so setting a time out
+ version(cgi_use_fiber)
+ source.blocking = false;
+ else
+ source.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(3));
+
+ this.source = source;
+ if(buffer is null) {
+ underlyingBuffer = new ubyte[4096];
+ this.allowGrowth = true;
+ } else {
+ underlyingBuffer = buffer;
+ this.allowGrowth = allowGrowth;
+ }
+
+ assert(underlyingBuffer.length);
+
+ // we assume view.ptr is always inside underlyingBuffer
+ view = underlyingBuffer[0 .. 0];
+
+ popFront(); // prime
+ }
+
+ version(cgi_use_fiber) {
+ bool registered;
+ }
+
+ void dispose() {
+ version(cgi_use_fiber) {
+ if(registered)
+ unregisterSource(source);
+ }
+ }
+
+ /**
+ A slight difference from regular ranges is you can give it the maximum
+ number of bytes to consume.
+
+ IMPORTANT NOTE: the default is to consume nothing, so if you don't call
+ consume() yourself and use a regular foreach, it will infinitely loop!
+
+ The default is to do what a normal range does, and consume the whole buffer
+ and wait for additional input.
+
+ You can also specify 0, to append to the buffer, or any other number
+ to remove the front n bytes and wait for more.
+ */
+ void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {
+ if(sourceClosed)
+ throw new ConnectionClosedException("can't get any more data from a closed source");
+ if(!skipConsume)
+ consume(maxBytesToConsume);
+
+ // we might have to grow the buffer
+ if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {
+ if(allowGrowth) {
+ //import std.stdio; writeln("growth");
+ auto viewStart = view.ptr - underlyingBuffer.ptr;
+ size_t growth = 4096;
+ // make sure we have enough for what we're being asked for
+ if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth)
+ growth = minBytesToSettleFor - underlyingBuffer.length;
+ //import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length);
+ underlyingBuffer.length += growth;
+ view = underlyingBuffer[viewStart .. view.length];
+ } else
+ throw new Exception("No room left in the buffer");
+ }
+
+ do {
+ auto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $];
+ try_again:
+ auto ret = source.receive(freeSpace);
+ if(ret == Socket.ERROR) {
+ if(wouldHaveBlocked()) {
+ version(cgi_use_fiber) {
+ registerEventWakeup(&registered, source, WakeupEvent.Read);
+ goto try_again;
+ } else {
+ // gonna treat a timeout here as a close
+ sourceClosed = true;
+ return;
+ }
+ }
+ version(Posix) {
+ import core.stdc.errno;
+ if(errno == EINTR || errno == EAGAIN) {
+ goto try_again;
+ }
+ if(errno == ECONNRESET) {
+ sourceClosed = true;
+ return;
+ }
+ }
+ throw new Exception(lastSocketError); // FIXME
+ }
+ if(ret == 0) {
+ sourceClosed = true;
+ return;
+ }
+
+ //import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, " ", ret, " = ", view.length + ret);
+ view = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret];
+ //import std.stdio; writeln(cast(string) view);
+ } while(view.length < minBytesToSettleFor);
+ }
+
+ /// Removes n bytes from the front of the buffer, and returns the new buffer slice.
+ /// You might want to idup the data you are consuming if you store it, since it may
+ /// be overwritten on the new popFront.
+ ///
+ /// You do not need to call this if you always want to wait for more data when you
+ /// consume some.
+ ubyte[] consume(size_t bytes) {
+ //import std.stdio; writeln("consuime ", bytes, "/", view.length);
+ view = view[bytes > $ ? $ : bytes .. $];
+ if(view.length == 0) {
+ view = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning
+ /*
+ writeln("HERE");
+ popFront(0, 0, true); // try to load more if we can, checks if the source is closed
+ writeln(cast(string)front);
+ writeln("DONE");
+ */
+ }
+ return front;
+ }
+
+ bool empty() {
+ return sourceClosed && view.length == 0;
+ }
+
+ ubyte[] front() {
+ return view;
+ }
+
+ invariant() {
+ assert(view.ptr >= underlyingBuffer.ptr);
+ // it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer
+ assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length);
+ }
+
+ ubyte[] underlyingBuffer;
+ bool allowGrowth;
+ ubyte[] view;
+ Socket source;
+ bool sourceClosed;
+}
+
+import core.sync.semaphore;
+import core.atomic;
+
+/**
+ To use this thing:
+
+ void handler(Socket s) { do something... }
+ auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler);
+ manager.listen();
+
+ I suggest you use BufferedInputRange(connection) to handle the input. As a packet
+ comes in, you will get control. You can just continue; though to fetch more.
+
+
+ FIXME: should I offer an event based async thing like netman did too? Yeah, probably.
+*/
+class ListeningConnectionManager {
+ Semaphore semaphore;
+ Socket[256] queue;
+ shared(ubyte) nextIndexFront;
+ ubyte nextIndexBack;
+ shared(int) queueLength;
+
+ void listen() {
+ running = true;
+ shared(int) loopBroken;
+
+ version(Posix) {
+ import core.sys.posix.signal;
+ signal(SIGPIPE, SIG_IGN);
+ }
+
+ version(cgi_no_threads) {
+ // NEVER USE THIS
+ // it exists only for debugging and other special occasions
+
+ // the thread mode is faster and less likely to stall the whole
+ // thing when a request is slow
+ while(!loopBroken && running) {
+ auto sn = listener.accept();
+ cloexec(sn);
+ try {
+ handler(sn);
+ } catch(Exception e) {
+ // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies)
+ sn.close();
+ }
+ }
+ } else {
+ import std.parallelism;
+
+ version(cgi_use_fork) {
+ //asm { int 3; }
+ fork();
+ }
+
+ version(cgi_use_fiber) {
+ import core.sys.linux.epoll;
+ epfd = epoll_create1(EPOLL_CLOEXEC);
+ if(epfd == -1)
+ throw new Exception("epoll_create1 " ~ to!string(errno));
+ scope(exit) {
+ import core.sys.posix.unistd;
+ close(epfd);
+ }
+
+ epoll_event ev;
+ ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough.
+ ev.data.fd = listener.handle;
+ if(epoll_ctl(epfd, EPOLL_CTL_ADD, listener.handle, &ev) == -1)
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+
+ WorkerThread[] threads = new WorkerThread[](totalCPUs * 1 + 1);
+ foreach(i, ref thread; threads) {
+ thread = new WorkerThread(this, handler, cast(int) i);
+ thread.start();
+ }
+
+ bool fiber_crash_check() {
+ bool hasAnyRunning;
+ foreach(thread; threads) {
+ if(!thread.isRunning) {
+ thread.join();
+ } else hasAnyRunning = true;
+ }
+
+ return (!hasAnyRunning);
+ }
+
+
+ while(running) {
+ Thread.sleep(1.seconds);
+ if(fiber_crash_check())
+ break;
+ }
+
+ } else {
+ semaphore = new Semaphore();
+
+ // I times 4 here because there's a good chance some will be blocked on i/o.
+ ConnectionThread[] threads = new ConnectionThread[](totalCPUs * 4);
+ foreach(i, ref thread; threads) {
+ thread = new ConnectionThread(this, handler, cast(int) i);
+ thread.start();
+ }
+ }
+
+ while(!loopBroken && running) {
+ Socket sn;
+
+ bool crash_check() {
+ bool hasAnyRunning;
+ foreach(thread; threads) {
+ if(!thread.isRunning) {
+ thread.join();
+ } else hasAnyRunning = true;
+ }
+
+ return (!hasAnyRunning);
+ }
+
+
+ void accept_new_connection() {
+ sn = listener.accept();
+ cloexec(sn);
+ if(tcp) {
+ // disable Nagle's algorithm to avoid a 40ms delay when we send/recv
+ // on the socket because we do some buffering internally. I think this helps,
+ // certainly does for small requests, and I think it does for larger ones too
+ sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);
+
+ sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10));
+ }
+ }
+
+ void existing_connection_new_data() {
+ // wait until a slot opens up
+ //int waited = 0;
+ while(queueLength >= queue.length) {
+ Thread.sleep(1.msecs);
+ //waited ++;
+ }
+ //if(waited) {import std.stdio; writeln(waited);}
+ synchronized(this) {
+ queue[nextIndexBack] = sn;
+ nextIndexBack++;
+ atomicOp!"+="(queueLength, 1);
+ }
+ semaphore.notify();
+ }
+
+
+ accept_new_connection();
+ existing_connection_new_data();
+
+ if(crash_check())
+ break;
+ }
+
+ // FIXME: i typically stop this with ctrl+c which never
+ // actually gets here. i need to do a sigint handler.
+ if(cleanup)
+ cleanup();
+ }
+ }
+
+ //version(linux)
+ //int epoll_fd;
+
+ bool tcp;
+ void delegate() cleanup;
+
+ private void function(Socket) fhandler;
+ private void dg_handler(Socket s) {
+ fhandler(s);
+ }
+ this(string host, ushort port, void function(Socket) handler) {
+ fhandler = handler;
+ this(host, port, &dg_handler);
+ }
+
+ this(string host, ushort port, void delegate(Socket) handler) {
+ this.handler = handler;
+
+ listener = startListening(host, port, tcp, cleanup, 128);
+
+ version(cgi_use_fiber) version(cgi_use_fork)
+ listener.blocking = false;
+
+ // this is the UI control thread and thus gets more priority
+ Thread.getThis.priority = Thread.PRIORITY_MAX;
+ }
+
+ Socket listener;
+ void delegate(Socket) handler;
+
+ bool running;
+ void quit() {
+ running = false;
+ }
+}
+
+Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue) {
+ Socket listener;
+ if(host.startsWith("unix:")) {
+ version(Posix) {
+ listener = new Socket(AddressFamily.UNIX, SocketType.STREAM);
+ cloexec(listener);
+ string filename = host["unix:".length .. $].idup;
+ listener.bind(new UnixAddress(filename));
+ cleanup = delegate() {
+ listener.close();
+ import std.file;
+ remove(filename);
+ };
+ tcp = false;
+ } else {
+ throw new Exception("unix sockets not supported on this system");
+ }
+ } else if(host.startsWith("abstract:")) {
+ version(linux) {
+ listener = new Socket(AddressFamily.UNIX, SocketType.STREAM);
+ cloexec(listener);
+ string filename = "\0" ~ host["abstract:".length .. $];
+ import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]);
+ listener.bind(new UnixAddress(filename));
+ tcp = false;
+ } else {
+ throw new Exception("abstract unix sockets not supported on this system");
+ }
+ } else {
+ listener = new TcpSocket();
+ cloexec(listener);
+ listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
+ listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port));
+ cleanup = delegate() {
+ listener.close();
+ };
+ tcp = true;
+ }
+
+ listener.listen(backQueue);
+
+ dropPrivs();
+
+ return listener;
+}
+
+// helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something.
+void sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) {
+ if(data.length == 0) return;
+ ptrdiff_t amount;
+ //import std.stdio; writeln("***",cast(string) data,"///");
+ do {
+ amount = s.send(data);
+ if(amount == Socket.ERROR) {
+ version(cgi_use_fiber) {
+ if(wouldHaveBlocked()) {
+ bool registered = true;
+ registerEventWakeup(&registered, s, WakeupEvent.Write);
+ continue;
+ }
+ }
+ throw new ConnectionException(s, lastSocketError, file, line);
+ }
+ assert(amount > 0);
+ data = data[amount .. $];
+ } while(data.length);
+}
+
+class ConnectionException : Exception {
+ Socket socket;
+ this(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) {
+ this.socket = s;
+ super(msg, file, line);
+ }
+}
+
+alias void delegate(Socket) CMT;
+
+import core.thread;
+/+
+ cgi.d now uses a hybrid of event i/o and threads at the top level.
+
+ Top level thread is responsible for accepting sockets and selecting on them.
+
+ It then indicates to a child that a request is pending, and any random worker
+ thread that is free handles it. It goes into blocking mode and handles that
+ http request to completion.
+
+ At that point, it goes back into the waiting queue.
+
+
+ This concept is only implemented on Linux. On all other systems, it still
+ uses the worker threads and semaphores (which is perfectly fine for a lot of
+ things! Just having a great number of keep-alive connections will break that.)
+
+
+ So the algorithm is:
+
+ select(accept, event, pending)
+ if accept -> send socket to free thread, if any. if not, add socket to queue
+ if event -> send the signaling thread a socket from the queue, if not, mark it free
+ - event might block until it can be *written* to. it is a fifo sending socket fds!
+
+ A worker only does one http request at a time, then signals its availability back to the boss.
+
+ The socket the worker was just doing should be added to the one-off epoll read. If it is closed,
+ great, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the
+ actual FD will not be kept out here.
+
+ So:
+ queue = sockets we know are ready to read now, but no worker thread is available
+ idle list = worker threads not doing anything else. they signal back and forth
+
+ the workers all read off the event fd. This is the semaphore wait
+
+ the boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read,
+ it puts it in the queue and writes to the event fd.
+
+ The child could put the socket back in the epoll thing itself.
+
+ The child needs to be able to gracefully handle being given a socket that just closed with no work.
++/
+class ConnectionThread : Thread {
+ this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) {
+ this.lcm = lcm;
+ this.dg = dg;
+ this.myThreadNumber = myThreadNumber;
+ super(&run);
+ }
+
+ void run() {
+ while(true) {
+ // so if there's a bunch of idle keep-alive connections, it can
+ // consume all the worker threads... just sitting there.
+ lcm.semaphore.wait();
+ Socket socket;
+ synchronized(lcm) {
+ auto idx = lcm.nextIndexFront;
+ socket = lcm.queue[idx];
+ lcm.queue[idx] = null;
+ atomicOp!"+="(lcm.nextIndexFront, 1);
+ atomicOp!"-="(lcm.queueLength, 1);
+ }
+ try {
+ //import std.stdio; writeln(myThreadNumber, " taking it");
+ dg(socket);
+ /+
+ if(socket.isAlive) {
+ // process it more later
+ version(linux) {
+ import core.sys.linux.epoll;
+ epoll_event ev;
+ ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
+ ev.data.fd = socket.handle;
+ import std.stdio; writeln("adding");
+ if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) {
+ if(errno == EEXIST) {
+ ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
+ ev.data.fd = socket.handle;
+ if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1)
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+ } else
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+ }
+ //import std.stdio; writeln("keep alive");
+ // writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later
+ __traits(getMember, socket, "sock") = cast(socket_t) -1;
+ } else {
+ continue; // hope it times out in a reasonable amount of time...
+ }
+ }
+ +/
+ } catch(ConnectionClosedException e) {
+ // can just ignore this, it is fairly normal
+ socket.close();
+ } catch(Throwable e) {
+ import std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite("\n");
+ socket.close();
+ }
+ }
+ }
+
+ ListeningConnectionManager lcm;
+ CMT dg;
+ int myThreadNumber;
+}
+
+version(cgi_use_fiber)
+class WorkerThread : Thread {
+ this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) {
+ this.lcm = lcm;
+ this.dg = dg;
+ this.myThreadNumber = myThreadNumber;
+ super(&run);
+ }
+
+ void run() {
+ while(lcm.running) {
+ Socket sn;
+
+ epoll_event[64] events;
+ auto nfds = epoll_wait(epfd, events.ptr, events.length, -1);
+ if(nfds == -1) {
+ if(errno == EINTR)
+ continue;
+ throw new Exception("epoll_wait " ~ to!string(errno));
+ }
+
+ foreach(idx; 0 .. nfds) {
+ auto flags = events[idx].events;
+
+ if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) {
+ // this try/catch is because it is set to non-blocking mode
+ // and Phobos' stupid api throws an exception instead of returning
+ // if it would block. Why would it block? because a forked process
+ // might have beat us to it, but the wakeup event thundered our herds.
+ version(cgi_use_fork) {
+ try
+ sn = lcm.listener.accept();
+ catch(SocketAcceptException e) { continue; }
+ } else {
+ sn = lcm.listener.accept();
+ }
+
+ cloexec(sn);
+ if(lcm.tcp) {
+ // disable Nagle's algorithm to avoid a 40ms delay when we send/recv
+ // on the socket because we do some buffering internally. I think this helps,
+ // certainly does for small requests, and I think it does for larger ones too
+ sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);
+
+ sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10));
+ }
+
+ dg(sn);
+ } else {
+ auto fiber = cast(CgiFiber) events[idx].data.ptr;
+ fiber.proceed();
+ }
+ }
+ }
+ }
+
+ ListeningConnectionManager lcm;
+ CMT dg;
+ int myThreadNumber;
+}
+
+
+/* Done with network helper */
+
+/* Helpers for doing temporary files. Used both here and in web.d */
+
+version(Windows) {
+ import core.sys.windows.windows;
+ extern(Windows) DWORD GetTempPathW(DWORD, LPWSTR);
+ alias GetTempPathW GetTempPath;
+}
+
+version(Posix) {
+ static import linux = core.sys.posix.unistd;
+}
+
+string getTempDirectory() {
+ string path;
+ version(Windows) {
+ wchar[1024] buffer;
+ auto len = GetTempPath(1024, buffer.ptr);
+ if(len == 0)
+ throw new Exception("couldn't find a temporary path");
+
+ auto b = buffer[0 .. len];
+
+ path = to!string(b);
+ } else
+ path = "/tmp/";
+
+ return path;
+}
+
+
+// I like std.date. These functions help keep my old code and data working with phobos changing.
+
+long sysTimeToDTime(in SysTime sysTime) {
+ return convert!("hnsecs", "msecs")(sysTime.stdTime - 621355968000000000L);
+}
+
+long dateTimeToDTime(in DateTime dt) {
+ return sysTimeToDTime(cast(SysTime) dt);
+}
+
+long getUtcTime() { // renamed primarily to avoid conflict with std.date itself
+ return sysTimeToDTime(Clock.currTime(UTC()));
+}
+
+// NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick
+SysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) {
+ immutable hnsecs = convert!("msecs", "hnsecs")(dTime) + 621355968000000000L;
+ return SysTime(hnsecs, tz);
+}
+
+
+
+// this is a helper to read HTTP transfer-encoding: chunked responses
+immutable(ubyte[]) dechunk(BufferedInputRange ir) {
+ immutable(ubyte)[] ret;
+
+ another_chunk:
+ // If here, we are at the beginning of a chunk.
+ auto a = ir.front();
+ int chunkSize;
+ int loc = locationOf(a, "\r\n");
+ while(loc == -1) {
+ ir.popFront();
+ a = ir.front();
+ loc = locationOf(a, "\r\n");
+ }
+
+ string hex;
+ hex = "";
+ for(int i = 0; i < loc; i++) {
+ char c = a[i];
+ if(c >= 'A' && c <= 'Z')
+ c += 0x20;
+ if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {
+ hex ~= c;
+ } else {
+ break;
+ }
+ }
+
+ assert(hex.length);
+
+ int power = 1;
+ int size = 0;
+ foreach(cc1; retro(hex)) {
+ dchar cc = cc1;
+ if(cc >= 'a' && cc <= 'z')
+ cc -= 0x20;
+ int val = 0;
+ if(cc >= '0' && cc <= '9')
+ val = cc - '0';
+ else
+ val = cc - 'A' + 10;
+
+ size += power * val;
+ power *= 16;
+ }
+
+ chunkSize = size;
+ assert(size >= 0);
+
+ if(loc + 2 > a.length) {
+ ir.popFront(0, a.length + loc + 2);
+ a = ir.front();
+ }
+
+ a = ir.consume(loc + 2);
+
+ if(chunkSize == 0) { // we're done with the response
+ // if we got here, will change must be true....
+ more_footers:
+ loc = locationOf(a, "\r\n");
+ if(loc == -1) {
+ ir.popFront();
+ a = ir.front;
+ goto more_footers;
+ } else {
+ assert(loc == 0);
+ ir.consume(loc + 2);
+ goto finish;
+ }
+ } else {
+ // if we got here, will change must be true....
+ if(a.length < chunkSize + 2) {
+ ir.popFront(0, chunkSize + 2);
+ a = ir.front();
+ }
+
+ ret ~= (a[0..chunkSize]);
+
+ if(!(a.length > chunkSize + 2)) {
+ ir.popFront(0, chunkSize + 2);
+ a = ir.front();
+ }
+ assert(a[chunkSize] == 13);
+ assert(a[chunkSize+1] == 10);
+ a = ir.consume(chunkSize + 2);
+ chunkSize = 0;
+ goto another_chunk;
+ }
+
+ finish:
+ return ret;
+}
+
+// I want to be able to get data from multiple sources the same way...
+interface ByChunkRange {
+ bool empty();
+ void popFront();
+ const(ubyte)[] front();
+}
+
+ByChunkRange byChunk(const(ubyte)[] data) {
+ return new class ByChunkRange {
+ override bool empty() {
+ return !data.length;
+ }
+
+ override void popFront() {
+ if(data.length > 4096)
+ data = data[4096 .. $];
+ else
+ data = null;
+ }
+
+ override const(ubyte)[] front() {
+ return data[0 .. $ > 4096 ? 4096 : $];
+ }
+ };
+}
+
+ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) {
+ const(ubyte)[] f;
+
+ f = ir.front;
+ if(f.length > atMost)
+ f = f[0 .. atMost];
+
+ return new class ByChunkRange {
+ override bool empty() {
+ return atMost == 0;
+ }
+
+ override const(ubyte)[] front() {
+ return f;
+ }
+
+ override void popFront() {
+ ir.consume(f.length);
+ atMost -= f.length;
+ auto a = ir.front();
+
+ if(a.length <= atMost) {
+ f = a;
+ atMost -= a.length;
+ a = ir.consume(a.length);
+ if(atMost != 0)
+ ir.popFront();
+ if(f.length == 0) {
+ f = ir.front();
+ }
+ } else {
+ // we actually have *more* here than we need....
+ f = a[0..atMost];
+ atMost = 0;
+ ir.consume(atMost);
+ }
+ }
+ };
+}
+
+version(cgi_with_websocket) {
+ // http://tools.ietf.org/html/rfc6455
+
+ /**
+ WEBSOCKET SUPPORT:
+
+ Full example:
+ ---
+ import arsd.cgi;
+
+ void websocketEcho(Cgi cgi) {
+ if(cgi.websocketRequested()) {
+ if(cgi.origin != "http://arsdnet.net")
+ throw new Exception("bad origin");
+ auto websocket = cgi.acceptWebsocket();
+
+ websocket.send("hello");
+ websocket.send(" world!");
+
+ auto msg = websocket.recv();
+ while(msg.opcode != WebSocketOpcode.close) {
+ if(msg.opcode == WebSocketOpcode.text) {
+ websocket.send(msg.textData);
+ } else if(msg.opcode == WebSocketOpcode.binary) {
+ websocket.send(msg.data);
+ }
+
+ msg = websocket.recv();
+ }
+
+ websocket.close();
+ } else assert(0, "i want a web socket!");
+ }
+
+ mixin GenericMain!websocketEcho;
+ ---
+ */
+
+ class WebSocket {
+ Cgi cgi;
+
+ private this(Cgi cgi) {
+ this.cgi = cgi;
+
+ Socket socket = cgi.idlol.source;
+ socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5));
+ }
+
+ // returns true if data available, false if it timed out
+ bool recvAvailable(Duration timeout = dur!"msecs"(0)) {
+ if(!waitForNextMessageWouldBlock())
+ return true;
+ if(isDataPending(timeout))
+ return true; // this is kinda a lie.
+
+ return false;
+ }
+
+ public bool lowLevelReceive() {
+ auto bfr = cgi.idlol;
+ top:
+ auto got = bfr.front;
+ if(got.length) {
+ if(receiveBuffer.length < receiveBufferUsedLength + got.length)
+ receiveBuffer.length += receiveBufferUsedLength + got.length;
+
+ receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[];
+ receiveBufferUsedLength += got.length;
+ bfr.consume(got.length);
+
+ return true;
+ }
+
+ if(bfr.sourceClosed)
+ return false;
+
+ bfr.popFront(0);
+ if(bfr.sourceClosed)
+ return false;
+ goto top;
+ }
+
+
+ bool isDataPending(Duration timeout = 0.seconds) {
+ Socket socket = cgi.idlol.source;
+
+ auto check = new SocketSet();
+ check.add(socket);
+
+ auto got = Socket.select(check, null, null, timeout);
+ if(got > 0)
+ return true;
+ return false;
+ }
+
+ // note: this blocks
+ WebSocketFrame recv() {
+ return waitForNextMessage();
+ }
+
+
+
+
+ private void llclose() {
+ cgi.close();
+ }
+
+ private void llsend(ubyte[] data) {
+ cgi.write(data);
+ cgi.flush();
+ }
+
+ void unregisterActiveSocket(WebSocket) {}
+
+ /* copy/paste section { */
+
+ private int readyState_;
+ private ubyte[] receiveBuffer;
+ private size_t receiveBufferUsedLength;
+
+ private Config config;
+
+ enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
+ enum OPEN = 1; /// The connection is open and ready to communicate.
+ enum CLOSING = 2; /// The connection is in the process of closing.
+ enum CLOSED = 3; /// The connection is closed or couldn't be opened.
+
+ /++
+
+ +/
+ /// Group: foundational
+ static struct Config {
+ /++
+ These control the size of the receive buffer.
+
+ It starts at the initial size, will temporarily
+ balloon up to the maximum size, and will reuse
+ a buffer up to the likely size.
+
+ Anything larger than the maximum size will cause
+ the connection to be aborted and an exception thrown.
+ This is to protect you against a peer trying to
+ exhaust your memory, while keeping the user-level
+ processing simple.
+ +/
+ size_t initialReceiveBufferSize = 4096;
+ size_t likelyReceiveBufferSize = 4096; /// ditto
+ size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
+
+ /++
+ Maximum combined size of a message.
+ +/
+ size_t maximumMessageSize = 10 * 1024 * 1024;
+
+ string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
+ string origin; /// Origin URL to send with the handshake, if desired.
+ string protocol; /// the protocol header, if desired.
+
+ int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
+ }
+
+ /++
+ Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
+ +/
+ int readyState() {
+ return readyState_;
+ }
+
+ /++
+ Closes the connection, sending a graceful teardown message to the other side.
+ +/
+ /// Group: foundational
+ void close(int code = 0, string reason = null)
+ //in (reason.length < 123)
+ in { assert(reason.length < 123); } do
+ {
+ if(readyState_ != OPEN)
+ return; // it cool, we done
+ WebSocketFrame wss;
+ wss.fin = true;
+ wss.opcode = WebSocketOpcode.close;
+ wss.data = cast(ubyte[]) reason;
+ wss.send(&llsend);
+
+ readyState_ = CLOSING;
+
+ llclose();
+ }
+
+ /++
+ Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
+ +/
+ /// Group: foundational
+ void ping() {
+ WebSocketFrame wss;
+ wss.fin = true;
+ wss.opcode = WebSocketOpcode.ping;
+ wss.send(&llsend);
+ }
+
+ // automatically handled....
+ void pong() {
+ WebSocketFrame wss;
+ wss.fin = true;
+ wss.opcode = WebSocketOpcode.pong;
+ wss.send(&llsend);
+ }
+
+ /++
+ Sends a text message through the websocket.
+ +/
+ /// Group: foundational
+ void send(in char[] textData) {
+ WebSocketFrame wss;
+ wss.fin = true;
+ wss.opcode = WebSocketOpcode.text;
+ wss.data = cast(ubyte[]) textData;
+ wss.send(&llsend);
+ }
+
+ /++
+ Sends a binary message through the websocket.
+ +/
+ /// Group: foundational
+ void send(in ubyte[] binaryData) {
+ WebSocketFrame wss;
+ wss.fin = true;
+ wss.opcode = WebSocketOpcode.binary;
+ wss.data = cast(ubyte[]) binaryData;
+ wss.send(&llsend);
+ }
+
+ /++
+ Waits for and returns the next complete message on the socket.
+
+ Note that the onmessage function is still called, right before
+ this returns.
+ +/
+ /// Group: blocking_api
+ public WebSocketFrame waitForNextMessage() {
+ do {
+ auto m = processOnce();
+ if(m.populated)
+ return m;
+ } while(lowLevelReceive());
+
+ throw new ConnectionClosedException("Websocket receive timed out");
+ //return WebSocketFrame.init; // FIXME? maybe.
+ }
+
+ /++
+ Tells if [waitForNextMessage] would block.
+ +/
+ /// Group: blocking_api
+ public bool waitForNextMessageWouldBlock() {
+ checkAgain:
+ if(isMessageBuffered())
+ return false;
+ if(!isDataPending())
+ return true;
+ while(isDataPending())
+ lowLevelReceive();
+ goto checkAgain;
+ }
+
+ /++
+ Is there a message in the buffer already?
+ If `true`, [waitForNextMessage] is guaranteed to return immediately.
+ If `false`, check [isDataPending] as the next step.
+ +/
+ /// Group: blocking_api
+ public bool isMessageBuffered() {
+ ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
+ auto s = d;
+ if(d.length) {
+ auto orig = d;
+ auto m = WebSocketFrame.read(d);
+ // that's how it indicates that it needs more data
+ if(d !is orig)
+ return true;
+ }
+
+ return false;
+ }
+
+ private ubyte continuingType;
+ private ubyte[] continuingData;
+ //private size_t continuingDataLength;
+
+ private WebSocketFrame processOnce() {
+ ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
+ auto s = d;
+ // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.
+ WebSocketFrame m;
+ if(d.length) {
+ auto orig = d;
+ m = WebSocketFrame.read(d);
+ // that's how it indicates that it needs more data
+ if(d is orig)
+ return WebSocketFrame.init;
+ m.unmaskInPlace();
+ switch(m.opcode) {
+ case WebSocketOpcode.continuation:
+ if(continuingData.length + m.data.length > config.maximumMessageSize)
+ throw new Exception("message size exceeded");
+
+ continuingData ~= m.data;
+ if(m.fin) {
+ if(ontextmessage)
+ ontextmessage(cast(char[]) continuingData);
+ if(onbinarymessage)
+ onbinarymessage(continuingData);
+
+ continuingData = null;
+ }
+ break;
+ case WebSocketOpcode.text:
+ if(m.fin) {
+ if(ontextmessage)
+ ontextmessage(m.textData);
+ } else {
+ continuingType = m.opcode;
+ //continuingDataLength = 0;
+ continuingData = null;
+ continuingData ~= m.data;
+ }
+ break;
+ case WebSocketOpcode.binary:
+ if(m.fin) {
+ if(onbinarymessage)
+ onbinarymessage(m.data);
+ } else {
+ continuingType = m.opcode;
+ //continuingDataLength = 0;
+ continuingData = null;
+ continuingData ~= m.data;
+ }
+ break;
+ case WebSocketOpcode.close:
+ readyState_ = CLOSED;
+ if(onclose)
+ onclose();
+
+ unregisterActiveSocket(this);
+ break;
+ case WebSocketOpcode.ping:
+ pong();
+ break;
+ case WebSocketOpcode.pong:
+ // just really references it is still alive, nbd.
+ break;
+ default: // ignore though i could and perhaps should throw too
+ }
+ }
+
+ // the recv thing can be invalidated so gotta copy it over ugh
+ if(d.length) {
+ m.data = m.data.dup();
+ }
+
+ import core.stdc.string;
+ memmove(receiveBuffer.ptr, d.ptr, d.length);
+ receiveBufferUsedLength = d.length;
+
+ return m;
+ }
+
+ private void autoprocess() {
+ // FIXME
+ do {
+ processOnce();
+ } while(lowLevelReceive());
+ }
+
+
+ void delegate() onclose; ///
+ void delegate() onerror; ///
+ void delegate(in char[]) ontextmessage; ///
+ void delegate(in ubyte[]) onbinarymessage; ///
+ void delegate() onopen; ///
+
+ /++
+
+ +/
+ /// Group: browser_api
+ void onmessage(void delegate(in char[]) dg) {
+ ontextmessage = dg;
+ }
+
+ /// ditto
+ void onmessage(void delegate(in ubyte[]) dg) {
+ onbinarymessage = dg;
+ }
+
+ /* } end copy/paste */
+
+
+ }
+
+ bool websocketRequested(Cgi cgi) {
+ return
+ "sec-websocket-key" in cgi.requestHeaders
+ &&
+ "connection" in cgi.requestHeaders &&
+ cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade")
+ &&
+ "upgrade" in cgi.requestHeaders &&
+ cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket")
+ ;
+ }
+
+ WebSocket acceptWebsocket(Cgi cgi) {
+ assert(!cgi.closed);
+ assert(!cgi.outputtedResponseData);
+ cgi.setResponseStatus("101 Switching Protocols");
+ cgi.header("Upgrade: WebSocket");
+ cgi.header("Connection: upgrade");
+
+ string key = cgi.requestHeaders["sec-websocket-key"];
+ key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec
+
+ import std.digest.sha;
+ auto hash = sha1Of(key);
+ auto accept = Base64.encode(hash);
+
+ cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup);
+
+ cgi.websocketMode = true;
+ cgi.write("");
+
+ cgi.flush();
+
+ return new WebSocket(cgi);
+ }
+
+ // FIXME get websocket to work on other modes, not just embedded_httpd
+
+ /* copy/paste in http2.d { */
+ enum WebSocketOpcode : ubyte {
+ continuation = 0,
+ text = 1,
+ binary = 2,
+ // 3, 4, 5, 6, 7 RESERVED
+ close = 8,
+ ping = 9,
+ pong = 10,
+ // 11,12,13,14,15 RESERVED
+ }
+
+ public struct WebSocketFrame {
+ private bool populated;
+ bool fin;
+ bool rsv1;
+ bool rsv2;
+ bool rsv3;
+ WebSocketOpcode opcode; // 4 bits
+ bool masked;
+ ubyte lengthIndicator; // don't set this when building one to send
+ ulong realLength; // don't use when sending
+ ubyte[4] maskingKey; // don't set this when sending
+ ubyte[] data;
+
+ static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) {
+ WebSocketFrame msg;
+ msg.fin = true;
+ msg.opcode = opcode;
+ msg.data = cast(ubyte[]) data;
+
+ return msg;
+ }
+
+ private void send(scope void delegate(ubyte[]) llsend) {
+ ubyte[64] headerScratch;
+ int headerScratchPos = 0;
+
+ realLength = data.length;
+
+ {
+ ubyte b1;
+ b1 |= cast(ubyte) opcode;
+ b1 |= rsv3 ? (1 << 4) : 0;
+ b1 |= rsv2 ? (1 << 5) : 0;
+ b1 |= rsv1 ? (1 << 6) : 0;
+ b1 |= fin ? (1 << 7) : 0;
+
+ headerScratch[0] = b1;
+ headerScratchPos++;
+ }
+
+ {
+ headerScratchPos++; // we'll set header[1] at the end of this
+ auto rlc = realLength;
+ ubyte b2;
+ b2 |= masked ? (1 << 7) : 0;
+
+ assert(headerScratchPos == 2);
+
+ if(realLength > 65535) {
+ // use 64 bit length
+ b2 |= 0x7f;
+
+ // FIXME: double check endinaness
+ foreach(i; 0 .. 8) {
+ headerScratch[2 + 7 - i] = rlc & 0x0ff;
+ rlc >>>= 8;
+ }
+
+ headerScratchPos += 8;
+ } else if(realLength > 125) {
+ // use 16 bit length
+ b2 |= 0x7e;
+
+ // FIXME: double check endinaness
+ foreach(i; 0 .. 2) {
+ headerScratch[2 + 1 - i] = rlc & 0x0ff;
+ rlc >>>= 8;
+ }
+
+ headerScratchPos += 2;
+ } else {
+ // use 7 bit length
+ b2 |= realLength & 0b_0111_1111;
+ }
+
+ headerScratch[1] = b2;
+ }
+
+ //assert(!masked, "masking key not properly implemented");
+ if(masked) {
+ // FIXME: randomize this
+ headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
+ headerScratchPos += 4;
+
+ // we'll just mask it in place...
+ int keyIdx = 0;
+ foreach(i; 0 .. data.length) {
+ data[i] = data[i] ^ maskingKey[keyIdx];
+ if(keyIdx == 3)
+ keyIdx = 0;
+ else
+ keyIdx++;
+ }
+ }
+
+ //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
+ llsend(headerScratch[0 .. headerScratchPos]);
+ llsend(data);
+ }
+
+ static WebSocketFrame read(ref ubyte[] d) {
+ WebSocketFrame msg;
+
+ auto orig = d;
+
+ WebSocketFrame needsMoreData() {
+ d = orig;
+ return WebSocketFrame.init;
+ }
+
+ if(d.length < 2)
+ return needsMoreData();
+
+ ubyte b = d[0];
+
+ msg.populated = true;
+
+ msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
+ b >>= 4;
+ msg.rsv3 = b & 0x01;
+ b >>= 1;
+ msg.rsv2 = b & 0x01;
+ b >>= 1;
+ msg.rsv1 = b & 0x01;
+ b >>= 1;
+ msg.fin = b & 0x01;
+
+ b = d[1];
+ msg.masked = (b & 0b1000_0000) ? true : false;
+ msg.lengthIndicator = b & 0b0111_1111;
+
+ d = d[2 .. $];
+
+ if(msg.lengthIndicator == 0x7e) {
+ // 16 bit length
+ msg.realLength = 0;
+
+ if(d.length < 2) return needsMoreData();
+
+ foreach(i; 0 .. 2) {
+ msg.realLength |= d[0] << ((1-i) * 8);
+ d = d[1 .. $];
+ }
+ } else if(msg.lengthIndicator == 0x7f) {
+ // 64 bit length
+ msg.realLength = 0;
+
+ if(d.length < 8) return needsMoreData();
+
+ foreach(i; 0 .. 8) {
+ msg.realLength |= d[0] << ((7-i) * 8);
+ d = d[1 .. $];
+ }
+ } else {
+ // 7 bit length
+ msg.realLength = msg.lengthIndicator;
+ }
+
+ if(msg.masked) {
+
+ if(d.length < 4) return needsMoreData();
+
+ msg.maskingKey = d[0 .. 4];
+ d = d[4 .. $];
+ }
+
+ if(msg.realLength > d.length) {
+ return needsMoreData();
+ }
+
+ msg.data = d[0 .. cast(size_t) msg.realLength];
+ d = d[cast(size_t) msg.realLength .. $];
+
+ return msg;
+ }
+
+ void unmaskInPlace() {
+ if(this.masked) {
+ int keyIdx = 0;
+ foreach(i; 0 .. this.data.length) {
+ this.data[i] = this.data[i] ^ this.maskingKey[keyIdx];
+ if(keyIdx == 3)
+ keyIdx = 0;
+ else
+ keyIdx++;
+ }
+ }
+ }
+
+ char[] textData() {
+ return cast(char[]) data;
+ }
+ }
+ /* } */
+}
+
+
+version(Windows)
+{
+ version(CRuntime_DigitalMars)
+ {
+ extern(C) int setmode(int, int) nothrow @nogc;
+ }
+ else version(CRuntime_Microsoft)
+ {
+ extern(C) int _setmode(int, int) nothrow @nogc;
+ alias setmode = _setmode;
+ }
+ else static assert(0);
+}
+
+version(Posix) {
+ version(CRuntime_Musl) {} else {
+ import core.sys.posix.unistd;
+ private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**);
+ }
+}
+
+
+// FIXME: these aren't quite public yet.
+//private:
+
+// template for laziness
+void startAddonServer()(string arg) {
+ version(OSX) {
+ assert(0, "Not implemented");
+ } else version(linux) {
+ import core.sys.posix.unistd;
+ pid_t pid;
+ const(char)*[16] args;
+ args[0] = "ARSD_CGI_ADDON_SERVER";
+ args[1] = arg.ptr;
+ posix_spawn(&pid, "/proc/self/exe",
+ null,
+ null,
+ args.ptr,
+ null // env
+ );
+ } else version(Windows) {
+ wchar[2048] filename;
+ auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length);
+ if(len == 0 || len == filename.length)
+ throw new Exception("could not get process name to start helper server");
+
+ STARTUPINFOW startupInfo;
+ startupInfo.cb = cast(DWORD) startupInfo.sizeof;
+ PROCESS_INFORMATION processInfo;
+
+ import std.utf;
+
+ // I *MIGHT* need to run it as a new job or a service...
+ auto ret = CreateProcessW(
+ filename.ptr,
+ toUTF16z(arg),
+ null, // process attributes
+ null, // thread attributes
+ false, // inherit handles
+ 0, // creation flags
+ null, // environment
+ null, // working directory
+ &startupInfo,
+ &processInfo
+ );
+
+ if(!ret)
+ throw new Exception("create process failed");
+
+ // when done with those, if we set them
+ /*
+ CloseHandle(hStdInput);
+ CloseHandle(hStdOutput);
+ CloseHandle(hStdError);
+ */
+
+ } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)");
+}
+
+// template for laziness
+/*
+ The websocket server is a single-process, single-thread, event
+ I/O thing. It is passed websockets from other CGI processes
+ and is then responsible for handling their messages and responses.
+ Note that the CGI process is responsible for websocket setup,
+ including authentication, etc.
+
+ It also gets data sent to it by other processes and is responsible
+ for distributing that, as necessary.
+*/
+void runWebsocketServer()() {
+ assert(0, "not implemented");
+}
+
+void sendToWebsocketServer(WebSocket ws, string group) {
+ assert(0, "not implemented");
+}
+
+void sendToWebsocketServer(string content, string group) {
+ assert(0, "not implemented");
+}
+
+
+void runEventServer()() {
+ runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation());
+}
+
+void runTimerServer()() {
+ runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation());
+}
+
+version(Posix) {
+ alias LocalServerConnectionHandle = int;
+ alias CgiConnectionHandle = int;
+ alias SocketConnectionHandle = int;
+
+ enum INVALID_CGI_CONNECTION_HANDLE = -1;
+} else version(Windows) {
+ alias LocalServerConnectionHandle = HANDLE;
+ version(embedded_httpd_threads) {
+ alias CgiConnectionHandle = SOCKET;
+ enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET;
+ } else version(fastcgi) {
+ alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point.
+ enum INVALID_CGI_CONNECTION_HANDLE = null;
+ } else version(scgi) {
+ alias CgiConnectionHandle = SOCKET;
+ enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET;
+ } else { /* version(plain_cgi) */
+ alias CgiConnectionHandle = HANDLE;
+ enum INVALID_CGI_CONNECTION_HANDLE = null;
+ }
+ alias SocketConnectionHandle = SOCKET;
+}
+
+version(with_addon_servers_connections)
+LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) {
+ version(Posix) {
+ import core.sys.posix.unistd;
+ import core.sys.posix.sys.un;
+
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if(sock == -1)
+ throw new Exception("socket " ~ to!string(errno));
+
+ scope(failure)
+ close(sock);
+
+ cloexec(sock);
+
+ // add-on server processes are assumed to be local, and thus will
+ // use unix domain sockets. Besides, I want to pass sockets to them,
+ // so it basically must be local (except for the session server, but meh).
+ sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+ version(linux) {
+ // on linux, we will use the abstract namespace
+ addr.sun_path[0] = 0;
+ addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[];
+ } else {
+ // but otherwise, just use a file cuz we must.
+ addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[];
+ }
+
+ bool alreadyTried;
+
+ try_again:
+
+ if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) {
+ if(!alreadyTried && errno == ECONNREFUSED) {
+ // try auto-spawning the server, then attempt connection again
+ startAddonServer(arg);
+ import core.thread;
+ Thread.sleep(50.msecs);
+ alreadyTried = true;
+ goto try_again;
+ } else
+ throw new Exception("connect " ~ to!string(errno));
+ }
+
+ return sock;
+ } else version(Windows) {
+ return null; // FIXME
+ }
+}
+
+version(with_addon_servers_connections)
+void closeLocalServerConnection(LocalServerConnectionHandle handle) {
+ version(Posix) {
+ import core.sys.posix.unistd;
+ close(handle);
+ } else version(Windows)
+ CloseHandle(handle);
+}
+
+void runSessionServer()() {
+ runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation());
+}
+
+version(Posix)
+private void makeNonBlocking(int fd) {
+ import core.sys.posix.fcntl;
+ auto flags = fcntl(fd, F_GETFL, 0);
+ if(flags == -1)
+ throw new Exception("fcntl get");
+ flags |= O_NONBLOCK;
+ auto s = fcntl(fd, F_SETFL, flags);
+ if(s == -1)
+ throw new Exception("fcntl set");
+}
+
+import core.stdc.errno;
+
+struct IoOp {
+ @disable this();
+ @disable this(this);
+
+ /*
+ So we want to be able to eventually handle generic sockets too.
+ */
+
+ enum Read = 1;
+ enum Write = 2;
+ enum Accept = 3;
+ enum ReadSocketHandle = 4;
+
+ // Your handler may be called in a different thread than the one that initiated the IO request!
+ // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution.
+ private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed
+ private void delegate(IoOp*) closeHandler;
+ private void delegate(IoOp*) completeHandler;
+ private int internalFd;
+ private int operation;
+ private int bufferLengthAllocated;
+ private int bufferLengthUsed;
+ private ubyte[1] internalBuffer; // it can be overallocated!
+
+ ubyte[] allocatedBuffer() return {
+ return internalBuffer.ptr[0 .. bufferLengthAllocated];
+ }
+
+ ubyte[] usedBuffer() return {
+ return allocatedBuffer[0 .. bufferLengthUsed];
+ }
+
+ void reset() {
+ bufferLengthUsed = 0;
+ }
+
+ int fd() {
+ return internalFd;
+ }
+}
+
+IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) {
+ import core.stdc.stdlib;
+
+ auto ptr = calloc(IoOp.sizeof + bufferSize, 1);
+ if(ptr is null)
+ assert(0); // out of memory!
+
+ auto op = cast(IoOp*) ptr;
+
+ op.handler = handler;
+ op.internalFd = fd;
+ op.operation = operation;
+ op.bufferLengthAllocated = bufferSize;
+ op.bufferLengthUsed = 0;
+
+ import core.memory;
+
+ GC.addRoot(ptr);
+
+ return op;
+}
+
+void freeIoOp(ref IoOp* ptr) {
+
+ import core.memory;
+ GC.removeRoot(ptr);
+
+ import core.stdc.stdlib;
+ free(ptr);
+ ptr = null;
+}
+
+version(Posix)
+version(with_addon_servers_connections)
+void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) {
+ import core.sys.posix.unistd;
+
+ auto ret = write(connection, data.ptr, data.length);
+ if(ret != data.length) {
+ if(ret == 0 || errno == EPIPE) {
+ // the file is closed, remove it
+ eis.fileClosed(connection);
+ } else
+ throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME
+ }
+}
+version(Windows)
+version(with_addon_servers_connections)
+void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) {
+ // FIXME
+}
+
+bool isInvalidHandle(CgiConnectionHandle h) {
+ return h == INVALID_CGI_CONNECTION_HANDLE;
+}
+
+/+
+https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv
+https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode
+https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive
+https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports
+https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport
+https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex
+https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects
+https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer
+https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call
+https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult
+
++/
+
+/++
+ You can customize your server by subclassing the appropriate server. Then, register your
+ subclass at compile time with the [registerEventIoServer] template, or implement your own
+ main function and call it yourself.
+
+ $(TIP If you make your subclass a `final class`, there is a slight performance improvement.)
++/
+version(with_addon_servers_connections)
+interface EventIoServer {
+ bool handleLocalConnectionData(IoOp* op, int receivedFd);
+ void handleLocalConnectionClose(IoOp* op);
+ void handleLocalConnectionComplete(IoOp* op);
+ void wait_timeout();
+ void fileClosed(int fd);
+
+ void epoll_fd(int fd);
+}
+
+// the sink should buffer it
+private void serialize(T)(scope void delegate(ubyte[]) sink, T t) {
+ static if(is(T == struct)) {
+ foreach(member; __traits(allMembers, T))
+ serialize(sink, __traits(getMember, t, member));
+ } else static if(is(T : int)) {
+ // no need to think of endianness just because this is only used
+ // for local, same-machine stuff anyway. thanks private lol
+ sink((cast(ubyte*) &t)[0 .. t.sizeof]);
+ } else static if(is(T == string) || is(T : const(ubyte)[])) {
+ // these are common enough to optimize
+ int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc.
+ sink((cast(ubyte*) &len)[0 .. int.sizeof]);
+ sink(cast(ubyte[]) t[]);
+ } else static if(is(T : A[], A)) {
+ // generic array is less optimal but still prolly ok
+ int len = cast(int) t.length;
+ sink((cast(ubyte*) &len)[0 .. int.sizeof]);
+ foreach(item; t)
+ serialize(sink, item);
+ } else static assert(0, T.stringof);
+}
+
+// all may be stack buffers, so use cautio
+private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) {
+ static if(is(T == struct)) {
+ T t;
+ foreach(member; __traits(allMembers, T))
+ deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; });
+ dg(t);
+ } else static if(is(T : int)) {
+ // no need to think of endianness just because this is only used
+ // for local, same-machine stuff anyway. thanks private lol
+ T t;
+ auto data = get(t.sizeof);
+ t = (cast(T[]) data)[0];
+ dg(t);
+ } else static if(is(T == string) || is(T : const(ubyte)[])) {
+ // these are common enough to optimize
+ int len;
+ auto data = get(len.sizeof);
+ len = (cast(int[]) data)[0];
+
+ /*
+ typeof(T[0])[2000] stackBuffer;
+ T buffer;
+
+ if(len < stackBuffer.length)
+ buffer = stackBuffer[0 .. len];
+ else
+ buffer = new T(len);
+
+ data = get(len * typeof(T[0]).sizeof);
+ */
+
+ T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof);
+
+ dg(t);
+ } else static if(is(T == E[], E)) {
+ T t;
+ int len;
+ auto data = get(len.sizeof);
+ len = (cast(int[]) data)[0];
+ t.length = len;
+ foreach(ref e; t) {
+ deserialize!E(get, (ele) { e = ele; });
+ }
+ dg(t);
+ } else static assert(0, T.stringof);
+}
+
+unittest {
+ serialize((ubyte[] b) {
+ deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); });
+ }, 1);
+ serialize((ubyte[] b) {
+ deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); });
+ }, 56674);
+ ubyte[1000] buffer;
+ int bufferPoint;
+ void add(ubyte[] b) {
+ buffer[bufferPoint .. bufferPoint + b.length] = b[];
+ bufferPoint += b.length;
+ }
+ ubyte[] get(int sz) {
+ auto b = buffer[bufferPoint .. bufferPoint + sz];
+ bufferPoint += sz;
+ return b;
+ }
+ serialize(&add, "test here");
+ bufferPoint = 0;
+ deserialize!string(&get, (t) { assert(t == "test here"); });
+ bufferPoint = 0;
+
+ struct Foo {
+ int a;
+ ubyte c;
+ string d;
+ }
+ serialize(&add, Foo(403, 37, "amazing"));
+ bufferPoint = 0;
+ deserialize!Foo(&get, (t) {
+ assert(t.a == 403);
+ assert(t.c == 37);
+ assert(t.d == "amazing");
+ });
+ bufferPoint = 0;
+}
+
+/*
+ Here's the way the RPC interface works:
+
+ You define the interface that lists the functions you can call on the remote process.
+ The interface may also have static methods for convenience. These forward to a singleton
+ instance of an auto-generated class, which actually sends the args over the pipe.
+
+ An impl class actually implements it. A receiving server deserializes down the pipe and
+ calls methods on the class.
+
+ I went with the interface to get some nice compiler checking and documentation stuff.
+
+ I could have skipped the interface and just implemented it all from the server class definition
+ itself, but then the usage may call the method instead of rpcing it; I just like having the user
+ interface and the implementation separate so you aren't tempted to `new impl` to call the methods.
+
+
+ I fiddled with newlines in the mixin string to ensure the assert line numbers matched up to the source code line number. Idk why dmd didn't do this automatically, but it was important to me.
+
+ Realistically though the bodies would just be
+ connection.call(this.mangleof, args...) sooooo.
+
+ FIXME: overloads aren't supported
+*/
+
+/// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this.
+interface SessionObject {}
+
+private immutable void delegate(string[])[string] scheduledJobHandlers;
+private immutable void delegate(string[])[string] websocketServers;
+
+version(with_breaking_cgi_features)
+mixin(q{
+
+mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) {
+ static import std.traits;
+
+ // derivedMembers on an interface seems to give exactly what I want: the virtual functions we need to implement. so I am just going to use it directly without more filtering.
+ static foreach(idx, member; __traits(derivedMembers, T)) {
+ static if(__traits(isVirtualFunction, __traits(getMember, T, member)))
+ mixin( q{
+ std.traits.ReturnType!(__traits(getMember, T, member))
+ } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params)
+ {
+ SerializationBuffer buffer;
+ auto i = cast(ushort) idx;
+ serialize(&buffer.sink, i);
+ serialize(&buffer.sink, __traits(getMember, T, member).mangleof);
+ foreach(param; params)
+ serialize(&buffer.sink, param);
+
+ auto sendable = buffer.sendable;
+
+ version(Posix) {{
+ auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0);
+
+ if(ret == -1) {
+ throw new Exception("send returned -1, errno: " ~ to!string(errno));
+ } else if(ret == 0) {
+ throw new Exception("Connection to addon server lost");
+ } if(ret < sendable.length)
+ throw new Exception("Send failed to send all");
+ assert(ret == sendable.length);
+ }} // FIXME Windows impl
+
+ static if(!is(typeof(return) == void)) {
+ // there is a return value; we need to wait for it too
+ version(Posix) {
+ ubyte[3000] revBuffer;
+ auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0);
+ auto got = revBuffer[0 .. ret];
+
+ int dataLocation;
+ ubyte[] grab(int sz) {
+ auto d = got[dataLocation .. dataLocation + sz];
+ dataLocation += sz;
+ return d;
+ }
+
+ typeof(return) retu;
+ deserialize!(typeof(return))(&grab, (a) { retu = a; });
+ return retu;
+ } else {
+ // FIXME Windows impl
+ return typeof(return).init;
+ }
+
+ }
+ }});
+ }
+
+ private static typeof(this) singletonInstance;
+ private LocalServerConnectionHandle connectionHandle;
+
+ static typeof(this) connection() {
+ if(singletonInstance is null) {
+ singletonInstance = new typeof(this)();
+ singletonInstance.connect();
+ }
+ return singletonInstance;
+ }
+
+ void connect() {
+ connectionHandle = openLocalServerConnection(serverPath, cmdArg);
+ }
+
+ void disconnect() {
+ closeLocalServerConnection(connectionHandle);
+ }
+}
+
+void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) {
+ ushort calledIdx;
+ string calledFunction;
+
+ int dataLocation;
+ ubyte[] grab(int sz) {
+ if(sz == 0) assert(0);
+ auto d = data[dataLocation .. dataLocation + sz];
+ dataLocation += sz;
+ return d;
+ }
+
+ again:
+
+ deserialize!ushort(&grab, (a) { calledIdx = a; });
+ deserialize!string(&grab, (a) { calledFunction = a; });
+
+ import std.traits;
+
+ sw: switch(calledIdx) {
+ foreach(idx, memberName; __traits(derivedMembers, Interface))
+ static if(__traits(isVirtualFunction, __traits(getMember, Interface, memberName))) {
+ case idx:
+ assert(calledFunction == __traits(getMember, Interface, memberName).mangleof);
+
+ Parameters!(__traits(getMember, Interface, memberName)) params;
+ foreach(ref param; params)
+ deserialize!(typeof(param))(&grab, (a) { param = a; });
+
+ static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) {
+ __traits(getMember, this_, memberName)(params);
+ } else {
+ auto ret = __traits(getMember, this_, memberName)(params);
+ SerializationBuffer buffer;
+ serialize(&buffer.sink, ret);
+
+ auto sendable = buffer.sendable;
+
+ version(Posix) {
+ auto r = send(fd, sendable.ptr, sendable.length, 0);
+ if(r == -1) {
+ throw new Exception("send returned -1, errno: " ~ to!string(errno));
+ } else if(r == 0) {
+ throw new Exception("Connection to addon client lost");
+ } if(r < sendable.length)
+ throw new Exception("Send failed to send all");
+
+ } // FIXME Windows impl
+ }
+ break sw;
+ }
+ default: assert(0);
+ }
+
+ if(dataLocation != data.length)
+ goto again;
+}
+
+
+private struct SerializationBuffer {
+ ubyte[2048] bufferBacking;
+ int bufferLocation;
+ void sink(scope ubyte[] data) {
+ bufferBacking[bufferLocation .. bufferLocation + data.length] = data[];
+ bufferLocation += data.length;
+ }
+
+ ubyte[] sendable() return {
+ return bufferBacking[0 .. bufferLocation];
+ }
+}
+
+/*
+ FIXME:
+ add a version command line arg
+ version data in the library
+ management gui as external program
+
+ at server with event_fd for each run
+ use .mangleof in the at function name
+
+ i think the at server will have to:
+ pipe args to the child
+ collect child output for logging
+ get child return value for logging
+
+ on windows timers work differently. idk how to best combine with the io stuff.
+
+ will have to have dump and restore too, so i can restart without losing stuff.
+*/
+
+/++
+ A convenience object for talking to the [BasicDataServer] from a higher level.
+ See: [Cgi.getSessionObject].
+
+ You pass it a `Data` struct describing the data you want saved in the session.
+ Then, this class will generate getter and setter properties that allow access
+ to that data.
+
+ Note that each load and store will be done as-accessed; it doesn't front-load
+ mutable data nor does it batch updates out of fear of read-modify-write race
+ conditions. (In fact, right now it does this for everything, but in the future,
+ I might batch load `immutable` members of the Data struct.)
+
+ At some point in the future, I might also let it do different backends, like
+ a client-side cookie store too, but idk.
+
+ Note that the plain-old-data members of your `Data` struct are wrapped by this
+ interface via a static foreach to make property functions.
+
+ See_Also: [MockSession]
++/
+interface Session(Data) : SessionObject {
+ @property string sessionId() const;
+
+ /++
+ Starts a new session. Note that a session is also
+ implicitly started as soon as you write data to it,
+ so if you need to alter these parameters from their
+ defaults, be sure to explicitly call this BEFORE doing
+ any writes to session data.
+
+ Params:
+ idleLifetime = How long, in seconds, the session
+ should remain in memory when not being read from
+ or written to. The default is one day.
+
+ NOT IMPLEMENTED
+
+ useExtendedLifetimeCookie = The session ID is always
+ stored in a HTTP cookie, and by default, that cookie
+ is discarded when the user closes their browser.
+
+ But if you set this to true, it will use a non-perishable
+ cookie for the given idleLifetime.
+
+ NOT IMPLEMENTED
+ +/
+ void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false);
+
+ /++
+ Regenerates the session ID and updates the associated
+ cookie.
+
+ This is also your chance to change immutable data
+ (not yet implemented).
+ +/
+ void regenerateId();
+
+ /++
+ Terminates this session, deleting all saved data.
+ +/
+ void terminate();
+
+ /++
+ Plain-old-data members of your `Data` struct are wrapped here via
+ the property getters and setters.
+
+ If the member is a non-string array, it returns a magical array proxy
+ object which allows for atomic appends and replaces via overloaded operators.
+ You can slice this to get a range representing a $(B const) view of the array.
+ This is to protect you against read-modify-write race conditions.
+ +/
+ static foreach(memberName; __traits(allMembers, Data))
+ static if(is(typeof(__traits(getMember, Data, memberName))))
+ mixin(q{
+ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout;
+ @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value);
+ });
+
+}
+
+/++
+ An implementation of [Session] that works on real cgi connections utilizing the
+ [BasicDataServer].
+
+ As opposed to a [MockSession] which is made for testing purposes.
+
+ You will not construct one of these directly. See [Cgi.getSessionObject] instead.
++/
+class BasicDataServerSession(Data) : Session!Data {
+ private Cgi cgi;
+ private string sessionId_;
+
+ public @property string sessionId() const {
+ return sessionId_;
+ }
+
+ protected @property string sessionId(string s) {
+ return this.sessionId_ = s;
+ }
+
+ private this(Cgi cgi) {
+ this.cgi = cgi;
+ if(auto ptr = "sessionId" in cgi.cookies)
+ sessionId = (*ptr).length ? *ptr : null;
+ }
+
+ void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {
+ assert(sessionId is null);
+
+ // FIXME: what if there is a session ID cookie, but no corresponding session on the server?
+
+ import std.random, std.conv;
+ sessionId = to!string(uniform(1, long.max));
+
+ BasicDataServer.connection.createSession(sessionId, idleLifetime);
+ setCookie();
+ }
+
+ protected void setCookie() {
+ cgi.setCookie(
+ "sessionId", sessionId,
+ 0 /* expiration */,
+ "/" /* path */,
+ null /* domain */,
+ true /* http only */,
+ cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */);
+ }
+
+ void regenerateId() {
+ if(sessionId is null) {
+ start();
+ return;
+ }
+ import std.random, std.conv;
+ auto oldSessionId = sessionId;
+ sessionId = to!string(uniform(1, long.max));
+ BasicDataServer.connection.renameSession(oldSessionId, sessionId);
+ setCookie();
+ }
+
+ void terminate() {
+ BasicDataServer.connection.destroySession(sessionId);
+ sessionId = null;
+ setCookie();
+ }
+
+ static foreach(memberName; __traits(allMembers, Data))
+ static if(is(typeof(__traits(getMember, Data, memberName))))
+ mixin(q{
+ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout {
+ if(sessionId is null)
+ return typeof(return).init;
+
+ import std.traits;
+ auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName);
+ if(v.length == 0)
+ return typeof(return).init;
+ import std.conv;
+ // why this cast? to doesn't like being given an inout argument. so need to do it without that, then
+ // we need to return it and that needed the cast. It should be fine since we basically respect constness..
+ // basically. Assuming the session is POD this should be fine.
+ return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v);
+ }
+ @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) {
+ if(sessionId is null)
+ start();
+ import std.conv;
+ import std.traits;
+ BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value));
+ return value;
+ }
+ });
+}
+
+/++
+ A mock object that works like the real session, but doesn't actually interact with any actual database or http connection.
+ Simply stores the data in its instance members.
++/
+class MockSession(Data) : Session!Data {
+ pure {
+ @property string sessionId() const { return "mock"; }
+ void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {}
+ void regenerateId() {}
+ void terminate() {}
+
+ private Data store_;
+
+ static foreach(memberName; __traits(allMembers, Data))
+ static if(is(typeof(__traits(getMember, Data, memberName))))
+ mixin(q{
+ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout {
+ return __traits(getMember, store_, memberName);
+ }
+ @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) {
+ return __traits(getMember, store_, memberName) = value;
+ }
+ });
+ }
+}
+
+/++
+ Direct interface to the basic data add-on server. You can
+ typically use [Cgi.getSessionObject] as a more convenient interface.
++/
+version(with_addon_servers_connections)
+interface BasicDataServer {
+ ///
+ void createSession(string sessionId, int lifetime);
+ ///
+ void renewSession(string sessionId, int lifetime);
+ ///
+ void destroySession(string sessionId);
+ ///
+ void renameSession(string oldSessionId, string newSessionId);
+
+ ///
+ void setSessionData(string sessionId, string dataKey, string dataValue);
+ ///
+ string getSessionData(string sessionId, string dataKey);
+
+ ///
+ static BasicDataServerConnection connection() {
+ return BasicDataServerConnection.connection();
+ }
+}
+
+version(with_addon_servers_connections)
+class BasicDataServerConnection : BasicDataServer {
+ mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server");
+}
+
+version(with_addon_servers)
+final class BasicDataServerImplementation : BasicDataServer, EventIoServer {
+
+ void createSession(string sessionId, int lifetime) {
+ sessions[sessionId.idup] = Session(lifetime);
+ }
+ void destroySession(string sessionId) {
+ sessions.remove(sessionId);
+ }
+ void renewSession(string sessionId, int lifetime) {
+ sessions[sessionId].lifetime = lifetime;
+ }
+ void renameSession(string oldSessionId, string newSessionId) {
+ sessions[newSessionId.idup] = sessions[oldSessionId];
+ sessions.remove(oldSessionId);
+ }
+ void setSessionData(string sessionId, string dataKey, string dataValue) {
+ if(sessionId !in sessions)
+ createSession(sessionId, 3600); // FIXME?
+ sessions[sessionId].values[dataKey.idup] = dataValue.idup;
+ }
+ string getSessionData(string sessionId, string dataKey) {
+ if(auto session = sessionId in sessions) {
+ if(auto data = dataKey in (*session).values)
+ return *data;
+ else
+ return null; // no such data
+
+ } else {
+ return null; // no session
+ }
+ }
+
+
+ protected:
+
+ struct Session {
+ int lifetime;
+
+ string[string] values;
+ }
+
+ Session[string] sessions;
+
+ bool handleLocalConnectionData(IoOp* op, int receivedFd) {
+ auto data = op.usedBuffer;
+ dispatchRpcServer!BasicDataServer(this, data, op.fd);
+ return false;
+ }
+
+ void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go
+ void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant
+ void wait_timeout() {}
+ void fileClosed(int fd) {} // stateless so irrelevant
+ void epoll_fd(int fd) {}
+}
+
+/++
+ See [schedule] to make one of these. You then call one of the methods here to set it up:
+
+ ---
+ schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC
+ schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds
+ schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it
+ ---
++/
+version(with_addon_servers_connections)
+struct ScheduledJobHelper {
+ private string func;
+ private string[] args;
+ private bool consumed;
+
+ private this(string func, string[] args) {
+ this.func = func;
+ this.args = args;
+ }
+
+ ~this() {
+ assert(consumed);
+ }
+
+ /++
+ Schedules the job to be run at the given time.
+ +/
+ void at(DateTime when, immutable TimeZone timezone = UTC()) {
+ consumed = true;
+
+ auto conn = ScheduledJobServerConnection.connection;
+ import std.file;
+ auto st = SysTime(when, timezone);
+ auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args);
+ }
+
+ /++
+ Schedules the job to run at least after the specified delay.
+ +/
+ void delay(Duration delay) {
+ consumed = true;
+
+ auto conn = ScheduledJobServerConnection.connection;
+ import std.file;
+ auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args);
+ }
+
+ /++
+ Runs the job in the background ASAP.
+
+ $(NOTE It may run in a background thread. Don't segfault!)
+ +/
+ void asap() {
+ consumed = true;
+
+ auto conn = ScheduledJobServerConnection.connection;
+ import std.file;
+ auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args);
+ }
+
+ /+
+ /++
+ Schedules the job to recur on the given pattern.
+ +/
+ void recur(string spec) {
+
+ }
+ +/
+}
+
+/++
+ First step to schedule a job on the scheduled job server.
+
+ The scheduled job needs to be a top-level function that doesn't read any
+ variables from outside its arguments because it may be run in a new process,
+ without any context existing later.
+
+ You MUST set details on the returned object to actually do anything!
++/
+template schedule(alias fn, T...) if(is(typeof(fn) == function)) {
+ ///
+ ScheduledJobHelper schedule(T args) {
+ // this isn't meant to ever be called, but instead just to
+ // get the compiler to type check the arguments passed for us
+ auto sample = delegate() {
+ fn(args);
+ };
+ string[] sargs;
+ foreach(arg; args)
+ sargs ~= to!string(arg);
+ return ScheduledJobHelper(fn.mangleof, sargs);
+ }
+
+ shared static this() {
+ scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) {
+ import std.traits;
+ Parameters!fn args;
+ foreach(idx, ref arg; args)
+ arg = to!(typeof(arg))(sargs[idx]);
+ fn(args);
+ };
+ }
+}
+
+///
+interface ScheduledJobServer {
+ /// Use the [schedule] function for a higher-level interface.
+ int scheduleJob(int whenIs, int when, string executable, string func, string[] args);
+ ///
+ void cancelJob(int jobId);
+}
+
+version(with_addon_servers_connections)
+class ScheduledJobServerConnection : ScheduledJobServer {
+ mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server");
+}
+
+version(with_addon_servers)
+final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer {
+ // FIXME: we need to handle SIGCHLD in this somehow
+ // whenIs is 0 for relative, 1 for absolute
+ protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) {
+ auto nj = nextJobId;
+ nextJobId++;
+
+ version(linux) {
+ import core.sys.linux.timerfd;
+ import core.sys.linux.epoll;
+ import core.sys.posix.unistd;
+
+
+ auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC);
+ if(fd == -1)
+ throw new Exception("fd timer create failed");
+
+ foreach(ref arg; args)
+ arg = arg.idup;
+ auto job = Job(executable.idup, func.idup, .dup(args), fd, nj);
+
+ itimerspec value;
+ value.it_value.tv_sec = when;
+ value.it_value.tv_nsec = 0;
+
+ value.it_interval.tv_sec = 0;
+ value.it_interval.tv_nsec = 0;
+
+ if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1)
+ throw new Exception("couldn't set fd timer");
+
+ auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) {
+ jobs.remove(nj);
+ epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null);
+ close(fd);
+
+
+ spawnProcess([job.executable, "--timed-job", job.func] ~ job.args);
+
+ return true;
+ });
+ scope(failure)
+ freeIoOp(op);
+
+ epoll_event ev;
+ ev.events = EPOLLIN | EPOLLET;
+ ev.data.ptr = op;
+ if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1)
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+
+ jobs[nj] = job;
+ return nj;
+ } else assert(0);
+ }
+
+ protected void cancelJob(int jobId) {
+ version(linux) {
+ auto job = jobId in jobs;
+ if(job is null)
+ return;
+
+ jobs.remove(jobId);
+
+ version(linux) {
+ import core.sys.linux.timerfd;
+ import core.sys.linux.epoll;
+ import core.sys.posix.unistd;
+ epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null);
+ close(job.timerfd);
+ }
+ }
+ jobs.remove(jobId);
+ }
+
+ int nextJobId = 1;
+ static struct Job {
+ string executable;
+ string func;
+ string[] args;
+ int timerfd;
+ int id;
+ }
+ Job[int] jobs;
+
+
+ // event io server methods below
+
+ bool handleLocalConnectionData(IoOp* op, int receivedFd) {
+ auto data = op.usedBuffer;
+ dispatchRpcServer!ScheduledJobServer(this, data, op.fd);
+ return false;
+ }
+
+ void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go
+ void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant
+ void wait_timeout() {}
+ void fileClosed(int fd) {} // stateless so irrelevant
+
+ int epoll_fd_;
+ void epoll_fd(int fd) {this.epoll_fd_ = fd; }
+ int epoll_fd() { return epoll_fd_; }
+}
+
+///
+version(with_addon_servers_connections)
+interface EventSourceServer {
+ /++
+ sends this cgi request to the event server so it will be fed events. You should not do anything else with the cgi object after this.
+
+ $(WARNING This API is extremely unstable. I might change it or remove it without notice.)
+
+ See_Also:
+ [sendEvent]
+ +/
+ public static void adoptConnection(Cgi cgi, in char[] eventUrl) {
+ /*
+ If lastEventId is missing or empty, you just get new events as they come.
+
+ If it is set from something else, it sends all since then (that are still alive)
+ down the pipe immediately.
+
+ The reason it can come from the header is that's what the standard defines for
+ browser reconnects. The reason it can come from a query string is just convenience
+ in catching up in a user-defined manner.
+
+ The reason the header overrides the query string is if the browser tries to reconnect,
+ it will send the header AND the query (it reconnects to the same url), so we just
+ want to do the restart thing.
+
+ Note that if you ask for "0" as the lastEventId, it will get ALL still living events.
+ */
+ string lastEventId = cgi.lastEventId;
+ if(lastEventId.length == 0 && "lastEventId" in cgi.get)
+ lastEventId = cgi.get["lastEventId"];
+
+ cgi.setResponseContentType("text/event-stream");
+ cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later
+ cgi.flush();
+
+ cgi.closed = true;
+ auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server");
+ scope(exit)
+ closeLocalServerConnection(s);
+
+ version(fastcgi)
+ throw new Exception("sending fcgi connections not supported");
+ else {
+ auto fd = cgi.getOutputFileHandle();
+ if(isInvalidHandle(fd))
+ throw new Exception("bad fd from cgi!");
+
+ EventSourceServerImplementation.SendableEventConnection sec;
+ sec.populate(cgi.responseChunked, eventUrl, lastEventId);
+
+ version(Posix) {
+ auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd);
+ assert(res == sec.sizeof);
+ } else version(Windows) {
+ // FIXME
+ }
+ }
+ }
+
+ /++
+ Sends an event to the event server, starting it if necessary. The event server will distribute it to any listening clients, and store it for `lifetime` seconds for any later listening clients to catch up later.
+
+ $(WARNING This API is extremely unstable. I might change it or remove it without notice.)
+
+ Params:
+ url = A string identifying this event "bucket". Listening clients must also connect to this same string. I called it `url` because I envision it being just passed as the url of the request.
+ event = the event type string, which is used in the Javascript addEventListener API on EventSource
+ data = the event data. Available in JS as `event.data`.
+ lifetime = the amount of time to keep this event for replaying on the event server.
+
+ See_Also:
+ [sendEventToEventServer]
+ +/
+ public static void sendEvent(string url, string event, string data, int lifetime) {
+ auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server");
+ scope(exit)
+ closeLocalServerConnection(s);
+
+ EventSourceServerImplementation.SendableEvent sev;
+ sev.populate(url, event, data, lifetime);
+
+ version(Posix) {
+ auto ret = send(s, &sev, sev.sizeof, 0);
+ assert(ret == sev.sizeof);
+ } else version(Windows) {
+ // FIXME
+ }
+ }
+
+ /++
+ Messages sent to `url` will also be sent to anyone listening on `forwardUrl`.
+
+ See_Also: [disconnect]
+ +/
+ void connect(string url, string forwardUrl);
+
+ /++
+ Disconnects `forwardUrl` from `url`
+
+ See_Also: [connect]
+ +/
+ void disconnect(string url, string forwardUrl);
+}
+
+///
+version(with_addon_servers)
+final class EventSourceServerImplementation : EventSourceServer, EventIoServer {
+
+ protected:
+
+ void connect(string url, string forwardUrl) {
+ pipes[url] ~= forwardUrl;
+ }
+ void disconnect(string url, string forwardUrl) {
+ auto t = url in pipes;
+ if(t is null)
+ return;
+ foreach(idx, n; (*t))
+ if(n == forwardUrl) {
+ (*t)[idx] = (*t)[$-1];
+ (*t) = (*t)[0 .. $-1];
+ break;
+ }
+ }
+
+ bool handleLocalConnectionData(IoOp* op, int receivedFd) {
+ if(receivedFd != -1) {
+ //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer);
+
+ //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5);
+
+ SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr;
+
+ auto url = got.url.idup;
+ eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false);
+
+ // FIXME: catch up on past messages here
+ } else {
+ auto data = op.usedBuffer;
+ auto event = cast(SendableEvent*) data.ptr;
+
+ if(event.magic == 0xdeadbeef) {
+ handleInputEvent(event);
+
+ if(event.url in pipes)
+ foreach(pipe; pipes[event.url]) {
+ event.url = pipe;
+ handleInputEvent(event);
+ }
+ } else {
+ dispatchRpcServer!EventSourceServer(this, data, op.fd);
+ }
+ }
+ return false;
+ }
+ void handleLocalConnectionClose(IoOp* op) {}
+ void handleLocalConnectionComplete(IoOp* op) {}
+
+ void wait_timeout() {
+ // just keeping alive
+ foreach(url, connections; eventConnectionsByUrl)
+ foreach(connection; connections)
+ if(connection.needsChunking)
+ nonBlockingWrite(this, connection.fd, "2\r\n:\n");
+ else
+ nonBlockingWrite(this, connection.fd, ":\n");
+ }
+
+ void fileClosed(int fd) {
+ outer: foreach(url, ref connections; eventConnectionsByUrl) {
+ foreach(idx, conn; connections) {
+ if(fd == conn.fd) {
+ connections[idx] = connections[$-1];
+ connections = connections[0 .. $ - 1];
+ continue outer;
+ }
+ }
+ }
+ }
+
+ void epoll_fd(int fd) {}
+
+
+ private:
+
+
+ struct SendableEventConnection {
+ ubyte responseChunked;
+
+ int urlLength;
+ char[256] urlBuffer = 0;
+
+ int lastEventIdLength;
+ char[32] lastEventIdBuffer = 0;
+
+ char[] url() return {
+ return urlBuffer[0 .. urlLength];
+ }
+ void url(in char[] u) {
+ urlBuffer[0 .. u.length] = u[];
+ urlLength = cast(int) u.length;
+ }
+ char[] lastEventId() return {
+ return lastEventIdBuffer[0 .. lastEventIdLength];
+ }
+ void populate(bool responseChunked, in char[] url, in char[] lastEventId)
+ in {
+ assert(url.length < this.urlBuffer.length);
+ assert(lastEventId.length < this.lastEventIdBuffer.length);
+ }
+ do {
+ this.responseChunked = responseChunked ? 1 : 0;
+ this.urlLength = cast(int) url.length;
+ this.lastEventIdLength = cast(int) lastEventId.length;
+
+ this.urlBuffer[0 .. url.length] = url[];
+ this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[];
+ }
+ }
+
+ struct SendableEvent {
+ int magic = 0xdeadbeef;
+ int urlLength;
+ char[256] urlBuffer = 0;
+ int typeLength;
+ char[32] typeBuffer = 0;
+ int messageLength;
+ char[2048] messageBuffer = 0;
+ int _lifetime;
+
+ char[] message() return {
+ return messageBuffer[0 .. messageLength];
+ }
+ char[] type() return {
+ return typeBuffer[0 .. typeLength];
+ }
+ char[] url() return {
+ return urlBuffer[0 .. urlLength];
+ }
+ void url(in char[] u) {
+ urlBuffer[0 .. u.length] = u[];
+ urlLength = cast(int) u.length;
+ }
+ int lifetime() {
+ return _lifetime;
+ }
+
+ ///
+ void populate(string url, string type, string message, int lifetime)
+ in {
+ assert(url.length < this.urlBuffer.length);
+ assert(type.length < this.typeBuffer.length);
+ assert(message.length < this.messageBuffer.length);
+ }
+ do {
+ this.urlLength = cast(int) url.length;
+ this.typeLength = cast(int) type.length;
+ this.messageLength = cast(int) message.length;
+ this._lifetime = lifetime;
+
+ this.urlBuffer[0 .. url.length] = url[];
+ this.typeBuffer[0 .. type.length] = type[];
+ this.messageBuffer[0 .. message.length] = message[];
+ }
+ }
+
+ struct EventConnection {
+ int fd;
+ bool needsChunking;
+ }
+
+ private EventConnection[][string] eventConnectionsByUrl;
+ private string[][string] pipes;
+
+ private void handleInputEvent(scope SendableEvent* event) {
+ static int eventId;
+
+ static struct StoredEvent {
+ int id;
+ string type;
+ string message;
+ int lifetimeRemaining;
+ }
+
+ StoredEvent[][string] byUrl;
+
+ int thisId = ++eventId;
+
+ if(event.lifetime)
+ byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime);
+
+ auto connectionsPtr = event.url in eventConnectionsByUrl;
+ EventConnection[] connections;
+ if(connectionsPtr is null)
+ return;
+ else
+ connections = *connectionsPtr;
+
+ char[4096] buffer;
+ char[] formattedMessage;
+
+ void append(const char[] a) {
+ // the 6's here are to leave room for a HTTP chunk header, if it proves necessary
+ buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[];
+ formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length];
+ }
+
+ import std.algorithm.iteration;
+
+ if(connections.length) {
+ append("id: ");
+ append(to!string(thisId));
+ append("\n");
+
+ append("event: ");
+ append(event.type);
+ append("\n");
+
+ foreach(line; event.message.splitter("\n")) {
+ append("data: ");
+ append(line);
+ append("\n");
+ }
+
+ append("\n");
+ }
+
+ // chunk it for HTTP!
+ auto len = toHex(formattedMessage.length);
+ buffer[4 .. 6] = "\r\n"[];
+ buffer[4 - len.length .. 4] = len[];
+
+ auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length];
+ // done
+
+ // FIXME: send back requests when needed
+ // FIXME: send a single ":\n" every 15 seconds to keep alive
+
+ foreach(connection; connections) {
+ if(connection.needsChunking)
+ nonBlockingWrite(this, connection.fd, chunkedMessage);
+ else
+ nonBlockingWrite(this, connection.fd, formattedMessage);
+ }
+ }
+}
+
+void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) {
+ version(Posix) {
+
+ import core.sys.posix.unistd;
+ import core.sys.posix.fcntl;
+ import core.sys.posix.sys.un;
+
+ import core.sys.posix.signal;
+ signal(SIGPIPE, SIG_IGN);
+
+ static extern(C) void sigchldhandler(int) {
+ int status;
+ import w = core.sys.posix.sys.wait;
+ w.wait(&status);
+ }
+ signal(SIGCHLD, &sigchldhandler);
+
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if(sock == -1)
+ throw new Exception("socket " ~ to!string(errno));
+
+ scope(failure)
+ close(sock);
+
+ cloexec(sock);
+
+ // add-on server processes are assumed to be local, and thus will
+ // use unix domain sockets. Besides, I want to pass sockets to them,
+ // so it basically must be local (except for the session server, but meh).
+ sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+ version(linux) {
+ // on linux, we will use the abstract namespace
+ addr.sun_path[0] = 0;
+ addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[];
+ } else {
+ // but otherwise, just use a file cuz we must.
+ addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[];
+ }
+
+ if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1)
+ throw new Exception("bind " ~ to!string(errno));
+
+ if(listen(sock, 128) == -1)
+ throw new Exception("listen " ~ to!string(errno));
+
+ makeNonBlocking(sock);
+
+ version(linux) {
+ import core.sys.linux.epoll;
+ auto epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ if(epoll_fd == -1)
+ throw new Exception("epoll_create1 " ~ to!string(errno));
+ scope(failure)
+ close(epoll_fd);
+ } else {
+ import core.sys.posix.poll;
+ }
+
+ version(linux)
+ eis.epoll_fd = epoll_fd;
+
+ auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null);
+ scope(exit)
+ freeIoOp(acceptOp);
+
+ version(linux) {
+ epoll_event ev;
+ ev.events = EPOLLIN | EPOLLET;
+ ev.data.ptr = acceptOp;
+ if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1)
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+
+ epoll_event[64] events;
+ } else {
+ pollfd[] pollfds;
+ IoOp*[int] ioops;
+ pollfds ~= pollfd(sock, POLLIN);
+ ioops[sock] = acceptOp;
+ }
+
+ while(true) {
+
+ // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently
+
+ int timeout_milliseconds = 15000; // -1; // infinite
+ //writeln("waiting for ", name);
+
+ version(linux) {
+ auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds);
+ if(nfds == -1) {
+ if(errno == EINTR)
+ continue;
+ throw new Exception("epoll_wait " ~ to!string(errno));
+ }
+ } else {
+ int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds);
+ size_t lastIdx = 0;
+ }
+
+ if(nfds == 0) {
+ eis.wait_timeout();
+ }
+
+ foreach(idx; 0 .. nfds) {
+ version(linux) {
+ auto flags = events[idx].events;
+ auto ioop = cast(IoOp*) events[idx].data.ptr;
+ } else {
+ IoOp* ioop;
+ foreach(tidx, thing; pollfds[lastIdx .. $]) {
+ if(thing.revents) {
+ ioop = ioops[thing.fd];
+ lastIdx += tidx + 1;
+ break;
+ }
+ }
+ }
+
+ //writeln(flags, " ", ioop.fd);
+
+ void newConnection() {
+ // on edge triggering, it is important that we get it all
+ while(true) {
+ auto size = cast(uint) addr.sizeof;
+ auto ns = accept(sock, cast(sockaddr*) &addr, &size);
+ if(ns == -1) {
+ if(errno == EAGAIN || errno == EWOULDBLOCK) {
+ // all done, got it all
+ break;
+ }
+ throw new Exception("accept " ~ to!string(errno));
+ }
+ cloexec(ns);
+
+ makeNonBlocking(ns);
+ auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096, &eis.handleLocalConnectionData);
+ niop.closeHandler = &eis.handleLocalConnectionClose;
+ niop.completeHandler = &eis.handleLocalConnectionComplete;
+ scope(failure) freeIoOp(niop);
+
+ version(linux) {
+ epoll_event nev;
+ nev.events = EPOLLIN | EPOLLET;
+ nev.data.ptr = niop;
+ if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1)
+ throw new Exception("epoll_ctl " ~ to!string(errno));
+ } else {
+ bool found = false;
+ foreach(ref pfd; pollfds) {
+ if(pfd.fd < 0) {
+ pfd.fd = ns;
+ found = true;
+ }
+ }
+ if(!found)
+ pollfds ~= pollfd(ns, POLLIN);
+ ioops[ns] = niop;
+ }
+ }
+ }
+
+ bool newConnectionCondition() {
+ version(linux)
+ return ioop.fd == sock && (flags & EPOLLIN);
+ else
+ return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN);
+ }
+
+ if(newConnectionCondition()) {
+ newConnection();
+ } else if(ioop.operation == IoOp.ReadSocketHandle) {
+ while(true) {
+ int in_fd;
+ auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd);
+ if(got == -1) {
+ if(errno == EAGAIN || errno == EWOULDBLOCK) {
+ // all done, got it all
+ if(ioop.completeHandler)
+ ioop.completeHandler(ioop);
+ break;
+ }
+ throw new Exception("recv " ~ to!string(errno));
+ }
+
+ if(got == 0) {
+ if(ioop.closeHandler) {
+ ioop.closeHandler(ioop);
+ version(linux) {} // nothing needed
+ else {
+ foreach(ref pfd; pollfds) {
+ if(pfd.fd == ioop.fd)
+ pfd.fd = -1;
+ }
+ }
+ }
+ close(ioop.fd);
+ freeIoOp(ioop);
+ break;
+ }
+
+ ioop.bufferLengthUsed = cast(int) got;
+ ioop.handler(ioop, in_fd);
+ }
+ } else if(ioop.operation == IoOp.Read) {
+ while(true) {
+ auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length);
+ if(got == -1) {
+ if(errno == EAGAIN || errno == EWOULDBLOCK) {
+ // all done, got it all
+ if(ioop.completeHandler)
+ ioop.completeHandler(ioop);
+ break;
+ }
+ throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno));
+ }
+
+ if(got == 0) {
+ if(ioop.closeHandler)
+ ioop.closeHandler(ioop);
+ close(ioop.fd);
+ freeIoOp(ioop);
+ break;
+ }
+
+ ioop.bufferLengthUsed = cast(int) got;
+ if(ioop.handler(ioop, ioop.fd)) {
+ close(ioop.fd);
+ freeIoOp(ioop);
+ break;
+ }
+ }
+ }
+
+ // EPOLLHUP?
+ }
+ }
+ } else version(Windows) {
+
+ // set up a named pipe
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx
+ // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw
+ // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid
+
+ } else static assert(0);
+}
+
+
+version(with_sendfd)
+// copied from the web and ported from C
+// see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t
+ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) {
+ msghdr msg;
+ iovec[1] iov;
+
+ version(OSX) {
+ //msg.msg_accrights = cast(cattr_t) &sendfd;
+ //msg.msg_accrightslen = int.sizeof;
+ } else {
+ union ControlUnion {
+ cmsghdr cm;
+ char[CMSG_SPACE(int.sizeof)] control;
+ }
+
+ ControlUnion control_un;
+ cmsghdr* cmptr;
+
+ msg.msg_control = control_un.control.ptr;
+ msg.msg_controllen = control_un.control.length;
+
+ cmptr = CMSG_FIRSTHDR(&msg);
+ cmptr.cmsg_len = CMSG_LEN(int.sizeof);
+ cmptr.cmsg_level = SOL_SOCKET;
+ cmptr.cmsg_type = SCM_RIGHTS;
+ *(cast(int *) CMSG_DATA(cmptr)) = sendfd;
+ }
+
+ msg.msg_name = null;
+ msg.msg_namelen = 0;
+
+ iov[0].iov_base = ptr;
+ iov[0].iov_len = nbytes;
+ msg.msg_iov = iov.ptr;
+ msg.msg_iovlen = 1;
+
+ return sendmsg(fd, &msg, 0);
+}
+
+version(with_sendfd)
+// copied from the web and ported from C
+ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) {
+ msghdr msg;
+ iovec[1] iov;
+ ssize_t n;
+ int newfd;
+
+ version(OSX) {
+ //msg.msg_accrights = cast(cattr_t) recvfd;
+ //msg.msg_accrightslen = int.sizeof;
+ } else {
+ union ControlUnion {
+ cmsghdr cm;
+ char[CMSG_SPACE(int.sizeof)] control;
+ }
+ ControlUnion control_un;
+ cmsghdr* cmptr;
+
+ msg.msg_control = control_un.control.ptr;
+ msg.msg_controllen = control_un.control.length;
+ }
+
+ msg.msg_name = null;
+ msg.msg_namelen = 0;
+
+ iov[0].iov_base = ptr;
+ iov[0].iov_len = nbytes;
+ msg.msg_iov = iov.ptr;
+ msg.msg_iovlen = 1;
+
+ if ( (n = recvmsg(fd, &msg, 0)) <= 0)
+ return n;
+
+ version(OSX) {
+ //if(msg.msg_accrightslen != int.sizeof)
+ //*recvfd = -1;
+ } else {
+ if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null &&
+ cmptr.cmsg_len == CMSG_LEN(int.sizeof)) {
+ if (cmptr.cmsg_level != SOL_SOCKET)
+ throw new Exception("control level != SOL_SOCKET");
+ if (cmptr.cmsg_type != SCM_RIGHTS)
+ throw new Exception("control type != SCM_RIGHTS");
+ *recvfd = *(cast(int *) CMSG_DATA(cmptr));
+ } else
+ *recvfd = -1; /* descriptor was not passed */
+ }
+
+ return n;
+}
+/* end read_fd */
+
+
+/*
+ Event source stuff
+
+ The api is:
+
+ sendEvent(string url, string type, string data, int timeout = 60*10);
+
+ attachEventListener(string url, int fd, lastId)
+
+
+ It just sends to all attached listeners, and stores it until the timeout
+ for replaying via lastEventId.
+*/
+
+/*
+ Session process stuff
+
+ it stores it all. the cgi object has a session object that can grab it
+
+ session may be done in the same process if possible, there is a version
+ switch to choose if you want to override.
+*/
+
+struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler;
+ alias handler = dispatchHandler;
+ string urlPrefix;
+ bool rejectFurther;
+ immutable(DispatcherDetails) details;
+}
+
+private string urlify(string name) pure {
+ return beautify(name, '-', true);
+}
+
+private string beautify(string name, char space = ' ', bool allLowerCase = false) pure {
+ if(name == "id")
+ return allLowerCase ? name : "ID";
+
+ char[160] buffer;
+ int bufferIndex = 0;
+ bool shouldCap = true;
+ bool shouldSpace;
+ bool lastWasCap;
+ foreach(idx, char ch; name) {
+ if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
+
+ if((ch >= 'A' && ch <= 'Z') || ch == '_') {
+ if(lastWasCap) {
+ // two caps in a row, don't change. Prolly acronym.
+ } else {
+ if(idx)
+ shouldSpace = true; // new word, add space
+ }
+
+ lastWasCap = true;
+ } else {
+ lastWasCap = false;
+ }
+
+ if(shouldSpace) {
+ buffer[bufferIndex++] = space;
+ if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
+ shouldSpace = false;
+ }
+ if(shouldCap) {
+ if(ch >= 'a' && ch <= 'z')
+ ch -= 32;
+ shouldCap = false;
+ }
+ if(allLowerCase && ch >= 'A' && ch <= 'Z')
+ ch += 32;
+ buffer[bufferIndex++] = ch;
+ }
+ return buffer[0 .. bufferIndex].idup;
+}
+
+/*
+string urlFor(alias func)() {
+ return __traits(identifier, func);
+}
+*/
+
+/++
+ UDA: The name displayed to the user in auto-generated HTML.
+
+ Default is `beautify(identifier)`.
++/
+struct DisplayName {
+ string name;
+}
+
+/++
+ UDA: The name used in the URL or web parameter.
+
+ Default is `urlify(identifier)` for functions and `identifier` for parameters and data members.
++/
+struct UrlName {
+ string name;
+}
+
+/++
+ UDA: default format to respond for this method
++/
+struct DefaultFormat { string value; }
+
+class MissingArgumentException : Exception {
+ string functionName;
+ string argumentName;
+ string argumentType;
+
+ this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ this.functionName = functionName;
+ this.argumentName = argumentName;
+ this.argumentType = argumentType;
+
+ super("Missing Argument: " ~ this.argumentName, file, line, next);
+ }
+}
+
+/++
+ This can be attached to any constructor or function called from the cgi system.
+
+ If it is present, the function argument can NOT be set from web params, but instead
+ is set to the return value of the given `func`.
+
+ If `func` can take a parameter of type [Cgi], it will be passed the one representing
+ the current request. Otherwise, it must take zero arguments.
+
+ Any params in your function of type `Cgi` are automatically assumed to take the cgi object
+ for the connection. Any of type [Session] (with an argument) is also assumed to come from
+ the cgi object.
+
+ const arguments are also supported.
++/
+struct ifCalledFromWeb(alias func) {}
+
+// it only looks at query params for GET requests, the rest must be in the body for a function argument.
+auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
+
+ // FIXME: any array of structs should also be settable or gettable from csv as well.
+
+ // FIXME: think more about checkboxes and bools.
+
+ import std.traits;
+
+ Parameters!method params;
+ alias idents = ParameterIdentifierTuple!method;
+ alias defaults = ParameterDefaults!method;
+
+ const(string)[] names;
+ const(string)[] values;
+
+ // first, check for missing arguments and initialize to defaults if necessary
+
+ static if(is(typeof(method) P == __parameters))
+ foreach(idx, param; P) {{
+ // see: mustNotBeSetFromWebParams
+ static if(is(param : Cgi)) {
+ static assert(!is(param == immutable));
+ cast() params[idx] = cgi;
+ } else static if(is(param == Session!D, D)) {
+ static assert(!is(param == immutable));
+ cast() params[idx] = cgi.getSessionObject!D();
+ } else {
+ bool populated;
+ foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) {
+ static if(is(uda == ifCalledFromWeb!func, alias func)) {
+ static if(is(typeof(func(cgi))))
+ params[idx] = func(cgi);
+ else
+ params[idx] = func();
+
+ populated = true;
+ }
+ }
+
+ if(!populated) {
+ static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) {
+ params[idx] = param.getAutomaticallyForCgi(cgi);
+ populated = true;
+ }
+ }
+
+ if(!populated) {
+ auto ident = idents[idx];
+ if(cgi.requestMethod == Cgi.RequestMethod.GET) {
+ if(ident !in cgi.get) {
+ static if(is(defaults[idx] == void)) {
+ static if(is(param == bool))
+ params[idx] = false;
+ else
+ throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof);
+ } else
+ params[idx] = defaults[idx];
+ }
+ } else {
+ if(ident !in cgi.post) {
+ static if(is(defaults[idx] == void)) {
+ static if(is(param == bool))
+ params[idx] = false;
+ else
+ throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof);
+ } else
+ params[idx] = defaults[idx];
+ }
+ }
+ }
+ }
+ }}
+
+ // second, parse the arguments in order to build up arrays, etc.
+
+ static bool setVariable(T)(string name, string paramName, T* what, string value) {
+ static if(is(T == struct)) {
+ if(name == paramName) {
+ *what = T.init;
+ return true;
+ } else {
+ // could be a child
+ if(name[paramName.length] == '.') {
+ paramName = name[paramName.length + 1 .. $];
+ name = paramName;
+ int p = 0;
+ foreach(ch; paramName) {
+ if(ch == '.' || ch == '[')
+ break;
+ p++;
+ }
+
+ // set the child member
+ switch(paramName) {
+ foreach(idx, memberName; __traits(allMembers, T))
+ static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
+ // data member!
+ case memberName:
+ return setVariable(name, paramName, &(__traits(getMember, *what, memberName)), value);
+ }
+ default:
+ // ok, not a member
+ }
+ }
+ }
+
+ return false;
+ } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {
+ *what = to!T(value);
+ return true;
+ } else static if(is(T == bool)) {
+ *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on";
+ return true;
+ } else static if(is(T == K[], K)) {
+ K tmp;
+ if(name == paramName) {
+ // direct - set and append
+ if(setVariable(name, paramName, &tmp, value)) {
+ (*what) ~= tmp;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ // child, append to last element
+ // FIXME: what about range violations???
+ auto ptr = &(*what)[(*what).length - 1];
+ return setVariable(name, paramName, ptr, value);
+
+ }
+ } else static if(is(T == V[K], K, V)) {
+ // assoc array, name[key] is valid
+ if(name == paramName) {
+ // no action necessary
+ return true;
+ } else if(name[paramName.length] == '[') {
+ int count = 1;
+ auto idx = paramName.length + 1;
+ while(idx < name.length && count > 0) {
+ if(name[idx] == '[')
+ count++;
+ else if(name[idx] == ']') {
+ count--;
+ if(count == 0) break;
+ }
+ idx++;
+ }
+ if(idx == name.length)
+ return false; // malformed
+
+ auto insideBrackets = name[paramName.length + 1 .. idx];
+ auto afterName = name[idx + 1 .. $];
+
+ auto k = to!K(insideBrackets);
+ V v;
+ if(auto ptr = k in *what)
+ v = *ptr;
+
+ name = name[0 .. paramName.length];
+ //writeln(name, afterName, " ", paramName);
+
+ auto ret = setVariable(name ~ afterName, paramName, &v, value);
+ if(ret) {
+ (*what)[k] = v;
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ static assert(0, "unsupported type for cgi call " ~ T.stringof);
+ }
+
+ //return false;
+ }
+
+ void setArgument(string name, string value) {
+ int p;
+ foreach(ch; name) {
+ if(ch == '.' || ch == '[')
+ break;
+ p++;
+ }
+
+ auto paramName = name[0 .. p];
+
+ sw: switch(paramName) {
+ static if(is(typeof(method) P == __parameters))
+ foreach(idx, param; P) {
+ static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) {
+ // cannot be set from the outside
+ } else {
+ case idents[idx]:
+ static if(is(param == Cgi.UploadedFile)) {
+ params[idx] = cgi.files[name];
+ } else {
+ setVariable(name, paramName, &params[idx], value);
+ }
+ break sw;
+ }
+ }
+ default:
+ // ignore; not relevant argument
+ }
+ }
+
+ if(cgi.requestMethod == Cgi.RequestMethod.GET) {
+ names = cgi.allGetNamesInOrder;
+ values = cgi.allGetValuesInOrder;
+ } else {
+ names = cgi.allPostNamesInOrder;
+ values = cgi.allPostValuesInOrder;
+ }
+
+ foreach(idx, name; names) {
+ setArgument(name, values[idx]);
+ }
+
+ static if(is(ReturnType!method == void)) {
+ typeof(null) ret;
+ dg(params);
+ } else {
+ auto ret = dg(params);
+ }
+
+ // FIXME: format return values
+ // options are: json, html, csv.
+ // also may need to wrap in envelope format: none, html, or json.
+ return ret;
+}
+
+private bool mustNotBeSetFromWebParams(T, attrs...)() {
+ static if(is(T : const(Cgi))) {
+ return true;
+ } else static if(is(T : const(Session!D), D)) {
+ return true;
+ } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) {
+ return true;
+ } else {
+ foreach(uda; attrs)
+ static if(is(uda == ifCalledFromWeb!func, alias func))
+ return true;
+ return false;
+ }
+}
+
+private bool hasIfCalledFromWeb(attrs...)() {
+ foreach(uda; attrs)
+ static if(is(uda == ifCalledFromWeb!func, alias func))
+ return true;
+ return false;
+}
+
+/++
+ Implies POST path for the thing itself, then GET will get the automatic form.
+
+ The given customizer, if present, will be called as a filter on the Form object.
+
+ History:
+ Added December 27, 2020
++/
+template AutomaticForm(alias customizer) { }
+
+/+
+ Argument conversions: for the most part, it is to!Thing(string).
+
+ But arrays and structs are a bit different. Arrays come from the cgi array. Thus
+ they are passed
+
+ arr=foo&arr=bar <-- notice the same name.
+
+ Structs are first declared with an empty thing, then have their members set individually,
+ with dot notation. The members are not required, just the initial declaration.
+
+ struct Foo {
+ int a;
+ string b;
+ }
+ void test(Foo foo){}
+
+ foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members
+
+ Arrays of structs use this declaration.
+
+ void test(Foo[] foo) {}
+
+ foo&foo.a=5&foo.b=bar&foo&foo.a=9
+
+ You can use a hidden input field in HTML forms to achieve this. The value of the naked name
+ declaration is ignored.
+
+ Mind that order matters! The declaration MUST come first in the string.
+
+ Arrays of struct members follow this rule recursively.
+
+ struct Foo {
+ int[] a;
+ }
+
+ foo&foo.a=1&foo.a=2&foo&foo.a=1
+
+
+ Associative arrays are formatted with brackets, after a declaration, like structs:
+
+ foo&foo[key]=value&foo[other_key]=value
+
+
+ Note: for maximum compatibility with outside code, keep your types simple. Some libraries
+ do not support the strict ordering requirements to work with these struct protocols.
+
+ FIXME: also perhaps accept application/json to better work with outside trash.
+
+
+ Return values are also auto-formatted according to user-requested type:
+ for json, it loops over and converts.
+ for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables!
++/
+
+/++
+ A web presenter is responsible for rendering things to HTML to be usable
+ in a web browser.
+
+ They are passed as template arguments to the base classes of [WebObject]
+
+ Responsible for displaying stuff as HTML. You can put this into your own aggregate
+ and override it. Use forwarding and specialization to customize it.
+
+ When you inherit from it, pass your own class as the CRTP argument. This lets the base
+ class templates and your overridden templates work with each other.
+
+ ---
+ class MyPresenter : WebPresenter!(MyPresenter) {
+ @Override
+ void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) {
+ // present the CustomType
+ }
+ @Override
+ void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) {
+ // handle everything else via the super class, which will call
+ // back to your class when appropriate
+ super.presentSuccessfulReturnAsHtml(cgi, ret);
+ }
+ }
+ ---
+
+ The meta argument in there can be overridden by your own facility.
+
++/
+class WebPresenter(CRTP) {
+
+ /// A UDA version of the built-in `override`, to be used for static template polymorphism
+ /// If you override a plain method, use `override`. If a template, use `@Override`.
+ enum Override;
+
+ string script() {
+ return `
+ `;
+ }
+
+ string style() {
+ return `
+ :root {
+ --mild-border: #ccc;
+ --middle-border: #999;
+ --accent-color: #e8e8e8;
+ --sidebar-color: #f2f2f2;
+ }
+ ` ~ genericFormStyling() ~ genericSiteStyling();
+ }
+
+ string genericFormStyling() {
+ return
+q"css
+ table.automatic-data-display {
+ border-collapse: collapse;
+ border: solid 1px var(--mild-border);
+ }
+
+ table.automatic-data-display td {
+ vertical-align: top;
+ border: solid 1px var(--mild-border);
+ padding: 2px 4px;
+ }
+
+ table.automatic-data-display th {
+ border: solid 1px var(--mild-border);
+ border-bottom: solid 1px var(--middle-border);
+ padding: 2px 4px;
+ }
+
+ ol.automatic-data-display {
+ margin: 0px;
+ list-style-position: inside;
+ padding: 0px;
+ }
+
+ dl.automatic-data-display {
+
+ }
+
+ .automatic-form {
+ max-width: 600px;
+ }
+
+ .form-field {
+ margin: 0.5em;
+ padding-left: 0.5em;
+ }
+
+ .label-text {
+ display: block;
+ font-weight: bold;
+ margin-left: -0.5em;
+ }
+
+ .submit-button-holder {
+ padding-left: 2em;
+ }
+
+ .add-array-button {
+
+ }
+css";
+ }
+
+ string genericSiteStyling() {
+ return
+q"css
+ * { box-sizing: border-box; }
+ html, body { margin: 0px; }
+ body {
+ font-family: sans-serif;
+ }
+ header {
+ background: var(--accent-color);
+ height: 64px;
+ }
+ footer {
+ background: var(--accent-color);
+ height: 64px;
+ }
+ #site-container {
+ display: flex;
+ }
+ main {
+ flex: 1 1 auto;
+ order: 2;
+ min-height: calc(100vh - 64px - 64px);
+ padding: 4px;
+ padding-left: 1em;
+ }
+ #sidebar {
+ flex: 0 0 16em;
+ order: 1;
+ background: var(--sidebar-color);
+ }
+css";
+ }
+
+ import arsd.dom;
+ Element htmlContainer() {
+ auto document = new Document(q"html
+<!DOCTYPE html>
+<html>
+<head>
+ <title>D Application</title>
+ <link rel="stylesheet" href="style.css" />
+</head>
+<body>
+ <header></header>
+ <div id="site-container">
+ <main></main>
+ <div id="sidebar"></div>
+ </div>
+ <footer></footer>
+ <script src="script.js"></script>
+</body>
+</html>
+html", true, true);
+
+ return document.requireSelector("main");
+ }
+
+ /// Renders a response as an HTTP error
+ void renderBasicError(Cgi cgi, int httpErrorCode) {
+ cgi.setResponseStatus(getHttpCodeText(httpErrorCode));
+ auto c = htmlContainer();
+ c.innerText = getHttpCodeText(httpErrorCode);
+ cgi.setResponseContentType("text/html; charset=utf-8");
+ cgi.write(c.parentDocument.toString(), true);
+ }
+
+ template methodMeta(alias method) {
+ enum methodMeta = null;
+ }
+
+ void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) {
+ // FIXME? format?
+ (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta);
+ }
+
+ /// typeof(null) (which is also used to represent functions returning `void`) do nothing
+ /// in the default presenter - allowing the function to have full low-level control over the
+ /// response.
+ void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) {
+ // nothing intentionally!
+ }
+
+ /// Redirections are forwarded to [Cgi.setResponseLocation]
+ void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) {
+ cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code));
+ }
+
+ /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime
+ void presentSuccessfulReturn(T : MultipleResponses!Types, Types...)(Cgi cgi, T ret, typeof(null) meta, string format) {
+ bool outputted = false;
+ foreach(index, type; Types) {
+ if(ret.contains == index) {
+ assert(!outputted);
+ outputted = true;
+ (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret.payload[index], meta);
+ }
+ }
+ if(!outputted)
+ assert(0);
+ }
+
+ /// An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort.
+ void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) {
+ cgi.setCache(true); // not necessarily true but meh
+ cgi.setResponseContentType(ret.contentType);
+ cgi.write(ret.getData(), true);
+ }
+
+ /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer].
+ void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) {
+ auto container = this.htmlContainer();
+ container.appendChild(formatReturnValueAsHtml(ret));
+ cgi.write(container.parentDocument.toString(), true);
+ }
+
+ /++
+ If you override this, you will need to cast the exception type `t` dynamically,
+ but can then use the template arguments here to refer back to the function.
+
+ `func` is an alias to the method itself, and `dg` is a callable delegate to the same
+ method on the live object. You could, in theory, change arguments and retry, but I
+ provide that information mostly with the expectation that you will use them to make
+ useful forms or richer error messages for the user.
+ +/
+ void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) {
+ presentExceptionAsHtmlImpl(cgi, t, createAutomaticFormForFunction!(func)(dg));
+ }
+
+ void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) {
+ if(auto mae = cast(MissingArgumentException) t) {
+ auto container = this.htmlContainer();
+ if(cgi.requestMethod == Cgi.RequestMethod.POST)
+ container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing"));
+ container.appendChild(automaticForm);
+
+ cgi.write(container.parentDocument.toString(), true);
+ } else {
+ auto container = this.htmlContainer();
+
+ // import std.stdio; writeln(t.toString());
+
+ container.appendChild(exceptionToElement(t));
+
+ container.addChild("h4", "GET");
+ foreach(k, v; cgi.get) {
+ auto deets = container.addChild("details");
+ deets.addChild("summary", k);
+ deets.addChild("div", v);
+ }
+
+ container.addChild("h4", "POST");
+ foreach(k, v; cgi.post) {
+ auto deets = container.addChild("details");
+ deets.addChild("summary", k);
+ deets.addChild("div", v);
+ }
+
+
+ if(!cgi.outputtedResponseData)
+ cgi.setResponseStatus("500 Internal Server Error");
+ cgi.write(container.parentDocument.toString(), true);
+ }
+ }
+
+ Element exceptionToElement(Throwable t) {
+ auto div = Element.make("div");
+ div.addClass("exception-display");
+
+ div.addChild("p", t.msg);
+ div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line));
+
+ auto pre = div.addChild("pre");
+ string s;
+ s = t.toString();
+ Element currentBox;
+ bool on = false;
+ foreach(line; s.splitLines) {
+ if(!on && line.startsWith("-----"))
+ on = true;
+ if(!on) continue;
+ if(line.indexOf("arsd/") != -1) {
+ if(currentBox is null) {
+ currentBox = pre.addChild("details");
+ currentBox.addChild("summary", "Framework code");
+ }
+ currentBox.addChild("span", line ~ "\n");
+ } else {
+ pre.addChild("span", line ~ "\n");
+ currentBox = null;
+ }
+ }
+
+ return div;
+ }
+
+ /++
+ Returns an element for a particular type
+ +/
+ Element elementFor(T)(string displayName, string name) {
+ import std.traits;
+
+ auto div = Element.make("div");
+ div.addClass("form-field");
+
+ static if(is(T == struct)) {
+ if(displayName !is null)
+ div.addChild("span", displayName, "label-text");
+ auto fieldset = div.addChild("fieldset");
+ fieldset.addChild("legend", beautify(T.stringof)); // FIXME
+ fieldset.addChild("input", name);
+ foreach(idx, memberName; __traits(allMembers, T))
+ static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
+ fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName));
+ }
+ } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {
+ Element lbl;
+ if(displayName !is null) {
+ lbl = div.addChild("label");
+ lbl.addChild("span", displayName, "label-text");
+ lbl.appendText(" ");
+ } else {
+ lbl = div;
+ }
+ auto i = lbl.addChild("input", name);
+ i.attrs.name = name;
+ static if(isSomeString!T)
+ i.attrs.type = "text";
+ else
+ i.attrs.type = "number";
+ i.attrs.value = to!string(T.init);
+ } else static if(is(T == bool)) {
+ Element lbl;
+ if(displayName !is null) {
+ lbl = div.addChild("label");
+ lbl.addChild("span", displayName, "label-text");
+ lbl.appendText(" ");
+ } else {
+ lbl = div;
+ }
+ auto i = lbl.addChild("input", name);
+ i.attrs.type = "checkbox";
+ i.attrs.value = "true";
+ i.attrs.name = name;
+ } else static if(is(T == Cgi.UploadedFile)) {
+ Element lbl;
+ if(displayName !is null) {
+ lbl = div.addChild("label");
+ lbl.addChild("span", displayName, "label-text");
+ lbl.appendText(" ");
+ } else {
+ lbl = div;
+ }
+ auto i = lbl.addChild("input", name);
+ i.attrs.name = name;
+ i.attrs.type = "file";
+ } else static if(is(T == K[], K)) {
+ auto templ = div.addChild("template");
+ templ.appendChild(elementFor!(K)(null, name));
+ if(displayName !is null)
+ div.addChild("span", displayName, "label-text");
+ auto btn = div.addChild("button");
+ btn.addClass("add-array-button");
+ btn.attrs.type = "button";
+ btn.innerText = "Add";
+ btn.attrs.onclick = q{
+ var a = document.importNode(this.parentNode.firstChild.content, true);
+ this.parentNode.insertBefore(a, this);
+ };
+ } else static if(is(T == V[K], K, V)) {
+ div.innerText = "assoc array not implemented for automatic form at this time";
+ } else {
+ static assert(0, "unsupported type for cgi call " ~ T.stringof);
+ }
+
+
+ return div;
+ }
+
+ /// creates a form for gathering the function's arguments
+ Form createAutomaticFormForFunction(alias method, T)(T dg) {
+
+ auto form = cast(Form) Element.make("form");
+
+ form.method = "POST"; // FIXME
+
+ form.addClass("automatic-form");
+
+ string formDisplayName = beautify(__traits(identifier, method));
+ foreach(attr; __traits(getAttributes, method))
+ static if(is(typeof(attr) == DisplayName))
+ formDisplayName = attr.name;
+ form.addChild("h3", formDisplayName);
+
+ import std.traits;
+
+ //Parameters!method params;
+ //alias idents = ParameterIdentifierTuple!method;
+ //alias defaults = ParameterDefaults!method;
+
+ static if(is(typeof(method) P == __parameters))
+ foreach(idx, _; P) {{
+
+ alias param = P[idx .. idx + 1];
+
+ static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) {
+ string displayName = beautify(__traits(identifier, param));
+ foreach(attr; __traits(getAttributes, param))
+ static if(is(typeof(attr) == DisplayName))
+ displayName = attr.name;
+ auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param)));
+ if(i.querySelector("input[type=file]") !is null)
+ form.setAttribute("enctype", "multipart/form-data");
+ }
+ }}
+
+ form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder");
+
+ return form;
+ }
+
+ /// creates a form for gathering object members (for the REST object thing right now)
+ Form createAutomaticFormForObject(T)(T obj) {
+ auto form = cast(Form) Element.make("form");
+
+ form.addClass("automatic-form");
+
+ form.addChild("h3", beautify(__traits(identifier, T)));
+
+ import std.traits;
+
+ //Parameters!method params;
+ //alias idents = ParameterIdentifierTuple!method;
+ //alias defaults = ParameterDefaults!method;
+
+ foreach(idx, memberName; __traits(derivedMembers, T)) {{
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ string displayName = beautify(memberName);
+ foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName)))
+ static if(is(typeof(attr) == DisplayName))
+ displayName = attr.name;
+ form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName));
+
+ form.setValue(memberName, to!string(__traits(getMember, obj, memberName)));
+ }}}
+
+ form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder");
+
+ return form;
+ }
+
+ ///
+ Element formatReturnValueAsHtml(T)(T t) {
+ import std.traits;
+
+ static if(is(T == typeof(null))) {
+ return Element.make("span");
+ } else static if(is(T : Element)) {
+ return t;
+ } else static if(is(T == MultipleResponses!Types, Types...)) {
+ foreach(index, type; Types) {
+ if(t.contains == index)
+ return formatReturnValueAsHtml(t.payload[index]);
+ }
+ assert(0);
+ } else static if(is(T == Paginated!E, E)) {
+ auto e = Element.make("div").addClass("paginated-result");
+ e.appendChild(formatReturnValueAsHtml(t.items));
+ if(t.nextPageUrl.length)
+ e.appendChild(Element.make("a", "Next Page", t.nextPageUrl));
+ return e;
+ } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) {
+ return Element.make("span", to!string(t), "automatic-data-display");
+ } else static if(is(T == V[K], K, V)) {
+ auto dl = Element.make("dl");
+ dl.addClass("automatic-data-display associative-array");
+ foreach(k, v; t) {
+ dl.addChild("dt", to!string(k));
+ dl.addChild("dd", formatReturnValueAsHtml(v));
+ }
+ return dl;
+ } else static if(is(T == struct)) {
+ auto dl = Element.make("dl");
+ dl.addClass("automatic-data-display struct");
+
+ foreach(idx, memberName; __traits(allMembers, T))
+ static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
+ dl.addChild("dt", beautify(memberName));
+ dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName)));
+ }
+
+ return dl;
+ } else static if(is(T == bool)) {
+ return Element.make("span", t ? "true" : "false", "automatic-data-display");
+ } else static if(is(T == E[], E)) {
+ static if(is(E : RestObject!Proxy, Proxy)) {
+ // treat RestObject similar to struct
+ auto table = cast(Table) Element.make("table");
+ table.addClass("automatic-data-display");
+ string[] names;
+ foreach(idx, memberName; __traits(derivedMembers, E))
+ static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
+ names ~= beautify(memberName);
+ }
+ table.appendHeaderRow(names);
+
+ foreach(l; t) {
+ auto tr = table.appendRow();
+ foreach(idx, memberName; __traits(derivedMembers, E))
+ static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
+ static if(memberName == "id") {
+ string val = to!string(__traits(getMember, l, memberName));
+ tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME
+ } else {
+ tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName)));
+ }
+ }
+ }
+
+ return table;
+ } else static if(is(E == struct)) {
+ // an array of structs is kinda special in that I like
+ // having those formatted as tables.
+ auto table = cast(Table) Element.make("table");
+ table.addClass("automatic-data-display");
+ string[] names;
+ foreach(idx, memberName; __traits(allMembers, E))
+ static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
+ names ~= beautify(memberName);
+ }
+ table.appendHeaderRow(names);
+
+ foreach(l; t) {
+ auto tr = table.appendRow();
+ foreach(idx, memberName; __traits(allMembers, E))
+ static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
+ tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName)));
+ }
+ }
+
+ return table;
+ } else {
+ // otherwise, I will just make a list.
+ auto ol = Element.make("ol");
+ ol.addClass("automatic-data-display");
+ foreach(e; t)
+ ol.addChild("li", formatReturnValueAsHtml(e));
+ return ol;
+ }
+ } else static assert(0, "bad return value for cgi call " ~ T.stringof);
+
+ assert(0);
+ }
+
+}
+
+/++
+ The base class for the [dispatcher] function and object support.
++/
+class WebObject {
+ //protected Cgi cgi;
+
+ protected void initialize(Cgi cgi) {
+ //this.cgi = cgi;
+ }
+}
+
+/++
+ Can return one of the given types, decided at runtime. The syntax
+ is to declare all the possible types in the return value, then you
+ can `return typeof(return)(...value...)` to construct it.
+
+ It has an auto-generated constructor for each value it can hold.
+
+ ---
+ MultipleResponses!(Redirection, string) getData(int how) {
+ if(how & 1)
+ return typeof(return)(Redirection("http://dpldocs.info/"));
+ else
+ return typeof(return)("hi there!");
+ }
+ ---
+
+ If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little.
++/
+struct MultipleResponses(T...) {
+ private size_t contains;
+ private union {
+ private T payload;
+ }
+
+ static foreach(index, type; T)
+ public this(type t) {
+ contains = index;
+ payload[index] = t;
+ }
+
+ /++
+ This is primarily for testing. It is your way of getting to the response.
+
+ Let's say you wanted to test that one holding a Redirection and a string actually
+ holds a string, by name of "test":
+
+ ---
+ auto valueToTest = your_test_function();
+
+ valueToTest.visit!(
+ (Redirection) { assert(0); }, // got a redirection instead of a string, fail the test
+ (string s) { assert(s == "test"); } // right value, go ahead and test it.
+ );
+ ---
+ +/
+ void visit(Handlers...)() {
+ template findHandler(type, HandlersToCheck...) {
+ static if(HandlersToCheck.length == 0)
+ alias findHandler = void;
+ else {
+ static if(is(typeof(HandlersToCheck[0](type.init))))
+ alias findHandler = handler;
+ else
+ alias findHandler = findHandler!(type, HandlersToCheck[1 .. $]);
+ }
+ }
+ foreach(index, type; T) {
+ alias handler = findHandler!(type, Handlers);
+ static if(is(handler == void))
+ static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor");
+ else {
+ if(index == contains)
+ handler(payload[index]);
+ }
+ }
+ }
+
+ /+
+ auto toArsdJsvar()() {
+ import arsd.jsvar;
+ return var(null);
+ }
+ +/
+}
+
+struct RawResponse {
+ int code;
+ string[] headers;
+ const(ubyte)[] responseBody;
+}
+
+/++
+ You can return this from [WebObject] subclasses for redirections.
+
+ (though note the static types means that class must ALWAYS redirect if
+ you return this directly. You might want to return [MultipleResponses] if it
+ can be conditional)
++/
+struct Redirection {
+ string to; /// The URL to redirect to.
+ int code = 303; /// The HTTP code to retrn.
+}
+
+/++
+ Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher].
+
+ Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overriden
+ the presenter in the dispatcher.
+
+ FIXME: explain this better
+
+ You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function,
+ and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads,
+ the runtime result of that is undefined.
+
+ A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those.
+ (this might change, like maybe i will use pure as an indicator GET is ok. idk.)
+
+ $(WARNING
+ ---
+ // legal in D, undefined runtime behavior with cgi.d, it may call either method
+ // even if you put different URL udas on it, the current code ignores them.
+ void foo(int a) {}
+ void foo(string a) {}
+ ---
+ )
+
+ See_Also: [serveRestObject], [serveStaticFile]
++/
+auto serveApi(T)(string urlPrefix) {
+ assert(urlPrefix[$ - 1] == '/');
+ return serveApiInternal!T(urlPrefix);
+}
+
+private string nextPieceFromSlash(ref string remainingUrl) {
+ if(remainingUrl.length == 0)
+ return remainingUrl;
+ int slash = 0;
+ while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.')
+ slash++;
+
+ // I am specifically passing `null` to differentiate it vs empty string
+ // so in your ctor, `items` means new T(null) and `items/` means new T("")
+ auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash];
+ // so if it is the last item, the dot can be used to load an alternative view
+ // otherwise tho the dot is considered part of the identifier
+ // FIXME
+
+ // again notice "" vs null here!
+ if(slash == remainingUrl.length)
+ remainingUrl = null;
+ else
+ remainingUrl = remainingUrl[slash + 1 .. $];
+
+ return ident;
+}
+
+enum AddTrailingSlash;
+
+private auto serveApiInternal(T)(string urlPrefix) {
+
+ import arsd.dom;
+ import arsd.jsvar;
+
+ static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) {
+ string remainingUrl = cgi.pathInfo[urlPrefix.length .. $];
+
+ try {
+ // see duplicated code below by searching subresource_ctor
+ // also see mustNotBeSetFromWebParams
+
+ static if(is(typeof(T.__ctor) P == __parameters)) {
+ P params;
+
+ foreach(pidx, param; P) {
+ static if(is(param : Cgi)) {
+ static assert(!is(param == immutable));
+ cast() params[pidx] = cgi;
+ } else static if(is(param == Session!D, D)) {
+ static assert(!is(param == immutable));
+ cast() params[pidx] = cgi.getSessionObject!D();
+
+ } else {
+ static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {
+ foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
+ static if(is(uda == ifCalledFromWeb!func, alias func)) {
+ static if(is(typeof(func(cgi))))
+ params[pidx] = func(cgi);
+ else
+ params[pidx] = func();
+ }
+ }
+ } else {
+
+ static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) {
+ params[pidx] = param.getAutomaticallyForCgi(cgi);
+ } else static if(is(param == string)) {
+ auto ident = nextPieceFromSlash(remainingUrl);
+ params[pidx] = ident;
+ } else static assert(0, "illegal type for subresource " ~ param.stringof);
+ }
+ }
+ }
+
+ auto obj = new T(params);
+ } else {
+ auto obj = new T();
+ }
+
+ return internalHandlerWithObject(obj, remainingUrl, cgi, presenter);
+ } catch(Throwable t) {
+ switch(cgi.request("format", "html")) {
+ case "html":
+ static void dummy() {}
+ presenter.presentExceptionAsHtml!(dummy)(cgi, t, &dummy);
+ return true;
+ case "json":
+ var envelope = var.emptyObject;
+ envelope.success = false;
+ envelope.result = null;
+ envelope.error = t.toString();
+ cgi.setResponseContentType("application/json");
+ cgi.write(envelope.toJson(), true);
+ return true;
+ default:
+ throw t;
+ // return true;
+ }
+ // return true;
+ }
+
+ assert(0);
+ }
+
+ static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) {
+
+ obj.initialize(cgi);
+
+ /+
+ Overload rules:
+ Any unique combination of HTTP verb and url path can be dispatched to function overloads
+ statically.
+
+ Moreover, some args vs no args can be overloaded dynamically.
+ +/
+
+ auto methodNameFromUrl = nextPieceFromSlash(remainingUrl);
+ /+
+ auto orig = remainingUrl;
+ assert(0,
+ (orig is null ? "__null" : orig)
+ ~ " .. " ~
+ (methodNameFromUrl is null ? "__null" : methodNameFromUrl));
+ +/
+
+ if(methodNameFromUrl is null)
+ methodNameFromUrl = "__null";
+
+ string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl;
+
+ if(remainingUrl.length)
+ hack ~= "/";
+
+ switch(hack) {
+ foreach(methodName; __traits(derivedMembers, T))
+ static if(methodName != "__ctor")
+ foreach(idx, overload; __traits(getOverloads, T, methodName)) {
+ static if(is(typeof(overload) P == __parameters))
+ static if(is(typeof(overload) R == return))
+ static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export")
+ {
+ static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName)))
+ case urlNameForMethod:
+
+ static if(is(R : WebObject)) {
+ // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above.
+
+ // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string
+
+ // subresource_ctor
+ // also see mustNotBeSetFromWebParams
+
+ P params;
+
+ string ident;
+
+ foreach(pidx, param; P) {
+ static if(is(param : Cgi)) {
+ static assert(!is(param == immutable));
+ cast() params[pidx] = cgi;
+ } else static if(is(param == typeof(presenter))) {
+ cast() param[pidx] = presenter;
+ } else static if(is(param == Session!D, D)) {
+ static assert(!is(param == immutable));
+ cast() params[pidx] = cgi.getSessionObject!D();
+ } else {
+ static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {
+ foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
+ static if(is(uda == ifCalledFromWeb!func, alias func)) {
+ static if(is(typeof(func(cgi))))
+ params[pidx] = func(cgi);
+ else
+ params[pidx] = func();
+ }
+ }
+ } else {
+
+ static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) {
+ params[pidx] = param.getAutomaticallyForCgi(cgi);
+ } else static if(is(param == string)) {
+ ident = nextPieceFromSlash(remainingUrl);
+ if(ident is null) {
+ // trailing slash mandated on subresources
+ cgi.setResponseLocation(cgi.pathInfo ~ "/");
+ return true;
+ } else {
+ params[pidx] = ident;
+ }
+ } else static assert(0, "illegal type for subresource " ~ param.stringof);
+ }
+ }
+ }
+
+ auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident);
+ return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter);
+ } else {
+ // 404 it if any url left - not a subresource means we don't get to play with that!
+ if(remainingUrl.length)
+ return false;
+
+ bool automaticForm;
+
+ foreach(attr; __traits(getAttributes, overload))
+ static if(is(attr == AddTrailingSlash)) {
+ if(remainingUrl is null) {
+ cgi.setResponseLocation(cgi.pathInfo ~ "/");
+ return true;
+ }
+ } else static if(__traits(isSame, AutomaticForm, attr)) {
+ automaticForm = true;
+ }
+
+ /+
+ int zeroArgOverload = -1;
+ int overloadCount = cast(int) __traits(getOverloads, T, methodName).length;
+ bool calledWithZeroArgs = true;
+ foreach(k, v; cgi.get)
+ if(k != "format") {
+ calledWithZeroArgs = false;
+ break;
+ }
+ foreach(k, v; cgi.post)
+ if(k != "format") {
+ calledWithZeroArgs = false;
+ break;
+ }
+
+ // first, we need to go through and see if there is an empty one, since that
+ // changes inside. But otherwise, all the stuff I care about can be done via
+ // simple looping (other improper overloads might be flagged for runtime semantic check)
+ //
+ // an argument of type Cgi is ignored for these purposes
+ static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{
+ static if(is(typeof(overload) P == __parameters))
+ static if(P.length == 0)
+ zeroArgOverload = cast(int) idx;
+ else static if(P.length == 1 && is(P[0] : Cgi))
+ zeroArgOverload = cast(int) idx;
+ }}
+ // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method.
+ bool overloadHasBeenCalled = false;
+ static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{
+ bool callFunction = true;
+ // there is a zero arg overload and this is NOT it, and we have zero args - don't call this
+ if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs)
+ callFunction = false;
+ // if this is the zero-arg overload, obviously it cannot be called if we got any args.
+ if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs)
+ callFunction = false;
+
+ // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea.
+
+ bool hadAnyMethodRestrictions = false;
+ bool foundAcceptableMethod = false;
+ foreach(attr; __traits(getAttributes, overload)) {
+ static if(is(typeof(attr) == Cgi.RequestMethod)) {
+ hadAnyMethodRestrictions = true;
+ if(attr == cgi.requestMethod)
+ foundAcceptableMethod = true;
+ }
+ }
+
+ if(hadAnyMethodRestrictions && !foundAcceptableMethod)
+ callFunction = false;
+
+ /+
+ The overloads we really want to allow are the sane ones
+ from the web perspective. Which is likely on HTTP verbs,
+ for the most part, but might also be potentially based on
+ some args vs zero args, or on argument names. Can't really
+ do argument types very reliable through the web though; those
+ should probably be different URLs.
+
+ Even names I feel is better done inside the function, so I'm not
+ going to support that here. But the HTTP verbs and zero vs some
+ args makes sense - it lets you define custom forms pretty easily.
+
+ Moreover, I'm of the opinion that empty overload really only makes
+ sense on GET for this case. On a POST, it is just a missing argument
+ exception and that should be handled by the presenter. But meh, I'll
+ let the user define that, D only allows one empty arg thing anyway
+ so the method UDAs are irrelevant.
+ +/
+ if(callFunction)
+ +/
+
+ if(automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET) {
+ // Should I still show the form on a json thing? idk...
+ auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx]));
+ presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), "html");
+ return true;
+ }
+ switch(cgi.request("format", defaultFormat!overload())) {
+ case "html":
+ // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control.
+ try {
+
+ auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi);
+ presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), "html");
+ } catch(Throwable t) {
+ presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx]));
+ }
+ return true;
+ case "json":
+ auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi);
+ static if(is(typeof(ret) == MultipleResponses!Types, Types...)) {
+ var json;
+ foreach(index, type; Types) {
+ if(ret.contains == index)
+ json = ret.payload[index];
+ }
+ } else {
+ var json = ret;
+ }
+ var envelope = json; // var.emptyObject;
+ /*
+ envelope.success = true;
+ envelope.result = json;
+ envelope.error = null;
+ */
+ cgi.setResponseContentType("application/json");
+ cgi.write(envelope.toJson(), true);
+ return true;
+ default:
+ cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of.
+ return true;
+ }
+ //}}
+
+ //cgi.header("Accept: POST"); // FIXME list the real thing
+ //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering.
+ //return true;
+ }
+ }
+ }
+ case "GET script.js":
+ cgi.setResponseContentType("text/javascript");
+ cgi.gzipResponse = true;
+ cgi.write(presenter.script(), true);
+ return true;
+ case "GET style.css":
+ cgi.setResponseContentType("text/css");
+ cgi.gzipResponse = true;
+ cgi.write(presenter.style(), true);
+ return true;
+ default:
+ return false;
+ }
+
+ assert(0);
+ }
+ return DispatcherDefinition!internalHandler(urlPrefix, false);
+}
+
+string defaultFormat(alias method)() {
+ bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true;
+ foreach(attr; __traits(getAttributes, method)) {
+ static if(is(typeof(attr) == DefaultFormat)) {
+ if(nonConstConditionForWorkingAroundASpuriousDmdWarning)
+ return attr.value;
+ }
+ }
+ return "html";
+}
+
+struct Paginated(T) {
+ T[] items;
+ string nextPageUrl;
+}
+
+template urlNamesForMethod(alias method, string default_) {
+ string[] helper() {
+ auto verb = Cgi.RequestMethod.GET;
+ bool foundVerb = false;
+ bool foundNoun = false;
+
+ string def = default_;
+
+ bool hasAutomaticForm = false;
+
+ foreach(attr; __traits(getAttributes, method)) {
+ static if(is(typeof(attr) == Cgi.RequestMethod)) {
+ verb = attr;
+ if(foundVerb)
+ assert(0, "Multiple http verbs on one function is not currently supported");
+ foundVerb = true;
+ }
+ static if(is(typeof(attr) == UrlName)) {
+ if(foundNoun)
+ assert(0, "Multiple url names on one function is not currently supported");
+ foundNoun = true;
+ def = attr.name;
+ }
+ static if(__traits(isSame, attr, AutomaticForm)) {
+ hasAutomaticForm = true;
+ }
+ }
+
+ if(def is null)
+ def = "__null";
+
+ string[] ret;
+
+ static if(is(typeof(method) R == return)) {
+ static if(is(R : WebObject)) {
+ def ~= "/";
+ foreach(v; __traits(allMembers, Cgi.RequestMethod))
+ ret ~= v ~ " " ~ def;
+ } else {
+ if(hasAutomaticForm) {
+ ret ~= "GET " ~ def;
+ ret ~= "POST " ~ def;
+ } else {
+ ret ~= to!string(verb) ~ " " ~ def;
+ }
+ }
+ } else static assert(0);
+
+ return ret;
+ }
+ enum urlNamesForMethod = helper();
+}
+
+
+ enum AccessCheck {
+ allowed,
+ denied,
+ nonExistant,
+ }
+
+ enum Operation {
+ show,
+ create,
+ replace,
+ remove,
+ update
+ }
+
+ enum UpdateResult {
+ accessDenied,
+ noSuchResource,
+ success,
+ failure,
+ unnecessary
+ }
+
+ enum ValidationResult {
+ valid,
+ invalid
+ }
+
+
+/++
+ The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf].
++/
+class RestObject(CRTP) : WebObject {
+
+ import arsd.dom;
+ import arsd.jsvar;
+
+ /// Prepare the object to be shown.
+ void show() {}
+ /// ditto
+ void show(string urlId) {
+ load(urlId);
+ show();
+ }
+
+ ValidationResult delegate(typeof(this)) validateFromReflection;
+ Element delegate(typeof(this)) toHtmlFromReflection;
+ var delegate(typeof(this)) toJsonFromReflection;
+
+ /// Override this to provide access control to this object.
+ AccessCheck accessCheck(string urlId, Operation operation) {
+ return AccessCheck.allowed;
+ }
+
+ ValidationResult validate() {
+ if(validateFromReflection !is null)
+ return validateFromReflection(this);
+ return ValidationResult.valid;
+ }
+
+ // The functions with more arguments are the low-level ones,
+ // they forward to the ones with fewer arguments by default.
+
+ // POST on a parent collection - this is called from a collection class after the members are updated
+ /++
+ Given a populated object, this creates a new entry. Returns the url identifier
+ of the new object.
+ +/
+ string create(scope void delegate() applyChanges) {
+ return null;
+ }
+
+ void replace() {
+ save();
+ }
+ void replace(string urlId, scope void delegate() applyChanges) {
+ load(urlId);
+ applyChanges();
+ replace();
+ }
+
+ void update(string[] fieldList) {
+ save();
+ }
+ void update(string urlId, scope void delegate() applyChanges, string[] fieldList) {
+ load(urlId);
+ applyChanges();
+ update(fieldList);
+ }
+
+ void remove() {}
+
+ void remove(string urlId) {
+ load(urlId);
+ remove();
+ }
+
+ abstract void load(string urlId);
+ abstract void save();
+
+ Element toHtml() {
+ if(toHtmlFromReflection)
+ return toHtmlFromReflection(this);
+ else
+ assert(0);
+ }
+
+ var toJson() {
+ if(toJsonFromReflection)
+ return toJsonFromReflection(this);
+ else
+ assert(0);
+ }
+
+ /+
+ auto structOf(this This) {
+
+ }
+ +/
+}
+
+// FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value
+// https://use-the-index-luke.com/sql/partial-results/fetch-next-page
+
+/++
+ Base class for REST collections.
++/
+class CollectionOf(Obj) : RestObject!(CollectionOf) {
+ /// You might subclass this and use the cgi object's query params
+ /// to implement a search filter, for example.
+ ///
+ /// FIXME: design a way to auto-generate that form
+ /// (other than using the WebObject thing above lol
+ // it'll prolly just be some searchParams UDA or maybe an enum.
+ //
+ // pagination too perhaps.
+ //
+ // and sorting too
+ IndexResult index() { return IndexResult.init; }
+
+ string[] sortableFields() { return null; }
+ string[] searchableFields() { return null; }
+
+ struct IndexResult {
+ Obj[] results;
+
+ string[] sortableFields;
+
+ string previousPageIdentifier;
+ string nextPageIdentifier;
+ string firstPageIdentifier;
+ string lastPageIdentifier;
+
+ int numberOfPages;
+ }
+
+ override string create(scope void delegate() applyChanges) { assert(0); }
+ override void load(string urlId) { assert(0); }
+ override void save() { assert(0); }
+ override void show() {
+ index();
+ }
+ override void show(string urlId) {
+ show();
+ }
+
+ /// Proxy POST requests (create calls) to the child collection
+ alias PostProxy = Obj;
+}
+
+/++
+ Serves a REST object, similar to a Ruby on Rails resource.
+
+ You put data members in your class. cgi.d will automatically make something out of those.
+
+ It will call your constructor with the ID from the URL. This may be null.
+ It will then populate the data members from the request.
+ It will then call a method, if present, telling what happened. You don't need to write these!
+ It finally returns a reply.
+
+ Your methods are passed a list of fields it actually set.
+
+ The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST
+ APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better
+ with relative linking. But meh.)
+
+ GET /items -> index. all values not set.
+ GET /items/id -> get. only ID will be set, other params ignored.
+ POST /items -> create. values set as given
+ PUT /items/id -> replace. values set as given
+ or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation
+ a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form.
+ PATCH /items/id -> update. values set as given, list of changed fields passed
+ or POST /items/id with cgi.post["_method"] == "PATCH"
+ DELETE /items/id -> destroy. only ID guaranteed to be set
+ or POST /items/id with cgi.post["_method"] == "DELETE"
+
+ Following the stupid convention, there will never be a trailing slash here, and if it is there, it will
+ redirect you away from it.
+
+ API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var.
+
+ I will also let you change the default, if you must.
+
+ // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes.
+
+ You can define sub-resources on your object inside the object. These sub-resources are also REST objects
+ that follow the same thing. They may be individual resources or collections themselves.
+
+ Your class is expected to have at least the following methods:
+
+ FIXME: i kinda wanna add a routes object to the initialize call
+
+ create
+ Create returns the new address on success, some code on failure.
+ show
+ index
+ update
+ remove
+
+ You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults
+ should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that.
+
+ Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar].
+
+ NOT IMPLEMENTED
+
+
+ Really, a collection is a resource with a bunch of subresources.
+
+ GET /items
+ index because it is GET on the top resource
+
+ GET /items/foo
+ item but different than items?
+
+ class Items {
+
+ }
+
+ ... but meh, a collection can be automated. not worth making it
+ a separate thing, let's look at a real example. Users has many
+ items and a virtual one, /users/current.
+
+ the individual users have properties and two sub-resources:
+ session, which is just one, and comments, a collection.
+
+ class User : RestObject!() { // no parent
+ int id;
+ string name;
+
+ // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one.
+ // but you can override them to do it differently.
+
+ // any member which is of type RestObject can be linked automatically via href btw.
+
+ void show() {}
+ void show(string urlId) {} // automated! GET of this specific thing
+ void create() {} // POST on a parent collection - this is called from a collection class after the members are updated
+ void replace(string urlId) {} // this is the PUT; really, it just updates all fields.
+ void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields.
+ void remove(string urlId) {} // DELETE
+
+ void load(string urlId) {} // the default implementation of show() populates the id, then
+
+ this() {}
+
+ mixin Subresource!Session;
+ mixin Subresource!Comment;
+ }
+
+ class Session : RestObject!() {
+ // the parent object may not be fully constructed/loaded
+ this(User parent) {}
+
+ }
+
+ class Comment : CollectionOf!Comment {
+ this(User parent) {}
+ }
+
+ class Users : CollectionOf!User {
+ // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects.
+ void index() {} // GET on this specific thing; just like show really, just different name for the different semantics.
+ User create() {} // You MAY implement this, but the default is to create a new object, populate it from args, and then call create() on the child
+ }
+
++/
+auto serveRestObject(T)(string urlPrefix) {
+ assert(urlPrefix[0] == '/');
+ assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects.");
+ static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) {
+ string url = cgi.pathInfo[urlPrefix.length .. $];
+
+ if(url.length && url[$ - 1] == '/') {
+ // remove the final slash...
+ cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]);
+ return true;
+ }
+
+ return restObjectServeHandler!T(cgi, presenter, url);
+ }
+ return DispatcherDefinition!internalHandler(urlPrefix, false);
+}
+
+/+
+/// Convenience method for serving a collection. It will be named the same
+/// as type T, just with an s at the end. If you need any further, just
+/// write the class yourself.
+auto serveRestCollectionOf(T)(string urlPrefix) {
+ assert(urlPrefix[0] == '/');
+ mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`);
+ return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix);
+}
++/
+
+bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) {
+ string urlId = null;
+ if(url.length && url[0] == '/') {
+ // asking for a subobject
+ urlId = url[1 .. $];
+ foreach(idx, ch; urlId) {
+ if(ch == '/') {
+ urlId = urlId[0 .. idx];
+ break;
+ }
+ }
+ }
+
+ // FIXME handle other subresources
+
+ static if(is(T : CollectionOf!(C), C)) {
+ if(urlId !is null) {
+ return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId);
+ }
+ }
+
+ // FIXME: support precondition failed, if-modified-since, expectation failed, etc.
+
+ auto obj = new T();
+ obj.toHtmlFromReflection = delegate(t) {
+ import arsd.dom;
+ auto div = Element.make("div");
+ div.addClass("Dclass_" ~ T.stringof);
+ div.dataset.url = urlId;
+ bool first = true;
+ foreach(idx, memberName; __traits(derivedMembers, T))
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ if(!first) div.addChild("br"); else first = false;
+ div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName)));
+ }
+ return div;
+ };
+ obj.toJsonFromReflection = delegate(t) {
+ import arsd.jsvar;
+ var v = var.emptyObject();
+ foreach(idx, memberName; __traits(derivedMembers, T))
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ v[memberName] = __traits(getMember, obj, memberName);
+ }
+ return v;
+ };
+ obj.validateFromReflection = delegate(t) {
+ // FIXME
+ return ValidationResult.valid;
+ };
+ obj.initialize(cgi);
+ // FIXME: populate reflection info delegates
+
+
+ // FIXME: I am not happy with this.
+ switch(urlId) {
+ case "script.js":
+ cgi.setResponseContentType("text/javascript");
+ cgi.gzipResponse = true;
+ cgi.write(presenter.script(), true);
+ return true;
+ case "style.css":
+ cgi.setResponseContentType("text/css");
+ cgi.gzipResponse = true;
+ cgi.write(presenter.style(), true);
+ return true;
+ default:
+ // intentionally blank
+ }
+
+
+
+
+ static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) {
+ foreach(idx, memberName; __traits(derivedMembers, Obj))
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName));
+ }
+ }
+ void applyChanges() {
+ applyChangesTemplate(cgi, obj);
+ }
+
+ string[] modifiedList;
+
+ void writeObject(bool addFormLinks) {
+ if(cgi.request("format") == "json") {
+ cgi.setResponseContentType("application/json");
+ cgi.write(obj.toJson().toString, true);
+ } else {
+ auto container = presenter.htmlContainer();
+ if(addFormLinks) {
+ static if(is(T : CollectionOf!(C), C))
+ container.appendHtml(`
+ <form>
+ <button type="submit" name="_method" value="POST">Create New</button>
+ </form>
+ `);
+ else
+ container.appendHtml(`
+ <form>
+ <button type="submit" name="_method" value="PATCH">Edit</button>
+ <button type="submit" name="_method" value="DELETE">Delete</button>
+ </form>
+ `);
+ }
+ container.appendChild(obj.toHtml());
+ cgi.write(container.parentDocument.toString, true);
+ }
+ }
+
+ // FIXME: I think I need a set type in here....
+ // it will be nice to pass sets of members.
+
+ try
+ switch(cgi.requestMethod) {
+ case Cgi.RequestMethod.GET:
+ // I could prolly use template this parameters in the implementation above for some reflection stuff.
+ // sure, it doesn't automatically work in subclasses... but I instantiate here anyway...
+
+ // automatic forms here for usable basic auto site from browser.
+ // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh.
+ switch(cgi.request("_method", "GET")) {
+ case "GET":
+ static if(is(T : CollectionOf!(C), C)) {
+ auto results = obj.index();
+ if(cgi.request("format", "html") == "html") {
+ auto container = presenter.htmlContainer();
+ auto html = presenter.formatReturnValueAsHtml(results.results);
+ container.appendHtml(`
+ <form>
+ <button type="submit" name="_method" value="POST">Create New</button>
+ </form>
+ `);
+
+ container.appendChild(html);
+ cgi.write(container.parentDocument.toString, true);
+ } else {
+ cgi.setResponseContentType("application/json");
+ import arsd.jsvar;
+ var json = var.emptyArray;
+ foreach(r; results.results) {
+ var o = var.emptyObject;
+ foreach(idx, memberName; __traits(derivedMembers, typeof(r)))
+ static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) {
+ o[memberName] = __traits(getMember, r, memberName);
+ }
+
+ json ~= o;
+ }
+ cgi.write(json.toJson(), true);
+ }
+ } else {
+ obj.show(urlId);
+ writeObject(true);
+ }
+ break;
+ case "PATCH":
+ obj.load(urlId);
+ goto case;
+ case "PUT":
+ case "POST":
+ // an editing form for the object
+ auto container = presenter.htmlContainer();
+ static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {
+ auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj);
+ } else {
+ auto form = presenter.createAutomaticFormForObject(obj);
+ }
+ form.attrs.method = "POST";
+ form.setValue("_method", cgi.request("_method", "GET"));
+ container.appendChild(form);
+ cgi.write(container.parentDocument.toString(), true);
+ break;
+ case "DELETE":
+ // FIXME: a delete form for the object (can be phrased "are you sure?")
+ auto container = presenter.htmlContainer();
+ container.appendHtml(`
+ <form method="POST">
+ Are you sure you want to delete this item?
+ <input type="hidden" name="_method" value="DELETE" />
+ <input type="submit" value="Yes, Delete It" />
+ </form>
+
+ `);
+ cgi.write(container.parentDocument.toString(), true);
+ break;
+ default:
+ cgi.write("bad method\n", true);
+ }
+ break;
+ case Cgi.RequestMethod.POST:
+ // this is to allow compatibility with HTML forms
+ switch(cgi.request("_method", "POST")) {
+ case "PUT":
+ goto PUT;
+ case "PATCH":
+ goto PATCH;
+ case "DELETE":
+ goto DELETE;
+ case "POST":
+ static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {
+ auto p = new obj.PostProxy();
+ void specialApplyChanges() {
+ applyChangesTemplate(cgi, p);
+ }
+ string n = p.create(&specialApplyChanges);
+ } else {
+ string n = obj.create(&applyChanges);
+ }
+
+ auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n;
+ cgi.setResponseLocation(newUrl);
+ cgi.setResponseStatus("201 Created");
+ cgi.write(`The object has been created.`);
+ break;
+ default:
+ cgi.write("bad method\n", true);
+ }
+ // FIXME this should be valid on the collection, but not the child....
+ // 303 See Other
+ break;
+ case Cgi.RequestMethod.PUT:
+ PUT:
+ obj.replace(urlId, &applyChanges);
+ writeObject(false);
+ break;
+ case Cgi.RequestMethod.PATCH:
+ PATCH:
+ obj.update(urlId, &applyChanges, modifiedList);
+ writeObject(false);
+ break;
+ case Cgi.RequestMethod.DELETE:
+ DELETE:
+ obj.remove(urlId);
+ cgi.setResponseStatus("204 No Content");
+ break;
+ default:
+ // FIXME: OPTIONS, HEAD
+ }
+ catch(Throwable t) {
+ presenter.presentExceptionAsHtml!(DUMMY)(cgi, t, null);
+ }
+
+ return true;
+}
+
+struct DUMMY {}
+
+/+
+struct SetOfFields(T) {
+ private void[0][string] storage;
+ void set(string what) {
+ //storage[what] =
+ }
+ void unset(string what) {}
+ void setAll() {}
+ void unsetAll() {}
+ bool isPresent(string what) { return false; }
+}
++/
+
+/+
+enum readonly;
+enum hideonindex;
++/
+
+/++
+ Serves a static file. To be used with [dispatcher].
+
+ See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect]
++/
+auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) {
+// https://baus.net/on-tcp_cork/
+// man 2 sendfile
+ assert(urlPrefix[0] == '/');
+ if(filename is null)
+ filename = urlPrefix[1 .. $];
+ if(contentType is null) {
+ contentType = contentTypeFromFileExtension(filename);
+ }
+
+ static struct DispatcherDetails {
+ string filename;
+ string contentType;
+ }
+
+ static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
+ if(details.contentType.indexOf("image/") == 0)
+ cgi.setCache(true);
+ cgi.setResponseContentType(details.contentType);
+ cgi.write(std.file.read(details.filename), true);
+ return true;
+ }
+ return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType));
+}
+
+string contentTypeFromFileExtension(string filename) {
+ if(filename.endsWith(".png"))
+ return "image/png";
+ if(filename.endsWith(".svg"))
+ return "image/svg+xml";
+ if(filename.endsWith(".jpg"))
+ return "image/jpeg";
+ if(filename.endsWith(".html"))
+ return "text/html";
+ if(filename.endsWith(".css"))
+ return "text/css";
+ if(filename.endsWith(".js"))
+ return "application/javascript";
+ if(filename.endsWith(".wasm"))
+ return "application/wasm";
+ if(filename.endsWith(".mp3"))
+ return "audio/mpeg";
+ return null;
+}
+
+/// This serves a directory full of static files, figuring out the content-types from file extensions.
+/// It does not let you to descend into subdirectories (or ascend out of it, of course)
+auto serveStaticFileDirectory(string urlPrefix, string directory = null) {
+ assert(urlPrefix[0] == '/');
+ assert(urlPrefix[$-1] == '/');
+
+ static struct DispatcherDetails {
+ string directory;
+ }
+
+ if(directory is null)
+ directory = urlPrefix[1 .. $];
+
+ assert(directory[$-1] == '/');
+
+ static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
+ auto file = cgi.pathInfo[urlPrefix.length .. $];
+ if(file.indexOf("/") != -1 || file.indexOf("\\") != -1)
+ return false;
+
+ auto contentType = contentTypeFromFileExtension(file);
+
+ auto fn = details.directory ~ file;
+ if(std.file.exists(fn)) {
+ //if(contentType.indexOf("image/") == 0)
+ //cgi.setCache(true);
+ //else if(contentType.indexOf("audio/") == 0)
+ cgi.setCache(true);
+ cgi.setResponseContentType(contentType);
+ cgi.write(std.file.read(fn), true);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory));
+}
+
+/++
+ Redirects one url to another
+
+ See_Also: [dispatcher], [serveStaticFile]
++/
+auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) {
+ assert(urlPrefix[0] == '/');
+ static struct DispatcherDetails {
+ string redirectTo;
+ string code;
+ }
+
+ static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
+ cgi.setResponseLocation(details.redirectTo, true, details.code);
+ return true;
+ }
+
+
+ return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code)));
+}
+
+/// Used exclusively with `dispatchTo`
+struct DispatcherData(Presenter) {
+ Cgi cgi; /// You can use this cgi object.
+ Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher.
+ size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only.
+}
+
+/++
+ Dispatches the URL to a specific function.
++/
+auto handleWith(alias handler)(string urlPrefix) {
+ // cuz I'm too lazy to do it better right now
+ static class Hack : WebObject {
+ static import std.traits;
+ @UrlName("")
+ auto handle(std.traits.Parameters!handler args) {
+ return handler(args);
+ }
+ }
+
+ return urlPrefix.serveApiInternal!Hack;
+}
+
+/++
+ Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this:
+
+ ---
+ bool other(DD)(DD dd) {
+ return dd.dispatcher!(
+ "/whatever".serveRedirect("/success"),
+ "/api/".serveApi!MyClass
+ );
+ }
+ ---
+
+ The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher
+ here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters.
+ Or, of course, you could just use the exact type in your own code.
+
+ You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a
+ good job.
+
+
++/
+auto dispatchTo(alias handler)(string urlPrefix) {
+ assert(urlPrefix[0] == '/');
+ assert(urlPrefix[$-1] != '/');
+ static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) {
+ return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length));
+ }
+
+ return DispatcherDefinition!(internalHandler)(urlPrefix, false);
+}
+
+/+
+/++
+ See [serveStaticFile] if you want to serve a file off disk.
++/
+auto serveStaticData(string urlPrefix, const(void)[] data, string contentType) {
+
+}
++/
+
+/++
+ A URL dispatcher.
+
+ ---
+ if(cgi.dispatcher!(
+ "/api/".serveApi!MyApiClass,
+ "/objects/lol".serveRestObject!MyRestObject,
+ "/file.js".serveStaticFile,
+ "/admin/".dispatchTo!adminHandler
+ )) return;
+ ---
+
+
+ You define a series of url prefixes followed by handlers.
+
+ [dispatchTo] will send the request to another function for handling.
+ You may want to do different pre- and post- processing there, for example,
+ an authorization check and different page layout. You can use different
+ presenters and different function chains. NOT IMPLEMENTED
++/
+template dispatcher(definitions...) {
+ bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) {
+ static if(is(Presenter == typeof(null))) {
+ static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {}
+ auto presenter = new GenericWebPresenter();
+ } else
+ alias presenter = presenterArg;
+
+ return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0));
+ }
+
+ bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) {
+ // I can prolly make this more efficient later but meh.
+ foreach(definition; definitions) {
+ if(definition.rejectFurther) {
+ if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) {
+ auto ret = definition.handler(
+ dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length],
+ dispatcherData.cgi, dispatcherData.presenter, definition.details);
+ if(ret)
+ return true;
+ }
+ } else if(
+ dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) &&
+ // cgi.d dispatcher urls must be complete or have a /;
+ // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing"
+ (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length
+ || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/')
+ ) {
+ auto ret = definition.handler(
+ dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length],
+ dispatcherData.cgi, dispatcherData.presenter, definition.details);
+ if(ret)
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+});
+
+private struct StackBuffer {
+ char[1024] initial = void;
+ char[] buffer;
+ size_t position;
+
+ this(int a) {
+ buffer = initial[];
+ position = 0;
+ }
+
+ void add(in char[] what) {
+ if(position + what.length > buffer.length)
+ buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases
+ buffer[position .. position + what.length] = what[];
+ position += what.length;
+ }
+
+ void add(in char[] w1, in char[] w2, in char[] w3 = null) {
+ add(w1);
+ add(w2);
+ add(w3);
+ }
+
+ void add(long v) {
+ char[16] buffer = void;
+ auto pos = buffer.length;
+ bool negative;
+ if(v < 0) {
+ negative = true;
+ v = -v;
+ }
+ do {
+ buffer[--pos] = cast(char) (v % 10 + '0');
+ v /= 10;
+ } while(v);
+
+ if(negative)
+ buffer[--pos] = '-';
+
+ auto res = buffer[pos .. $];
+
+ add(res[]);
+ }
+
+ char[] get() @nogc {
+ return buffer[0 .. position];
+ }
+}
+
+// duplicated in http2.d
+private static string getHttpCodeText(int code) pure nothrow @nogc {
+ switch(code) {
+ case 200: return "200 OK";
+ case 201: return "201 Created";
+ case 202: return "202 Accepted";
+ case 203: return "203 Non-Authoritative Information";
+ case 204: return "204 No Content";
+ case 205: return "205 Reset Content";
+ //
+ case 300: return "300 Multiple Choices";
+ case 301: return "301 Moved Permanently";
+ case 302: return "302 Found";
+ case 303: return "303 See Other";
+ case 307: return "307 Temporary Redirect";
+ case 308: return "308 Permanent Redirect";
+ //
+ // FIXME: add more common 400 ones cgi.d might return too
+ case 400: return "400 Bad Request";
+ case 403: return "403 Forbidden";
+ case 404: return "404 Not Found";
+ case 405: return "405 Method Not Allowed";
+ case 406: return "406 Not Acceptable";
+ case 409: return "409 Conflict";
+ case 410: return "410 Gone";
+ //
+ case 500: return "500 Internal Server Error";
+ case 501: return "501 Not Implemented";
+ case 502: return "502 Bad Gateway";
+ case 503: return "503 Service Unavailable";
+ //
+ default: assert(0, "Unsupported http code");
+ }
+}
+
+
+/+
+/++
+ This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object.
+
+ It relies on jsvar.d and dom.d.
+
+
+ You can get javascript out of it to call. The generated functions need to look
+ like
+
+ function name(a,b,c,d,e) {
+ return _call("name", {"realName":a,"sds":b});
+ }
+
+ And _call returns an object you can call or set up or whatever.
++/
+bool apiDispatcher()(Cgi cgi) {
+ import arsd.jsvar;
+ import arsd.dom;
+}
++/
+/*
+Copyright: Adam D. Ruppe, 2008 - 2021
+License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0].
+Authors: Adam D. Ruppe
+
+ Copyright Adam D. Ruppe 2008 - 2021.
+Distributed under the Boost Software License, Version 1.0.
+ (See accompanying file LICENSE_1_0.txt or copy at
+ http://www.boost.org/LICENSE_1_0.txt)
+*/
diff --git a/sundry/misc/util/d/cgi/search/README b/sundry/misc/util/d/cgi/search/README
new file mode 100644
index 0000000..eb8fcde
--- /dev/null
+++ b/sundry/misc/util/d/cgi/search/README
@@ -0,0 +1,11 @@
+change db name to match name of db you create
+cv.db_selected = "spine.search.sql.db";
+
+~dr/bin/spine-ldc -v --sqlite-db-create --sqlite-db-filename="spine.search.db" --cgi-sqlite-search-filename="spine-search" --output=/var/www ~grotto/repo/git.repo/code/project-spine/doc-reform-markup/markup_samples/markup/pod/*
+
+~dr/bin/spine-ldc -v --sqlite-update --sqlite-db-filename="spine.search.db" --output=/var/www ~grotto/repo/git.repo/code/project-spine/doc-reform-markup/markup_samples/markup/pod/*
+
+cd util/d/cgi/search/src
+dub --force --compiler=ldc2 && sudo cp -v cgi-bin/spine-search /usr/lib/cgi-bin/.
+
+http://localhost/cgi-bin/spine-search?
diff --git a/sundry/misc/util/d/cgi/search/dub.sdl b/sundry/misc/util/d/cgi/search/dub.sdl
new file mode 100644
index 0000000..30b076d
--- /dev/null
+++ b/sundry/misc/util/d/cgi/search/dub.sdl
@@ -0,0 +1,16 @@
+name "spine_search"
+description "A minimal D application."
+authors "ralph"
+copyright "Copyright © 2021, ralph"
+license "GPL-3.0+"
+dependency "d2sqlite3" version="~>0.18.3"
+dependency "arsd-official": "7.2.0"
+ subConfiguration "arsd-official:cgi" "cgi"
+targetType "executable"
+targetPath "./cgi-bin"
+mainSourceFile "src/spine_cgi_sqlite_search.d"
+configuration "default" {
+ targetType "executable"
+ targetName "spine-search"
+ postGenerateCommands "notify-send -t 0 'D executable ready' 'spine cgi sqlite search d'"
+}
diff --git a/sundry/misc/util/d/cgi/search/src/spine_cgi_sqlite_search.d b/sundry/misc/util/d/cgi/search/src/spine_cgi_sqlite_search.d
new file mode 100644
index 0000000..987c319
--- /dev/null
+++ b/sundry/misc/util/d/cgi/search/src/spine_cgi_sqlite_search.d
@@ -0,0 +1,963 @@
+/+
+- Name: Spine, Doc Reform [a part of]
+ - Description: documents, structuring, processing, publishing, search
+ - static content generator
+
+ - Author: Ralph Amissah
+ [ralph.amissah@gmail.com]
+
+ - Copyright: (C) 2015 - 2021 Ralph Amissah, All Rights
+ Reserved.
+
+ - License: AGPL 3 or later:
+
+ Spine (SiSU), a framework for document structuring, publishing and
+ search
+
+ Copyright (C) Ralph Amissah
+
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the GNU AFERO General Public License as published by the
+ Free Software Foundation, either version 3 of the License, or (at your
+ option) any later version.
+
+ This program 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. See the GNU General Public License for
+ more details.
+
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see [https://www.gnu.org/licenses/].
+
+ If you have Internet connection, the latest version of the AGPL should be
+ available at these locations:
+ [https://www.fsf.org/licensing/licenses/agpl.html]
+ [https://www.gnu.org/licenses/agpl.html]
+
+ - Spine (by Doc Reform, related to SiSU) uses standard:
+ - docReform markup syntax
+ - standard SiSU markup syntax with modified headers and minor modifications
+ - docReform object numbering
+ - standard SiSU object citation numbering & system
+
+ - Hompages:
+ [https://www.doc_reform.org]
+ [https://www.sisudoc.org]
+
+ - Git
+ [https://git.sisudoc.org/projects/?p=software/spine.git;a=summary]
+
++/
+/+ dub.sdl
+ name "spine search"
+ description "spine cgi search"
++/
+import std.format;
+import std.range;
+import std.regex;
+import arsd.cgi;
+import d2sqlite3;
+import std.process : environment;
+void cgi_function_intro(Cgi cgi) {
+ string header;
+ string table;
+ string form;
+ struct Config {
+ string http_request_type;
+ string http_host;
+ // string server_name;
+ string doc_root;
+ string cgi_root;
+ string cgi_script;
+ string data_path_html;
+ string db_path;
+ string query_string;
+ string http_url;
+ string request_method;
+ }
+ auto conf = Config();
+ conf.http_request_type = environment.get("REQUEST_SCHEME", "http");
+ conf.http_host = environment.get("HTTP_HOST", "localhost");
+ // conf.server_name = environment.get("SERVER_NAME", "localhost");
+ conf.doc_root = environment.get("DOCUMENT_ROOT", "/var/www/html");
+ conf.cgi_root = environment.get("CONTEXT_DOCUMENT_ROOT", "/usr/lib/cgi-bin/");
+ // conf.cgi_script = environment.get("SCRIPT_NAME", "/cgi-bin/spine-search");
+ conf.query_string = environment.get("QUERY_STRING", "");
+ conf.http_url = environment.get("HTTP_REFERER", conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ conf.query_string);
+ conf.db_path = "/var/www/html/sqlite/"; // conf.http_host ~ "/sqlite/";
+ conf.request_method = environment.get("REQUEST_METHOD", "POST");
+ struct CGI_val {
+ string db_selected = "";
+ string sql_match_limit = ""; // radio: ( 1000 | 2500 )
+ string sql_match_offset = "";
+ string search_text = "";
+ string results_type = ""; // index
+ bool checked_echo = false;
+ bool checked_stats = false;
+ bool checked_url = false;
+ bool checked_searched = false;
+ bool checked_tip = false;
+ bool checked_sql = false;
+ }
+ auto cv = CGI_val();
+ cv.db_selected = "spine.search.db"; // config, set db name
+ auto text_fields() {
+ string canned_query_str = environment.get("QUERY_STRING", "");
+ if ("query_string" in cgi.post) {
+ canned_query_str = environment.get("QUERY_STRING", "");
+ }
+ string[string] canned_query;
+ if (conf.request_method == "POST") {
+ } else if (conf.request_method == "GET") {
+ foreach (pair_str; canned_query_str.split("&")) {
+ // cgi.write(pair_str ~ "<br>");
+ string[] pair = pair_str.split("=");
+ canned_query[pair[0]] = pair[1];
+ }
+ // foreach (field, content; canned_query) {
+ // cgi.write(field ~ ": " ~ content ~ "<br>");
+ // }
+ }
+ static struct Rgx {
+ // static canned_query = ctRegex!(`\A(?P<matched>.+)\Z`, "m");
+ static search_text_area = ctRegex!(`\A(?P<matched>.+)\Z`, "m");
+ // static fulltext = ctRegex!(`\A(?P<matched>.+)\Z`, "m");
+ static line = ctRegex!(`^(?P<matched>.+?)(?: ~|$)`, "m");
+ static text = ctRegex!(`(?:^|\s~\s*)text:\s+(?P<matched>.+?)(?: ~|$)`, "m");
+ static author = ctRegex!(`(?:^|\s~\s*)author:\s+(?P<matched>.+)$`, "m");
+ static title = ctRegex!(`(?:^|\s~\s*)title:\s+(?P<matched>.+)$`, "m");
+ static uid = ctRegex!(`(?:^|\s~\s*)uid:\s+(?P<matched>.+)$`, "m");
+ static fn = ctRegex!(`(?:^|\s~\s*)fn:\s+(?P<matched>.+)$`, "m");
+ static keywords = ctRegex!(`(?:^|\s~\s*)keywords:\s+(?P<matched>.+)$`, "m");
+ static topic_register = ctRegex!(`(?:^|\s~\s*)topic_register:\s+(?P<matched>.+)$`, "m");
+ static subject = ctRegex!(`(?:^|\s~\s*)subject:\s+(?P<matched>.+)$`, "m");
+ static description = ctRegex!(`(?:^|\s~\s*)description:\s+(?P<matched>.+)$`, "m");
+ static publisher = ctRegex!(`(?:^|\s~\s*)publisher:\s+(?P<matched>.+)$`, "m");
+ static editor = ctRegex!(`(?:^|\s~\s*)editor:\s+(?P<matched>.+)$`, "m");
+ static contributor = ctRegex!(`(?:^|\s~\s*)contributor:\s+(?P<matched>.+)$`, "m");
+ static date = ctRegex!(`(?:^|\s~\s*)date:\s+(?P<matched>.+)$`, "m");
+ static results_type = ctRegex!(`(?:^|\s~\s*)type:\s+(?P<matched>.+)$`, "m");
+ static format = ctRegex!(`(?:^|\s~\s*)format:\s+(?P<matched>.+)$`, "m");
+ static identifier = ctRegex!(`(?:^|\s~\s*)identifier:\s+(?P<matched>.+)$`, "m");
+ static source = ctRegex!(`(?:^|\s~\s*)source:\s+(?P<matched>.+)$`, "m");
+ static language = ctRegex!(`(?:^|\s~\s*)language:\s+(?P<matched>.+)$`, "m");
+ static relation = ctRegex!(`(?:^|\s~\s*)relation:\s+(?P<matched>.+)$`, "m");
+ static coverage = ctRegex!(`(?:^|\s~\s*)coverage:\s+(?P<matched>.+)$`, "m");
+ static rights = ctRegex!(`(?:^|\s~\s*)rights:\s+(?P<matched>.+)$`, "m");
+ static comment = ctRegex!(`(?:^|\s~\s*)comment:\s+(?P<matched>.+)$`, "m");
+ // static abstract_ = ctRegex!(`(?:^|\s~\s*)abstract:\s+(?P<matched>.+)$`, "m");
+ static src_filename_base = ctRegex!(`^src_filename_base:\s+(?P<matched>.+)$`, "m");
+ }
+ struct searchFields {
+ string canned_query = ""; // GET canned_query == cq
+ string search_text_area = ""; // POST search_text_area == tsa
+ string text = ""; // text == txt
+ string author = ""; // author == au
+ string title = ""; // title == ti
+ string uid = ""; // uid == uid
+ string fn = ""; // fn == fn
+ string keywords = ""; // keywords == kw
+ string topic_register = ""; // topic_register == tr
+ string subject = ""; // subject == su
+ string description = ""; // description == de
+ string publisher = ""; // publisher == pb
+ string editor = ""; // editor == ed
+ string contributor = ""; // contributor == ct
+ string date = ""; // date == dt
+ string format = ""; // format == fmt
+ string identifier = ""; // identifier == id
+ string source = ""; // source == src sfn
+ string language = ""; // language == lng
+ string relation = ""; // relation == rl
+ string coverage = ""; // coverage == cv
+ string rights = ""; // rights == rgt
+ string comment = ""; // comment == cmt
+ // string abstract = "";
+ string src_filename_base = ""; // src_filename_base == bfn
+ string results_type = ""; // results_type == rt radio
+ string sql_match_limit = ""; // sql_match_limit == sml radio
+ string sql_match_offset = ""; // sql_match_offset == smo
+ string stats = ""; // stats == sts checked
+ string echo = ""; // echo == ec checked
+ string url = ""; // url == url checked
+ string searched = ""; // searched == se checked
+ string sql = ""; // sql == sql checked
+ }
+ auto rgx = Rgx();
+ auto got = searchFields();
+ if (environment.get("REQUEST_METHOD", "POST") == "POST") {
+ if ("sf" in cgi.post) {
+ got.search_text_area = cgi.post["sf"];
+ if (auto m = got.search_text_area.matchFirst(rgx.text)) {
+ got.text = m["matched"];
+ got.canned_query ~= "sf=" ~ m["matched"];
+ } else if (auto m = got.search_text_area.matchFirst(rgx.line)) {
+ if (
+ !(m["matched"].matchFirst(rgx.author))
+ && !(m["matched"].matchFirst(rgx.title))
+ ) {
+ got.text = m["matched"];
+ got.canned_query ~= "sf=" ~ m["matched"];
+ }
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.author)) {
+ got.author = m["matched"];
+ got.canned_query ~= "&au=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.title)) {
+ got.title = m["matched"];
+ got.canned_query ~= "&ti=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.uid)) {
+ got.uid = m["matched"];
+ got.canned_query ~= "&uid=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.fn)) {
+ got.fn = m["matched"];
+ got.canned_query ~= "&fn=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.keywords)) {
+ got.keywords = m["matched"];
+ got.canned_query ~= "&kw=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.topic_register)) {
+ got.topic_register = m["matched"];
+ got.canned_query ~= "&tr=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.subject)) {
+ got.subject = m["matched"];
+ got.canned_query ~= "&su=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.description)) {
+ got.description = m["matched"];
+ got.canned_query ~= "&de=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.publisher)) {
+ got.publisher = m["matched"];
+ got.canned_query ~= "&pb=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.editor)) {
+ got.editor = m["matched"];
+ got.canned_query ~= "&ed=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.contributor)) {
+ got.contributor = m["matched"];
+ got.canned_query ~= "&ct=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.date)) {
+ got.date = m["matched"];
+ got.canned_query ~= "&dt=" ~ m["matched"];
+ }
+ // if (auto m = got.search_text_area.matchFirst(rgx.results_type)) {
+ // got.results_type = m["matched"];
+ // got.canned_query ~= "&rt=" ~ m["matched"];
+ // }
+ if (auto m = got.search_text_area.matchFirst(rgx.format)) {
+ got.format = m["matched"];
+ got.canned_query ~= "&fmt=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.identifier)) {
+ got.identifier = m["matched"];
+ got.canned_query ~= "&id=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.source)) {
+ got.source = m["matched"];
+ got.canned_query ~= "&src=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.language)) {
+ got.language = m["matched"];
+ got.canned_query ~= "&lng=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.relation)) {
+ got.relation = m["matched"];
+ got.canned_query ~= "&rl=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.coverage)) {
+ got.coverage = m["matched"];
+ got.canned_query ~= "&cv=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.rights)) {
+ got.rights = m["matched"];
+ got.canned_query ~= "&rgt=" ~ m["matched"];
+ }
+ if (auto m = got.search_text_area.matchFirst(rgx.comment)) {
+ got.comment = m["matched"];
+ got.canned_query ~= "&cmt=" ~ m["matched"];
+ }
+ // if (auto m = search_text_area.matchFirst(rgx.abstract)) {
+ // got.abstract = m["matched"];
+ // }
+ if (auto m = got.search_text_area.matchFirst(rgx.src_filename_base)) {
+ got.src_filename_base = m["matched"];
+ got.canned_query ~= "&bfn=" ~ m["matched"];
+ }
+ }
+ if ("fn" in cgi.post) {
+ got.fn = cgi.post["fn"];
+ got.canned_query ~= "&fn=" ~ cgi.post["fn"];
+ }
+ if ("rt" in cgi.post) {
+ got.results_type = cgi.post["rt"];
+ got.canned_query ~= "&rt=" ~ cgi.post["rt"];
+ }
+ if ("sts" in cgi.post) {
+ got.stats = cgi.post["sts"];
+ got.canned_query ~= "&sts=" ~ cgi.post["sts"];
+ }
+ if ("ec" in cgi.post) {
+ got.echo = cgi.post["ec"];
+ got.canned_query ~= "&ec=" ~ cgi.post["ec"];
+ }
+ if ("url" in cgi.post) {
+ got.url = cgi.post["url"];
+ got.canned_query ~= "&url=" ~ cgi.post["url"];
+ }
+ if ("se" in cgi.post) {
+ got.searched = cgi.post["se"];
+ got.canned_query ~= "&se=" ~ cgi.post["se"];
+ }
+ if ("sql" in cgi.post) {
+ got.sql = cgi.post["sql"];
+ got.canned_query ~= "&sql=" ~ cgi.post["sql"];
+ }
+ if ("sml" in cgi.post) {
+ got.sql_match_limit = cgi.post["sml"];
+ got.canned_query ~= "&sml=" ~ cgi.post["sml"];
+ }
+ if ("smo" in cgi.post) {
+ got.sql_match_offset = "0"; // cgi.post["smo"];
+ got.canned_query ~= "&smo=0"; // ~ cgi.post["smo"];
+ }
+ got.canned_query = got.canned_query.strip.split(" ").join("%20");
+ conf.query_string = got.canned_query;
+ // cgi.write("f.canned_query: " ~ got.canned_query ~ "<br>");
+ } else if (environment.get("REQUEST_METHOD", "POST") == "GET") {
+ got.canned_query = environment.get("QUERY_STRING", "");
+ // cgi.write("f.canned_query: " ~ got.canned_query ~ "<br>");
+ got.search_text_area = "";
+ if ("sf" in canned_query && !(canned_query["sf"]).empty) {
+ got.text = canned_query["sf"].split("%20").join(" ");
+ got.search_text_area ~= "text: " ~ got.text ~ "\n";
+ }
+ if ("au" in canned_query && !(canned_query["au"]).empty) {
+ got.author = canned_query["au"].split("%20").join(" ");
+ got.search_text_area ~= "author: " ~ got.author ~ "\n";
+ }
+ if ("ti" in canned_query && !(canned_query["ti"]).empty) {
+ got.title = canned_query["ti"].split("%20").join(" ");
+ got.search_text_area ~= "title: " ~ got.title ~ "\n";
+ }
+ if ("uid" in canned_query && !(canned_query["uid"]).empty) {
+ got.uid = canned_query["uid"].split("%20").join(" ");
+ got.search_text_area ~= "uid: " ~ got.uid ~ "\n";
+ }
+ if ("fn" in canned_query && !(canned_query["fn"]).empty) {
+ got.fn = canned_query["fn"].split("%20").join(" ");
+ got.search_text_area ~= "fn: " ~ got.fn ~ "\n";
+ }
+ if ("kw" in canned_query && !(canned_query["kw"]).empty) {
+ got.keywords = canned_query["kw"].split("%20").join(" ");
+ got.search_text_area ~= "keywords: " ~ got.keywords ~ "\n";
+ }
+ if ("tr" in canned_query && !(canned_query["tr"]).empty) {
+ got.topic_register = canned_query["tr"].split("%20").join(" ");
+ got.search_text_area ~= "topic_register: " ~ got.topic_register ~ "\n";
+ }
+ if ("su" in canned_query && !(canned_query["su"]).empty) {
+ got.subject = canned_query["su"].split("%20").join(" ");
+ got.search_text_area ~= "subject: " ~ got.subject ~ "\n";
+ }
+ if ("de" in canned_query && !(canned_query["de"]).empty) {
+ got.description = canned_query["de"].split("%20").join(" ");
+ got.search_text_area ~= "description: " ~ got.description ~ "\n";
+ }
+ if ("pb" in canned_query && !(canned_query["pb"]).empty) {
+ got.publisher = canned_query["pb"].split("%20").join(" ");
+ got.search_text_area ~= "publisher: " ~ got.publisher ~ "\n";
+ }
+ if ("ed" in canned_query && !(canned_query["ed"]).empty) {
+ got.editor = canned_query["ed"].split("%20").join(" ");
+ got.search_text_area ~= "editor: " ~ got.editor ~ "\n";
+ }
+ if ("ct" in canned_query && !(canned_query["ct"]).empty) {
+ got.contributor = canned_query["ct"].split("%20").join(" ");
+ got.search_text_area ~= "contributor: " ~ got.contributor ~ "\n";
+ }
+ if ("dt" in canned_query && !(canned_query["dt"]).empty) {
+ got.date = canned_query["dt"].split("%20").join(" ");
+ got.search_text_area ~= "date: " ~ got.date ~ "\n";
+ }
+ if ("rt" in canned_query && !(canned_query["rt"]).empty) {
+ got.results_type = canned_query["rt"].split("%20").join(" ");
+ // got.search_text_area ~= "results_type: " ~ got.results_type ~ "\n";
+ }
+ if ("fmt" in canned_query && !(canned_query["fmt"]).empty) {
+ got.format = canned_query["fmt"].split("%20").join(" ");
+ got.search_text_area ~= "format: " ~ got.format ~ "\n";
+ }
+ if ("id" in canned_query && !(canned_query["id"]).empty) {
+ got.identifier = canned_query["id"].split("%20").join(" ");
+ got.search_text_area ~= "identifier: " ~ got.identifier ~ "\n";
+ }
+ if ("src" in canned_query && !(canned_query["src"]).empty) {
+ got.source = canned_query["src"].split("%20").join(" ");
+ got.search_text_area ~= "source: " ~ got.source ~ "\n";
+ }
+ if ("lng" in canned_query && !(canned_query["lng"]).empty) {
+ got.language = canned_query["lng"].split("%20").join(" ");
+ got.search_text_area ~= "language: " ~ got.language ~ "\n";
+ }
+ if ("rl" in canned_query && !(canned_query["rl"]).empty) {
+ got.relation = canned_query["rl"].split("%20").join(" ");
+ got.search_text_area ~= "relation: " ~ got.relation ~ "\n";
+ }
+ if ("cv" in canned_query && !(canned_query["cv"]).empty) {
+ got.coverage = canned_query["cv"].split("%20").join(" ");
+ got.search_text_area ~= "coverage: " ~ got.coverage ~ "\n";
+ }
+ if ("rgt" in canned_query && !(canned_query["rgt"]).empty) {
+ got.rights = canned_query["rgt"].split("%20").join(" ");
+ got.search_text_area ~= "rights: " ~ got.rights ~ "\n";
+ }
+ if ("cmt" in canned_query && !(canned_query["cmt"]).empty) {
+ got.comment = canned_query["cmt"].split("%20").join(" ");
+ got.search_text_area ~= "comment: " ~ got.comment ~ "\n";
+ }
+ // if ("abstract" in canned_query && !(canned_query["abstract"]).empty) {
+ // got.abstract = canned_query["abstract"];
+ // }
+ if ("bfn" in canned_query && !(canned_query["bfn"]).empty) { // search_field
+ got.src_filename_base = canned_query["bfn"].split("%20").join(" ");
+ got.search_text_area ~= "src_filename_base: " ~ got.src_filename_base ~ "\n";
+ }
+ if ("sml" in canned_query && !(canned_query["sml"]).empty) {
+ got.sql_match_limit = canned_query["sml"].split("%20").join(" ");
+ // got.search_text_area ~= "sql_match_limit: " ~ got.sql_match_limit ~ "\n";
+ }
+ // cgi.write("f.search_text_area: " ~ got.search_text_area ~ "<br>");
+ }
+ return got;
+ }
+ auto tf = text_fields; //
+ struct SQL_select {
+ string the_body = "";
+ string the_range = "";
+ }
+ auto sql_select = SQL_select();
+ string base ; // = "";
+ string tip ; // = "";
+ string search_note ; // = "";
+ uint sql_match_offset_count = 0;
+ string previous_next () {
+ static struct Rgx {
+ static track_offset = ctRegex!(`(?P<offset_key>[&]smo=)(?P<offset_val>[0-9]+)`, "m");
+ }
+ auto rgx = Rgx();
+ string _previous_next = "";
+ int _current_offset_value = 0;
+ string _set_offset_next = "";
+ string _set_offset_previous = "";
+ string _url = "";
+ string _url_previous = "";
+ string _url_next = "";
+ string arrow_previous = "";
+ string arrow_next = "";
+ if (environment.get("REQUEST_METHOD", "POST") == "POST") {
+ _url = conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ tf.canned_query;
+ } else if (environment.get("REQUEST_METHOD", "POST") == "GET") {
+ _url = conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ environment.get("QUERY_STRING", "");
+ }
+ if (auto m = _url.matchFirst(rgx.track_offset)) {
+ _current_offset_value = m.captures["offset_val"].to!int;
+ _set_offset_next = m.captures["offset_key"] ~ ((m.captures["offset_val"]).to!int + cv.sql_match_limit.to!int).to!string;
+ _url_next = _url.replace(rgx.track_offset, _set_offset_next);
+ if (_current_offset_value < cv.sql_match_limit.to!int) {
+ _url_previous = "";
+ } else {
+ _url_previous = "";
+ _set_offset_previous = m.captures["offset_key"] ~ ((m.captures["offset_val"]).to!int - cv.sql_match_limit.to!int).to!string;
+ _url_previous = _url.replace(rgx.track_offset, _set_offset_previous);
+ }
+ } else {// _current_offset_value = 0;
+ _url_next = _url ~= "&smo=" ~ cv.sql_match_limit.to!string;
+ }
+ if (_url_previous.empty) {
+ arrow_previous = "";
+ } else {
+ arrow_previous =
+ "<font size=\"2\" color=\"#666666\">"
+ ~ "<a href=\""
+ ~ _url_previous
+ ~ "\">"
+ ~ "&lt;&lt prev"
+ ~ "</a> || </font>";
+ }
+ arrow_next =
+ "<font size=\"2\" color=\"#666666\">"
+ ~ "<a href=\""
+ ~ _url_next
+ ~ "\">"
+ ~ "next &gt;&gt"
+ ~ "</a></font>";
+ _previous_next = "<hr>" ~ arrow_previous ~ arrow_next;
+ return _previous_next;
+ }
+ {
+ header = format(q"┃
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <title>
+ SiSU spine search form (sample)
+ </title>
+ <meta name="sourcefile" content="SiSU.sst" />
+ <link rel="generator" href="sisudoc.org" />
+ <link rel="shortcut icon" href="https://%s/image_sys/spine.ico" />
+ <style media = "all">
+ *{
+ padding : 0px;
+ margin : 2px;
+ }
+ body {
+ height : 100vh;
+ background-color : #FFFFFF;
+ }
+ body {
+ color : #000000;
+ background : #FFFFFF;
+ background-color : #FFFFFF;
+ }
+ a:link {
+ color : #003399;
+ text-decoration : none;
+ }
+ a:visited {
+ color : #003399;
+ text-decoration : none;
+ }
+ a:hover {
+ color : #000000;
+ background-color : #F9F9AA;
+ }
+ a.lnkocn:link {
+ color : #777777;
+ text-decoration : none;
+ }
+ a.lnkocn:visited {
+ color : #32CD32;
+ text-decoration : none;
+ }
+ a.lnkocn:hover {
+ color : #777777;
+ font-size : 15px;
+ }
+ a:hover img {
+ background-color : #FFFFFF;
+ }
+ a:active {
+ color : #003399;
+ text-decoration : underline;
+ }
+ hr {
+ width : 100%%;
+ margin-left : 0%%;
+ margin-right : 0em;
+ margin-top : 0.5em;
+ margin-bottom : 0.5em;
+ }
+</style>
+</head>
+<body lang="en" xml:lang="en">
+┃",
+ conf.http_host,
+ );
+ }
+ {
+ table = format(q"┃
+ <table summary="band" border="0" cellpadding="2" cellspacing="0">
+ <tr><td width="20%%">
+ <table summary="home button / home information" border="0" cellpadding="2" cellspacing="0">
+ <tr><td align="left">
+ <br /><a href="https://sisudoc.org/" target="_top">
+ <b>SiSU</b>
+ </a>
+ <br /><a href="https://git.sisudoc.org/" target="_top">
+ git
+ </a>
+ </td></tr>
+ </table>
+ </td>
+ <td>
+ <label for="find"><b>SiSU spine (generated) search form</b></label>
+ </td></tr>
+ </table>
+ ┃");
+ }
+ {
+ string post_value(string field_name, string type="box", string set="on") {
+ string val = "";
+ switch (type) {
+ case "field":
+ val = ((field_name in cgi.post && !(cgi.post[field_name]).empty)
+ ? cgi.post[field_name]
+ : (field_name in cgi.get)
+ ? cgi.get[field_name]
+ : "");
+ val = tf.search_text_area;
+ break;
+ case "box": // generic for checkbox or radio; checkbox set == "on" radio set == "name set"
+ val = ((field_name in cgi.post && !(cgi.post[field_name]).empty)
+ ? (cgi.post[field_name] == set ? "checked" : "off")
+ : (field_name in cgi.get)
+ ? (cgi.get[field_name] == set ? "checked" : "off")
+ : "off");
+ break;
+ case "radio": // used generic bo
+ val = ((field_name in cgi.post && !(cgi.post[field_name]).empty)
+ ? (cgi.post[field_name] == set ? "checked" : "off")
+ : (field_name in cgi.get)
+ ? (cgi.get[field_name] == set ? "checked" : "off")
+ : "checked");
+ break;
+ case "checkbox": // used generic bo
+ val = ((field_name in cgi.post && !(cgi.post[field_name]).empty)
+ ? (cgi.post[field_name] == set ? "checked" : "off")
+ : (field_name in cgi.get)
+ ? (cgi.get[field_name] == set ? "checked" : "off")
+ : "checked");
+ break;
+ default:
+ }
+ return val;
+ }
+ string the_can(string fv) {
+ string show_the_can = post_value("url");
+ string _the_can = "";
+ if (show_the_can == "checked") {
+ tf = text_fields;
+ string method_get_url = conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ environment.get("QUERY_STRING", "");
+ string method_post_url_construct = conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ tf.canned_query;
+ // assert(method_get_url == environment.get("HTTP_REFERER", conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ "?" ~ conf.query_string));
+ if (conf.request_method == "POST") {
+ _the_can =
+ "<font size=\"2\" color=\"#666666\">"
+ ~ "POST:&nbsp;"
+ ~ "<a href=\""
+ ~ method_post_url_construct
+ ~ "\">"
+ ~ method_post_url_construct
+ ~ "</a></font>"
+ ~ "<br>";
+ } else if (conf.request_method == "GET") {
+ _the_can =
+ "<font size=\"2\" color=\"#666666\">"
+ ~ "GET:&nbsp;&nbsp;"
+ ~ "<a href=\""
+ ~ method_get_url
+ ~ "\">"
+ ~ method_get_url
+ ~ "</a></font>";
+ }
+ conf.http_url = conf.http_request_type ~ "://" ~ conf.http_host ~ conf.cgi_script ~ tf.canned_query;
+ }
+ return _the_can;
+ }
+ string provide_tip() {
+ string searched_tip = post_value("se");
+ string tip = "";
+ if (searched_tip == "checked") {
+ string search_field = post_value("sf", "field");
+ tf = text_fields;
+ tip = format(q"┃
+<font size="2" color="#666666">
+<b>database:</b> <font size="2" color="#004000">%s</font>; <b>selected view:</b> <font size="2" color="#004000">index</font>
+<b>search string:</b> %s %s %s %s %s %s<br />
+%s %s %s %s %s %s
+</font>
+┃",
+ cv.db_selected,
+ (tf.text.empty ? "" : "\"text: <font size=\"2\" color=\"#004000\">" ~ tf.text ~ "</font>; "),
+ (tf.title.empty ? "" : "\"title: <font size=\"2\" color=\"#004000\">" ~ tf.title ~ "</font>; "),
+ (tf.author.empty ? "" : "\"author: <font size=\"2\" color=\"#004000\">" ~ tf.author ~ "</font>; "),
+ (tf.date.empty ? "" : "\"date <font size=\"2\" color=\"#004000\">" ~ tf.date ~ "</font>; "),
+ (tf.uid.empty ? "" : "\"uid: <font size=\"2\" color=\"#004000\">" ~ tf.uid ~ "</font>; "),
+ (tf.fn.empty ? "" : "\"fn: <font size=\"2\" color=\"#004000\">" ~ tf.fn ~ "</font>; "),
+ (tf.text.empty ? "" : "text: <font size=\"2\" color=\"#004000\">" ~ tf.text ~ "</font><br />"),
+ (tf.title.empty ? "" : "title: <font size=\"2\" color=\"#004000\">" ~ tf.title ~ "</font><br />"),
+ (tf.author.empty ? "" : "author: <font size=\"2\" color=\"#004000\">" ~ tf.author ~ "</font><br />"),
+ (tf.date.empty ? "" : "date: <font size=\"2\" color=\"#004000\">" ~ tf.date ~ "</font><br />"),
+ (tf.uid.empty ? "" : "\"uid: <font size=\"2\" color=\"#004000\">" ~ tf.uid ~ "</font>; "),
+ (tf.fn.empty ? "" : "\"fn: <font size=\"2\" color=\"#004000\">" ~ tf.fn ~ "</font>; "),
+ );
+ }
+ return tip;
+ }
+ form = format(q"┃
+<form action="%s" id="SubmitForm" method="post" accept-charset="UTF-8">
+ <table cellpadding="2">
+ <tr><td valign=\"top\">
+ <textarea id="find" name="sf" type="text" rows="6" cols="40" maxlength="256" wrap="virtual">%s</textarea>
+ </td>
+ <td valign=\"top\">
+ %s
+ %s
+ %s
+ </td></tr></table>
+ <td valign=\"top\"><tr><td>
+ <font size="2" color="#222222">
+ <b>to search:</b> select which database to search (drop-down menu below); enter your search query (in the form above); and <b>click on the search button</b> (below)
+ <br />
+ <select name="db" size="1">
+ %s
+ <option value="%s">spine</option>
+ </select>
+ <input type="submit" value="SiSU search" />
+ <input type="radio" name="rt" id="results_type_index" value="idx" %s> index
+ <input type="radio" name="rt" id="results_type_text" value="txt" %s> text / grep;
+ match limit:
+ <input type="radio" name="sml" id="sql_match_limit_1000" value="1000" %s> 1,000
+ <input type="radio" name="sml" id="sql_match_limit_2500" value="2500" %s> 2,500
+ <br />
+ <input type="checkbox" name="ec" %s> echo query
+ <input type="checkbox" name="sts" %s> result stats
+ <input type="checkbox" name="url" %s> search url
+ <input type="checkbox" name="se" %s> searched
+ <input type="checkbox" name="tip" %s> available fields
+ <input type="checkbox" name="sql" %s> sql statement
+ <input type="hidden" name="smo" value="0">
+ <br />
+ </font>
+ </td></tr>
+ </table>
+</form>
+┃",
+ "spine-search",
+ (post_value("ec") == "checked") ? post_value("sf", "field") : "",
+ provide_tip,
+ search_note,
+ the_can(post_value("sf", "field")),
+ cv.db_selected,
+ cv.db_selected,
+ post_value("rt", "box", "idx"),
+ post_value("rt", "box", "txt"),
+ post_value("sml", "box", "1000"),
+ post_value("sml", "box", "2500"),
+ post_value("ec"),
+ post_value("sts"),
+ post_value("url"),
+ post_value("se"),
+ post_value("tip"),
+ post_value("sql"),
+ );
+ {
+ string set_value(string field_name, string default_val) {
+ string val;
+ if (field_name in cgi.post) {
+ val = cgi.post[field_name];
+ } else if (field_name in cgi.get) {
+ val = cgi.get[field_name];
+ } else { val = default_val; }
+ return val;
+ }
+ bool set_bool(string field_name) {
+ bool val;
+ if (field_name in cgi.post
+ && cgi.post[field_name] == "on") {
+ val = true;
+ } else if (field_name in cgi.get
+ && cgi.get[field_name] == "on") {
+ val = true;
+ } else { val = false; }
+ return val;
+ }
+ cv.db_selected = set_value("selected_db", "spine.search.db"); // selected_db == db
+ cv.sql_match_limit = set_value("sml", "1000");
+ cv.sql_match_offset = set_value("smo", "0");
+ cv.search_text = set_value("sf", "test"); // remove test
+ cv.results_type = set_value("rt", "idx");
+ cv.checked_echo = set_bool("ec");
+ cv.checked_stats = set_bool("sts");
+ cv.checked_url = set_bool("url");
+ cv.checked_searched = set_bool("se");
+ cv.checked_tip = set_bool("tip");
+ cv.checked_sql = set_bool("sql");
+ tf = text_fields;
+ }
+ }
+ {
+ cgi.write(header);
+ cgi.write(table);
+ cgi.write(form);
+ // cgi.write(previous_next);
+ { // debug environment
+ // foreach (k, d; environment.toAA) {
+ // cgi.write(k ~ ": " ~ d ~ "<br>");
+ // }
+ }
+ { // debug cgi info
+ // cgi.write("db_selected: " ~ cv.db_selected ~ "<br>\n");
+ // cgi.write("search_text: " ~ cv.search_text ~ "<br>\n");
+ // cgi.write("sql_match_limit: " ~ cv.sql_match_limit ~ ";\n");
+ // cgi.write("sql_match_offset: " ~ cv.sql_match_offset ~ ";\n");
+ // cgi.write("results_type: " ~ cv.results_type ~ "<br>\n");
+ // cgi.write("cv.checked_echo: " ~ (cv.checked_echo ? "checked" : "off") ~ "; \n");
+ // cgi.write("cv.checked_stats: " ~ (cv.checked_stats ? "checked" : "off") ~ "; \n");
+ // cgi.write("cv.checked_url: " ~ (cv.checked_url ? "checked" : "off") ~ "; \n");
+ // cgi.write("cv.checked_searched: " ~ (cv.checked_searched ? "checked" : "off") ~ ";<br>\n");
+ // cgi.write("cv.checked_tip: " ~ (cv.checked_tip ? "checked" : "off") ~ "; \n");
+ // cgi.write("cv.checked_sql: " ~ (cv.checked_sql ? "checked" : "off") ~ "<br>\n");
+ }
+ }
+ auto db = Database(conf.db_path ~ cv.db_selected);
+ {
+ uint sql_match_offset_counter(T)(T cv) {
+ sql_match_offset_count += cv.sql_match_limit.to!uint;
+ return sql_match_offset_count;
+ }
+ void sql_search_query() {
+ string select_field_like(string db_field, string search_field) {
+ string where_ = "";
+ if (!(search_field.empty)) {
+ string _sf = search_field.strip.split("%20").join(" ");
+ if (_sf.match(r" OR ")) {
+ _sf = _sf.split(" OR ").join("%' OR " ~ db_field ~ " LIKE '%");
+ }
+ if (_sf.match(r" AND ")) {
+ _sf = _sf.split(" AND ").join("%' AND " ~ db_field ~ " LIKE '%");
+ }
+ _sf = "( " ~ db_field ~ " LIKE\n '%" ~ _sf ~ "%' )";
+ where_ ~= format(q"┃
+ %s
+┃",
+ _sf
+ );
+ }
+ return where_;
+ }
+ string[] _fields;
+ _fields ~= select_field_like("doc_objects.clean", tf.text);
+ _fields ~= select_field_like("metadata_and_text.title", tf.title);
+ _fields ~= select_field_like("metadata_and_text.creator_author", tf.author);
+ _fields ~= select_field_like("metadata_and_text.uid", tf.uid);
+ _fields ~= select_field_like("metadata_and_text.src_filename_base", tf.fn);
+ _fields ~= select_field_like("metadata_and_text.src_filename_base", tf.src_filename_base);
+ _fields ~= select_field_like("metadata_and_text.language_document_char", tf.language);
+ _fields ~= select_field_like("metadata_and_text.date_published", tf.date);
+ _fields ~= select_field_like("metadata_and_text.classify_keywords", tf.keywords);
+ _fields ~= select_field_like("metadata_and_text.classify_topic_register", tf.topic_register);
+ string[] fields;
+ foreach (f; _fields) {
+ if (!(f.empty)) { fields ~= f; }
+ }
+ string fields_str = "";
+ fields_str ~= fields.join(" AND ");
+ sql_select.the_body ~= format(q"┃
+SELECT
+ metadata_and_text.uid,
+ metadata_and_text.title,
+ metadata_and_text.creator_author_last_first,
+ metadata_and_text.creator_author,
+ metadata_and_text.src_filename_base,
+ metadata_and_text.language_document_char,
+ metadata_and_text.date_published,
+ metadata_and_text.classify_keywords,
+ metadata_and_text.classify_topic_register,
+ doc_objects.body,
+ doc_objects.seg_name,
+ doc_objects.ocn,
+ metadata_and_text.uid
+FROM
+ doc_objects,
+ metadata_and_text
+WHERE (
+ %s
+ )
+AND
+ doc_objects.uid_metadata_and_text = metadata_and_text.uid
+ORDER BY
+ metadata_and_text.creator_author_last_first,
+ metadata_and_text.date_published DESC,
+ metadata_and_text.title,
+ metadata_and_text.language_document_char,
+ metadata_and_text.src_filename_base,
+ doc_objects.ocn
+LIMIT %s OFFSET %s
+;┃",
+ fields_str,
+ cv.sql_match_limit,
+ cv.sql_match_offset,
+ );
+ (cv.checked_sql)
+ ? cgi.write(previous_next ~ "<hr><font size=\"2\" color=\"#666666\">" ~ sql_select.the_body.split("\n ").join(" ").split("\n").join("<br>") ~ "</font>\n")
+ : "";
+ cgi.write(previous_next);
+ auto select_query_results = db.execute(sql_select.the_body).cached;
+ string _old_uid = "";
+ if (!select_query_results.empty) {
+ foreach (row; select_query_results) {
+ if (row["uid"].as!string != _old_uid) {
+ _old_uid = row["uid"].as!string;
+ auto m = (row["date_published"].as!string).match(regex(r"^([0-9]{4})")); // breaks if row missing or no match?
+ cgi.write(
+ "<hr><a href=\""
+ ~ "https://" ~ conf.http_host ~ "/"
+ ~ row["language_document_char"].as!string ~ "/html/"
+ ~ row["src_filename_base"].as!string ~ "/"
+ ~ "toc.html"
+ ~ "\">\""
+ ~ row["title"].as!string ~ "\""
+ ~ "</a> ("
+ ~ m.hit
+ ~ ") "
+ ~ "["
+ ~ row["language_document_char"].as!string
+ ~ "] "
+ ~ row["creator_author_last_first"].as!string
+ ~ ":<br>\n"
+ );
+ }
+ if (cv.results_type == "txt") {
+ cgi.write(
+ "<hr><a href=\""
+ ~ "https://" ~ conf.http_host ~ "/"
+ ~ row["language_document_char"].as!string ~ "/html/"
+ ~ row["src_filename_base"].as!string ~ "/"
+ ~ row["seg_name"].as!string ~ ".html#" ~ row["ocn"].as!string
+ ~ "\">"
+ ~ row["ocn"].as!string
+ ~ "</a>"
+ ~ "<br>"
+ ~ row["body"].as!string
+ );
+ } else {
+ cgi.write(
+ "<a href=\""
+ ~ "https://" ~ conf.http_host ~ "/"
+ ~ row["language_document_char"].as!string ~ "/html/"
+ ~ row["src_filename_base"].as!string ~ "/"
+ ~ row["seg_name"].as!string ~ ".html#" ~ row["ocn"].as!string
+ ~ "\">"
+ ~ row["ocn"].as!string
+ ~ "</a>, "
+ );
+ }
+ }
+ cgi.write( previous_next);
+ } else { // offset_not_beyond_limit = false;
+ cgi.write("select_query_results empty<p>\n");
+ }
+ }
+ sql_search_query;
+ }
+ {
+ db.close;
+ }
+ {
+ string tail = format(q"┃
+</body>
+┃");
+ cgi.write(tail);
+ }
+}
+mixin GenericMain!cgi_function_intro;
diff --git a/sundry/misc/util/d/tools/markup_conversion/README b/sundry/misc/util/d/tools/markup_conversion/README
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/sundry/misc/util/d/tools/markup_conversion/README
@@ -0,0 +1 @@
+
diff --git a/sundry/misc/util/d/tools/markup_conversion/endnotes_inline_from_binary.d b/sundry/misc/util/d/tools/markup_conversion/endnotes_inline_from_binary.d
new file mode 100755
index 0000000..b084052
--- /dev/null
+++ b/sundry/misc/util/d/tools/markup_conversion/endnotes_inline_from_binary.d
@@ -0,0 +1,123 @@
+#!/usr/bin/env rdmd
+/+
+ - read in file .sst .ssi .ssm
+ - loop twice
+ - first
+ - check for and skip code blocks
+ - use unique code marker for endnote markers in text and give an endnote
+ number ★1, increment
+ - extract all endnotes in array
+ - second
+ - check that the footnote marker number count matches the number of notes
+ in the array
+ - if they match either:
+ - substitute each endnote marker with the array footnote[number-1]
+ - substitute each endnote marker with footnote
+ as inlined footnote markup (footnote number not needed)
+ - if they do not match exit
+ - check whether changes have been made
+ - if so write file with inline footnotes in sub-directory converted_output_/
+ using the same name as the original file
+ - else, exit
++/
+import std.stdio;
+import std.file;
+import std.array : split;
+import std.exception;
+import core.stdc.errno;
+import std.regex;
+import std.format;
+import std.conv;
+void main(string[] args) {
+ static comment = ctRegex!(`^%+ `);
+ static block_tic_code_open = ctRegex!("^`{3} code(?:[.](?P<syntax>[a-z][0-9a-z#+_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?");
+ static block_tic_close = ctRegex!("^(`{3})$","m");
+ static block_curly_code_open = ctRegex!(`^(?:code(?:[.](?P<syntax>[a-z][0-9a-z_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?[{][ ]*$)`);
+ static block_curly_code_close = ctRegex!(`^([}]code)`);
+ auto rgx_endnote_ref = ctRegex!(`([~]\^)(?P<tail>[)\]]? |$)`, "gm");
+ auto rgx_endnote = ctRegex!(`^\^~\s+(.+|\n)`, "gm");
+ foreach(arg; args[1..$]) {
+ if (
+ !(arg.match(regex(r"--\w+")))
+ && arg.match(regex(r"\w+?\.ss[itm]"))
+ ) {
+ writeln(arg);
+ string filename = arg;
+ try {
+ string[] contents, endnotes, endnote_refs;
+ string text = filename.readText;
+ string[] paragraphs = text.split("\n\n");
+ int endnote_ref_count = 0;
+ int code_block_status = 0;
+ enum codeBlock { off, curly, tic, }
+ foreach (paragraph; paragraphs) { /+ loop to gather binary endnotes +/
+ if (code_block_status == codeBlock.off
+ && paragraph.match(rgx_endnote)
+ ) {
+ endnotes ~= replaceAll!(m => m[1])
+ (paragraph, rgx_endnote);
+ } else {
+ if ((code_block_status == codeBlock.curly
+ && paragraph.matchFirst(block_curly_code_close))
+ || ((code_block_status == codeBlock.tic
+ && paragraph.matchFirst(block_tic_close))
+ ) {
+ code_block_status = codeBlock.off;
+ } else if ( type["curly_code"] == 1 || type["tic_code"] == 1) {
+ // skip, prevent search for endnotes
+ } else if (paragraph.matchFirst(block_curly_code_open)) {
+ code_block_status = codeBlock.curly;
+ } else if (paragraph.matchFirst(block_tic_code_open)) {
+ code_block_status = codeBlock.tic;
+ } else if (auto m = paragraph.matchAll(rgx_endnote_ref)) {
+ foreach (n; m) {
+ endnote_ref_count++; // endnote_refs ~= (n.captures[1]);
+ }
+ }
+ contents ~= paragraph;
+ }
+ }
+ if (endnotes.length == endnote_ref_count) {
+ import std.outbuffer;
+ writeln("endnote ref count: ", endnote_ref_count);
+ writeln("number of binary endnotes: ", endnotes.length);
+ int endnote_count = -1;
+ auto buffer = new OutBuffer();
+ foreach (content; contents) { /+ loop to inline endnotes +/
+ content = replaceAll!(m => "~{ " ~ endnotes[++endnote_count] ~ " }~" ~ m["tail"] )
+ (content, rgx_endnote_ref);
+ buffer.write(content ~ "\n\n");
+ }
+ if (buffer) {
+ try {
+ string dir_out = "converted_output_";
+ string path_and_file_out = dir_out ~ "/" ~ filename;
+ dir_out.mkdirRecurse;
+ auto f = File(path_and_file_out, "w");
+ f.write(buffer);
+ writeln("wrote: ", path_and_file_out);
+ } catch (FileException ex) {
+ writeln("did not write file");
+ // Handle errors
+ }
+ }
+ } else {
+ writeln("ERROR binary endnote mismatch, check markup,\nmisatch in the number of endnotes & endnote references!");
+ writeln(" number of endnotes: ", endnotes.length);
+ writeln(" number of endnote refs: ", endnote_ref_count); // endnote_refs.length,
+ }
+ // assert(endnotes.length == endnote_ref_count);
+ } catch (ErrnoException ex) {
+ switch(ex.errno) {
+ case EPERM:
+ case EACCES: // Permission denied
+ break;
+ case ENOENT: // File does not exist
+ break;
+ default: // Handle other errors
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/sundry/misc/util/d/tools/markup_conversion/markup_changes.d b/sundry/misc/util/d/tools/markup_conversion/markup_changes.d
new file mode 100644
index 0000000..4274f78
--- /dev/null
+++ b/sundry/misc/util/d/tools/markup_conversion/markup_changes.d
@@ -0,0 +1,136 @@
+#!/usr/bin/env rdmd
+/+
+ - read in file .sst .ssi .ssm
+ - loop twice
+ - first
+ - check for and skip code blocks
+ - use unique code marker for endnote markers in text and give an endnote
+ number ★1, increment
+ - extract all endnotes in array
+ - second
+ - check that the footnote marker number count matches the number of notes
+ in the array
+ - if they match either:
+ - substitute each endnote marker with the array footnote[number-1]
+ - substitute each endnote marker with footnote
+ as inlined footnote markup (footnote number not needed)
+ - if they do not match exit
+ - check whether changes have been made
+ - if so write file with inline footnotes in sub-directory converted_output_/
+ using the same name as the original file
+ - else, exit
++/
+import std.stdio;
+import std.file;
+import std.array : split;
+import std.exception;
+import core.stdc.errno;
+import std.regex;
+import std.format;
+import std.conv;
+void main(string[] args) {
+ static comment = ctRegex!(`^%+ `);
+ static block_tic_code_open = ctRegex!("^`{3} code(?:[.](?P<syntax>[a-z][0-9a-z#+_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?");
+ static block_tic_close = ctRegex!("^(`{3})$","m");
+ static block_curly_code_open = ctRegex!(`^(?:code(?:[.](?P<syntax>[a-z][0-9a-z_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?[{][ ]*$)`);
+ static block_curly_code_close = ctRegex!(`^([}]code)`);
+ auto rgx_endnote_ref = ctRegex!(`([~]\^)(?P<tail>[)\]]? |$)`, "gm");
+ auto rgx_endnote = ctRegex!(`^\^~\s+(.+|\n)`, "gm");
+ foreach(arg; args[1..$]) {
+ if (
+ !(arg.match(regex(r"--\w+")))
+ && arg.match(regex(r"\w+?\.ss[itm]"))
+ ) {
+ writeln(arg);
+ string filename = arg;
+ try {
+ string[] contents, endnotes, endnote_refs;
+ string text = filename.readText;
+ string[] paragraphs = text.split("\n\n");
+ int endnote_ref_count = 0;
+ int[string] type = [
+ "curly_code" : 0,
+ "tic_code" : 0,
+ ];
+ foreach (paragraph; paragraphs) { /+ loop to gather binary endnotes +/
+ if ( !( type["curly_code"] == 1 || type["tic_code"] == 1)
+ && paragraph.match(rgx_endnote)
+ ) {
+ endnotes ~= replaceAll!(m => m[1])
+ (paragraph, rgx_endnote);
+ } else {
+ if ( type["curly_code"] == 1 || type["tic_code"] == 1
+ || paragraph.matchFirst(block_curly_code_open)
+ || paragraph.matchFirst(block_tic_code_open)
+ ) { /+ code blocks identified, no munging +/
+ if ( type["curly_code"] == 1
+ && paragraph.matchFirst(block_curly_code_close)
+ ) {
+ type["curly_code"] = 0;
+ } else if (type["tic_code"] == 1
+ && paragraph.matchFirst(block_tic_close)
+ ) {
+ type["tic_code"] = 0;
+ } else if (paragraph.matchFirst(block_curly_code_open)) {
+ type["curly_code"] = 1;
+ } else if (paragraph.matchFirst(block_tic_code_open)) {
+ type["tic_code"] = 1;
+ }
+ contents ~= paragraph;
+ } else { /+ regular content, not a code block +/
+ if (auto m = paragraph.matchAll(rgx_endnote_ref)) {
+ foreach (n; m) {
+ endnote_ref_count++; // endnote_refs ~= (n.captures[1]);
+ }
+ }
+ paragraph = replaceAll!(m => " \\\\ " )
+ (paragraph, regex(r"\s*<(?:/\s*|:)?br>\s*")); // (paragraph, regex(r"(<br>)"));
+ contents ~= paragraph;
+ }
+ }
+ }
+ {
+ import std.outbuffer;
+ auto buffer = new OutBuffer();
+ if (endnotes.length == endnote_ref_count) {
+ // writeln("endnote ref count: ", endnote_ref_count);
+ // writeln("number of binary endnotes: ", endnotes.length);
+ int endnote_count = -1;
+ foreach (content; contents) { /+ loop to inline endnotes +/
+ content = replaceAll!(m => "~{ " ~ endnotes[++endnote_count] ~ " }~" ~ m["tail"] )
+ (content, rgx_endnote_ref); // endnote_ref cannot occur in a code block or else fail
+ buffer.write(content ~ "\n\n");
+ }
+ if (buffer) {
+ try {
+ string dir_out = "converted_output_";
+ string path_and_file_out = dir_out ~ "/" ~ filename;
+ dir_out.mkdirRecurse;
+ auto f = File(path_and_file_out, "w");
+ f.write(buffer);
+ writeln("wrote: ", path_and_file_out);
+ } catch (FileException ex) {
+ writeln("did not write file");
+ // Handle errors
+ }
+ }
+ } else {
+ foreach (content; contents) { /+ loop to inline endnotes +/
+ buffer.write(content ~ "\n\n");
+ }
+ }
+ }
+ } catch (ErrnoException ex) {
+ switch(ex.errno) {
+ case EPERM:
+ case EACCES: // Permission denied
+ break;
+ case ENOENT: // File does not exist
+ break;
+ default: // Handle other errors
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/sundry/misc/util/d/tools/markup_conversion/markup_changes_header_and_content.d b/sundry/misc/util/d/tools/markup_conversion/markup_changes_header_and_content.d
new file mode 100755
index 0000000..86792ff
--- /dev/null
+++ b/sundry/misc/util/d/tools/markup_conversion/markup_changes_header_and_content.d
@@ -0,0 +1,244 @@
+#!/usr/bin/env rdmd
+/+
+ - read in file .sst .ssi .ssm
+ - loop twice
+ - first
+ - check for and skip code blocks
+ - use unique code marker for endnote markers in text and give an endnote
+ number ★1, increment
+ - extract all endnotes in array
+ - second
+ - check that the footnote marker number count matches the number of notes
+ in the array
+ - if they match either:
+ - substitute each endnote marker with the array footnote[number-1]
+ - substitute each endnote marker with footnote
+ as inlined footnote markup (footnote number not needed)
+ - if they do not match exit
+ - check whether changes have been made
+ - if so write file with inline footnotes in sub-directory converted_output_/
+ using the same name as the original file
+ - else, exit
++/
+import std.stdio;
+import std.file;
+import std.array : split;
+import std.exception;
+// import std.range;
+import core.stdc.errno;
+import std.regex;
+import std.format;
+import std.conv;
+void main(string[] args) {
+ static heading_a = ctRegex!(`^:?[A][~] `, "m");
+ static comment = ctRegex!(`^%+ `);
+ static block_tic_code_open = ctRegex!("^`{3} code(?:[.](?P<syntax>[a-z][0-9a-z#+_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?");
+ static block_tic_close = ctRegex!("^(`{3})$","m");
+ static block_curly_code_open = ctRegex!(`^(?:code(?:[.](?P<syntax>[a-z][0-9a-z_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?[{][ ]*$)`);
+ static block_curly_code_close = ctRegex!(`^([}]code)`);
+ auto rgx_endnote_ref = ctRegex!(`([~]\^)(?P<tail>[)\]]? |$)`, "gm");
+ auto rgx_endnote = ctRegex!(`^\^~\s+(.+|\n)`, "gm");
+ char[][] header0Content1(in string src_text) { // cast(char[])
+ /+ split string on _first_ match of "^:?A~\s" into [header, content] array/tuple +/
+ char[][] header_and_content;
+ auto m = (cast(char[]) src_text).matchFirst(heading_a);
+ header_and_content ~= m.pre;
+ header_and_content ~= m.hit ~ m.post;
+ assert(header_and_content.length == 2,
+ "document markup is broken, header body split == "
+ ~ header_and_content.length.to!string
+ ~ "; (header / body array split should == 2 (split is on level A~))"
+ );
+ return header_and_content;
+ }
+ foreach(arg; args[1..$]) {
+ if (
+ !(arg.match(regex(r"--\w+")))
+ && arg.match(regex(r"\w+?\.ss[itm]"))
+ ) {
+ writeln(arg);
+ string filename = arg;
+ try {
+ string[] munged_header, munged_contents, munged_endnotes, endnote_refs;
+ string text = filename.readText;
+ char[][] hc = header0Content1(text);
+ char[] src_header = hc[0];
+ string[] headers = src_header.to!string.split("\n\n");
+ char[] src_txt = hc[1];
+ string[] paragraphs = src_txt.to!string.split("\n\n");
+ int endnote_ref_count = 0;
+ int[string] type = [
+ "curly_code" : 0,
+ "tic_code" : 0,
+ ];
+ string _tmp_header;
+ foreach (h_; headers) { /+ loop to inline endnotes +/
+ _tmp_header = "";
+ if (h_.match(regex(r"^[@\[]?title[:\]]?"))) { // title
+ if (auto m = h_.match(regex(r"^@title:(?:\s+(?P<c>.+)|$)"))) { // sisu bespoke markup
+ if (m.captures["c"].length == 0) {
+ _tmp_header ~= "title:";
+ } else {
+ _tmp_header ~= "title:\n main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ } else if (auto m = h_.match(regex(r"^title\s*=\s*(?P<c>.+)"))) { // toml?
+ if (m.captures["c"].length == 0) {
+ _tmp_header ~= "title:";
+ } else {
+ _tmp_header ~= "title:\n main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ } else if (auto m = h_.match(regex(r"^\[title\]"))) { // toml markup
+ _tmp_header ~= "title:";
+ } else if (auto m = h_.match(regex(r"^title(?:\s+(?P<c>.+)|\s+\\$)"))) { // sdlang markup
+ if (m.captures["c"].length == 0) {
+ _tmp_header ~= "title:";
+ } else {
+ _tmp_header ~= "title:\n main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ }
+ if (h_.match(regex(r"^\s*[:]?(?:main)[:= ]?", "m"))) {
+ if (auto m = h_.match(regex(r"^\s+(?P<h>:main):(?:\s+(?P<c>.+)|$)", "m"))) { // sisu bespoke markup
+ _tmp_header ~= " main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s*(?P<h>main)\s*=\s*(?P<c>.+)", "m"))) { // toml?
+ _tmp_header ~= " main: " ~ m.captures["c"];
+ } else if (auto m = h_.match(regex(r"^\s+(?P<h>main)(?:\s*\s*(?P<c>.+)|$)", "m"))) { // toml markup
+ _tmp_header ~= " main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s+(?P<h>main)(?:\s+(?P<c>.+)|\s+\\$)", "m"))) { // sdlang markup
+ _tmp_header ~= " main: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ }
+ if (h_.match(regex(r"^\s*[:]?(?:sub(title)?)[:= ]?", "m"))) {
+ if (auto m = h_.match(regex(r"^\s+:sub(?:title)?:(?:\s+(?P<c>.+)|$)", "m"))) { // sisu bespoke markup
+ _tmp_header ~= " subtitle: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s*sub(?:title)?\s*=\s*(?P<c>.+)$", "m"))) { // toml?
+ _tmp_header ~= " subtitle: " ~ m.captures["c"];
+ } else if (auto m = h_.match(regex(r"^\s+(?:title)?(?:\s*\s*(?P<c>.+)|$)", "m"))) { // toml markup
+ _tmp_header ~= " subtitle: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s+(?:title)?(?:\s+(?P<c>.+)|\s+\\$)", "m"))) { // sdlang markup
+ _tmp_header ~= " subtitle: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ }
+ }
+ if (h_.match(regex(r"^[@\[]?rights[:\]]?"))) { // rights
+ if (auto m = h_.match(regex(r"^@rights:[ ]+(?P<c>.+)$"))) { // sisu bespoke markup
+ _tmp_header ~= "rights: \n copyright: \"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^@rights:"))) { // sisu bespoke markup
+ _tmp_header ~= "rights:";
+ } else if (auto m = h_.match(regex(r"^\[rights\]", "m"))) { // toml markup
+ _tmp_header ~= "rights:";
+ } else if (auto m = h_.match(regex(r"^rights:"))) { // sdlang markup
+ _tmp_header ~= "rights:";
+ }
+ if (h_.match(regex(r"^\s*[:]?copyright[:= ]?", "m"))) {
+ if (auto m = h_.match(regex(r"^\s+:copyright:(?:\s+(?P<c>.+)|$)", "m"))) { // sisu bespoke markup
+ _tmp_header ~= " copyright: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s*copyright\s*=\s*(?P<c>.+)", "m"))) { // toml?
+ _tmp_header ~= " copyright: " ~ m.captures["c"];
+ } else if (auto m = h_.match(regex(r"^\s+<h>copyright(?:\s*\s*(?P<c>.+)|$)", "m"))) { // toml markup
+ _tmp_header ~= " copyright: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s+copyright(?:\s+(?P<c>.+)|\s+\\$)", "m"))) { // sdlang markup
+ _tmp_header ~= " copyright: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ }
+ if (h_.match(regex(r"^\s*[:]?licen[cs]e[:= ]?", "m"))) {
+ if (auto m = h_.match(regex(r"^\s+:licen[cs]e:(?:\s+(?P<c>.+)|$)", "m"))) { // sisu bespoke markup
+ _tmp_header ~= " license: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s*licen[cs]e\s*=\s*(?P<c>.+)$", "m"))) { // toml?
+ _tmp_header ~= " license: " ~ m.captures["c"];
+ } else if (auto m = h_.match(regex(r"^\s+licen[cs]e(?:\s*\s*(?P<c>.+)|$)", "m"))) { // toml markup
+ _tmp_header ~= " license: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ } else if (auto m = h_.match(regex(r"^\s+licen[cs]e(?:\s+(?P<c>.+)|\s+\\$)", "m"))) { // sdlang markup
+ _tmp_header ~= " license: " ~ "\"" ~ m.captures["c"] ~ "\"";
+ }
+ }
+ }
+ if (_tmp_header.length > 0) {
+ munged_header ~= _tmp_header;
+ } else {
+ munged_header ~= h_;
+ }
+ }
+ writeln(munged_header);
+ foreach (paragraph; paragraphs) { /+ loop to gather binary endnotes +/
+ if ( !( type["curly_code"] == 1 || type["tic_code"] == 1)
+ && paragraph.match(rgx_endnote)
+ ) {
+ munged_endnotes ~= replaceAll!(m => m[1])
+ (paragraph, rgx_endnote);
+ } else {
+ if ( type["curly_code"] == 1 || type["tic_code"] == 1
+ || paragraph.matchFirst(block_curly_code_open)
+ || paragraph.matchFirst(block_tic_code_open)
+ ) { /+ code blocks identified, no munging +/
+ if ( type["curly_code"] == 1
+ && paragraph.matchFirst(block_curly_code_close)
+ ) {
+ type["curly_code"] = 0;
+ } else if (type["tic_code"] == 1
+ && paragraph.matchFirst(block_tic_close)
+ ) {
+ type["tic_code"] = 0;
+ } else if (paragraph.matchFirst(block_curly_code_open)) {
+ type["curly_code"] = 1;
+ } else if (paragraph.matchFirst(block_tic_code_open)) {
+ type["tic_code"] = 1;
+ }
+ munged_contents ~= paragraph;
+ } else { /+ regular content, not a code block +/
+ if (auto m = paragraph.matchAll(rgx_endnote_ref)) {
+ foreach (n; m) {
+ endnote_ref_count++; // endnote_refs ~= (n.captures[1]);
+ }
+ }
+ paragraph = replaceAll!(m => " \\\\ " )
+ (paragraph, regex(r"\s*<(?:/\s*|:)?br>\s*")); // (paragraph, regex(r"(<br>)"));
+ munged_contents ~= paragraph;
+ }
+ }
+ }
+ {
+ import std.outbuffer;
+ auto buffer = new OutBuffer();
+ foreach (header; munged_header) { /+ loop to inline endnotes +/
+ buffer.write(header ~ "\n\n");
+ }
+ if (munged_endnotes.length == endnote_ref_count) {
+ int endnote_count = -1;
+ foreach (content; munged_contents) { /+ loop to inline endnotes +/
+ content = replaceAll!(m => "~{ " ~ munged_endnotes[++endnote_count] ~ " }~" ~ m["tail"] )
+ (content, rgx_endnote_ref); // endnote_ref cannot occur in a code block or else fail
+ buffer.write(content ~ "\n\n");
+ }
+ if (buffer) {
+ try {
+ string dir_out = "converted_output_";
+ string path_and_file_out = dir_out ~ "/" ~ filename;
+ dir_out.mkdirRecurse;
+ auto f = File(path_and_file_out, "w");
+ f.write(buffer);
+ // writeln("wrote: ", path_and_file_out);
+ } catch (FileException ex) {
+ writeln("did not write file");
+ // Handle errors
+ }
+ }
+ } else {
+ foreach (content; munged_contents) { /+ loop to inline endnotes +/
+ buffer.write(content ~ "\n\n");
+ }
+ }
+ }
+ } catch (ErrnoException ex) {
+ switch(ex.errno) {
+ case EPERM:
+ case EACCES: // Permission denied
+ break;
+ case ENOENT: // File does not exist
+ break;
+ default: // Handle other errors
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/sundry/misc/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d b/sundry/misc/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d
new file mode 100755
index 0000000..0ec541d
--- /dev/null
+++ b/sundry/misc/util/d/tools/markup_conversion/markup_conversion_from_sisu_ruby_to_sisu_spine.d
@@ -0,0 +1,367 @@
+#!/usr/bin/env rdmd
+/+
+ - read in file .sst .ssi .ssm
+ - loop twice
+ - first
+ - check for and skip code blocks
+ - use unique code marker for endnote markers in text and give an endnote
+ number ★1, increment
+ - extract all endnotes in array
+ - second
+ - check that the footnote marker number count matches the number of notes
+ in the array
+ - if they match either:
+ - substitute each endnote marker with the array footnote[number-1]
+ - substitute each endnote marker with footnote
+ as inlined footnote markup (footnote number not needed)
+ - if they do not match exit
+ - check whether changes have been made
+ - if so write file with inline footnotes in sub-directory converted_output_/
+ using the same name as the original file
+ - else, exit
++/
+import std.stdio;
+import std.file;
+import std.array : split, join;
+import std.exception;
+// import std.range;
+import core.stdc.errno;
+import std.regex;
+import std.format;
+import std.conv;
+void main(string[] args) {
+ static heading_a = ctRegex!(`^:?[A][~] `, "m");
+ static comment = ctRegex!(`^%+ `);
+ static block_tic_code_open = ctRegex!("^`{3} code(?:[.](?P<syntax>[a-z][0-9a-z#+_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?");
+ static block_tic_close = ctRegex!("^(`{3})$","m");
+ static block_curly_code_open = ctRegex!(`^(?:code(?:[.](?P<syntax>[a-z][0-9a-z_]+))?(?:[(](?P<attrib>[ a-zA-Z0-9;:,]*)[)])?[{][ ]*$)`);
+ static block_curly_code_close = ctRegex!(`^([}]code)`);
+ auto rgx_endnote_ref = ctRegex!(`([~]\^)(?P<tail>[)\]]? |$)`, "gm");
+ auto rgx_endnote = ctRegex!(`^\^~\s+(.+|\n)`, "gm");
+ char[][] header0Content1(in string src_text) { // cast(char[])
+ /+ split string on _first_ match of "^:?A~\s" into [header, content] array/tuple +/
+ char[][] header_and_content;
+ auto m = (cast(char[]) src_text).matchFirst(heading_a);
+ header_and_content ~= m.pre;
+ header_and_content ~= m.hit ~ m.post;
+ assert(header_and_content.length == 2,
+ "document markup is broken, header body split == "
+ ~ header_and_content.length.to!string
+ ~ "; (header / body array split should == 2 (split is on level A~))"
+ );
+ return header_and_content;
+ }
+ string format_body_string(string s) {
+ string o;
+ o = s
+ .replaceAll(regex("^<(?:/[ ]*)?br>[ ]*"), " \\\\ ")
+ .replaceAll(regex("[ ]*<(?:/[ ]*)?br>$"), " \\\\")
+ .replaceAll(regex("[ ]*<(?:/[ ]*)?br>[ ]*"), " \\\\ ");
+ return o;
+ }
+ string format_header_string(string s) {
+ string o;
+ o = s
+ .replaceAll(regex("\""), "\\\"")
+ .replaceAll(regex("[ ]*<(?:/[ ]*)?br>$"), " \\\\")
+ .replaceAll(regex("[ ]*<(?:/[ ]*)?br>[ ]*"), " \\\\ ");
+ return o;
+ }
+ string format_main_header(string hm, string hs = "", string c = "") {
+ string o;
+ if (c.length == 0) {
+ o ~= hm ~ ":\n";
+ } else {
+ o ~= hm ~ ":\n"
+ ~ " " ~ hs ~ ": "
+ ~ "\"" ~ format_header_string(c) ~ "\"\n";
+ }
+ return o;
+ }
+ string format_sub_header(string hs, string c) {
+ string o;
+ o ~= " " ~ hs ~ ": "
+ ~ "\"" ~ format_header_string(c) ~ "\"\n";
+ return o;
+ }
+ foreach(arg; args[1..$]) {
+ if (
+ !(arg.match(regex(r"--\w+")))
+ && arg.match(regex(r"\w+?\.ss[itm]"))
+ ) {
+ writeln(arg);
+ string filename = arg;
+ try {
+ string[] munged_header, munged_contents, munged_endnotes, endnote_refs;
+ char[][] hc;
+ char[] src_header;
+ string[] headers;
+ char[] src_txt;
+ string[] paragraphs;
+ enum codeBlock { off, curly, tic, }
+ string _tmp_header;
+ int endnote_ref_count = 0;
+ int code_block_status = codeBlock.off;
+ string text = filename.readText;
+ if (arg.match(regex(r"\w+?\.ss[tm]"))) {
+ hc = header0Content1(text);
+ src_header = hc[0];
+ headers = src_header.to!string.split("\n\n");
+ src_txt = hc[1];
+ paragraphs = src_txt.to!string.split("\n\n");
+ } else if (arg.match(regex(r"\w+?\.ssi"))) {
+ headers = [];
+ paragraphs = text.split("\n\n");
+ }
+ if (headers.length > 0) {
+ headers[0] = headers[0].replaceFirst(regex(r"^%\s+SiSU.+", "i"), "# SiSU 8.0 spine (auto-conversion)");
+ foreach (h_; headers) {
+ _tmp_header = "";
+ if (auto m = h_.match(regex(r"^%\s*", "m"))) {
+ h_ = h_.replaceAll(regex(r"^%\s*", "m"), "# ") ~ "\n";
+ }
+ if (h_.match(regex(r"^@title:|@subtitle"))) {
+ if (auto m = h_.match(regex(r"^@(?P<h>title):(?:[ ]+(?P<c>.+)|\n)"))) {
+ _tmp_header ~= format_main_header(m.captures["h"], "main", m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@(?P<h>subtitle):(?:[ ]+(?P<c>.+)|$)"))) {
+ if (m.captures["c"].length == 0) {
+ } else {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>main):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:sub(?:title)?:(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header("subtitle", m.captures["c"]);
+ }
+ } else if (h_.match(regex(r"^@creator:|@author:"))) {
+ if (auto m = h_.match(regex(r"^(?:@creator:|@author:)(?:[ ]+(?P<c>.+)|\n)"))) {
+ _tmp_header ~= format_main_header("creator", "author", m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>author):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ } else if (h_.match(regex(r"^@rights:"))) {
+ if (auto m = h_.match(regex(r"^@(?P<h>rights):(?:[ ]+(?P<c>.+)|\n)"))) {
+ _tmp_header ~= format_main_header(m.captures["h"], "copyright", m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>copyright):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:licen[cs]e:(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header("license", m.captures["c"]);
+ }
+ } else if (h_.match(regex(r"^@date:|@date\."))) {
+ if (auto m = h_.match(regex(r"^@(?P<h>date):(?:[ ]+(?P<c>.+)|\n)"))) {
+ _tmp_header ~= format_main_header(m.captures["h"], "published", m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>published):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>available):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>modified):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>created):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>issued):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>valid):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@date\.(?P<h>available):[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@date\.(?P<h>modified):[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@date\.(?P<h>created):[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@date\.(?P<h>issued):[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^@date\.(?P<h>valid):[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ } else if (h_.match(regex(r"^@classify:"))) {
+ if (auto m = h_.match(regex(r"^@classify:"))) {
+ _tmp_header ~= "classify:\n";
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>topic_register):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:type:(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= "# type: " ~ "\"" ~ m.captures["c"] ~ "\"\n";
+ }
+ } else if (h_.match(regex(r"^(?:@identifier:|@identify:)"))) {
+ if (auto m = h_.match(regex(r"^(?:@identifier:|@idenfify)"))) {
+ _tmp_header ~= "identify:\n";
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>oclc):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>isbn):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>dewey):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ } else if (h_.match(regex(r"^@publisher:"))) {
+ if (auto m = h_.match(regex(r"^@publisher:[ ]+(?P<c>.+)$"))) {
+ _tmp_header ~= "publisher: " ~ "\"" ~ m.captures["c"] ~ "\"\n";
+ }
+ } else if (h_.match(regex(r"^@make:"))) {
+ // writeln(h_);
+ if (auto m = h_.match(regex(r"^@make:"))) {
+ _tmp_header ~= "make:\n";
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>breaks):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>num_top):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>headings):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>italics):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>bold):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>emphasis):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>substitute):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>texpdf_font):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>home_button_text):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>home_button_image):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>cover_image):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ if (auto m = h_.match(regex(r"^\s+:(?P<h>footer):(?:[ ]+(?P<c>.+)|$)", "m"))) {
+ _tmp_header ~= format_sub_header(m.captures["h"], m.captures["c"]);
+ }
+ // writeln(_tmp_header);
+ } else if (h_.match(regex(r"^@\w+:"))) {
+ _tmp_header ~= "# " ~ h_.split("\n").join("\n# ") ~ "\n";
+ } else if (h_.match(regex(r"^\s+:\w+:", "m"))) {
+ if (auto m = h_.match(regex(r"^(?P<g>\s+:\w+:.*)"))) {
+ _tmp_header ~= "# " ~ m.captures["g"] ~ "\n";
+ }
+ }
+ if (h_.match(regex(r"^#", "m"))) {
+ if (auto m = h_.match(regex(r"^(?P<g>#.*)", "m"))) {
+ _tmp_header ~= m.captures["g"] ~ "\n";
+ }
+ }
+ if (_tmp_header.length > 0) {
+ munged_header ~= _tmp_header.split("\n\n");
+ } else if (h_.length > 0) {
+ writeln("munging required: ", h_);
+ h_ = h_.replaceAll((regex(r"\n\n\n+", "m")), "\n\n");
+ munged_header ~= h_;
+ }
+ }
+ // writeln(munged_header.join("\n"));
+ }
+ foreach (paragraph; paragraphs) { /+ loop to gather binary endnotes +/
+ if (code_block_status == codeBlock.off
+ && paragraph.match(rgx_endnote)
+ ) {
+ munged_endnotes ~= replaceAll!(m => m[1])
+ (paragraph, rgx_endnote);
+ } else {
+ if ( code_block_status != codeBlock.off
+ || paragraph.matchFirst(block_curly_code_open)
+ || paragraph.matchFirst(block_tic_code_open)
+ ) { /+ code blocks identified, no munging +/
+ if ((code_block_status == codeBlock.curly
+ && paragraph.matchFirst(block_curly_code_close))
+ || (code_block_status == codeBlock.tic
+ && paragraph.matchFirst(block_tic_close))
+ ) {
+ code_block_status = codeBlock.off;
+ } else if (paragraph.matchFirst(block_curly_code_open)) {
+ code_block_status = codeBlock.curly;
+ } else if (paragraph.matchFirst(block_tic_code_open)) {
+ code_block_status = codeBlock.tic;
+ }
+ munged_contents ~= paragraph;
+ } else { /+ regular content, not a code block +/
+ if (auto m = paragraph.matchAll(rgx_endnote_ref)) {
+ foreach (n; m) {
+ endnote_ref_count++; // endnote_refs ~= (n.captures[1]);
+ }
+ }
+ paragraph = format_body_string(paragraph);
+ // paragraph = replaceAll!(m => " \\\\ " )
+ // (paragraph, regex(r"\s*<(?:/\s*|:)?br>\s*")); // (paragraph, regex(r"(<br>)"));
+ munged_contents ~= paragraph;
+ }
+ }
+ }
+ {
+ import std.outbuffer;
+ auto buffer = new OutBuffer();
+ if (munged_header.length > 0) {
+ foreach (header; munged_header) { /+ loop to inline endnotes +/
+ buffer.write(header ~ "\n");
+ }
+ }
+ if (munged_endnotes.length == endnote_ref_count) {
+ int endnote_count = -1;
+ foreach (k, content; munged_contents) { /+ loop to inline endnotes +/
+ content = replaceAll!(m => "~{ " ~ munged_endnotes[++endnote_count] ~ " }~" ~ m["tail"] )
+ (content, rgx_endnote_ref); // endnote_ref cannot occur in a code block or else fail
+ buffer.write(content ~ ((k == munged_contents.length - 1) ? "" : "\n\n"));
+ }
+ if (buffer) {
+ try {
+ string dir_out = "converted_output_";
+ string path_and_file_out = dir_out ~ "/" ~ filename;
+ dir_out.mkdirRecurse;
+ auto f = File(path_and_file_out, "w");
+ f.write(buffer);
+ // writeln("wrote: ", path_and_file_out);
+ } catch (FileException ex) {
+ writeln("did not write file");
+ // Handle errors
+ }
+ }
+ } else {
+ foreach (content; munged_contents) { /+ loop to inline endnotes +/
+ buffer.write(content ~ "\n\n");
+ }
+ }
+ }
+ } catch (ErrnoException ex) {
+ switch(ex.errno) {
+ case EPERM:
+ case EACCES: // Permission denied
+ break;
+ case ENOENT: // File does not exist
+ break;
+ default: // Handle other errors
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/sundry/misc/util/d/tools/spine_scaffold.d b/sundry/misc/util/d/tools/spine_scaffold.d
new file mode 100755
index 0000000..dbcc857
--- /dev/null
+++ b/sundry/misc/util/d/tools/spine_scaffold.d
@@ -0,0 +1,134 @@
+#!/usr/bin/env rund
+void main( string[] args ) {
+ import std;
+ if (args.length > 1) {
+ string base_fn_path = args[1].expandTilde;
+ string base_fn = base_fn_path.baseName;
+ string sst_fn = base_fn ~ ".sst";
+ string txt_for_pod_manifest = format(q"┃doc:
+ filename: %s
+ language: en
+┃",
+ sst_fn
+ );
+ string txt_for_sisu_document_make = format(q"┃
+┃",
+ );
+ string txt_for_document_scaffold = format(q"┃# SiSU 8.0
+
+title: "As Yet Unnamed"
+
+creator:
+ author: "Annon, Unnamed"
+
+:A~ @title @author
+
+1~ Summary
+
+To get you started, the first paragraph following a section or chapter heading.
+
+Spine / SiSU documents minimum requirements:
+
+_* a header containing document metadata that must at least contain the fields Title and Creator Author.
+
+_* text body, identified as starting by the A~ marker at the start of a line, followed by at least one level 1~ section heading with the text that follows it.~{ the document provided here would be a valid Spine document, and this text contained within the tilde and curly braces delimiters would be the first footnote/endnote }~
+
+To generate this document to have html and epub output for example you would run:
+
+``` code
+spine --html --epub --output=/tmp/spine-sample-output %s
+```
+
+1~ Conclusion
+
+This sample pod is provided to get you started.
+
+Good luck and good speed.
+┃",
+ base_fn_path
+ );
+ if (!exists(base_fn_path)) {
+ try {
+ base_fn_path.mkdirRecurse;
+ } catch (ErrnoException ex) {
+ writeln(ex);
+ }
+ if (exists(args[1].expandTilde)) {
+ try {
+ base_fn_path.buildPath("conf").mkdirRecurse;
+ } catch (ErrnoException ex) {
+ writeln(ex);
+ }
+ try {
+ base_fn_path.buildPath("media/text/en").mkdirRecurse;
+ } catch (ErrnoException ex) {
+ writeln(ex);
+ }
+ {
+ // 1 // create/write pod.manifest
+ string fn = base_fn_path.buildPath("pod.manifest");
+ File(fn, "w").writeln(txt_for_pod_manifest);
+ string tell = format(q"┃OK - pod.manifest (yaml file containing filename and languages)
+ %s
+%s
+┃",
+ fn,
+ txt_for_pod_manifest.strip
+ );
+ writeln(tell);
+ }
+ if (exists(base_fn_path.buildPath("conf"))) {
+ // 2 // create/write conf/sisu_document_make
+ string fn = base_fn_path.buildPath("conf/sisu_document_make");
+ File(fn, "w").writeln(txt_for_sisu_document_make);
+ // auto f = File(fn, "w");
+ // foreach (line; content_array) {
+ // f.writeln(line);
+ // }
+ string tell = format(q"┃OK - sisu_document_make
+ %s
+┃",
+ fn
+ );
+ writeln(tell);
+ }
+ if (exists(base_fn_path.buildPath("media/text/en"))) {
+ // 3 // create/write media/text/[lang code]/[filename].sst
+ string fn = base_fn_path.buildPath("media/text/en/" ~ sst_fn);
+ File(fn, "w").writeln(txt_for_document_scaffold);
+ // auto f = File(fn, "w");
+ // foreach (line; content_array) {
+ // f.writeln(line);
+ // }
+ string tell = format(q"┃OK - .sst [document text content]
+ %s
+ - To start editing document (spine pod content):
+ ${EDITOR} %s
+ - To generate this document to have html and epub output for example you would run:
+ spine --html --epub --output=/tmp/spine-sample-output %s
+┃",
+ fn,
+ fn,
+ base_fn_path
+ );
+ writeln(tell);
+ }
+ }
+ /+
+ pod/[filename]
+ │
+ ├── conf
+ │   └── sisu_document_make
+ ├── media
+ │   └── text
+ │   └── en
+ │   └── [filename].charles_stross.sst
+ └── pod.manifest
+ +/
+ } else {
+ writeln("requested output pod name with path already exists:\n ", args[1].expandTilde);
+ }
+ } else {
+ writeln( "please provide directory path to operate on, e.g.\n spine_scaffold.d ./pod/filetest" );
+ }
+}
diff --git a/sundry/misc/util/rb/cgi/spine.search.cgi b/sundry/misc/util/rb/cgi/spine.search.cgi
new file mode 100755
index 0000000..ff2bfc2
--- /dev/null
+++ b/sundry/misc/util/rb/cgi/spine.search.cgi
@@ -0,0 +1,952 @@
+#!/usr/bin/env ruby
+=begin
+ * Name: SiSU information Structuring Universe
+ * Author: Ralph Amissah
+ * https://sisudoc.org
+ * https://git.sisudoc.org
+
+ * Description: generates naive cgi search form for search of sisu database (sqlite)
+ * Name: SiSU generated sample cgi search form
+
+ * Description: generated sample cgi search form for SiSU
+ (SiSU is a framework for document structuring, publishing and search)
+
+ * Author: Ralph Amissah
+
+ * Copyright: (C) 1997 - 2014, Ralph Amissah, All Rights Reserved.
+
+ * License: GPL 3 or later:
+
+ SiSU, a framework for document structuring, publishing and search
+
+ Copyright (C) Ralph Amissah
+
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option)
+ any later version.
+
+ This program 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. See the GNU General Public License for
+ more details.
+
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see <https://www.gnu.org/licenses/>.
+
+ If you have Internet connection, the latest version of the GPL should be
+ available at these locations:
+ <https://www.fsf.org/licenses/gpl.html>
+ <https://www.gnu.org/licenses/gpl.html>
+
+ * SiSU uses:
+ * Standard SiSU markup syntax,
+ * Standard SiSU meta-markup syntax, and the
+ * Standard SiSU object citation numbering and system
+
+ * Hompages:
+ <https://www.sisudoc.org>
+
+ * Ralph Amissah
+ <ralph@amissah.com>
+ <ralph.amissah@gmail.com>
+
+=end
+begin
+ require 'cgi'
+ require 'fcgi'
+ require 'sqlite3'
+rescue LoadError
+ puts 'cgi, fcgi or sqlite3 NOT FOUND (LoadError)'
+end
+@stub_default = 'search'
+@image_src = "https://#{ENV['HTTP_HOST']}/image_sys"
+@hosturl_cgi = "https://#{ENV['HTTP_HOST']}#{ENV['PATH_INFO']}"
+@hosturl_files = "https://#{ENV['HTTP_HOST']}"
+@output_dir_structure_by = 'language'
+@lingual = 'multi'
+@db_name_prefix = 'spine.'
+@base = "https://#{ENV['HTTP_HOST']}#{ENV['PATH_INFO']}#{ENV['SCRIPT_NAME']}"
+#Common TOP
+@@offset = 0
+@@canned_search_url = @base
+@color_heading = '#DDFFAA'
+@color_match = '#ffff48'
+class Form
+ def initialize(base,search_field,selected_db,result_type,checked_sql_limit,checked_tip,checked_stats,checked_searched,checked_url,checked_case,checked_echo,checked_sql,checked_all,checked_none,checked_selected,checked_default,search_note,the_can='')
+ search_note = '' if checked_searched !~ /\S/
+ the_can = '' if checked_url !~ /\S/
+ search_field = '' if checked_echo !~ /\S/
+ @base,@search_field,@selected_db,@result_type,@checked_sql_limit,@checked_tip,@checked_stats,@checked_searched,@checked_url,@checked_case,@checked_echo,@checked_sql,@checked_all,@checked_none,@checked_selected,@checked_default,@search_note,@the_can=base,search_field,selected_db,result_type,checked_sql_limit,checked_tip,checked_stats,checked_searched,checked_url,checked_case,checked_echo,checked_sql,checked_all,checked_none,checked_selected,checked_default,search_note,the_can
+ @tip = if checked_tip =~ /\S/
+ '<font size="2" color="#666666">text:__; fulltxt:__; keywords:__; title:__; author:__; topic_register:__; subject:__; description:__; publisher:__; editor:__; contributor:__; date:__; type:__; format:__; identifier:__; source:__; language:__; relation:__; coverage:__; rights:__; comment:__; abstract:__; src_filename_base:__;</font><br />'
+ else ''
+ end
+ end
+ def submission_form
+ search_form =<<-WOK
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>
+ <meta charset="utf-8">
+ <meta name="sourcefile" content="SiSU._sst" />
+ SiSU search form (sample): SiSU information Structuring Universe
+ </title>
+ <link rel="generator" href="https://sisudoc.org/" />
+ <link rel="shortcut icon" href="https://#{ENV['HTTP_HOST']}/_sisu/image_sys/rb7.ico" />
+ <link href="../_sisu/css/html.css" rel="stylesheet">
+ </head>
+ <body lang="en" xml:lang="en">
+ <table summary="band" border="0" cellpadding="3" cellspacing="0">
+ <tr><td width="20%">
+ <table summary="home button / home information" border="0" cellpadding="3" cellspacing="0">
+ <tr><td align="left">
+ <br /><a href="https://sisudoc.org/" target="_top">
+ <b>SiSU</b>
+ </a>
+ <br /><a href="https://git.sisudoc.org/" target="_top">
+ git
+ </a>
+ </td></tr>
+ </table>
+ </td>
+ <td>
+ <label for="find"><b>SiSU (generated sample) search form (content organised by filetype)</b></label>
+ </td></tr>
+ </table>
+ <form action="#{@base}" id="Test Form" method="post">
+ <table cellpadding="2">
+ <tr><td valign=\"top\">
+ <textarea id="find" name="find" type="text" rows="6" cols="40" maxlength="256">#{@search_field}</textarea>
+ </td>
+ <td valign=\"top\">
+ #{@tip}
+ #{@search_note}
+ #{@the_can}
+ </td></tr></table>
+ <td valign=\"top\"><tr><td>
+ <!input type="text" id="find" name="find" value="#{@search_field}" />
+ <!input type="text" id="find" name="find" value="" />
+ <font size="2" color="#222222">
+ <b>to search:</b> select which database to search (drop-down menu below); enter your search query (in the form above); and <b>click on the search button</b> (below)
+ <br />
+ <select name="db" size="1">
+ #{@selected_db}
+ <option value="spine.sqlite">spine</option>
+ </select>
+ <input type="submit" value="SiSU search" />
+ <input type="radio" name="view" value="index" #{@result_type[:index]}> index
+ <input type="radio" name="view" value="text" #{@result_type[:text]}> text / grep
+ <br />
+ match limit:
+ <input type="radio" name="sql_match_limit" value="1000" #{@checked_sql_limit[:l1000]}> 1,000
+ <input type="radio" name="sql_match_limit" value="2500" #{@checked_sql_limit[:l2500]}> 2,500
+ <br />
+ <input type="checkbox" name="echo" #{@checked_echo}> echo query
+ <input type="checkbox" name="stats" #{@checked_stats}> result stats
+ <input type="checkbox" name="url" #{@checked_url}> search url
+ <input type="checkbox" name="searched" #{@checked_searched}> searched
+ <input type="checkbox" name="tip" #{@checked_tip}> available fields
+ <input type="checkbox" name="sql" #{@checked_sql}> sql statement
+ <br />
+ checks:
+ <input type="radio" name="checks" value="check_default" #{@checked_default}> default
+ <input type="radio" name="checks" value="check_selected" #{@checked_selected}> selected
+ <input type="radio" name="checks" value="check_all" #{@checked_all}> all
+ <input type="radio" name="checks" value="check_none" #{@checked_none}> none
+ </font>
+ </td></tr>
+ </table>
+ </form>
+ WOK
+ end
+end
+class SearchRequest #% search_for
+ attr_accessor :text1,:fulltext,:keywords,:title,:author,:topic_register,:subject,:description,:publisher,:editor,:contributor,:date,:type,:format,:identifier,:source,:language,:relation,:coverage,:rights,:comment,:abstract,:owner,:date_created,:date_issued,:date_modified,:date_available,:date_valid,:src_filename_base
+ def initialize(search_field='',q='')
+ @search_field,@q=search_field,q
+ @text1=@fulltext=@keywords=@title=@author=@topic_register=@subject=@description=@publisher=@editor=@contributor=@date=@type=@format=@identifier=@source=@language=@relation=@coverage=@rights=@comment=@abstract=@owner=@date_created=@date_issued=@date_modified=@date_available=@date_valid=@filename=''
+ if @search_field=~/\S/
+ @text1 = text_to_match('text:')
+ @fulltext = text_to_match('fulltxt:')
+ @topic_register = text_to_match('topic_register:')
+ @title = text_to_match('title:') # DublinCore 1 - title
+ @author = text_to_match('(?:author|creator)s?:') # DublinCore 2 - creator/author
+ @subject = text_to_match('subj(?:ect)?:') # DublinCore 3 - subject
+ @description = text_to_match('description:') # DublinCore 4 - description
+ @publisher = text_to_match('pub(?:lisher)?:') # DublinCore 5 - publisher
+ @editor = text_to_match('editor:')
+ @contributor = text_to_match('contributor:') # DublinCore 6 - contributor
+ @date = text_to_match('date:') # DublinCore 7 - date dd-mm-yy
+ @type = text_to_match('type:') # DublinCore 8 - type
+ @format = text_to_match('format:') # DublinCore 9 - format
+ @identifier = text_to_match('identifier:') # DublinCore 10 - identifier
+ @source = text_to_match('source:') # DublinCore 11 - source
+ @language = text_to_match('language:') # DublinCore 12 - language
+ @relation = text_to_match('relation:') # DublinCore 13 - relation
+ @coverage = text_to_match('coverage:') # DublinCore 14 - coverage
+ @rights = text_to_match('rights:') # DublinCore 15 - rights
+ @keywords = text_to_match('key(?:words?)?:')
+ @comment = text_to_match('comment:')
+ @abstract = text_to_match('abs(?:tract)?:')
+ @owner = text_to_match('owner:')
+ @date_created = text_to_match('date_created:')
+ @date_issued = text_to_match('date_issued:')
+ @date_modified = text_to_match('date_modified:')
+ @date_available = text_to_match('date_available:')
+ @date_valid = text_to_match('date_valid:')
+ @filename = text_to_match('src_filename_base:')
+ @text1 = text_to_match unless @keywords or @author or @title or @text1 or @fulltext or @comment or @abstract or @rights or @subject or @publisher or @date or @filename or @topic_register
+ else
+ @text1 = q['s1'] if q['s1'] =~ /\S/
+ @fulltext = q['ft'] if q['ft'] =~ /\S/
+ @keywords = q['key'] if q['key'] =~ /\S/
+ @title = q['ti'] if q['ti'] =~ /\S/
+ @author = q['au'] if q['au'] =~ /\S/
+ @topic_register = q['tr'] if q['tr'] =~ /\S/
+ @subject = q['sj'] if q['sj'] =~ /\S/
+ @description = q['dsc'] if q['dsc'] =~ /\S/
+ @publisher = q['pb'] if q['pb'] =~ /\S/
+ @editor = q['cntr'] if q['cntr'] =~ /\S/
+ @contributor = q['cntr'] if q['cntr'] =~ /\S/
+ @date = q['dt'] if q['dt'] =~ /\S/
+ @type = q['ty'] if q['ty'] =~ /\S/
+ @identifier = q['id'] if q['id'] =~ /\S/
+ @source = q['src'] if q['src'] =~ /\S/
+ @language = q['lang'] if q['lang'] =~ /\S/
+ @relation = q['rel'] if q['rel'] =~ /\S/
+ @coverage = q['cov'] if q['cov'] =~ /\S/
+ @rights = q['cr'] if q['cr'] =~ /\S/
+ @comment = q['co'] if q['co'] =~ /\S/
+ @abstract = q['ab'] if q['ab'] =~ /\S/
+ @date_created = q['dtc'] if q['dtc'] =~ /\S/
+ @date_issued = q['dti'] if q['dti'] =~ /\S/
+ @date_modified = q['dtm'] if q['dtm'] =~ /\S/
+ @date_available = q['dta'] if q['dta'] =~ /\S/
+ @date_valid = q['dtv'] if q['dtv'] =~ /\S/
+ @filename = if q['doc'] and q['search'] !~ /search db/ then q['doc']
+ elsif q['fns'] =~ /\S/ then q['fns']
+ end
+ @@limit = q['ltd'] if q['ltd'] =~ /\d+/ # 1000
+ @@offset = q['off'] if q['off'] =~ /\d+/ # 0
+ end
+ end
+ def text_to_match(identifier='')
+ m={
+ string: /#{identifier}\s*(.+?)/,
+ string: /#{identifier}\s*(.+?)(?:;|\n|\r|$)/,
+ word: /#{identifier}[\s(]*(\S+)/
+ }
+ search_string=if @search_field =~m[:word]
+ search_string=if @search_field =~m[:braces] then m[:braces].match(@search_field)[1]
+ elsif @search_field =~m[:string] then m[:string].match(@search_field)[1]
+ else
+ str=m[:word].match(@search_field)[1]
+ str=str.gsub(/[()]/,'')
+ str
+ end
+ search_string=search_string.strip.gsub(/\s+/,'+')
+ #else
+ # "__"
+ end
+ end
+end
+class DBI_SearchString
+ def initialize(l,t,q,cse=false)
+ @l,@t,@q=l,t,q
+ end
+ def string
+ search={ search: [], flag: false }
+ if @t =~/\S+/ or @q =~/\S+/
+ if @t =~/\S+/ then unescaped_search=CGI.unescape(@t)
+ elsif @q =~/\S+/ then unescaped_search=CGI.unescape(@q)
+ end
+ search_construct=[]
+ unescaped_search=unescaped_search.gsub(/\s*(AND|OR)\s*/,"%' \) \\1 #{@l} LIKE \( '%").
+ gsub(/(.+)/,"#{@l} LIKE \( '%\\1%' \)")
+ search_construct << unescaped_search
+ search_construct=search_construct.join(' ')
+ search[:search] << search_construct
+ search[:flag]=true
+ search
+ end
+ search
+ end
+end
+class DBI_SearchStatement
+ attr_reader :text_search_flag,:sql_select_body_format,:sql_offset,:sql_limit
+ def initialize(conn,search_for,q,c)
+ @conn=conn
+ @text_search_flag=false
+ @sql_statement={ body: '', endnotes: '', range: '' }
+ #@offset||=@@offset
+ #@offset+=@@limit
+ search={ text: [], endnotes: [] }
+ cse=(c =~/\S/) ? true : false
+ st=DBI_SearchString.new('doc_objects.clean',search_for.text1,q['s1'],cse).string
+ se=DBI_SearchString.new('endnotes.clean',search_for.text1,q['s1'],cse).string
+ @text_search_flag=st[:flag]
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.fulltext',search_for.fulltext,q['ft'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.title',search_for.title,q['ti'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.creator_author',search_for.author,q['au'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.classify_topic_register',search_for.topic_register,q['tr'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.classify_subject',search_for.subject,q['sj'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.classify_keywords',search_for.keywords,q['key'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_description',search_for.description,q['dsc'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.publisher',search_for.publisher,q['pb'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.creator_editor',search_for.editor,q['cntr'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.creator_contributor',search_for.contributor,q['cntr'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.date_published',search_for.date,q['dt'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_type',search_for.type,q['ty'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.original_source',search_for.source,q['src'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.language_document_char',search_for.language,q['lang'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_relation',search_for.relation,q['rel'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_coverage',search_for.coverage,q['cov'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.rights_all',search_for.rights,q['cr'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_comment',search_for.comment,q['co'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.notes_abstract',search_for.abstract,q['ab'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ st = DBI_SearchString.new('metadata_and_text.src_filename_base',search_for.src_filename_base,q['fns'],cse).string
+ if st[:flag]
+ search[:text] << st[:search]
+ end
+ @@limit=q['ltd'] if q['ltd']=~/\d+/ # 1000
+ @@offset=q['off'] if q['off']=~/\d+/ # 0
+ @search_text=''
+ @search_text=search[:text].flatten.join(' AND ')
+ @search_text=@search_text.gsub(/(doc_objects\.clean\s+LIKE\s+\(\s*'%[^']+%'\s*\)\s+(?:(?:AND|OR)\s+doc_objects\.clean\s+LIKE\s+\(\s*'%[^']+%'\s*\))+)/,'(\1)')
+ end
+ def sql_offset
+ @@offset
+ end
+ def sql_match_limit
+ @@limit
+ end
+ def sql_canned_search
+ @offset_next=sql_offset.to_i + sql_match_limit.to_i
+ @offset_previous=sql_offset.to_i - sql_match_limit.to_i
+ def current
+ @@canned_search_url.to_s + '&ltd=' + sql_match_limit.to_s + '&off=' + sql_offset.to_s
+ end
+ def next
+ @@canned_search_url.to_s + '&ltd=' + sql_match_limit.to_s + '&off=' + @offset_next.to_s
+ end
+ def previous
+ @offset_previous >= 0 \
+ ? (@@canned_search_url.to_s + '&ltd=' + sql_match_limit.to_s + '&off=' + @offset_previous.to_s)
+ : ''
+ end
+ def start
+ @@canned_search_url.to_s + '&ltd=' + sql_match_limit.to_s + '&off=' + 0.to_s
+ end
+ self
+ end
+ def pre_next(beyond_limit,img)
+ can=sql_canned_search
+ page=(sql_offset.to_i + sql_match_limit.to_i)/sql_match_limit.to_i
+ if beyond_limit
+ if page.to_s =~ /^1$/
+ %{<br /><center>
+ pg. #{page.to_s}
+ <a href="#{can.next}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_next_red.png" alt="&nbsp;&gt;&gt;" />
+ </a>
+ </center>}
+ elsif page.to_s =~ /^2$/
+ %{<br /><center>
+ <a href="#{can.previous}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="&lt;&lt;&nbsp;" />
+ </a>
+ pg. #{page.to_s}
+ <a href="#{can.next}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_next_red.png" alt="&nbsp;&gt;&gt;" />
+ </a>
+ </center>}
+ else
+ %{<br /><center>
+ <a href="#{can.start}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="|&lt;&nbsp;" />
+ </a>
+ <a href="#{can.previous}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="&lt;&lt;&nbsp;" />
+ </a>
+ pg. #{page.to_s}
+ <a href="#{can.next}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_next_red.png" alt="&nbsp;&gt;&gt;" />
+ </a>
+ </center>}
+ end
+ else
+ if page.to_s =~ /^1$/ then ''
+ elsif page.to_s =~ /^2$/
+ %{<br /><center>
+ <a href="#{can.previous}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="&lt;&lt;&nbsp;" />
+ </a>
+ pg. #{page.to_s}
+ </center>}
+ else
+ %{<br /><center>
+ <a href="#{can.start}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="|&lt;&nbsp;" />
+ </a>
+ <a href="#{can.previous}">
+ <img border="0" width="22" height="22" src="#{img}/arrow_prev_red.png" alt="&lt;&lt;&nbsp;" />
+ </a>
+ pg. #{page.to_s}
+ </center>}
+ end
+ end
+ end
+ def sql_select_body
+ limit ||= @@limit
+ offset ||= @@offset
+ @sql_statement[:body] = %{
+ SELECT metadata_and_text.title, metadata_and_text.creator_author, metadata_and_text.src_filename_base, metadata_and_text.language_document_char, metadata_and_text.notes_suffix, doc_objects.body, doc_objects.seg_name, doc_objects.ocn, metadata_and_text.uid
+ FROM doc_objects, metadata_and_text
+ WHERE #{@search_text} AND doc_objects.uid_metadata_and_text = metadata_and_text.uid
+ ORDER BY metadata_and_text.language_document_char, metadata_and_text.title, metadata_and_text.src_filename_base, doc_objects.ocn
+ }
+ @sql_statement[:range] = %{LIMIT #{limit} OFFSET #{offset} ;}
+ select = @sql_statement[:body] + ' ' + @sql_statement[:range]
+ select
+ end
+ def sql_select_body_format
+ %{<font color="#666666" size="2">#{sql_select_body}</font>}
+ end
+ def contents
+ @conn.execute(sql_select_body)
+ end
+end
+def tail
+ <<-'WOK'
+ <br /><hr /><br />
+<table summary="SiSU summary" cellpadding="2" border="0">
+ <!-- widget sisu -->
+<tr><td valign="top" width="10%">
+ <table summary="home button / home information" border="0" cellpadding="3" cellspacing="0">
+ <tr><td align="left">
+ <br /><a href="https://sisudoc.org/" target="_top">
+ <b>SiSU</b>
+ </a>
+ <br /><a href="https://git.sisudoc.org/" target="_top">
+ git
+ </a>
+ </td></tr>
+ </table>
+</td>
+<td valign="top" width="45%">
+<!-- SiSU Rights -->
+ <p class="tiny_left"><font color="#666666" size="2">
+ Generated by
+ SiSU 6.3.1 2014-10-19 (2014w41/7)
+ <br />
+ <a href="https://www.sisudoc.org" >
+ <b>SiSU</b></a> <sup>&copy;</sup> Ralph Amissah
+ 1993, current 2014.
+ All Rights Reserved.
+ <br />
+ SiSU is software for document structuring, publishing and search,
+ <br />
+ <a href="https://www.sisudoc.org" >
+ www.sisudoc.org
+ </a>
+ sources
+ <a href="https://git.sisudoc.org" >
+ git.sisudoc.org
+ </a>
+ <br />
+ <i>w3 since October 3 1993</i>
+ <a href="mailto:ralph@amissah.com" >
+ ralph@amissah.com
+ </a>
+ <br />
+ mailing list subscription
+ <a href="https://lists.sisudoc.org/listinfo/sisu" >
+ https://lists.sisudoc.org/listinfo/sisu
+ </a>
+ <br />
+ <a href="mailto:sisu@lists.sisudoc.org" >
+ sisu@lists.sisudoc.org
+ </a>
+ </font></p>
+</td><td valign="top" width="45%">
+ <p class="tiny_left"><font color="#666666" size="2">
+ SiSU using:
+ <br />Standard SiSU markup syntax,
+ <br />Standard SiSU meta-markup syntax, and the
+ <br />Standard SiSU <u>object citation numbering</u> and system, (object/text identifying/locating system)
+ <br />
+ <sup>&copy;</sup> Ralph Amissah 1997, current 2014.
+ All Rights Reserved.
+ </font></p>
+</td></tr>
+ <!-- widget way better -->
+<tr><td valign="top" width="10%">
+ <p class="tiny_left"><font color="#666666" size="2">
+ <a href="https://www.gnu.org/licenses/gpl.html">
+ .:
+ </a>
+ </font></p>
+</td><td valign="top" width="45%">
+ <p class="tiny_left"><font color="#666666" size="2">
+ SiSU is released under
+ <a href="https://www.gnu.org/licenses/gpl.html">GPL&nbsp;v3</a>
+ or later,
+ <a href="https://www.gnu.org/licenses/gpl.html">
+ https://www.gnu.org/licenses/gpl.html
+ </a>
+ </font></p>
+</td><td valign="top" width="45%">
+ <p class="tiny_left"><font color="#666666" size="2">
+ SiSU, developed using
+ <a href="https://www.ruby-lang.org/en/">
+ Ruby
+ </a>
+ on
+ <a href="https://www.debian.org/">
+ Debian/Gnu/Linux
+ </a>
+ software infrastructure,
+ with the usual GPL (or OSS) suspects.
+ </font></p>
+</td></tr>
+</table>
+ <a name="bottom" id="bottom"></a><a name="down" id="down"></a><a name="end" id="end"></a><a name="finish" id="finish"></a><a name="stop" id="stop"></a><a name="credits" id="credits"></a>
+ </body></html>
+ WOK
+end
+@tail=tail
+@counter_txt_doc,@counter_txt_ocn,@counter_endn_doc,@counter_endn_ocn=0,0,0,0
+@counters_txt,@counters_endn,@sql_select_body='','',''
+FCGI.each_cgi do |cgi|
+ begin # all code goes in begin section
+ @search={ text: [], endnotes: [] }
+ q=CGI.new
+ @db=if cgi['db'] =~ /#{@db_name_prefix}(\S+)/
+ @stub=$1
+ cgi['db']
+ else
+ @stub=@stub_default
+ @db_name_prefix + @stub
+ end
+ checked_url,checked_stats,checked_searched,checked_tip,checked_case,checked_echo,checked_sql,checked_all,checked_none,checked_selected,checked_default,selected_db='','','','','','','','',''
+ result_type=(cgi['view']=~/text/) \
+ ? result_type={ index: '', text: 'checked'}
+ : result_type={ index: 'checked', text: ''}
+ @@limit=if cgi['sql_match_limit'].to_s=~/2500/
+ checked_sql_limit={ l1000: '', l2500: 'checked'}
+ '2500'
+ else
+ checked_sql_limit={ l1000: 'checked', l2500: ''}
+ '1000'
+ end
+ checked_echo = 'checked' if cgi['echo'] =~/\S/
+ checked_stats = 'checked' if cgi['stats'] =~/\S/
+ checked_url = 'checked' if cgi['url'] =~/\S/ or cgi['u'].to_i==1
+ checked_searched = 'checked' if cgi['searched'] =~/\S/
+ checked_tip = 'checked' if cgi['tip'] =~/\S/
+ checked_case = 'checked' if cgi['casesense'] =~/\S/
+ checked_sql = 'checked' if cgi['sql'] =~/\S/
+ if cgi['checks'] =~ /check_all/ or cgi['check_all'] =~/\S/ or cgi['a'].to_i==1
+ checked_all = 'checked'
+ checked_echo=checked_stats=checked_url=checked_searched=checked_tip=checked_sql='checked'
+ checked_none =''
+ elsif cgi['checks'] =~ /check_none/
+ checked_none = 'checked'
+ checked_all=checked_url=checked_stats=checked_searched=checked_tip=checked_echo=checked_sql=''
+ elsif cgi['checks'] =~ /check_selected/
+ checked_selected = 'checked'
+ elsif cgi['checks'] =~ /check_default/
+ checked_default = 'checked'
+ checked_echo=checked_stats=checked_url='checked'
+ checked_searched=checked_tip=checked_case=checked_sql=''
+ else
+ checked_selected='checked'
+ checked_echo=checked_stats=checked_url='checked'
+ checked_searched=checked_tip=checked_case=checked_sql=''
+ end
+ selected_db=case cgi['db']
+ when /spine.sqlite/ then '<option value="spine.sqlite">spine</option>'
+ end
+ db_name='spine.search.sql.db'
+ #db_name='spine.sqlite.db'
+ #db_name='sisu_sqlite.db'
+ db_sqlite=case cgi['db']
+ when /spine.sqlite/ then "/srv/complete.sisudoc.org/web/manual/#{db_name}"
+ else "/var/www/sqlite/#{db_name}"
+ end
+ #when /spine.sqlite/ then "/srv/complete.sisudoc.org/web/manual/#{db_name}"
+ #else "/srv/complete.sisudoc.org/web/manual/#{db_name}"
+ #end
+ #@conn=SQLite3::Database.new(db_sqlite)
+ @conn=SQLite3::Database.new("/var/www/sqlite/spine.search.sql.db")
+ #@conn=SQLite3::Database.new("/var/www/spine.sqlite.db")
+ @conn.results_as_hash=true
+ search_field=cgi['find'] if cgi['find'] # =~/\S+/
+ @search_for=SearchRequest.new(search_field,q) #.analyze #% search_for
+ #% searches
+ #Canned_search.new(@base,@search_for.text1,cgi)
+ if @search_for.text1=~/\S+/ or @search_for.fulltext=~/\S+/ or @search_for.author=~/\S+/ or @search_for.topic_register=~/\S+/ #and search_field =~/\S/
+ s1 = 's1=' + CGI.escape(@search_for.text1) if @search_for.text1 =~ /\S/
+ ft = '&ft=' + CGI.escape(@search_for.fulltext) if @search_for.fulltext =~ /\S/
+ key = 'key=' + CGI.escape(@search_for.keywords) if @search_for.keywords =~ /\S/
+ ti = '&ti=' + CGI.escape(@search_for.title) if @search_for.title =~ /\S/
+ au = '&au=' + CGI.escape(@search_for.author) if @search_for.author =~ /\S/
+ tr = '&tr=' + CGI.escape(@search_for.topic_register) if @search_for.topic_register =~ /\S/
+ sj = '&sj=' + CGI.escape(@search_for.subject) if @search_for.subject =~ /\S/
+ dsc = '&dsc=' + CGI.escape(@search_for.description) if @search_for.description =~ /\S/
+ pb = '&pb=' + CGI.escape(@search_for.publisher) if @search_for.publisher =~ /\S/
+ edt = '&edt=' + CGI.escape(@search_for.editor) if @search_for.editor =~ /\S/
+ cntr = '&cntr=' + CGI.escape(@search_for.contributor) if @search_for.contributor =~ /\S/
+ dt = '&dt=' + CGI.escape(@search_for.date) if @search_for.date =~ /\S/
+ ty = '&ty=' + CGI.escape(@search_for.type) if @search_for.type =~ /\S/
+ id = '&id=' + CGI.escape(@search_for.identifier) if @search_for.identifier =~ /\S/
+ src = '&src=' + CGI.escape(@search_for.source) if @search_for.source =~ /\S/
+ lang = '&lang=' + CGI.escape(@search_for.language) if @search_for.language =~ /\S/
+ rel = '&rel=' + CGI.escape(@search_for.relation) if @search_for.relation =~ /\S/
+ cov = '&cov=' + CGI.escape(@search_for.coverage) if @search_for.coverage =~ /\S/
+ cr = '&cr=' + CGI.escape(@search_for.rights) if @search_for.rights =~ /\S/
+ co = '&co=' + CGI.escape(@search_for.comment) if @search_for.comment =~ /\S/
+ ab = '&ab=' + CGI.escape(@search_for.abstract) if @search_for.abstract =~ /\S/
+ dtc = '&dtc=' + CGI.escape(@search_for.date_created) if @search_for.date_created =~ /\S/
+ dti = '&dti=' + CGI.escape(@search_for.date_issued) if @search_for.date_issued =~ /\S/
+ dtm = '&dtm=' + CGI.escape(@search_for.date_modified) if @search_for.date_modified =~ /\S/
+ dta = '&dta=' + CGI.escape(@search_for.date_available) if @search_for.date_available =~ /\S/
+ dtv = '&dtv=' + CGI.escape(@search_for.date_valid) if @search_for.date_valid =~ /\S/
+ fns = '&fns=' + CGI.escape(@search_for.src_filename_base) if @search_for.src_filename_base =~ /\S/
+ @@canned_search_url=(checked_all =~/checked/) \
+ ? "#{@base}?#{s1}#{ft}#{key}#{ti}#{au}#{tr}#{sj}#{dsc}#{pb}#{edt}#{cntr}#{dt}#{ty}#{id}#{src}#{lang}#{rel}#{cov}#{cr}#{co}#{ab}#{dtc}#{dti}#{dtm}#{dta}#{dtv}#{fns}&db=#{cgi['db']}&view=#{cgi['view']}&a=1"
+ : "#{@base}?#{s1}#{ft}#{key}#{ti}#{au}#{tr}#{sj}#{dsc}#{pb}#{edt}#{cntr}#{dt}#{ty}#{id}#{src}#{lang}#{rel}#{cov}#{cr}#{co}#{ab}#{dtc}#{dti}#{dtm}#{dta}#{dtv}#{fns}&db=#{cgi['db']}&view=#{cgi['view']}"
+ mod=ft=~/\S+/ ? (ft.gsub(/ft/,'s1')) : s1
+ @canned_base_url="#{@base}?#{mod}&db=#{cgi['db']}"
+ if checked_case=~/\S/
+ @search[:text][1]=%{doc_objects.clean~'#{@search_for.text1}'} #s1
+ else
+ @search[:text][1]=%{doc_objects.clean~*'#{@search_for.text1}'} #s1
+ end
+ canned_note='search url:'
+ else
+ @@canned_search_url="#{@base}?db=#{@db}&view=index"
+ canned_note='search url example:'
+ end
+ if search_field =~/\S+/
+ analyze_format=search_field.gsub(/\s*\n/,'; ')
+ elsif checked_all =~/checked/ or checked_url =~/checked/
+ canned_search=@@canned_search_url.scan(/(?:s1|ft|au|ti|fns|tr)=[^&]+/)
+ af=canned_search.join('; ')
+ af=af.gsub(/s1=/,'text: ').
+ gsub(/ft=/,'fulltxt: ').
+ gsub(/au=/,'author: ').
+ gsub(/ti=/,'title: ').
+ gsub(/fns=/,'src_filename_base: ').
+ gsub(/tr=/,'topic_register: ').
+ gsub(/%2B/,' ')
+ analyze_format=af
+ st=af.split(/\s*;\s*/)
+ search_field=st.join("\n")
+ end
+ green=%{<font size="2" color="#004000">}
+ canned_search_url_txt=CGI.escapeHTML(@@canned_search_url)
+ the_can=%{<font size="2" color="#666666">#{canned_note} <a href="#{@@canned_search_url}">#{canned_search_url_txt}</a></font><br />}
+ p_text=p_fulltext=p_keywords=p_title=p_author=p_topic_register=p_subject=p_description=p_publisher=p_editor=p_contributor=p_date=p_type=p_format=p_identifier=p_source=p_language=p_relation=p_coverage=p_rights=p_comment=p_abstract=p_filename=''
+ p_filename = %{src_filename_base: #{green}#{@search_for.src_filename_base}</font><br />} if @search_for.src_filename_base =~ /\S+/
+ p_text = %{text: #{green}#{@search_for.text1}</font><br />} if @search_for.text1 =~ /\S+/
+ p_fulltext = %{fulltxt: #{green}#{@search_for.fulltext}</font><br />} if @search_for.fulltext =~ /\S+/
+ p_title = %{title: #{green}#{@search_for.title}</font><br />} if @search_for.title =~ /\S+/
+ p_author = %{author: #{green}#{@search_for.author}</font><br />} if @search_for.author =~ /\S+/
+ p_editor = %{editor: #{green}#{@search_for.editor}</font><br />} if @search_for.editor =~ /\S+/
+ p_contributor = %{contributor: #{green}#{@search_for.contributor}</font><br />} if @search_for.contributor =~ /\S+/
+ p_date = %{date: #{green}#{@search_for.date}</font><br />} if @search_for.date =~ /\S+/
+ p_rights = %{rights: #{green}#{@search_for.rights}</font><br />} if @search_for.rights =~ /\S+/
+ p_topic_register = %{topic_register: #{green}#{@search_for.topic_register}</font><br />} if @search_for.topic_register =~ /\S+/
+ p_subject = %{subject: #{green}#{@search_for.subject}</font><br />} if @search_for.subject =~ /\S+/
+ p_keywords = %{keywords: #{green}#{@search_for.keywords}</font><br />} if @search_for.keywords =~ /\S+/
+ p_identifier = %{identifier: #{green}#{@search_for.identifier}</font><br />} if @search_for.identifier =~ /\S+/
+ p_type = %{type: #{green}#{@search_for.type}</font><br />} if @search_for.type =~ /\S+/
+ p_format = %{format: #{green}#{@search_for.format}</font><br />} if @search_for.format =~ /\S+/
+ p_relation = %{relation: #{green}#{@search_for.relation}</font><br />} if @search_for.relation =~ /\S+/
+ p_coverage = %{coverage: #{green}#{@search_for.coverage}</font><br />} if @search_for.coverage =~ /\S+/
+ p_description = %{description: #{green}#{@search_for.description}</font><br />} if @search_for.description =~ /\S+/
+ p_abstract = %{abstract: #{green}#{@search_for.abstract}</font><br />} if @search_for.abstract =~ /\S+/
+ p_comment = %{comment: #{green}#{@search_for.comment}</font><br />} if @search_for.comment =~ /\S+/
+ p_publisher = %{publisher: #{green}#{@search_for.publisher}</font><br />} if @search_for.publisher =~ /\S+/
+ p_source = %{source: #{green}#{@search_for.source}</font><br />} if @search_for.source =~ /\S+/
+ p_language = %{language: #{green}#{@search_for.language}</font><br />} if @search_for.language =~ /\S+/
+ search_note=<<-WOK
+ <font size="2" color="#666666">
+ <b>database:</b> #{green}#{@db}</font>; <b>selected view:</b> #{green}#{cgi['view']}</font>
+ <b>search string:</b> "#{green}#{analyze_format}</font>"<br />
+ #{p_text} #{p_fulltext} #{p_keywords} #{p_title} #{p_author} #{p_topic_register} #{p_subject} #{p_description} #{p_publisher} #{p_editor} #{p_contributor} #{p_date} #{p_type} #{p_format} #{p_identifier} #{p_source} #{p_language} #{p_relation} #{p_coverage} #{p_rights} #{p_comment} #{p_abstract} #{p_filename}
+ </font>
+ WOK
+ #eg = %{canned search e.g.:<br /> <a href="#{url}">#{url}</a><br />find: #{analyze}<br />database: #{database}}
+ #% dbi_canning
+ @header = Form.new(@base,search_field,selected_db,result_type,checked_sql_limit,checked_tip,checked_stats,checked_searched,checked_url,checked_case,checked_echo,checked_sql,checked_all,checked_none,checked_selected,checked_default,search_note,the_can).submission_form #% form
+ unless q['s1'] =~/\S/ or q['au'] =~/\S/ or @search[:text][1] =~/\S/
+ print "Content-type: text/html\n\n"
+ puts (@header+@tail)
+ else #% searches
+ s1=(@search_for.text1 =~/\S/) \
+ ? @search_for.text1
+ : 'Unavailable'
+ if checked_case=~/\S/
+ @search[:text]<<%{doc_objects.clean~'#{CGI.unescape(s1)}'}
+ else
+ @search[:text]<<%{doc_objects.clean~*'#{CGI.unescape(s1)}'}
+ end
+ #% dbi_request
+ dbi_statement=DBI_SearchStatement.new(@conn,@search_for,q,checked_case)
+ @text_search_flag=false
+ @text_search_flag=dbi_statement.text_search_flag
+ s_contents=dbi_statement.contents
+ @body_main=''
+ @search_regx=nil
+ olduid=""
+ if @text_search_flag
+ if checked_sql =~/\S/
+ sql_select_body=dbi_statement.sql_select_body_format
+ else sql_select_body=''
+ end
+ @body_main << sql_select_body
+ #@body_main << '<p><hr><br /><b>Main Text:</b><br />' << sql_select_body
+ else
+ end
+ @hostpath = "#{@hosturl_files}"
+ #@hostpath="#{@hosturl_files}/#{@stub}"
+ def path_manifest(fn,ln=nil)
+ case @output_dir_structure_by
+ when 'filename'
+ @lingual =='mono' \
+ ? "#{@hostpath}/#{fn}/sisu_manifest.html"
+ : "#{@hostpath}/#{fn}/sisu_manifest.#{ln}.html"
+ when 'filetype'
+ @lingual =='mono' \
+ ? "#{@hostpath}/manifest/#{fn}.html"
+ : "#{@hostpath}/manifest/#{fn}.#{ln}.html"
+ else
+ "#{@hostpath}/#{ln}/manifest/#{fn}.html"
+ end
+ end
+ def path_html_seg(fn,ln=nil)
+ case @output_dir_structure_by
+ when 'filename'
+ "#{@hostpath}/#{fn}"
+ when 'filetype'
+ "#{@hostpath}/html/#{fn}"
+ else
+ "#{@hostpath}/#{ln}/html/#{fn}"
+ end
+ end
+ def path_toc(fn,ln=nil)
+ if @output_dir_structure_by =='filename' \
+ or @output_dir_structure_by =='filetype'
+ @lingual =='mono' \
+ ? "#{path_html_seg(fn,ln)}/toc.html"
+ : "#{path_html_seg(fn,ln)}/toc.#{ln}.html"
+ else
+ "#{path_html_seg(fn,ln)}/toc.html"
+ end
+ end
+ def path_filename(fn,seg_name,ln=nil)
+ if @output_dir_structure_by =='filename' \
+ or @output_dir_structure_by =='filetype'
+ @lingual =='mono' \
+ ? "#{path_html_seg(fn,ln)}/#{seg_name}.html"
+ : "#{path_html_seg(fn,ln)}/#{seg_name}.#{ln}.html"
+ else
+ "#{path_html_seg(fn,ln)}/#{seg_name}.html"
+ end
+ end
+ def path_html_doc(fn,ln=nil)
+ case @output_dir_structure_by
+ when 'filename'
+ @lingual =='mono' \
+ ? "#{path_html_seg(fn,ln)}/scroll.html"
+ : "#{path_html_seg(fn,ln)}/scroll.#{ln}.html"
+ when 'filetype'
+ @lingual =='mono' \
+ ? "#{@hostpath}/html/#{fn}.html"
+ : "#{@hostpath}/html/#{fn}.#{ln}.html"
+ else
+ "#{@hostpath}/#{ln}/html/#{fn}.html"
+ end
+ end
+ #% text_objects_body
+ s_contents.each do |c| #% text body
+ location=c['src_filename_base'][/(.+?)\.(?:ssm\.sst|sst)$/,1]
+ file_suffix=c['src_filename_base'][/.+?\.(ssm\.sst|sst)$/,1]
+ lang=if location =~ /\S+?~(\S\S\S?)$/
+ l=location[/\S+?~(\S\S\S?)$/,1]
+ location=location.gsub(/(\S+?)~\S\S\S?/,'\1')
+ l=".#{l}"
+ else ''
+ end
+ #% metadata_found_body
+ if c['uid'] != olduid
+ ti=c['title']
+ can_txt_srch=(cgi['view']=~/index/) \
+ ? %{<a href="#{@canned_base_url}&fns=#{c['src_filename_base']}&lang=#{c['language_document_char']}&view=text"><img border="0" width="24" height="16" src="#{@image_src}/b_search.png" alt="search"></a>&nbsp;}
+ : %{<a href="#{@canned_base_url}&fns=#{c['src_filename_base']}&lang=#{c['language_document_char']}&view=index"><img border="0" width="24" height="16" src="#{@image_src}/b_search.png" alt="search"></a>&nbsp;}
+ title = %{<span style="background-color: #{@color_heading}"><a href="#{path_toc(location,c['language_document_char'])}"><img border="0" width="15" height="18" src="#{@image_src}/b_toc.png" alt="">&nbsp;#{ti}</a></span> [#{c['language_document_char']}] by #{c['creator_author']} <a href="#{path_manifest(location,c['language_document_char'])}"><img border="0" width="15" height="15" src="#{@image_src}/b_info.png" alt=""></a> #{can_txt_srch}<br />}
+ title=@text_search_flag \
+ ? '<br /><hr>'+title
+ : '<br />'+title
+ @counter_txt_doc+=1
+ olduid=c['uid']
+ else title=''
+ end
+ if @text_search_flag
+ if cgi['view']=~/text/ \
+ or (cgi['view']!~/index/ and cgi['search'] !~/search db/) #% txt body
+ text=if c['suffix'] !~/1/ #seg
+ if @search_for.text1 =~/\S+/ \
+ or q['s1'] =~/\S+/ #% only this branch is working !!
+ unescaped_search=if @search_for.text1 =~/\S+/
+ CGI.unescape(@search_for.text1)
+ elsif q['s1'] =~/\S+/
+ CGI.unescape(q['s1'])
+ else nil
+ end
+ @search_regx=if unescaped_search #check
+ search_regex=[]
+ build=unescaped_search.scan(/\S+/).each do |g|
+ (g.to_s =~/(AND|OR)/) \
+ ? (search_regex << '|')
+ : (search_regex << %{#{g.to_s}})
+ end
+ search_regex=search_regex.join(' ')
+ search_regex=search_regex.gsub(/\s*\|\s*/,'|')
+ Regexp.new(search_regex, Regexp::IGNORECASE)
+ else nil
+ end
+ else nil
+ end
+ matched_para=(@search_regx.to_s.class==String && @search_regx.to_s=~/\S\S+/) \
+ ? (c['body'].gsub(/(<a\s+href="https?:\/\/[^><\s]+#{@search_regx}[^>]+?>|#{@search_regx})/mi,%{<span style="background-color: #{@color_match}">\\1</span>}))
+ : c['body']
+ %{<hr><p><font size="2">ocn <b><a href="#{path_filename(location,c['seg_name'],c['language_document_char'])}##{c['ocn']}">#{c['ocn']}</a></b>:</font></p>#{matched_para}}
+ elsif c['suffix'] =~/1/ #doc
+ %{#{title}<hr><p><font size="2">ocn #{c['ocn']}:#{c['body']}}
+ end
+ @counter_txt_ocn+=1
+ output=title+text
+ else #elsif cgi['view']=~/index/ #% idx body
+ if c['suffix'] !~/1/ #seg
+ index=%{<a href="#{path_filename(location,c['seg_name'],c['language_document_char'])}##{c['ocn']}">#{c['ocn']}</a>, } if @text_search_flag
+ elsif c['suffix'] =~/1/ #doc #FIX
+ index=%{<a href="#{path_html_doc(location,c['language_document_char'])}##{c['ocn']}">#{c['ocn']}</a>, }
+ end
+ if c['seg_name'] =~/\S+/
+ if @text_search_flag
+ @counter_txt_ocn+=1
+ output=title+index
+ end
+ else
+ @counter_txt_ocn+=1
+ output=c['suffix'] !~/1/ \
+ ? title+index
+ : %{#{title}#{c['ocn'].sort}, }
+ end
+ end
+ else output=title
+ end
+ @counters_txt=if @counter_txt_doc > 0
+ if checked_stats =~/\S/
+ @@lt_t=(@counter_txt_ocn==dbi_statement.sql_match_limit.to_i) ? true : false
+ start=(@@offset.to_i+1).to_s
+ range=(@@offset.to_i+@counter_txt_ocn.to_i).to_s
+ %{<hr /><font size="2" color="#666666">Found #{@counter_txt_ocn} times in the main body of #{@counter_txt_doc} documents [ matches #{start} to #{range} ]</font><br />}
+ else ''
+ end
+ else ''
+ end
+ @body_main << output #+ details
+ end
+ olduid = ""
+ offset=dbi_statement.sql_offset.to_s
+ limit=dbi_statement.sql_match_limit.to_s
+ @@lt_t ||=false; @@lt_e ||=false
+ canned=(@@lt_t or @@lt_e) \
+ ? dbi_statement.pre_next(true,@image_src).to_s
+ : dbi_statement.pre_next(false,@image_src).to_s
+ limit=dbi_statement.sql_match_limit.to_s
+ cgi.out{
+ @header.force_encoding("UTF-8") \
+ + @counters_txt.force_encoding("UTF-8") \
+ + @counters_endn.force_encoding("UTF-8") \
+ + canned.force_encoding("UTF-8") \
+ + @body_main.force_encoding("UTF-8") \
+ + canned.force_encoding("UTF-8") \
+ + @tail.force_encoding("UTF-8")
+ } #% print cgi_output_header+counters+body
+ end
+ rescue Exception => e
+ s='<pre>' + CGI::escapeHTML(e.backtrace.reverse.join("\n"))
+ s << CGI::escapeHTML(e.message) + '</pre>'
+ cgi.out{s}
+ next
+ ensure # eg. disconnect from server
+ @conn.disconnect if @conn
+ end
+end
diff --git a/sundry/misc/util/rb/tex/dr_tex.rb b/sundry/misc/util/rb/tex/dr_tex.rb
new file mode 100755
index 0000000..b71d8c1
--- /dev/null
+++ b/sundry/misc/util/rb/tex/dr_tex.rb
@@ -0,0 +1,120 @@
+#!/usr/bin/env ruby
+# /+
+# - Name: Spine, Doc Reform [a part of]
+# - Description: documents, structuring, processing, publishing, search
+# - static content generator
+#
+# - Author: Ralph Amissah
+# [ralph.amissah@gmail.com]
+#
+# - Copyright: (C) 2015 - 2021 Ralph Amissah, All Rights
+# Reserved.
+#
+# - License: AGPL 3 or later:
+#
+# Spine (SiSU), a framework for document structuring, publishing and
+# search
+#
+# Copyright (C) Ralph Amissah
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU AFERO General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program 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. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see [https://www.gnu.org/licenses/].
+#
+# If you have Internet connection, the latest version of the AGPL should be
+# available at these locations:
+# [https://www.fsf.org/licensing/licenses/agpl.html]
+# [https://www.gnu.org/licenses/agpl.html]
+#
+# - Spine (by Doc Reform, related to SiSU) uses standard:
+# - docReform markup syntax
+# - standard SiSU markup syntax with modified headers and minor modifications
+# - docReform object numbering
+# - standard SiSU object citation numbering & system
+#
+# - Hompages:
+# [https://www.doc_reform.org]
+# [https://www.sisudoc.org]
+#
+# - Git
+# [https://git.sisudoc.org/projects/?p=software/spine.git;a=summary]
+#
+# +/
+require 'fileutils'
+pwd = Dir.pwd
+argv,texfiles_with_path,flags=[],[],[]
+lngs = %{(am|bg|bn|br|ca|cs|cy|da|de|el|en|eo|es|et|eu|fi|fr|ga|gl|he|hi|hr|hy|ia|is|it|ja|ko|la|lo|lt|lv|ml|mr|nl|no|nn|oc|pl|pt|pt_BR|ro|ru|sa|se|sk|sl|sq|sr|sv|ta|te|th|tk|tr|uk|ur|vi|zh)}
+Regexp.new(lngs, Regexp::IGNORECASE)
+argv=$*
+argv.sort.each{|y| (y =~/^--\S+$/i) ? (flags << y) : (texfiles_with_path << y) }
+if flags.length==0 \
+|| flags.inspect =~/"--help"/
+ cmd=(/([^\/]+)$/).match($0)[1]
+ puts <<WOK
+#{cmd} --help
+#{cmd} --out=[output path]
+#{cmd} --paper-size=a5 --out=~/test
+WOK
+end
+// paper_size_orientation = (flags.inspect.match(/"--paper-size=(a4|a5|b5|letter|legal)"/)) ? $1 : "a4"
+out_path = Dir.pwd
+if (flags.inspect.match(/"--out=\S+"/))
+ out_path = flags.inspect.match(/"--out=(\S+)"/)[1]
+ unless (FileTest.directory?(out_path))
+ puts "Creating output directory: --out=#{out_path}"
+ FileUtils::mkdir_p(out_path)
+ unless (FileTest.directory?(out_path))
+ puts "FAILS unable to create directory: #{out_path}"
+ exit
+ end
+ end
+end
+if texfiles_with_path.length == 0
+ texfiles_with_path=Dir.glob('*.tex')
+end
+if texfiles_with_path.length > 0
+ texfiles_with_path.each do |texfile_with_path|
+ if texfile_with_path =~/.+\.tex/
+ #puts texfile_with_path
+ if FileTest.file?(texfile_with_path)
+ file_basename_with_path = texfile_with_path.sub(/\.tex$/,'')
+ file_basename = file_basename_with_path.sub(/.*?([^\/]+)$/,'\1')
+ _out_path = out_path
+ if file_basename =~ /\.#{lngs}$/
+ lng = file_basename.match(/\.#{lngs}$/)[1]
+ puts file_basename
+ puts lng
+ puts _out_path
+ unless _out_path.match(/\/#{lng}\/pdf$/)
+ _out_path = "#{out_path}/#{lng}/pdf"
+ FileUtils::mkdir_p(_out_path)
+ end
+ end
+ texpdf_cmd = %{xetex -interaction=batchmode -fmt=xelatex #{texfile_with_path}\n}
+ puts texpdf_cmd
+ 2.times { |i| system(texpdf_cmd) }
+ if (FileTest.file?(%{#{pwd}/#{file_basename}.pdf})) && (FileTest.directory?(_out_path))
+ FileUtils::Verbose::mv(%{#{pwd}/#{file_basename}.pdf}, %{#{_out_path}/#{file_basename}.pdf})
+ puts (%{#{_out_path}/#{file_basename}.pdf})
+ else
+ puts "issue with pdf file or output directory"
+ puts "pdf file: #{pwd}/#{file_basename}.pdf}"
+ puts "output dir: #{_out_path}/"
+ end
+ suffix = ['log', 'out', 'toc', 'aux']
+ suffix.each { |s| FileUtils::rm_f(%{#{pwd}/#{file_basename}.#{s}})}
+ end
+ end
+ end
+end
+Dir.chdir(pwd)
+__END__