summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--HyperSpec/Data/Map_Sym.txt1956
-rw-r--r--HyperSpec/Mop_Sym.txt1
-rw-r--r--package.lisp4
-rw-r--r--server/admin.lisp574
-rw-r--r--server/handlers.lisp903
-rw-r--r--server/main.lisp61
-rw-r--r--server/models.lisp35
-rw-r--r--server/rss.lisp122
-rw-r--r--server/storage.lisp43
-rw-r--r--server/views.lisp1009
-rw-r--r--static/about.html41
-rw-r--r--static/art.icobin0 -> 32038 bytes
-rw-r--r--static/errors/400.html21
-rw-r--r--static/errors/403.html21
-rw-r--r--static/errors/404.html21
-rw-r--r--static/errors/405.html21
-rw-r--r--static/errors/429.html14
-rw-r--r--static/errors/500.html21
-rw-r--r--static/errors/502.html21
-rw-r--r--static/errors/503.html21
-rw-r--r--static/errors/504.html21
-rw-r--r--static/favicon.icobin0 -> 15406 bytes
-rw-r--r--static/img/cloudflare.pngbin0 -> 82869 bytes
-rw-r--r--static/img/freebsd.pngbin0 -> 14218 bytes
-rw-r--r--static/img/gnu.pngbin0 -> 13357 bytes
-rw-r--r--static/img/i2p.pngbin0 -> 9192 bytes
-rw-r--r--static/img/mit-scheme.pngbin0 -> 16104 bytes
-rw-r--r--static/img/nginx.pngbin0 -> 18535 bytes
-rw-r--r--static/img/nocookie.pngbin0 -> 2191 bytes
-rw-r--r--static/img/nojs.pngbin0 -> 17763 bytes
-rw-r--r--static/img/snake.pngbin0 -> 15476 bytes
-rw-r--r--static/img/src/freebsd.svg1
-rw-r--r--static/img/src/gnu.svg94
-rw-r--r--static/img/src/mit-scheme.svg599
-rw-r--r--static/img/src/nginx.svg20
-rw-r--r--static/img/src/sicp-snake.pngbin0 -> 18342 bytes
-rw-r--r--static/img/tux.pngbin0 -> 11913 bytes
-rw-r--r--static/img/valid-html.pngbin0 -> 2066 bytes
-rw-r--r--static/index.html86
-rw-r--r--static/jscl-snippets.js319
-rw-r--r--static/larp.icobin0 -> 32038 bytes
-rw-r--r--static/lisp.html14
-rw-r--r--static/manifest.json18
-rw-r--r--static/schemebbs.pngbin0 -> 2109 bytes
-rw-r--r--static/styles/about.css82
-rw-r--r--static/styles/common.css248
-rw-r--r--static/styles/syntax/colorful.css13
-rw-r--r--static/styles/syntax/simple.css19
-rw-r--r--static/styles/themes/colored.css6
-rw-r--r--static/styles/themes/default.css288
-rw-r--r--static/styles/themes/light.css287
-rw-r--r--static/styles/themes/matrix.css423
-rw-r--r--static/styles/themes/no.css1
-rw-r--r--static/sw.js31
-rw-r--r--static/userscripts/highlight.user.js22
-rw-r--r--static/userscripts/localjump.user.js17
-rw-r--r--static/userscripts/unvip.user.js18
-rw-r--r--static/userscripts/wordfilter.user.js29
58 files changed, 7566 insertions, 0 deletions
diff --git a/HyperSpec/Data/Map_Sym.txt b/HyperSpec/Data/Map_Sym.txt
new file mode 100644
index 0000000..eb1a000
--- /dev/null
+++ b/HyperSpec/Data/Map_Sym.txt
@@ -0,0 +1,1956 @@
+&ALLOW-OTHER-KEYS
+../Body/03_da.htm
+&AUX
+../Body/03_da.htm
+&BODY
+../Body/03_dd.htm
+&ENVIRONMENT
+../Body/03_dd.htm
+&KEY
+../Body/03_da.htm
+&OPTIONAL
+../Body/03_da.htm
+&REST
+../Body/03_da.htm
+&WHOLE
+../Body/03_dd.htm
+*
+../Body/a_st.htm
+**
+../Body/v__stst_.htm
+***
+../Body/v__stst_.htm
+*BREAK-ON-SIGNALS*
+../Body/v_break_.htm
+*COMPILE-FILE-PATHNAME*
+../Body/v_cmp_fi.htm
+*COMPILE-FILE-TRUENAME*
+../Body/v_cmp_fi.htm
+*COMPILE-PRINT*
+../Body/v_cmp_pr.htm
+*COMPILE-VERBOSE*
+../Body/v_cmp_pr.htm
+*DEBUG-IO*
+../Body/v_debug_.htm
+*DEBUGGER-HOOK*
+../Body/v_debugg.htm
+*DEFAULT-PATHNAME-DEFAULTS*
+../Body/v_defaul.htm
+*ERROR-OUTPUT*
+../Body/v_debug_.htm
+*FEATURES*
+../Body/v_featur.htm
+*GENSYM-COUNTER*
+../Body/v_gensym.htm
+*LOAD-PATHNAME*
+../Body/v_ld_pns.htm
+*LOAD-PRINT*
+../Body/v_ld_prs.htm
+*LOAD-TRUENAME*
+../Body/v_ld_pns.htm
+*LOAD-VERBOSE*
+../Body/v_ld_prs.htm
+*MACROEXPAND-HOOK*
+../Body/v_mexp_h.htm
+*MODULES*
+../Body/v_module.htm
+*PACKAGE*
+../Body/v_pkg.htm
+*PRINT-ARRAY*
+../Body/v_pr_ar.htm
+*PRINT-BASE*
+../Body/v_pr_bas.htm
+*PRINT-CASE*
+../Body/v_pr_cas.htm
+*PRINT-CIRCLE*
+../Body/v_pr_cir.htm
+*PRINT-ESCAPE*
+../Body/v_pr_esc.htm
+*PRINT-GENSYM*
+../Body/v_pr_gen.htm
+*PRINT-LENGTH*
+../Body/v_pr_lev.htm
+*PRINT-LEVEL*
+../Body/v_pr_lev.htm
+*PRINT-LINES*
+../Body/v_pr_lin.htm
+*PRINT-MISER-WIDTH*
+../Body/v_pr_mis.htm
+*PRINT-PPRINT-DISPATCH*
+../Body/v_pr_ppr.htm
+*PRINT-PRETTY*
+../Body/v_pr_pre.htm
+*PRINT-RADIX*
+../Body/v_pr_bas.htm
+*PRINT-READABLY*
+../Body/v_pr_rda.htm
+*PRINT-RIGHT-MARGIN*
+../Body/v_pr_rig.htm
+*QUERY-IO*
+../Body/v_debug_.htm
+*RANDOM-STATE*
+../Body/v_rnd_st.htm
+*READ-BASE*
+../Body/v_rd_bas.htm
+*READ-DEFAULT-FLOAT-FORMAT*
+../Body/v_rd_def.htm
+*READ-EVAL*
+../Body/v_rd_eva.htm
+*READ-SUPPRESS*
+../Body/v_rd_sup.htm
+*READTABLE*
+../Body/v_rdtabl.htm
+*STANDARD-INPUT*
+../Body/v_debug_.htm
+*STANDARD-OUTPUT*
+../Body/v_debug_.htm
+*TERMINAL-IO*
+../Body/v_termin.htm
+*TRACE-OUTPUT*
+../Body/v_debug_.htm
++
+../Body/a_pl.htm
+++
+../Body/v_pl_plp.htm
++++
+../Body/v_pl_plp.htm
+-
+../Body/a__.htm
+/
+../Body/a_sl.htm
+//
+../Body/v_sl_sls.htm
+///
+../Body/v_sl_sls.htm
+/=
+../Body/f_eq_sle.htm
+1+
+../Body/f_1pl_1_.htm
+1-
+../Body/f_1pl_1_.htm
+<
+../Body/f_eq_sle.htm
+<=
+../Body/f_eq_sle.htm
+=
+../Body/f_eq_sle.htm
+>
+../Body/f_eq_sle.htm
+>=
+../Body/f_eq_sle.htm
+ABORT
+../Body/a_abort.htm
+ABS
+../Body/f_abs.htm
+ACONS
+../Body/f_acons.htm
+ACOS
+../Body/f_asin_.htm
+ACOSH
+../Body/f_sinh_.htm
+ADD-METHOD
+../Body/f_add_me.htm
+ADJOIN
+../Body/f_adjoin.htm
+ADJUST-ARRAY
+../Body/f_adjust.htm
+ADJUSTABLE-ARRAY-P
+../Body/f_adju_1.htm
+ALLOCATE-INSTANCE
+../Body/f_alloca.htm
+ALPHA-CHAR-P
+../Body/f_alpha_.htm
+ALPHANUMERICP
+../Body/f_alphan.htm
+AND
+../Body/a_and.htm
+APPEND
+../Body/f_append.htm
+APPLY
+../Body/f_apply.htm
+APROPOS
+../Body/f_apropo.htm
+APROPOS-LIST
+../Body/f_apropo.htm
+AREF
+../Body/f_aref.htm
+ARITHMETIC-ERROR
+../Body/e_arithm.htm
+ARITHMETIC-ERROR-OPERANDS
+../Body/f_arithm.htm
+ARITHMETIC-ERROR-OPERATION
+../Body/f_arithm.htm
+ARRAY
+../Body/t_array.htm
+ARRAY-DIMENSION
+../Body/f_ar_dim.htm
+ARRAY-DIMENSION-LIMIT
+../Body/v_ar_dim.htm
+ARRAY-DIMENSIONS
+../Body/f_ar_d_1.htm
+ARRAY-DISPLACEMENT
+../Body/f_ar_dis.htm
+ARRAY-ELEMENT-TYPE
+../Body/f_ar_ele.htm
+ARRAY-HAS-FILL-POINTER-P
+../Body/f_ar_has.htm
+ARRAY-IN-BOUNDS-P
+../Body/f_ar_in_.htm
+ARRAY-RANK
+../Body/f_ar_ran.htm
+ARRAY-RANK-LIMIT
+../Body/v_ar_ran.htm
+ARRAY-ROW-MAJOR-INDEX
+../Body/f_ar_row.htm
+ARRAY-TOTAL-SIZE
+../Body/f_ar_tot.htm
+ARRAY-TOTAL-SIZE-LIMIT
+../Body/v_ar_tot.htm
+ARRAYP
+../Body/f_arrayp.htm
+ASH
+../Body/f_ash.htm
+ASIN
+../Body/f_asin_.htm
+ASINH
+../Body/f_sinh_.htm
+ASSERT
+../Body/m_assert.htm
+ASSOC
+../Body/f_assocc.htm
+ASSOC-IF
+../Body/f_assocc.htm
+ASSOC-IF-NOT
+../Body/f_assocc.htm
+ATAN
+../Body/f_asin_.htm
+ATANH
+../Body/f_sinh_.htm
+ATOM
+../Body/a_atom.htm
+BASE-CHAR
+../Body/t_base_c.htm
+BASE-STRING
+../Body/t_base_s.htm
+BIGNUM
+../Body/t_bignum.htm
+BIT
+../Body/a_bit.htm
+BIT-AND
+../Body/f_bt_and.htm
+BIT-ANDC1
+../Body/f_bt_and.htm
+BIT-ANDC2
+../Body/f_bt_and.htm
+BIT-EQV
+../Body/f_bt_and.htm
+BIT-IOR
+../Body/f_bt_and.htm
+BIT-NAND
+../Body/f_bt_and.htm
+BIT-NOR
+../Body/f_bt_and.htm
+BIT-NOT
+../Body/f_bt_and.htm
+BIT-ORC1
+../Body/f_bt_and.htm
+BIT-ORC2
+../Body/f_bt_and.htm
+BIT-VECTOR
+../Body/t_bt_vec.htm
+BIT-VECTOR-P
+../Body/f_bt_vec.htm
+BIT-XOR
+../Body/f_bt_and.htm
+BLOCK
+../Body/s_block.htm
+BOOLE
+../Body/f_boole.htm
+BOOLE-1
+../Body/v_b_1_b.htm
+BOOLE-2
+../Body/v_b_1_b.htm
+BOOLE-AND
+../Body/v_b_1_b.htm
+BOOLE-ANDC1
+../Body/v_b_1_b.htm
+BOOLE-ANDC2
+../Body/v_b_1_b.htm
+BOOLE-C1
+../Body/v_b_1_b.htm
+BOOLE-C2
+../Body/v_b_1_b.htm
+BOOLE-CLR
+../Body/v_b_1_b.htm
+BOOLE-EQV
+../Body/v_b_1_b.htm
+BOOLE-IOR
+../Body/v_b_1_b.htm
+BOOLE-NAND
+../Body/v_b_1_b.htm
+BOOLE-NOR
+../Body/v_b_1_b.htm
+BOOLE-ORC1
+../Body/v_b_1_b.htm
+BOOLE-ORC2
+../Body/v_b_1_b.htm
+BOOLE-SET
+../Body/v_b_1_b.htm
+BOOLE-XOR
+../Body/v_b_1_b.htm
+BOOLEAN
+../Body/t_ban.htm
+BOTH-CASE-P
+../Body/f_upper_.htm
+BOUNDP
+../Body/f_boundp.htm
+BREAK
+../Body/f_break.htm
+BROADCAST-STREAM
+../Body/t_broadc.htm
+BROADCAST-STREAM-STREAMS
+../Body/f_broadc.htm
+BUILT-IN-CLASS
+../Body/t_built_.htm
+BUTLAST
+../Body/f_butlas.htm
+BYTE
+../Body/f_by_by.htm
+BYTE-POSITION
+../Body/f_by_by.htm
+BYTE-SIZE
+../Body/f_by_by.htm
+CAAAAR
+../Body/f_car_c.htm
+CAAADR
+../Body/f_car_c.htm
+CAAAR
+../Body/f_car_c.htm
+CAADAR
+../Body/f_car_c.htm
+CAADDR
+../Body/f_car_c.htm
+CAADR
+../Body/f_car_c.htm
+CAAR
+../Body/f_car_c.htm
+CADAAR
+../Body/f_car_c.htm
+CADADR
+../Body/f_car_c.htm
+CADAR
+../Body/f_car_c.htm
+CADDAR
+../Body/f_car_c.htm
+CADDDR
+../Body/f_car_c.htm
+CADDR
+../Body/f_car_c.htm
+CADR
+../Body/f_car_c.htm
+CALL-ARGUMENTS-LIMIT
+../Body/v_call_a.htm
+CALL-METHOD
+../Body/m_call_m.htm
+CALL-NEXT-METHOD
+../Body/f_call_n.htm
+CAR
+../Body/f_car_c.htm
+CASE
+../Body/m_case_.htm
+CATCH
+../Body/s_catch.htm
+CCASE
+../Body/m_case_.htm
+CDAAAR
+../Body/f_car_c.htm
+CDAADR
+../Body/f_car_c.htm
+CDAAR
+../Body/f_car_c.htm
+CDADAR
+../Body/f_car_c.htm
+CDADDR
+../Body/f_car_c.htm
+CDADR
+../Body/f_car_c.htm
+CDAR
+../Body/f_car_c.htm
+CDDAAR
+../Body/f_car_c.htm
+CDDADR
+../Body/f_car_c.htm
+CDDAR
+../Body/f_car_c.htm
+CDDDAR
+../Body/f_car_c.htm
+CDDDDR
+../Body/f_car_c.htm
+CDDDR
+../Body/f_car_c.htm
+CDDR
+../Body/f_car_c.htm
+CDR
+../Body/f_car_c.htm
+CEILING
+../Body/f_floorc.htm
+CELL-ERROR
+../Body/e_cell_e.htm
+CELL-ERROR-NAME
+../Body/f_cell_e.htm
+CERROR
+../Body/f_cerror.htm
+CHANGE-CLASS
+../Body/f_chg_cl.htm
+CHAR
+../Body/f_char_.htm
+CHAR-CODE
+../Body/f_char_c.htm
+CHAR-CODE-LIMIT
+../Body/v_char_c.htm
+CHAR-DOWNCASE
+../Body/f_char_u.htm
+CHAR-EQUAL
+../Body/f_chareq.htm
+CHAR-GREATERP
+../Body/f_chareq.htm
+CHAR-INT
+../Body/f_char_i.htm
+CHAR-LESSP
+../Body/f_chareq.htm
+CHAR-NAME
+../Body/f_char_n.htm
+CHAR-NOT-EQUAL
+../Body/f_chareq.htm
+CHAR-NOT-GREATERP
+../Body/f_chareq.htm
+CHAR-NOT-LESSP
+../Body/f_chareq.htm
+CHAR-UPCASE
+../Body/f_char_u.htm
+CHAR/=
+../Body/f_chareq.htm
+CHAR<
+../Body/f_chareq.htm
+CHAR<=
+../Body/f_chareq.htm
+CHAR=
+../Body/f_chareq.htm
+CHAR>
+../Body/f_chareq.htm
+CHAR>=
+../Body/f_chareq.htm
+CHARACTER
+../Body/a_ch.htm
+CHARACTERP
+../Body/f_chp.htm
+CHECK-TYPE
+../Body/m_check_.htm
+CIS
+../Body/f_cis.htm
+CLASS
+../Body/t_class.htm
+CLASS-NAME
+../Body/f_class_.htm
+CLASS-OF
+../Body/f_clas_1.htm
+CLEAR-INPUT
+../Body/f_clear_.htm
+CLEAR-OUTPUT
+../Body/f_finish.htm
+CLOSE
+../Body/f_close.htm
+CLRHASH
+../Body/f_clrhas.htm
+CODE-CHAR
+../Body/f_code_c.htm
+COERCE
+../Body/f_coerce.htm
+COMPILATION-SPEED
+../Body/d_optimi.htm
+COMPILE
+../Body/f_cmp.htm
+COMPILE-FILE
+../Body/f_cmp_fi.htm
+COMPILE-FILE-PATHNAME
+../Body/f_cmp__1.htm
+COMPILED-FUNCTION
+../Body/t_cmpd_f.htm
+COMPILED-FUNCTION-P
+../Body/f_cmpd_f.htm
+COMPILER-MACRO
+../Body/f_docume.htm
+COMPILER-MACRO-FUNCTION
+../Body/f_cmp_ma.htm
+COMPLEMENT
+../Body/f_comple.htm
+COMPLEX
+../Body/a_comple.htm
+COMPLEXP
+../Body/f_comp_3.htm
+COMPUTE-APPLICABLE-METHODS
+../Body/f_comput.htm
+COMPUTE-RESTARTS
+../Body/f_comp_1.htm
+CONCATENATE
+../Body/f_concat.htm
+CONCATENATED-STREAM
+../Body/t_concat.htm
+CONCATENATED-STREAM-STREAMS
+../Body/f_conc_1.htm
+COND
+../Body/m_cond.htm
+CONDITION
+../Body/e_cnd.htm
+CONJUGATE
+../Body/f_conjug.htm
+CONS
+../Body/a_cons.htm
+CONSP
+../Body/f_consp.htm
+CONSTANTLY
+../Body/f_cons_1.htm
+CONSTANTP
+../Body/f_consta.htm
+CONTINUE
+../Body/a_contin.htm
+CONTROL-ERROR
+../Body/e_contro.htm
+COPY-ALIST
+../Body/f_cp_ali.htm
+COPY-LIST
+../Body/f_cp_lis.htm
+COPY-PPRINT-DISPATCH
+../Body/f_cp_ppr.htm
+COPY-READTABLE
+../Body/f_cp_rdt.htm
+COPY-SEQ
+../Body/f_cp_seq.htm
+COPY-STRUCTURE
+../Body/f_cp_stu.htm
+COPY-SYMBOL
+../Body/f_cp_sym.htm
+COPY-TREE
+../Body/f_cp_tre.htm
+COS
+../Body/f_sin_c.htm
+COSH
+../Body/f_sinh_.htm
+COUNT
+../Body/f_countc.htm
+COUNT-IF
+../Body/f_countc.htm
+COUNT-IF-NOT
+../Body/f_countc.htm
+CTYPECASE
+../Body/m_tpcase.htm
+DEBUG
+../Body/d_optimi.htm
+DECF
+../Body/m_incf_.htm
+DECLAIM
+../Body/m_declai.htm
+DECLARATION
+../Body/d_declar.htm
+DECLARE
+../Body/s_declar.htm
+DECODE-FLOAT
+../Body/f_dec_fl.htm
+DECODE-UNIVERSAL-TIME
+../Body/f_dec_un.htm
+DEFCLASS
+../Body/m_defcla.htm
+DEFCONSTANT
+../Body/m_defcon.htm
+DEFGENERIC
+../Body/m_defgen.htm
+DEFINE-COMPILER-MACRO
+../Body/m_define.htm
+DEFINE-CONDITION
+../Body/m_defi_5.htm
+DEFINE-METHOD-COMBINATION
+../Body/m_defi_4.htm
+DEFINE-MODIFY-MACRO
+../Body/m_defi_2.htm
+DEFINE-SETF-EXPANDER
+../Body/m_defi_3.htm
+DEFINE-SYMBOL-MACRO
+../Body/m_defi_1.htm
+DEFMACRO
+../Body/m_defmac.htm
+DEFMETHOD
+../Body/m_defmet.htm
+DEFPACKAGE
+../Body/m_defpkg.htm
+DEFPARAMETER
+../Body/m_defpar.htm
+DEFSETF
+../Body/m_defset.htm
+DEFSTRUCT
+../Body/m_defstr.htm
+DEFTYPE
+../Body/m_deftp.htm
+DEFUN
+../Body/m_defun.htm
+DEFVAR
+../Body/m_defpar.htm
+DELETE
+../Body/f_rm_rm.htm
+DELETE-DUPLICATES
+../Body/f_rm_dup.htm
+DELETE-FILE
+../Body/f_del_fi.htm
+DELETE-IF
+../Body/f_rm_rm.htm
+DELETE-IF-NOT
+../Body/f_rm_rm.htm
+DELETE-PACKAGE
+../Body/f_del_pk.htm
+DENOMINATOR
+../Body/f_numera.htm
+DEPOSIT-FIELD
+../Body/f_deposi.htm
+DESCRIBE
+../Body/f_descri.htm
+DESCRIBE-OBJECT
+../Body/f_desc_1.htm
+DESTRUCTURING-BIND
+../Body/m_destru.htm
+DIGIT-CHAR
+../Body/f_digit_.htm
+DIGIT-CHAR-P
+../Body/f_digi_1.htm
+DIRECTORY
+../Body/f_dir.htm
+DIRECTORY-NAMESTRING
+../Body/f_namest.htm
+DISASSEMBLE
+../Body/f_disass.htm
+DIVISION-BY-ZERO
+../Body/e_divisi.htm
+DO
+../Body/m_do_do.htm
+DO*
+../Body/m_do_do.htm
+DO-ALL-SYMBOLS
+../Body/m_do_sym.htm
+DO-EXTERNAL-SYMBOLS
+../Body/m_do_sym.htm
+DO-SYMBOLS
+../Body/m_do_sym.htm
+DOCUMENTATION
+../Body/f_docume.htm
+DOLIST
+../Body/m_dolist.htm
+DOTIMES
+../Body/m_dotime.htm
+DOUBLE-FLOAT
+../Body/t_short_.htm
+DOUBLE-FLOAT-EPSILON
+../Body/v_short_.htm
+DOUBLE-FLOAT-NEGATIVE-EPSILON
+../Body/v_short_.htm
+DPB
+../Body/f_dpb.htm
+DRIBBLE
+../Body/f_dribbl.htm
+DYNAMIC-EXTENT
+../Body/d_dynami.htm
+ECASE
+../Body/m_case_.htm
+ECHO-STREAM
+../Body/t_echo_s.htm
+ECHO-STREAM-INPUT-STREAM
+../Body/f_echo_s.htm
+ECHO-STREAM-OUTPUT-STREAM
+../Body/f_echo_s.htm
+ED
+../Body/f_ed.htm
+EIGHTH
+../Body/f_firstc.htm
+ELT
+../Body/f_elt.htm
+ENCODE-UNIVERSAL-TIME
+../Body/f_encode.htm
+END-OF-FILE
+../Body/e_end_of.htm
+ENDP
+../Body/f_endp.htm
+ENOUGH-NAMESTRING
+../Body/f_namest.htm
+ENSURE-DIRECTORIES-EXIST
+../Body/f_ensu_1.htm
+ENSURE-GENERIC-FUNCTION
+../Body/f_ensure.htm
+EQ
+../Body/f_eq.htm
+EQL
+../Body/a_eql.htm
+EQUAL
+../Body/f_equal.htm
+EQUALP
+../Body/f_equalp.htm
+ERROR
+../Body/a_error.htm
+ETYPECASE
+../Body/m_tpcase.htm
+EVAL
+../Body/f_eval.htm
+EVAL-WHEN
+../Body/s_eval_w.htm
+EVENP
+../Body/f_evenpc.htm
+EVERY
+../Body/f_everyc.htm
+EXP
+../Body/f_exp_e.htm
+EXPORT
+../Body/f_export.htm
+EXPT
+../Body/f_exp_e.htm
+EXTENDED-CHAR
+../Body/t_extend.htm
+FBOUNDP
+../Body/f_fbound.htm
+FCEILING
+../Body/f_floorc.htm
+FDEFINITION
+../Body/f_fdefin.htm
+FFLOOR
+../Body/f_floorc.htm
+FIFTH
+../Body/f_firstc.htm
+FILE-AUTHOR
+../Body/f_file_a.htm
+FILE-ERROR
+../Body/e_file_e.htm
+FILE-ERROR-PATHNAME
+../Body/f_file_e.htm
+FILE-LENGTH
+../Body/f_file_l.htm
+FILE-NAMESTRING
+../Body/f_namest.htm
+FILE-POSITION
+../Body/f_file_p.htm
+FILE-STREAM
+../Body/t_file_s.htm
+FILE-STRING-LENGTH
+../Body/f_file_s.htm
+FILE-WRITE-DATE
+../Body/f_file_w.htm
+FILL
+../Body/f_fill.htm
+FILL-POINTER
+../Body/f_fill_p.htm
+FIND
+../Body/f_find_.htm
+FIND-ALL-SYMBOLS
+../Body/f_find_a.htm
+FIND-CLASS
+../Body/f_find_c.htm
+FIND-IF
+../Body/f_find_.htm
+FIND-IF-NOT
+../Body/f_find_.htm
+FIND-METHOD
+../Body/f_find_m.htm
+FIND-PACKAGE
+../Body/f_find_p.htm
+FIND-RESTART
+../Body/f_find_r.htm
+FIND-SYMBOL
+../Body/f_find_s.htm
+FINISH-OUTPUT
+../Body/f_finish.htm
+FIRST
+../Body/f_firstc.htm
+FIXNUM
+../Body/t_fixnum.htm
+FLET
+../Body/s_flet_.htm
+FLOAT
+../Body/a_float.htm
+FLOAT-DIGITS
+../Body/f_dec_fl.htm
+FLOAT-PRECISION
+../Body/f_dec_fl.htm
+FLOAT-RADIX
+../Body/f_dec_fl.htm
+FLOAT-SIGN
+../Body/f_dec_fl.htm
+FLOATING-POINT-INEXACT
+../Body/e_floa_1.htm
+FLOATING-POINT-INVALID-OPERATION
+../Body/e_floati.htm
+FLOATING-POINT-OVERFLOW
+../Body/e_floa_2.htm
+FLOATING-POINT-UNDERFLOW
+../Body/e_floa_3.htm
+FLOATP
+../Body/f_floatp.htm
+FLOOR
+../Body/f_floorc.htm
+FMAKUNBOUND
+../Body/f_fmakun.htm
+FORCE-OUTPUT
+../Body/f_finish.htm
+FORMAT
+../Body/f_format.htm
+FORMATTER
+../Body/m_format.htm
+FOURTH
+../Body/f_firstc.htm
+FRESH-LINE
+../Body/f_terpri.htm
+FROUND
+../Body/f_floorc.htm
+FTRUNCATE
+../Body/f_floorc.htm
+FTYPE
+../Body/d_ftype.htm
+FUNCALL
+../Body/f_funcal.htm
+FUNCTION
+../Body/a_fn.htm
+FUNCTION-KEYWORDS
+../Body/f_fn_kwd.htm
+FUNCTION-LAMBDA-EXPRESSION
+../Body/f_fn_lam.htm
+FUNCTIONP
+../Body/f_fnp.htm
+GCD
+../Body/f_gcd.htm
+GENERIC-FUNCTION
+../Body/t_generi.htm
+GENSYM
+../Body/f_gensym.htm
+GENTEMP
+../Body/f_gentem.htm
+GET
+../Body/f_get.htm
+GET-DECODED-TIME
+../Body/f_get_un.htm
+GET-DISPATCH-MACRO-CHARACTER
+../Body/f_set__1.htm
+GET-INTERNAL-REAL-TIME
+../Body/f_get_in.htm
+GET-INTERNAL-RUN-TIME
+../Body/f_get__1.htm
+GET-MACRO-CHARACTER
+../Body/f_set_ma.htm
+GET-OUTPUT-STREAM-STRING
+../Body/f_get_ou.htm
+GET-PROPERTIES
+../Body/f_get_pr.htm
+GET-SETF-EXPANSION
+../Body/f_get_se.htm
+GET-UNIVERSAL-TIME
+../Body/f_get_un.htm
+GETF
+../Body/f_getf.htm
+GETHASH
+../Body/f_gethas.htm
+GO
+../Body/s_go.htm
+GRAPHIC-CHAR-P
+../Body/f_graphi.htm
+HANDLER-BIND
+../Body/m_handle.htm
+HANDLER-CASE
+../Body/m_hand_1.htm
+HASH-TABLE
+../Body/t_hash_t.htm
+HASH-TABLE-COUNT
+../Body/f_hash_1.htm
+HASH-TABLE-P
+../Body/f_hash_t.htm
+HASH-TABLE-REHASH-SIZE
+../Body/f_hash_2.htm
+HASH-TABLE-REHASH-THRESHOLD
+../Body/f_hash_3.htm
+HASH-TABLE-SIZE
+../Body/f_hash_4.htm
+HASH-TABLE-TEST
+../Body/f_hash_5.htm
+HOST-NAMESTRING
+../Body/f_namest.htm
+IDENTITY
+../Body/f_identi.htm
+IF
+../Body/s_if.htm
+IGNORABLE
+../Body/d_ignore.htm
+IGNORE
+../Body/d_ignore.htm
+IGNORE-ERRORS
+../Body/m_ignore.htm
+IMAGPART
+../Body/f_realpa.htm
+IMPORT
+../Body/f_import.htm
+IN-PACKAGE
+../Body/m_in_pkg.htm
+INCF
+../Body/m_incf_.htm
+INITIALIZE-INSTANCE
+../Body/f_init_i.htm
+INLINE
+../Body/d_inline.htm
+INPUT-STREAM-P
+../Body/f_in_stm.htm
+INSPECT
+../Body/f_inspec.htm
+INTEGER
+../Body/t_intege.htm
+INTEGER-DECODE-FLOAT
+../Body/f_dec_fl.htm
+INTEGER-LENGTH
+../Body/f_intege.htm
+INTEGERP
+../Body/f_inte_1.htm
+INTERACTIVE-STREAM-P
+../Body/f_intera.htm
+INTERN
+../Body/f_intern.htm
+INTERNAL-TIME-UNITS-PER-SECOND
+../Body/v_intern.htm
+INTERSECTION
+../Body/f_isec_.htm
+INVALID-METHOD-ERROR
+../Body/f_invali.htm
+INVOKE-DEBUGGER
+../Body/f_invoke.htm
+INVOKE-RESTART
+../Body/f_invo_1.htm
+INVOKE-RESTART-INTERACTIVELY
+../Body/f_invo_2.htm
+ISQRT
+../Body/f_sqrt_.htm
+KEYWORD
+../Body/t_kwd.htm
+KEYWORDP
+../Body/f_kwdp.htm
+LABELS
+../Body/s_flet_.htm
+LAMBDA
+../Body/a_lambda.htm
+LAMBDA-LIST-KEYWORDS
+../Body/v_lambda.htm
+LAMBDA-PARAMETERS-LIMIT
+../Body/v_lamb_1.htm
+LAST
+../Body/f_last.htm
+LCM
+../Body/f_lcm.htm
+LDB
+../Body/f_ldb.htm
+LDB-TEST
+../Body/f_ldb_te.htm
+LDIFF
+../Body/f_ldiffc.htm
+LEAST-NEGATIVE-DOUBLE-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-LONG-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-NORMALIZED-DOUBLE-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-NORMALIZED-LONG-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-NORMALIZED-SHORT-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-NORMALIZED-SINGLE-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-SHORT-FLOAT
+../Body/v_most_1.htm
+LEAST-NEGATIVE-SINGLE-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-DOUBLE-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-LONG-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-NORMALIZED-DOUBLE-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-NORMALIZED-LONG-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-NORMALIZED-SHORT-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-NORMALIZED-SINGLE-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-SHORT-FLOAT
+../Body/v_most_1.htm
+LEAST-POSITIVE-SINGLE-FLOAT
+../Body/v_most_1.htm
+LENGTH
+../Body/f_length.htm
+LET
+../Body/s_let_l.htm
+LET*
+../Body/s_let_l.htm
+LISP-IMPLEMENTATION-TYPE
+../Body/f_lisp_i.htm
+LISP-IMPLEMENTATION-VERSION
+../Body/f_lisp_i.htm
+LIST
+../Body/a_list.htm
+LIST*
+../Body/f_list_.htm
+LIST-ALL-PACKAGES
+../Body/f_list_a.htm
+LIST-LENGTH
+../Body/f_list_l.htm
+LISTEN
+../Body/f_listen.htm
+LISTP
+../Body/f_listp.htm
+LOAD
+../Body/f_load.htm
+LOAD-LOGICAL-PATHNAME-TRANSLATIONS
+../Body/f_ld_log.htm
+LOAD-TIME-VALUE
+../Body/s_ld_tim.htm
+LOCALLY
+../Body/s_locall.htm
+LOG
+../Body/f_log.htm
+LOGAND
+../Body/f_logand.htm
+LOGANDC1
+../Body/f_logand.htm
+LOGANDC2
+../Body/f_logand.htm
+LOGBITP
+../Body/f_logbtp.htm
+LOGCOUNT
+../Body/f_logcou.htm
+LOGEQV
+../Body/f_logand.htm
+LOGICAL-PATHNAME
+../Body/a_logica.htm
+LOGICAL-PATHNAME-TRANSLATIONS
+../Body/f_logica.htm
+LOGIOR
+../Body/f_logand.htm
+LOGNAND
+../Body/f_logand.htm
+LOGNOR
+../Body/f_logand.htm
+LOGNOT
+../Body/f_logand.htm
+LOGORC1
+../Body/f_logand.htm
+LOGORC2
+../Body/f_logand.htm
+LOGTEST
+../Body/f_logtes.htm
+LOGXOR
+../Body/f_logand.htm
+LONG-FLOAT
+../Body/t_short_.htm
+LONG-FLOAT-EPSILON
+../Body/v_short_.htm
+LONG-FLOAT-NEGATIVE-EPSILON
+../Body/v_short_.htm
+LONG-SITE-NAME
+../Body/f_short_.htm
+LOOP
+../Body/m_loop.htm
+LOOP-FINISH
+../Body/m_loop_f.htm
+LOWER-CASE-P
+../Body/f_upper_.htm
+MACHINE-INSTANCE
+../Body/f_mach_i.htm
+MACHINE-TYPE
+../Body/f_mach_t.htm
+MACHINE-VERSION
+../Body/f_mach_v.htm
+MACRO-FUNCTION
+../Body/f_macro_.htm
+MACROEXPAND
+../Body/f_mexp_.htm
+MACROEXPAND-1
+../Body/f_mexp_.htm
+MACROLET
+../Body/s_flet_.htm
+MAKE-ARRAY
+../Body/f_mk_ar.htm
+MAKE-BROADCAST-STREAM
+../Body/f_mk_bro.htm
+MAKE-CONCATENATED-STREAM
+../Body/f_mk_con.htm
+MAKE-CONDITION
+../Body/f_mk_cnd.htm
+MAKE-DISPATCH-MACRO-CHARACTER
+../Body/f_mk_dis.htm
+MAKE-ECHO-STREAM
+../Body/f_mk_ech.htm
+MAKE-HASH-TABLE
+../Body/f_mk_has.htm
+MAKE-INSTANCE
+../Body/f_mk_ins.htm
+MAKE-INSTANCES-OBSOLETE
+../Body/f_mk_i_1.htm
+MAKE-LIST
+../Body/f_mk_lis.htm
+MAKE-LOAD-FORM
+../Body/f_mk_ld_.htm
+MAKE-LOAD-FORM-SAVING-SLOTS
+../Body/f_mk_l_1.htm
+MAKE-METHOD
+../Body/m_call_m.htm
+MAKE-PACKAGE
+../Body/f_mk_pkg.htm
+MAKE-PATHNAME
+../Body/f_mk_pn.htm
+MAKE-RANDOM-STATE
+../Body/f_mk_rnd.htm
+MAKE-SEQUENCE
+../Body/f_mk_seq.htm
+MAKE-STRING
+../Body/f_mk_stg.htm
+MAKE-STRING-INPUT-STREAM
+../Body/f_mk_s_1.htm
+MAKE-STRING-OUTPUT-STREAM
+../Body/f_mk_s_2.htm
+MAKE-SYMBOL
+../Body/f_mk_sym.htm
+MAKE-SYNONYM-STREAM
+../Body/f_mk_syn.htm
+MAKE-TWO-WAY-STREAM
+../Body/f_mk_two.htm
+MAKUNBOUND
+../Body/f_makunb.htm
+MAP
+../Body/f_map.htm
+MAP-INTO
+../Body/f_map_in.htm
+MAPC
+../Body/f_mapc_.htm
+MAPCAN
+../Body/f_mapc_.htm
+MAPCAR
+../Body/f_mapc_.htm
+MAPCON
+../Body/f_mapc_.htm
+MAPHASH
+../Body/f_maphas.htm
+MAPL
+../Body/f_mapc_.htm
+MAPLIST
+../Body/f_mapc_.htm
+MASK-FIELD
+../Body/f_mask_f.htm
+MAX
+../Body/f_max_m.htm
+MEMBER
+../Body/a_member.htm
+MEMBER-IF
+../Body/f_mem_m.htm
+MEMBER-IF-NOT
+../Body/f_mem_m.htm
+MERGE
+../Body/f_merge.htm
+MERGE-PATHNAMES
+../Body/f_merge_.htm
+METHOD
+../Body/t_method.htm
+METHOD-COMBINATION
+../Body/a_method.htm
+METHOD-COMBINATION-ERROR
+../Body/f_meth_1.htm
+METHOD-QUALIFIERS
+../Body/f_method.htm
+MIN
+../Body/f_max_m.htm
+MINUSP
+../Body/f_minusp.htm
+MISMATCH
+../Body/f_mismat.htm
+MOD
+../Body/a_mod.htm
+MOST-NEGATIVE-DOUBLE-FLOAT
+../Body/v_most_1.htm
+MOST-NEGATIVE-FIXNUM
+../Body/v_most_p.htm
+MOST-NEGATIVE-LONG-FLOAT
+../Body/v_most_1.htm
+MOST-NEGATIVE-SHORT-FLOAT
+../Body/v_most_1.htm
+MOST-NEGATIVE-SINGLE-FLOAT
+../Body/v_most_1.htm
+MOST-POSITIVE-DOUBLE-FLOAT
+../Body/v_most_1.htm
+MOST-POSITIVE-FIXNUM
+../Body/v_most_p.htm
+MOST-POSITIVE-LONG-FLOAT
+../Body/v_most_1.htm
+MOST-POSITIVE-SHORT-FLOAT
+../Body/v_most_1.htm
+MOST-POSITIVE-SINGLE-FLOAT
+../Body/v_most_1.htm
+MUFFLE-WARNING
+../Body/a_muffle.htm
+MULTIPLE-VALUE-BIND
+../Body/m_multip.htm
+MULTIPLE-VALUE-CALL
+../Body/s_multip.htm
+MULTIPLE-VALUE-LIST
+../Body/m_mult_1.htm
+MULTIPLE-VALUE-PROG1
+../Body/s_mult_1.htm
+MULTIPLE-VALUE-SETQ
+../Body/m_mult_2.htm
+MULTIPLE-VALUES-LIMIT
+../Body/v_multip.htm
+NAME-CHAR
+../Body/f_name_c.htm
+NAMESTRING
+../Body/f_namest.htm
+NBUTLAST
+../Body/f_butlas.htm
+NCONC
+../Body/f_nconc.htm
+NEXT-METHOD-P
+../Body/f_next_m.htm
+NIL
+../Body/a_nil.htm
+NINTERSECTION
+../Body/f_isec_.htm
+NINTH
+../Body/f_firstc.htm
+NO-APPLICABLE-METHOD
+../Body/f_no_app.htm
+NO-NEXT-METHOD
+../Body/f_no_nex.htm
+NOT
+../Body/a_not.htm
+NOTANY
+../Body/f_everyc.htm
+NOTEVERY
+../Body/f_everyc.htm
+NOTINLINE
+../Body/d_inline.htm
+NRECONC
+../Body/f_revapp.htm
+NREVERSE
+../Body/f_revers.htm
+NSET-DIFFERENCE
+../Body/f_set_di.htm
+NSET-EXCLUSIVE-OR
+../Body/f_set_ex.htm
+NSTRING-CAPITALIZE
+../Body/f_stg_up.htm
+NSTRING-DOWNCASE
+../Body/f_stg_up.htm
+NSTRING-UPCASE
+../Body/f_stg_up.htm
+NSUBLIS
+../Body/f_sublis.htm
+NSUBST
+../Body/f_substc.htm
+NSUBST-IF
+../Body/f_substc.htm
+NSUBST-IF-NOT
+../Body/f_substc.htm
+NSUBSTITUTE
+../Body/f_sbs_s.htm
+NSUBSTITUTE-IF
+../Body/f_sbs_s.htm
+NSUBSTITUTE-IF-NOT
+../Body/f_sbs_s.htm
+NTH
+../Body/f_nth.htm
+NTH-VALUE
+../Body/m_nth_va.htm
+NTHCDR
+../Body/f_nthcdr.htm
+NULL
+../Body/a_null.htm
+NUMBER
+../Body/t_number.htm
+NUMBERP
+../Body/f_nump.htm
+NUMERATOR
+../Body/f_numera.htm
+NUNION
+../Body/f_unionc.htm
+ODDP
+../Body/f_evenpc.htm
+OPEN
+../Body/f_open.htm
+OPEN-STREAM-P
+../Body/f_open_s.htm
+OPTIMIZE
+../Body/d_optimi.htm
+OR
+../Body/a_or.htm
+OTHERWISE
+../Body/m_case_.htm
+OUTPUT-STREAM-P
+../Body/f_in_stm.htm
+PACKAGE
+../Body/t_pkg.htm
+PACKAGE-ERROR
+../Body/e_pkg_er.htm
+PACKAGE-ERROR-PACKAGE
+../Body/f_pkg_er.htm
+PACKAGE-NAME
+../Body/f_pkg_na.htm
+PACKAGE-NICKNAMES
+../Body/f_pkg_ni.htm
+PACKAGE-SHADOWING-SYMBOLS
+../Body/f_pkg_sh.htm
+PACKAGE-USE-LIST
+../Body/f_pkg_us.htm
+PACKAGE-USED-BY-LIST
+../Body/f_pkg__1.htm
+PACKAGEP
+../Body/f_pkgp.htm
+PAIRLIS
+../Body/f_pairli.htm
+PARSE-ERROR
+../Body/e_parse_.htm
+PARSE-INTEGER
+../Body/f_parse_.htm
+PARSE-NAMESTRING
+../Body/f_pars_1.htm
+PATHNAME
+../Body/a_pn.htm
+PATHNAME-DEVICE
+../Body/f_pn_hos.htm
+PATHNAME-DIRECTORY
+../Body/f_pn_hos.htm
+PATHNAME-HOST
+../Body/f_pn_hos.htm
+PATHNAME-MATCH-P
+../Body/f_pn_mat.htm
+PATHNAME-NAME
+../Body/f_pn_hos.htm
+PATHNAME-TYPE
+../Body/f_pn_hos.htm
+PATHNAME-VERSION
+../Body/f_pn_hos.htm
+PATHNAMEP
+../Body/f_pnp.htm
+PEEK-CHAR
+../Body/f_peek_c.htm
+PHASE
+../Body/f_phase.htm
+PI
+../Body/v_pi.htm
+PLUSP
+../Body/f_minusp.htm
+POP
+../Body/m_pop.htm
+POSITION
+../Body/f_pos_p.htm
+POSITION-IF
+../Body/f_pos_p.htm
+POSITION-IF-NOT
+../Body/f_pos_p.htm
+PPRINT
+../Body/f_wr_pr.htm
+PPRINT-DISPATCH
+../Body/f_ppr_di.htm
+PPRINT-EXIT-IF-LIST-EXHAUSTED
+../Body/m_ppr_ex.htm
+PPRINT-FILL
+../Body/f_ppr_fi.htm
+PPRINT-INDENT
+../Body/f_ppr_in.htm
+PPRINT-LINEAR
+../Body/f_ppr_fi.htm
+PPRINT-LOGICAL-BLOCK
+../Body/m_ppr_lo.htm
+PPRINT-NEWLINE
+../Body/f_ppr_nl.htm
+PPRINT-POP
+../Body/m_ppr_po.htm
+PPRINT-TAB
+../Body/f_ppr_ta.htm
+PPRINT-TABULAR
+../Body/f_ppr_fi.htm
+PRIN1
+../Body/f_wr_pr.htm
+PRIN1-TO-STRING
+../Body/f_wr_to_.htm
+PRINC
+../Body/f_wr_pr.htm
+PRINC-TO-STRING
+../Body/f_wr_to_.htm
+PRINT
+../Body/f_wr_pr.htm
+PRINT-NOT-READABLE
+../Body/e_pr_not.htm
+PRINT-NOT-READABLE-OBJECT
+../Body/f_pr_not.htm
+PRINT-OBJECT
+../Body/f_pr_obj.htm
+PRINT-UNREADABLE-OBJECT
+../Body/m_pr_unr.htm
+PROBE-FILE
+../Body/f_probe_.htm
+PROCLAIM
+../Body/f_procla.htm
+PROG
+../Body/m_prog_.htm
+PROG*
+../Body/m_prog_.htm
+PROG1
+../Body/m_prog1c.htm
+PROG2
+../Body/m_prog1c.htm
+PROGN
+../Body/s_progn.htm
+PROGRAM-ERROR
+../Body/e_progra.htm
+PROGV
+../Body/s_progv.htm
+PROVIDE
+../Body/f_provid.htm
+PSETF
+../Body/m_setf_.htm
+PSETQ
+../Body/m_psetq.htm
+PUSH
+../Body/m_push.htm
+PUSHNEW
+../Body/m_pshnew.htm
+QUOTE
+../Body/s_quote.htm
+RANDOM
+../Body/f_random.htm
+RANDOM-STATE
+../Body/t_rnd_st.htm
+RANDOM-STATE-P
+../Body/f_rnd_st.htm
+RASSOC
+../Body/f_rassoc.htm
+RASSOC-IF
+../Body/f_rassoc.htm
+RASSOC-IF-NOT
+../Body/f_rassoc.htm
+RATIO
+../Body/t_ratio.htm
+RATIONAL
+../Body/a_ration.htm
+RATIONALIZE
+../Body/f_ration.htm
+RATIONALP
+../Body/f_rati_1.htm
+READ
+../Body/f_rd_rd.htm
+READ-BYTE
+../Body/f_rd_by.htm
+READ-CHAR
+../Body/f_rd_cha.htm
+READ-CHAR-NO-HANG
+../Body/f_rd_c_1.htm
+READ-DELIMITED-LIST
+../Body/f_rd_del.htm
+READ-FROM-STRING
+../Body/f_rd_fro.htm
+READ-LINE
+../Body/f_rd_lin.htm
+READ-PRESERVING-WHITESPACE
+../Body/f_rd_rd.htm
+READ-SEQUENCE
+../Body/f_rd_seq.htm
+READER-ERROR
+../Body/e_rder_e.htm
+READTABLE
+../Body/t_rdtabl.htm
+READTABLE-CASE
+../Body/f_rdtabl.htm
+READTABLEP
+../Body/f_rdta_1.htm
+REAL
+../Body/t_real.htm
+REALP
+../Body/f_realp.htm
+REALPART
+../Body/f_realpa.htm
+REDUCE
+../Body/f_reduce.htm
+REINITIALIZE-INSTANCE
+../Body/f_reinit.htm
+REM
+../Body/f_mod_r.htm
+REMF
+../Body/m_remf.htm
+REMHASH
+../Body/f_remhas.htm
+REMOVE
+../Body/f_rm_rm.htm
+REMOVE-DUPLICATES
+../Body/f_rm_dup.htm
+REMOVE-IF
+../Body/f_rm_rm.htm
+REMOVE-IF-NOT
+../Body/f_rm_rm.htm
+REMOVE-METHOD
+../Body/f_rm_met.htm
+REMPROP
+../Body/f_rempro.htm
+RENAME-FILE
+../Body/f_rn_fil.htm
+RENAME-PACKAGE
+../Body/f_rn_pkg.htm
+REPLACE
+../Body/f_replac.htm
+REQUIRE
+../Body/f_provid.htm
+REST
+../Body/f_rest.htm
+RESTART
+../Body/t_rst.htm
+RESTART-BIND
+../Body/m_rst_bi.htm
+RESTART-CASE
+../Body/m_rst_ca.htm
+RESTART-NAME
+../Body/f_rst_na.htm
+RETURN
+../Body/m_return.htm
+RETURN-FROM
+../Body/s_ret_fr.htm
+REVAPPEND
+../Body/f_revapp.htm
+REVERSE
+../Body/f_revers.htm
+ROOM
+../Body/f_room.htm
+ROTATEF
+../Body/m_rotate.htm
+ROUND
+../Body/f_floorc.htm
+ROW-MAJOR-AREF
+../Body/f_row_ma.htm
+RPLACA
+../Body/f_rplaca.htm
+RPLACD
+../Body/f_rplaca.htm
+SAFETY
+../Body/d_optimi.htm
+SATISFIES
+../Body/t_satisf.htm
+SBIT
+../Body/f_bt_sb.htm
+SCALE-FLOAT
+../Body/f_dec_fl.htm
+SCHAR
+../Body/f_char_.htm
+SEARCH
+../Body/f_search.htm
+SECOND
+../Body/f_firstc.htm
+SEQUENCE
+../Body/t_seq.htm
+SERIOUS-CONDITION
+../Body/e_seriou.htm
+SET
+../Body/f_set.htm
+SET-DIFFERENCE
+../Body/f_set_di.htm
+SET-DISPATCH-MACRO-CHARACTER
+../Body/f_set__1.htm
+SET-EXCLUSIVE-OR
+../Body/f_set_ex.htm
+SET-MACRO-CHARACTER
+../Body/f_set_ma.htm
+SET-PPRINT-DISPATCH
+../Body/f_set_pp.htm
+SET-SYNTAX-FROM-CHAR
+../Body/f_set_sy.htm
+SETF
+../Body/a_setf.htm
+SETQ
+../Body/s_setq.htm
+SEVENTH
+../Body/f_firstc.htm
+SHADOW
+../Body/f_shadow.htm
+SHADOWING-IMPORT
+../Body/f_shdw_i.htm
+SHARED-INITIALIZE
+../Body/f_shared.htm
+SHIFTF
+../Body/m_shiftf.htm
+SHORT-FLOAT
+../Body/t_short_.htm
+SHORT-FLOAT-EPSILON
+../Body/v_short_.htm
+SHORT-FLOAT-NEGATIVE-EPSILON
+../Body/v_short_.htm
+SHORT-SITE-NAME
+../Body/f_short_.htm
+SIGNAL
+../Body/f_signal.htm
+SIGNED-BYTE
+../Body/t_sgn_by.htm
+SIGNUM
+../Body/f_signum.htm
+SIMPLE-ARRAY
+../Body/t_smp_ar.htm
+SIMPLE-BASE-STRING
+../Body/t_smp_ba.htm
+SIMPLE-BIT-VECTOR
+../Body/t_smp_bt.htm
+SIMPLE-BIT-VECTOR-P
+../Body/f_smp_bt.htm
+SIMPLE-CONDITION
+../Body/e_smp_cn.htm
+SIMPLE-CONDITION-FORMAT-ARGUMENTS
+../Body/f_smp_cn.htm
+SIMPLE-CONDITION-FORMAT-CONTROL
+../Body/f_smp_cn.htm
+SIMPLE-ERROR
+../Body/e_smp_er.htm
+SIMPLE-STRING
+../Body/t_smp_st.htm
+SIMPLE-STRING-P
+../Body/f_smp_st.htm
+SIMPLE-TYPE-ERROR
+../Body/e_smp_tp.htm
+SIMPLE-VECTOR
+../Body/t_smp_ve.htm
+SIMPLE-VECTOR-P
+../Body/f_smp_ve.htm
+SIMPLE-WARNING
+../Body/e_smp_wa.htm
+SIN
+../Body/f_sin_c.htm
+SINGLE-FLOAT
+../Body/t_short_.htm
+SINGLE-FLOAT-EPSILON
+../Body/v_short_.htm
+SINGLE-FLOAT-NEGATIVE-EPSILON
+../Body/v_short_.htm
+SINH
+../Body/f_sinh_.htm
+SIXTH
+../Body/f_firstc.htm
+SLEEP
+../Body/f_sleep.htm
+SLOT-BOUNDP
+../Body/f_slt_bo.htm
+SLOT-EXISTS-P
+../Body/f_slt_ex.htm
+SLOT-MAKUNBOUND
+../Body/f_slt_ma.htm
+SLOT-MISSING
+../Body/f_slt_mi.htm
+SLOT-UNBOUND
+../Body/f_slt_un.htm
+SLOT-VALUE
+../Body/f_slt_va.htm
+SOFTWARE-TYPE
+../Body/f_sw_tpc.htm
+SOFTWARE-VERSION
+../Body/f_sw_tpc.htm
+SOME
+../Body/f_everyc.htm
+SORT
+../Body/f_sort_.htm
+SPACE
+../Body/d_optimi.htm
+SPECIAL
+../Body/d_specia.htm
+SPECIAL-OPERATOR-P
+../Body/f_specia.htm
+SPEED
+../Body/d_optimi.htm
+SQRT
+../Body/f_sqrt_.htm
+STABLE-SORT
+../Body/f_sort_.htm
+STANDARD
+../Body/07_ffb.htm
+STANDARD-CHAR
+../Body/t_std_ch.htm
+STANDARD-CHAR-P
+../Body/f_std_ch.htm
+STANDARD-CLASS
+../Body/t_std_cl.htm
+STANDARD-GENERIC-FUNCTION
+../Body/t_std_ge.htm
+STANDARD-METHOD
+../Body/t_std_me.htm
+STANDARD-OBJECT
+../Body/t_std_ob.htm
+STEP
+../Body/m_step.htm
+STORAGE-CONDITION
+../Body/e_storag.htm
+STORE-VALUE
+../Body/a_store_.htm
+STREAM
+../Body/t_stream.htm
+STREAM-ELEMENT-TYPE
+../Body/f_stm_el.htm
+STREAM-ERROR
+../Body/e_stm_er.htm
+STREAM-ERROR-STREAM
+../Body/f_stm_er.htm
+STREAM-EXTERNAL-FORMAT
+../Body/f_stm_ex.htm
+STREAMP
+../Body/f_stmp.htm
+STRING
+../Body/a_string.htm
+STRING-CAPITALIZE
+../Body/f_stg_up.htm
+STRING-DOWNCASE
+../Body/f_stg_up.htm
+STRING-EQUAL
+../Body/f_stgeq_.htm
+STRING-GREATERP
+../Body/f_stgeq_.htm
+STRING-LEFT-TRIM
+../Body/f_stg_tr.htm
+STRING-LESSP
+../Body/f_stgeq_.htm
+STRING-NOT-EQUAL
+../Body/f_stgeq_.htm
+STRING-NOT-GREATERP
+../Body/f_stgeq_.htm
+STRING-NOT-LESSP
+../Body/f_stgeq_.htm
+STRING-RIGHT-TRIM
+../Body/f_stg_tr.htm
+STRING-STREAM
+../Body/t_stg_st.htm
+STRING-TRIM
+../Body/f_stg_tr.htm
+STRING-UPCASE
+../Body/f_stg_up.htm
+STRING/=
+../Body/f_stgeq_.htm
+STRING<
+../Body/f_stgeq_.htm
+STRING<=
+../Body/f_stgeq_.htm
+STRING=
+../Body/f_stgeq_.htm
+STRING>
+../Body/f_stgeq_.htm
+STRING>=
+../Body/f_stgeq_.htm
+STRINGP
+../Body/f_stgp.htm
+STRUCTURE
+../Body/f_docume.htm
+STRUCTURE-CLASS
+../Body/t_stu_cl.htm
+STRUCTURE-OBJECT
+../Body/t_stu_ob.htm
+STYLE-WARNING
+../Body/e_style_.htm
+SUBLIS
+../Body/f_sublis.htm
+SUBSEQ
+../Body/f_subseq.htm
+SUBSETP
+../Body/f_subset.htm
+SUBST
+../Body/f_substc.htm
+SUBST-IF
+../Body/f_substc.htm
+SUBST-IF-NOT
+../Body/f_substc.htm
+SUBSTITUTE
+../Body/f_sbs_s.htm
+SUBSTITUTE-IF
+../Body/f_sbs_s.htm
+SUBSTITUTE-IF-NOT
+../Body/f_sbs_s.htm
+SUBTYPEP
+../Body/f_subtpp.htm
+SVREF
+../Body/f_svref.htm
+SXHASH
+../Body/f_sxhash.htm
+SYMBOL
+../Body/t_symbol.htm
+SYMBOL-FUNCTION
+../Body/f_symb_1.htm
+SYMBOL-MACROLET
+../Body/s_symbol.htm
+SYMBOL-NAME
+../Body/f_symb_2.htm
+SYMBOL-PACKAGE
+../Body/f_symb_3.htm
+SYMBOL-PLIST
+../Body/f_symb_4.htm
+SYMBOL-VALUE
+../Body/f_symb_5.htm
+SYMBOLP
+../Body/f_symbol.htm
+SYNONYM-STREAM
+../Body/t_syn_st.htm
+SYNONYM-STREAM-SYMBOL
+../Body/f_syn_st.htm
+T
+../Body/a_t.htm
+TAGBODY
+../Body/s_tagbod.htm
+TAILP
+../Body/f_ldiffc.htm
+TAN
+../Body/f_sin_c.htm
+TANH
+../Body/f_sinh_.htm
+TENTH
+../Body/f_firstc.htm
+TERPRI
+../Body/f_terpri.htm
+THE
+../Body/s_the.htm
+THIRD
+../Body/f_firstc.htm
+THROW
+../Body/s_throw.htm
+TIME
+../Body/m_time.htm
+TRACE
+../Body/m_tracec.htm
+TRANSLATE-LOGICAL-PATHNAME
+../Body/f_tr_log.htm
+TRANSLATE-PATHNAME
+../Body/f_tr_pn.htm
+TREE-EQUAL
+../Body/f_tree_e.htm
+TRUENAME
+../Body/f_tn.htm
+TRUNCATE
+../Body/f_floorc.htm
+TWO-WAY-STREAM
+../Body/t_two_wa.htm
+TWO-WAY-STREAM-INPUT-STREAM
+../Body/f_two_wa.htm
+TWO-WAY-STREAM-OUTPUT-STREAM
+../Body/f_two_wa.htm
+TYPE
+../Body/a_type.htm
+TYPE-ERROR
+../Body/e_tp_err.htm
+TYPE-ERROR-DATUM
+../Body/f_tp_err.htm
+TYPE-ERROR-EXPECTED-TYPE
+../Body/f_tp_err.htm
+TYPE-OF
+../Body/f_tp_of.htm
+TYPECASE
+../Body/m_tpcase.htm
+TYPEP
+../Body/f_typep.htm
+UNBOUND-SLOT
+../Body/e_unboun.htm
+UNBOUND-SLOT-INSTANCE
+../Body/f_unboun.htm
+UNBOUND-VARIABLE
+../Body/e_unbo_1.htm
+UNDEFINED-FUNCTION
+../Body/e_undefi.htm
+UNEXPORT
+../Body/f_unexpo.htm
+UNINTERN
+../Body/f_uninte.htm
+UNION
+../Body/f_unionc.htm
+UNLESS
+../Body/m_when_.htm
+UNREAD-CHAR
+../Body/f_unrd_c.htm
+UNSIGNED-BYTE
+../Body/t_unsgn_.htm
+UNTRACE
+../Body/m_tracec.htm
+UNUSE-PACKAGE
+../Body/f_unuse_.htm
+UNWIND-PROTECT
+../Body/s_unwind.htm
+UPDATE-INSTANCE-FOR-DIFFERENT-CLASS
+../Body/f_update.htm
+UPDATE-INSTANCE-FOR-REDEFINED-CLASS
+../Body/f_upda_1.htm
+UPGRADED-ARRAY-ELEMENT-TYPE
+../Body/f_upgr_1.htm
+UPGRADED-COMPLEX-PART-TYPE
+../Body/f_upgrad.htm
+UPPER-CASE-P
+../Body/f_upper_.htm
+USE-PACKAGE
+../Body/f_use_pk.htm
+USE-VALUE
+../Body/a_use_va.htm
+USER-HOMEDIR-PATHNAME
+../Body/f_user_h.htm
+VALUES
+../Body/a_values.htm
+VALUES-LIST
+../Body/f_vals_l.htm
+VARIABLE
+../Body/f_docume.htm
+VECTOR
+../Body/a_vector.htm
+VECTOR-POP
+../Body/f_vec_po.htm
+VECTOR-PUSH
+../Body/f_vec_ps.htm
+VECTOR-PUSH-EXTEND
+../Body/f_vec_ps.htm
+VECTORP
+../Body/f_vecp.htm
+WARN
+../Body/f_warn.htm
+WARNING
+../Body/e_warnin.htm
+WHEN
+../Body/m_when_.htm
+WILD-PATHNAME-P
+../Body/f_wild_p.htm
+WITH-ACCESSORS
+../Body/m_w_acce.htm
+WITH-COMPILATION-UNIT
+../Body/m_w_comp.htm
+WITH-CONDITION-RESTARTS
+../Body/m_w_cnd_.htm
+WITH-HASH-TABLE-ITERATOR
+../Body/m_w_hash.htm
+WITH-INPUT-FROM-STRING
+../Body/m_w_in_f.htm
+WITH-OPEN-FILE
+../Body/m_w_open.htm
+WITH-OPEN-STREAM
+../Body/m_w_op_1.htm
+WITH-OUTPUT-TO-STRING
+../Body/m_w_out_.htm
+WITH-PACKAGE-ITERATOR
+../Body/m_w_pkg_.htm
+WITH-SIMPLE-RESTART
+../Body/m_w_smp_.htm
+WITH-SLOTS
+../Body/m_w_slts.htm
+WITH-STANDARD-IO-SYNTAX
+../Body/m_w_std_.htm
+WRITE
+../Body/f_wr_pr.htm
+WRITE-BYTE
+../Body/f_wr_by.htm
+WRITE-CHAR
+../Body/f_wr_cha.htm
+WRITE-LINE
+../Body/f_wr_stg.htm
+WRITE-SEQUENCE
+../Body/f_wr_seq.htm
+WRITE-STRING
+../Body/f_wr_stg.htm
+WRITE-TO-STRING
+../Body/f_wr_to_.htm
+Y-OR-N-P
+../Body/f_y_or_n.htm
+YES-OR-NO-P
+../Body/f_y_or_n.htm
+ZEROP
+../Body/f_zerop.htm
diff --git a/HyperSpec/Mop_Sym.txt b/HyperSpec/Mop_Sym.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/HyperSpec/Mop_Sym.txt
@@ -0,0 +1 @@
+
diff --git a/package.lisp b/package.lisp
new file mode 100644
index 0000000..5861fe3
--- /dev/null
+++ b/package.lisp
@@ -0,0 +1,4 @@
+(defpackage :cl-bbs/server
+ (:use :cl)
+ (:export #:start-app
+ #:stop-app))
diff --git a/server/admin.lisp b/server/admin.lisp
new file mode 100644
index 0000000..3864fad
--- /dev/null
+++ b/server/admin.lisp
@@ -0,0 +1,574 @@
+(defpackage :cl-bbs-admin
+ (:use :cl)
+ (:export #:main))
+
+(in-package :cl-bbs-admin)
+
+(defun expand-tilde (path)
+ (if (and (>= (length path) 2) (string= (subseq path 0 2) "~/"))
+ (concatenate 'string (namestring (user-homedir-pathname)) (subseq path 2))
+ path))
+
+(defun get-data-dir ()
+ (let ((env (uiop:getenv "SBBS_DATADIR")))
+ (if (and env (not (string= env "")))
+ (namestring (uiop:ensure-absolute-pathname (expand-tilde env) (uiop:getcwd)))
+ (let ((root-dir (asdf:system-source-directory :cl-bbs/server)))
+ (if root-dir
+ (namestring (merge-pathnames "data/" root-dir))
+ (namestring (merge-pathnames "bbs/" (user-homedir-pathname))))))))
+
+(defun lookup-def (key alist)
+ (let ((p (assoc key alist :test (lambda (k1 k2) (string-equal (string k1) (string k2))))))
+ (if p
+ (let ((res (cdr p)))
+ (cond ((string-equal (string key) "posts") res)
+ ((not (consp res)) res)
+ ((null (cdr res)) (car res))
+ (t res)))
+ nil)))
+
+(defun last-element (lst)
+ (car (last lst)))
+
+(defun take-right (lst n)
+ (let ((len (length lst)))
+ (if (<= len n)
+ lst
+ (nthcdr (- len n) lst))))
+
+(defun take (lst n)
+ (if (or (null lst) (<= n 0))
+ nil
+ (cons (car lst) (take (cdr lst) (1- n)))))
+
+(defun latest-posts (posts)
+ (if (> (length posts) 6)
+ `((cl-bbs/models:truncated . ,(mapcar #'car (butlast (cdr posts) 5)))
+ (cl-bbs/models:posts ,(cons (car posts) (take-right posts 5))))
+ `((cl-bbs/models:truncated . nil) (cl-bbs/models:posts ,posts))))
+
+(defun get-flat-posts (posts)
+ (when posts
+ (let* ((cdr-val posts)
+ (cadr-val (and (listp cdr-val) (car cdr-val))))
+ (if (and (listp cadr-val) (listp (car cadr-val)))
+ cadr-val
+ cdr-val))))
+
+(defun build-list-entry (t-entry)
+ (let* ((id (car t-entry))
+ (thread-data (cdr t-entry))
+ (headline (lookup-def 'cl-bbs/models:headline thread-data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts thread-data)))
+ (last-post (last-element posts))
+ (date (lookup-def 'cl-bbs/models:date (cdr last-post)))
+ (messages (length posts)))
+ `(,id (cl-bbs/models:headline . ,headline) (cl-bbs/models:date . ,date) (cl-bbs/models:messages . ,messages))))
+
+(defun build-index-entry (t-entry)
+ (let* ((id (car t-entry))
+ (thread-data (cdr t-entry))
+ (headline (lookup-def 'cl-bbs/models:headline thread-data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts thread-data))))
+ `(,id (cl-bbs/models:headline . ,headline) . ,(latest-posts posts))))
+
+(defun read-sexp-file (path)
+ (with-open-file (stream path :direction :input :if-does-not-exist nil)
+ (if stream
+ (let ((*read-eval* nil))
+ (read stream nil nil))
+ nil)))
+
+(defun write-sexp-file (path data)
+ (with-open-file (stream path :direction :output :if-exists :supersede
+ :if-does-not-exist :create
+ :external-format :utf-8)
+ (write data :stream stream :pretty t)
+ (terpri stream)))
+
+(defun get-threads (dir)
+ (let ((threads nil))
+ (loop for i from 1
+ for misses = 0 then (if exists 0 (1+ misses))
+ for filepath = (format nil "~A~D" dir i)
+ for exists = (probe-file filepath)
+ while (<= misses 200)
+ do (when exists
+ (let ((data (read-sexp-file filepath)))
+ (when data
+ (push (cons i data) threads)))))
+ threads))
+
+(defun generate-index (board)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (dir (format nil "~A/sexp/~A/" data-dir board))
+ (threads-data (get-threads dir)))
+ (unless threads-data
+ (format t "No threads found for board ~A.~%" board)
+ (return-from generate-index nil))
+ (let* ((sorted-threads
+ (sort threads-data
+ (lambda (a b)
+ (let* ((posts-a (get-flat-posts (lookup-def 'cl-bbs/models:posts (cdr a))))
+ (posts-b (get-flat-posts (lookup-def 'cl-bbs/models:posts (cdr b))))
+ (date-a (or (lookup-def 'cl-bbs/models:date (cdr (last-element posts-a))) ""))
+ (date-b (or (lookup-def 'cl-bbs/models:date (cdr (last-element posts-b))) "")))
+ (string> date-a date-b)))))
+ (list-entries (mapcar #'build-list-entry sorted-threads))
+ (frontpage-count 10)
+ (index-entries (mapcar #'build-index-entry
+ (if (> (length sorted-threads) frontpage-count)
+ (take sorted-threads frontpage-count)
+ sorted-threads)))
+ (list-path (format nil "~Alist" dir))
+ (index-path (format nil "~Aindex" dir)))
+ (write-sexp-file list-path list-entries)
+ (write-sexp-file index-path index-entries)
+ (format t "Generated list and index for ~A~%" board))))
+
+(defun get-iso-datetime ()
+ (multiple-value-bind (second minute hour date month year)
+ (get-decoded-time)
+ (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D"
+ year month date hour minute second)))
+
+(defun read-backup-metadata (archive-name)
+ (let ((output (make-string-output-stream)))
+ (handler-case
+ (progn
+ (uiop:run-program (list "tar" "-xzf" archive-name "-O" "metadata.txt")
+ :output output :error-output nil)
+ (string-trim '(#\Space #\Tab #\Newline #\Return) (get-output-stream-string output)))
+ (error () ""))))
+
+(defun list-backups ()
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (backup-dir (format nil "~A/backup" data-dir))
+ (backups (and (probe-file backup-dir)
+ (sort (directory (format nil "~A/*.tar.gz" backup-dir))
+ #'string> :key #'namestring))))
+ (if backups
+ (progn
+ (format t "Available backups:~%")
+ (dolist (b backups)
+ (let ((meta (read-backup-metadata (namestring b))))
+ (format t " ~A ~A~%" (file-namestring b)
+ (if (and meta (not (string= meta "")))
+ (format nil "- ~A" meta)
+ "")))))
+ (format t "No backups found.~%"))))
+
+(defun ask-confirmation (prompt)
+ (format t "~A [y/N]: " prompt)
+ (force-output)
+ (let ((response (read-line *standard-input* nil "")))
+ (or (string-equal response "y")
+ (string-equal response "yes"))))
+
+(defun backup (&optional arg1)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (backup-dir (format nil "~A/backup" data-dir))
+ (is-filename (and arg1 (uiop:string-suffix-p arg1 ".tar.gz")))
+ (message (if is-filename nil arg1))
+ (archive-name (if is-filename
+ arg1
+ (format nil "~A/sbbs-~A.tar.gz" backup-dir (get-iso-datetime))))
+ (metadata-file (format nil "~A/metadata.txt" backup-dir)))
+ (ensure-directories-exist (format nil "~A/" backup-dir))
+ (when message
+ (with-open-file (stream metadata-file :direction :output :if-does-not-exist :create :if-exists :supersede)
+ (write-line message stream)))
+ (if message
+ (uiop:run-program (list "tar" "-czf" archive-name "-C" data-dir "sexp" "-C" backup-dir "metadata.txt")
+ :output *standard-output* :error-output *error-output*)
+ (uiop:run-program (list "tar" "-czf" archive-name "-C" data-dir "sexp")
+ :output *standard-output* :error-output *error-output*))
+ (when (and message (probe-file metadata-file))
+ (delete-file metadata-file))
+ (format t "Backup created: ~A~%" archive-name)))
+
+(defun restore (&optional archive-name)
+ (if archive-name
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (backup-dir (format nil "~A/backup" data-dir))
+ (archive-path (if (probe-file archive-name)
+ archive-name
+ (format nil "~A/~A" backup-dir archive-name))))
+ (if (probe-file archive-path)
+ (progn
+ (uiop:run-program (list "tar" "-xzf" archive-path "-C" data-dir)
+ :output *standard-output* :error-output *error-output*)
+ (format t "Restore completed from: ~A~%" archive-path))
+ (format t "Archive not found: ~A~%" archive-name)))
+ (list-backups)))
+
+(defun remove-post (board post-id)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-file (format nil "~A/sexp/~A/~A" data-dir board post-id)))
+ (if (probe-file sexp-file)
+ (let* ((data (read-sexp-file sexp-file))
+ (headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (first-post (car posts))
+ (date (lookup-def 'cl-bbs/models:date (cdr first-post)))
+ (content (lookup-def 'cl-bbs/models:content (cdr first-post))))
+ (format t "Post Headline: ~A~%" headline)
+ (format t "Post Date: ~A~%" date)
+ (format t "Post Content:~%~A~%" content)
+ (when (ask-confirmation (format nil "Are you sure you want to remove thread ~A?" post-id))
+ (backup (format nil "Before removing thread ~A from board ~A" post-id board))
+ (delete-file sexp-file)
+ (format t "Removed thread ~A.~%" sexp-file)
+ (generate-index board)))
+ (format t "Thread ~A does not exist.~%" sexp-file))))
+
+(defun remove-comment (board post-id comment-id-str)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-file (format nil "~A/sexp/~A/~A" data-dir board post-id))
+ (comment-id (parse-integer comment-id-str :junk-allowed t)))
+ (unless (probe-file sexp-file)
+ (format t "Thread ~A does not exist.~%" sexp-file)
+ (return-from remove-comment nil))
+ (let* ((data (read-sexp-file sexp-file))
+ (headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (comment (find comment-id posts :key #'car)))
+ (if (not comment)
+ (format t "Comment ~A not found in thread ~A.~%" comment-id post-id)
+ (let ((comment-content (lookup-def 'cl-bbs/models:content (cdr comment)))
+ (comment-date (lookup-def 'cl-bbs/models:date (cdr comment))))
+ (format t "Post Headline: ~A~%" headline)
+ (format t "Comment Date: ~A~%" comment-date)
+ (format t "Comment Content:~%~A~%" comment-content)
+ (when (ask-confirmation (format nil "Are you sure you want to remove comment ~A from thread ~A?"
+ comment-id post-id))
+ (backup (format nil "Before removing comment ~A from thread ~A on board ~A" comment-id post-id board))
+ (let ((new-posts (remove-if (lambda (p) (= (car p) comment-id)) posts)))
+ (if (null new-posts)
+ (progn
+ (when (probe-file sexp-file) (delete-file sexp-file))
+ (format t "Removed thread ~A entirely as it has no remaining comments.~%" post-id))
+ (progn
+ (write-sexp-file sexp-file `((cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:posts ,new-posts)))
+ (format t "Removed comment ~A from thread ~A.~%" comment-id post-id)))
+ (generate-index board))))))))
+
+(defun edit-comment (board post-id comment-id-str)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-file (format nil "~A/sexp/~A/~A" data-dir board post-id))
+ (comment-id (parse-integer comment-id-str :junk-allowed t)))
+ (unless (probe-file sexp-file)
+ (format t "Thread ~A does not exist.~%" sexp-file)
+ (return-from edit-comment nil))
+ (let* ((data (read-sexp-file sexp-file))
+ (headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (comment (find comment-id posts :key #'car)))
+ (if (not comment)
+ (format t "Comment ~A not found in thread ~A.~%" comment-id post-id)
+ (let* ((content-cell (assoc 'cl-bbs/models:content (cdr comment)))
+ (content-value (cdr content-cell)))
+ (uiop:with-temporary-file (:pathname tmp-path :keep t)
+ (with-open-file (stream tmp-path :direction :output :if-exists :supersede
+ :external-format :utf-8)
+ (write-line content-value stream))
+ (let ((editor (or (uiop:getenv "EDITOR") "vi")))
+ (format t "Opening ~A with ~A...~%" tmp-path editor)
+ (uiop:run-program (format nil "~A ~A" editor (namestring tmp-path))
+ :output :interactive
+ :input :interactive
+ :error-output :interactive)
+ (let ((new-content-value (uiop:read-file-string tmp-path)))
+ (delete-file tmp-path)
+ (when (and new-content-value (string/= new-content-value ""))
+ (backup (format nil "Before editing comment ~A from thread ~A on board ~A"
+ comment-id post-id board))
+ (let ((new-posts (mapcar (lambda (p)
+ (if (= (car p) comment-id)
+ (cons (car p)
+ (mapcar (lambda (kv)
+ (if (eq (car kv) 'cl-bbs/models:content)
+ (cons (car kv)
+ (string-right-trim '(#\Newline #\Return)
+ new-content-value))
+ kv))
+ (cdr p)))
+ p))
+ posts)))
+ (write-sexp-file sexp-file `((cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:posts ,new-posts)))
+ (format t "Edited comment ~A from thread ~A.~%" comment-id post-id)
+ (generate-index board)))))))))))
+
+(defun get-next-post-id (board)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-dir (format nil "~A/sexp/~A/" data-dir board))
+ (files (and (probe-file sexp-dir)
+ (uiop:directory-files (uiop:ensure-absolute-pathname sexp-dir (uiop:getcwd)))))
+ (max-id 0))
+ (dolist (file files)
+ (let ((id (handler-case (parse-integer (pathname-name file))
+ (error () nil))))
+ (when (and id (> id max-id))
+ (setf max-id id))))
+ (1+ max-id)))
+
+(defun move-post (source-board post-id target-board)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (source-file (format nil "~A/sexp/~A/~A" data-dir source-board post-id))
+ (target-dir (format nil "~A/sexp/~A/" data-dir target-board)))
+ (if (probe-file source-file)
+ (progn
+ (unless (uiop:directory-exists-p (uiop:ensure-absolute-pathname target-dir (uiop:getcwd)))
+ (format t "Target board ~A does not exist.~%" target-board)
+ (return-from move-post nil))
+ (let* ((new-id (get-next-post-id target-board))
+ (target-file (format nil "~A~A" target-dir new-id)))
+ (backup (format nil "Before moving thread ~A from ~A to ~A as ~A" post-id source-board target-board new-id))
+ (uiop:copy-file source-file target-file)
+ (delete-file source-file)
+ (format t "Moved thread ~A from ~A to ~A as thread ~A.~%" post-id source-board target-board new-id)
+ (generate-index source-board)
+ (generate-index target-board)))
+ (format t "Thread ~A does not exist in board ~A.~%" post-id source-board))))
+
+(defun parse-post-date (date-string)
+ (let ((year (parse-integer date-string :start 0 :end 4))
+ (month (parse-integer date-string :start 5 :end 7))
+ (day (parse-integer date-string :start 8 :end 10))
+ (hour (parse-integer date-string :start 11 :end 13))
+ (minute (parse-integer date-string :start 14 :end 16)))
+ (encode-universal-time 0 minute hour day month year 0)))
+
+(defun format-post-date (universal-time)
+ (multiple-value-bind (second minute hour day month year)
+ (decode-universal-time universal-time 0)
+ (declare (ignore second))
+ (format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D" year month day hour minute)))
+
+(defun add-timezone-offset (date-string offset-hours)
+ (let* ((ut (parse-post-date date-string))
+ (new-ut (+ ut (* offset-hours 3600))))
+ (format-post-date new-ut)))
+
+(defun list-all (&optional board post-id-str)
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-dir (format nil "~A/sexp/" data-dir)))
+ (cond
+ ((null board)
+ (let ((boards (and (probe-file sexp-dir)
+ (uiop:subdirectories (uiop:ensure-absolute-pathname sexp-dir (uiop:getcwd))))))
+ (if boards
+ (progn
+ (format t "Boards:~%")
+ (dolist (b-path boards)
+ (format t " ~A~%" (car (last (pathname-directory b-path))))))
+ (format t "No boards found in ~A~%" sexp-dir))))
+ ((and board post-id-str)
+ (let ((sexp-file (format nil "~A/sexp/~A/~A" data-dir board post-id-str))
+ (post-id (parse-integer post-id-str :junk-allowed t)))
+ (if (and post-id (probe-file sexp-file))
+ (let* ((data (read-sexp-file sexp-file))
+ (headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data))))
+ (format t "Thread ~A: ~A~%" post-id-str headline)
+ (dolist (p posts)
+ (let* ((cid (car p))
+ (pdata (cdr p))
+ (date (lookup-def 'cl-bbs/models:date pdata))
+ (author (lookup-def 'cl-bbs/models:name pdata))
+ (content (lookup-def 'cl-bbs/models:content pdata)))
+ (format t "~%[~A] #~A by ~A~%" date cid (or author "Anonymous"))
+ (format t "~A~%" content))))
+ (format t "Thread ~A not found in board ~A.~%" post-id-str board))))
+ (board
+ (let* ((board-dir (format nil "~A/sexp/~A/" data-dir board))
+ (threads-data (get-threads board-dir)))
+ (if threads-data
+ (let ((sorted (sort threads-data
+ (lambda (a b)
+ (let* ((posts-a (get-flat-posts (lookup-def 'cl-bbs/models:posts (cdr a))))
+ (last-a (last-element posts-a))
+ (date-a (lookup-def 'cl-bbs/models:date (cdr last-a)))
+ (ut-a (parse-post-date date-a))
+ (posts-b (get-flat-posts (lookup-def 'cl-bbs/models:posts (cdr b))))
+ (last-b (last-element posts-b))
+ (date-b (lookup-def 'cl-bbs/models:date (cdr last-b)))
+ (ut-b (parse-post-date date-b)))
+ (> ut-a ut-b))))))
+ (progn
+ (format t "Threads in board ~A:~%" board)
+ (format t "~10A ~20A ~A~%" "ID" "Last Update" "Headline")
+ (format t "~v@{~A~:*~}~%" 60 "-")
+ (dolist (t-data sorted)
+ (let* ((tid (car t-data))
+ (t-content (cdr t-data))
+ (headline (lookup-def 'cl-bbs/models:headline t-content))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts t-content)))
+ (last-date (lookup-def 'cl-bbs/models:date (cdr (last-element posts)))))
+ (format t "~10D ~20A ~A~%" tid last-date headline)))))
+ (format t "No threads found in board ~A.~%" board)))))))
+
+(defun set-timezone (offset-hours)
+ (backup (format nil "Before setting timezone offset to ~D" offset-hours))
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-dir (format nil "~A/sexp/" data-dir))
+ (boards (uiop:subdirectories (uiop:ensure-absolute-pathname sexp-dir (uiop:getcwd)))))
+ (dolist (board-path boards)
+ (let ((board (car (last (pathname-directory board-path))))
+ (threads (uiop:directory-files board-path)))
+ (dolist (thread threads)
+ (when (handler-case (parse-integer (pathname-name thread))
+ (error () nil))
+ (let* ((data (read-sexp-file thread))
+ (headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (new-posts (mapcar (lambda (p)
+ (let* ((post-id (car p))
+ (post-data (cdr p))
+ (old-date (lookup-def 'cl-bbs/models:date post-data))
+ (new-date (add-timezone-offset old-date offset-hours)))
+ (cons post-id
+ (mapcar (lambda (kv)
+ (if (eq (car kv) 'cl-bbs/models:date)
+ (cons (car kv) new-date)
+ kv))
+ post-data))))
+ posts)))
+ (write-sexp-file thread `((cl-bbs/models:headline . ,headline) (cl-bbs/models:posts ,new-posts)))))
+ (generate-index board))))))
+
+(defun find-duplicates (posts)
+ (let ((duplicates nil)
+ (prev-post (car posts)))
+ (dolist (curr-post (cdr posts))
+ (let ((prev-content (lookup-def 'cl-bbs/models:content (cdr prev-post)))
+ (curr-content (lookup-def 'cl-bbs/models:content (cdr curr-post))))
+ (if (equal prev-content curr-content)
+ (push curr-post duplicates)
+ (setf prev-post curr-post))))
+ (nreverse duplicates)))
+
+(defun remove-duplicates-command ()
+ (let* ((data-dir (string-right-trim "/" (get-data-dir)))
+ (sexp-dir (format nil "~A/sexp/" data-dir))
+ (boards (uiop:subdirectories (uiop:ensure-absolute-pathname sexp-dir (uiop:getcwd))))
+ (all-duplicates nil))
+ (dolist (board-path boards)
+ (let ((board (car (last (pathname-directory board-path))))
+ (threads (uiop:directory-files board-path)))
+ (dolist (thread-path threads)
+ (let ((thread-id (pathname-name thread-path)))
+ (when (handler-case (parse-integer thread-id)
+ (error () nil))
+ (let* ((data (read-sexp-file thread-path))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (dups (find-duplicates posts)))
+ (when dups
+ (push (list board thread-id dups thread-path data) all-duplicates)))))))
+ (if (null all-duplicates)
+ (format t "No sequential duplicate comments found.~%")
+ (progn
+ (format t "Found sequential duplicate comments:~%~%")
+ (dolist (entry (reverse all-duplicates))
+ (destructuring-bind (board thread-id dups thread-path data) entry
+ (declare (ignore thread-path data))
+ (format t "Board: ~A, Thread: ~A~%" board thread-id)
+ (dolist (dup dups)
+ (let ((comment-id (car dup))
+ (content (lookup-def 'cl-bbs/models:content (cdr dup))))
+ (format t " - Duplicate Comment ID: ~A~%" comment-id)
+ (format t " Content: ~A~%" content)))))
+ (format t "~%")
+ (when (ask-confirmation "Are you sure you want to delete these duplicate comments?")
+ (backup "Before removing sequential duplicate comments")
+ (let ((affected-boards nil))
+ (dolist (entry all-duplicates)
+ (destructuring-bind (board thread-id dups thread-path data) entry
+ (let* ((headline (lookup-def 'cl-bbs/models:headline data))
+ (posts (get-flat-posts (lookup-def 'cl-bbs/models:posts data)))
+ (dup-ids (mapcar #'car dups))
+ (new-posts (remove-if (lambda (p) (member (car p) dup-ids)) posts)))
+ (write-sexp-file thread-path `((cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:posts ,new-posts)))
+ (pushnew board affected-boards :test #'string-equal)
+ (format t "Removed ~D duplicates from ~A/~A~%" (length dups) board thread-id))))
+ (dolist (board affected-boards)
+ (generate-index board)))))))))
+
+(defun print-help ()
+ (format t "Usage: cl-bbs-admin <command> [args...]~%~%")
+ (format t "Commands:~%")
+ (format t " generate-index <board> Regenerate index and list files for a board~%")
+ (format t " list [board] [post-id] List boards, threads in a board, or comments in a thread~%")
+ (format t " remove-post <board> <post-id> Remove an entire thread/post~%")
+ (format t " remove-duplicates Remove sequential duplicate comments across all boards~%")
+ (format t " remove-comment <board> <post-id> <comment-id> Remove a specific comment from a thread~%")
+ (format t " edit <board> <post-id> <comment-id> Edit a specific comment using $EDITOR~%")
+ (format t " move <source-board> <post-id> <target-board> Move a thread to a different board~%")
+ (format t " backup [\"description msg\"] Backup the sexp directory to a tarball~%")
+ (format t " restore [archive.tar.gz] Restore or list backups~%")
+ (format t " set-timezone <int> Offset all posts timezone and recreate indices~%~%")
+ (format t "Environment Variables:~%")
+ (format t " SBBS_DATADIR Path to data directory (default: project data/)~%"))
+
+(defun main (&rest argv)
+ "Main entry point for cl-bbs-admin command line interface, parsing and dispatching command line arguments ARGV."
+ (handler-case
+ (if (null argv)
+ (print-help)
+ (let ((cmd (first argv)))
+ (cond
+ ((string= cmd "list")
+ (list-all (second argv) (third argv)))
+ ((string= cmd "generate-index")
+ (if (>= (length argv) 2)
+ (generate-index (second argv))
+ (format t "Usage: cl-bbs-admin generate-index <board>~%")))
+ ((string= cmd "remove-post")
+ (if (>= (length argv) 3)
+ (remove-post (second argv) (third argv))
+ (format t "Usage: cl-bbs-admin remove-post <board> <post-id>~%")))
+ ((string= cmd "remove-duplicates")
+ (remove-duplicates-command))
+ ((string= cmd "remove-comment")
+ (if (>= (length argv) 4)
+ (remove-comment (second argv) (third argv) (fourth argv))
+ (format t "Usage: cl-bbs-admin remove-comment <board> <post-id> <comment-id>~%")))
+ ((string= cmd "edit")
+ (if (>= (length argv) 4)
+ (edit-comment (second argv) (third argv) (fourth argv))
+ (format t "Usage: cl-bbs-admin edit <board> <post-id> <comment-id>~%")))
+ ((string= cmd "move")
+ (if (>= (length argv) 4)
+ (move-post (second argv) (third argv) (fourth argv))
+ (format t "Usage: cl-bbs-admin move <source-board> <post-id> <target-board>~%")))
+ ((string= cmd "backup")
+ (if (>= (length argv) 2)
+ (backup (format nil "~{~A~^ ~}" (cdr argv)))
+ (backup)))
+ ((string= cmd "restore")
+ (if (>= (length argv) 2)
+ (restore (second argv))
+ (restore)))
+ ((string= cmd "set-timezone")
+ (if (>= (length argv) 2)
+ (let ((offset (parse-integer (second argv) :junk-allowed t)))
+ (if offset
+ (set-timezone offset)
+ (format t "Invalid timezone offset: ~A~%" (second argv))))
+ (format t "Usage: cl-bbs-admin set-timezone <int>~%")))
+ (t
+ (format t "Unknown command: ~A~%" cmd)
+ (print-help)))))
+ (#+sbcl sb-sys:interactive-interrupt
+ #+ccl ccl:interrupt-condition
+ #+clisp system:simple-interrupt-condition
+ #+ecl ext:interactive-interrupt
+ #+allegro excl:interrupt-signal
+ ()
+ (progn
+ (format *error-output* "~&Interrupted.~%")
+ (uiop:quit 1)))))
diff --git a/server/handlers.lisp b/server/handlers.lisp
new file mode 100644
index 0000000..11ec701
--- /dev/null
+++ b/server/handlers.lisp
@@ -0,0 +1,903 @@
+(defpackage :cl-bbs/handlers
+ (:use :cl)
+ (:import-from :cl-bbs/storage
+ #:*base-dir*
+ #:read-sexp-file
+ #:write-sexp-file
+ #:is-board-locked
+ #:ensure-board-dirs)
+ (:import-from :cl-bbs/rss
+ #:generate-rss
+ #:get-all-boards-rss-threads)
+ (:import-from :cl-bbs/views
+ #:render-moderation
+ #:render-index
+ #:render-list
+ #:render-preferences
+ #:render-error-page
+ #:render-thread
+ #:render-search-results
+ #:render-playground)
+ (:export #:handle-request
+ #:*headline-limit*
+ #:*body-limit*))
+
+(in-package :cl-bbs/handlers)
+
+(defparameter *headline-limit*
+ (or (and (uiop:getenv "SBBS_HEADLINE_LIMIT")
+ (parse-integer (uiop:getenv "SBBS_HEADLINE_LIMIT") :junk-allowed t))
+ 128)
+ "Maximum character limit for post headlines.")
+
+(defparameter *body-limit*
+ (or (and (uiop:getenv "SBBS_BODY_LIMIT")
+ (parse-integer (uiop:getenv "SBBS_BODY_LIMIT") :junk-allowed t))
+ 4096)
+ "Maximum character limit for post bodies.")
+
+(defun parse-cookies (cookie-string)
+ "Parses a Cookie header string like 'theme=dark; foo=bar' into an alist."
+ (when cookie-string
+ (let ((cookies '()))
+ (dolist (part (cl-ppcre:split ";" cookie-string))
+ (let ((pair (cl-ppcre:split "=" (string-trim '(#\Space #\Tab #\Newline #\Return) part) :limit 2)))
+ (when (= (length pair) 2)
+ (let ((name (string-trim '(#\Space #\Tab #\Newline #\Return) (first pair)))
+ (val (string-trim '(#\Space #\Tab #\Newline #\Return) (second pair))))
+ (push (cons name val) cookies)))))
+ (nreverse cookies))))
+
+(defun sanitize-theme-name (theme-str)
+ "Validates and returns theme-str if it consists only of alphanumeric characters,
+hyphens, and underscores. Otherwise returns nil to prevent injection/directory traversal."
+ (when (and theme-str (cl-ppcre:scan "^[a-zA-Z0-9_-]+$" theme-str))
+ theme-str))
+
+(defun sanitize-board-name (board-str)
+ "Validates and returns board-str if it consists only of alphanumeric characters,
+hyphens, and underscores. Otherwise returns nil to prevent injection/directory traversal."
+ (when (and board-str (cl-ppcre:scan "^[a-zA-Z0-9_-]+$" board-str))
+ board-str))
+
+(defun get-default-board-from-env (env)
+ "Retrieves the default board name from the request cookies."
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (cookie-board (sanitize-board-name (cdr (assoc "default_board" cookies :test #'string=)))))
+ (and (not (string= cookie-board "")) cookie-board)))
+
+(defun get-theme-from-env (env)
+ "Retrieves the active theme name from the request query parameters or cookies."
+ (let* ((query-str (getf env :query-string))
+ (query-theme (when (and query-str (not (string= query-str "")))
+ (let ((params (quri:url-decode-params query-str)))
+ (sanitize-theme-name (cdr (assoc "theme" params :test #'string=)))))))
+ (or query-theme
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (cookie-theme (sanitize-theme-name (cdr (assoc "theme" cookies :test #'string=)))))
+ (or cookie-theme "default")))))
+
+(defun get-syntax-theme-from-env (env)
+ "Retrieves the active syntax theme name from the request cookies."
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (cookie-theme (sanitize-theme-name (cdr (assoc "syntax_theme" cookies :test #'string=)))))
+ (or cookie-theme "simple")))
+
+(defun get-search-hide-input-from-env (env)
+ "Retrieves the setting for hiding search input in board view (returns \"yes\" or \"no\", default \"no\")."
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (val (cdr (assoc "search_hide_input" cookies :test #'string=))))
+ (if (member val '("yes" "no") :test #'string=) val "no")))
+
+(defun get-search-local-only-from-env (env)
+ "Retrieves the setting for forcing local search in current board (returns \"yes\" or \"no\", default \"no\")."
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (val (cdr (assoc "search_local_only" cookies :test #'string=))))
+ (if (member val '("yes" "no") :test #'string=) val "no")))
+
+(defun get-search-position-from-env (env)
+ "Retrieves the setting for search input placement (returns \"top\" or \"bottom\", default \"top\")."
+ (let* ((headers (getf env :headers))
+ (cookie-str (and headers (gethash "cookie" headers)))
+ (cookies (parse-cookies cookie-str))
+ (val (cdr (assoc "search_position" cookies :test #'string=))))
+ (if (member val '("top" "bottom") :test #'string=) val "top")))
+
+(defun get-board-description (board-name)
+ "Maps a board-name string to a friendly human-readable title."
+ (cond
+ ((string= board-name "prog") "Programming")
+ ((string= board-name "foo") "Foo Reference")
+ ((string= board-name "b") "random")
+ (t (string-capitalize board-name))))
+
+(defun generate-boards-html-list ()
+ (let* ((sexp-dir (merge-pathnames "sexp/" cl-bbs/storage:*base-dir*))
+ (paths (and (probe-file sexp-dir) (uiop:subdirectories sexp-dir)))
+ (boards (sort (mapcar (lambda (path)
+ (car (last (pathname-directory path))))
+ paths)
+ #'string<))
+ (html-list '()))
+ (dolist (b boards)
+ (push (format nil "<li><a href=\"/~a/\">/~a/ - ~a</a></li>" b b (get-board-description b)) html-list))
+ (format nil "<ul>~{~A~%~}</ul>" (nreverse html-list))))
+
+(defun authenticate-admin (env)
+ "Checks if the request has valid admin credentials in environment variables."
+ (let* ((admin-user (or (uiop:getenv "SBBS_ADMIN_USER") "admin"))
+ (admin-password (or (uiop:getenv "SBBS_ADMIN_PASSWORD") "superchanner"))
+ (headers (getf env :headers))
+ (auth-str (and headers (gethash "authorization" headers)))
+ (creds (and auth-str (cl-ppcre:regex-replace-all "^Basic " auth-str ""))))
+ (when creds
+ (let* ((decoded (handler-case (cl-base64:base64-string-to-string creds)
+ (error () nil)))
+ (parts (and decoded (cl-ppcre:split ":" decoded :limit 2))))
+ (and (= (length parts) 2)
+ (string= (first parts) admin-user)
+ (string= (second parts) admin-password))))))
+
+(defun delete-board-dir (board)
+ (let ((board-dir (merge-pathnames (format nil "sexp/~a/" board) *base-dir*)))
+ (when (probe-file board-dir)
+ (uiop:delete-directory-tree board-dir :validate t))))
+
+(defun get-board-threads (dir)
+ (let ((threads nil))
+ (loop for i from 1
+ for filepath = (merge-pathnames (format nil "~D" i) dir)
+ for exists = (probe-file filepath)
+ while (or exists (<= i 200))
+ do (when exists
+ (let ((data (read-sexp-file filepath)))
+ (when data
+ (push (cons i data) threads)))))
+ threads))
+
+(defun search-posts (query &optional board-filter)
+ "Searches across all boards (or a specific board if BOARD-FILTER is provided) for posts or headlines matching QUERY."
+ (let ((results '())
+ (sexp-dir (merge-pathnames "sexp/" *base-dir*)))
+ (when (and query (string/= "" (string-trim '(#\Space #\Tab #\Newline #\Return) query)))
+ (let ((board-dirs (if (and board-filter (string/= "" board-filter))
+ (list (merge-pathnames (format nil "~a/" board-filter) sexp-dir))
+ (and (probe-file sexp-dir) (uiop:subdirectories sexp-dir)))))
+ (dolist (board-dir board-dirs)
+ (let ((board-name (car (last (pathname-directory board-dir))))
+ ;; Get only files whose namestring consists only of digits (which are the thread S-expression files)
+ (thread-files (and (probe-file board-dir)
+ (remove-if-not (lambda (file)
+ (let ((name (file-namestring file)))
+ (and (string/= name "")
+ (every #'digit-char-p name))))
+ (uiop:directory-files board-dir)))))
+ (dolist (thread-file thread-files)
+ (let* ((thread-id (file-namestring thread-file))
+ (thread-data (read-sexp-file thread-file))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (headline (cdr (assoc 'cl-bbs/models:headline raw-thread)))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-thread)))
+ (when posts-assoc
+ (let ((posts (get-flat-posts posts-assoc)))
+ (dolist (post posts)
+ (let* ((post-id (car post))
+ (post-data (cdr post))
+ (content (cdr (assoc 'cl-bbs/models:content post-data)))
+ (date (cdr (assoc 'cl-bbs/models:date post-data))))
+ (when (or (and headline (search query headline :test #'char-equal))
+ (and content (search query content :test #'char-equal)))
+ (push (list :board board-name
+ :thread-id thread-id
+ :headline (or headline "No Headline")
+ :post-id post-id
+ :date (or date "")
+ :content (or content ""))
+ results))))))))))))
+ (nreverse results)))
+
+(defun get-flat-posts (posts-assoc)
+ (when posts-assoc
+ (let* ((cdr-val (cdr posts-assoc))
+ (cadr-val (and (listp cdr-val) (car cdr-val))))
+ (if (and (listp cadr-val) (listp (car cadr-val)))
+ cadr-val
+ cdr-val))))
+
+(defun regenerate-board-index (board)
+ (let* ((board-dir (merge-pathnames (format nil "sexp/~a/" board) *base-dir*))
+ (threads-data (get-board-threads board-dir)))
+ (if (null threads-data)
+ (let ((list-path (merge-pathnames "list" board-dir))
+ (index-path (merge-pathnames "index" board-dir)))
+ (when (probe-file list-path) (delete-file list-path))
+ (when (probe-file index-path) (delete-file index-path)))
+ (let* ((sorted-threads
+ (sort threads-data
+ (lambda (a b)
+ (let* ((raw-a (if (and (consp (cdr a)) (consp (cadr a)) (consp (caadr a))) (cadr a) (cdr a)))
+ (raw-b (if (and (consp (cdr b)) (consp (cadr b)) (consp (caadr b))) (cadr b) (cdr b)))
+ (posts-assoc-a (assoc 'cl-bbs/models:posts raw-a))
+ (posts-assoc-b (assoc 'cl-bbs/models:posts raw-b))
+ (posts-a (get-flat-posts posts-assoc-a))
+ (posts-b (get-flat-posts posts-assoc-b))
+ (date-a (or (cdr (assoc 'cl-bbs/models:date (cdr (car (last posts-a))))) ""))
+ (date-b (or (cdr (assoc 'cl-bbs/models:date (cdr (car (last posts-b))))) "")))
+ (string> date-a date-b)))))
+ (list-entries
+ (mapcar (lambda (t-entry)
+ (let* ((id (car t-entry))
+ (t-data (cdr t-entry))
+ (raw-t (if (and (consp t-data)
+ (consp (car t-data))
+ (consp (caar t-data)))
+ (car t-data)
+ t-data))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-t))
+ (posts (get-flat-posts posts-assoc))
+ (headline (cdr (assoc 'cl-bbs/models:headline raw-t)))
+ (date (cdr (assoc 'cl-bbs/models:date (cdr (car (last posts))))))
+ (messages (length posts)))
+ `(,id (cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:date . ,date)
+ (cl-bbs/models:messages . ,messages))))
+ sorted-threads))
+ (frontpage-count (min 10 (length sorted-threads)))
+ (index-entries
+ (mapcar (lambda (t-entry)
+ (let* ((id (car t-entry))
+ (t-data (cdr t-entry))
+ (raw-t (if (and (consp t-data)
+ (consp (car t-data))
+ (consp (caar t-data)))
+ (car t-data)
+ t-data))
+ (headline (cdr (assoc 'cl-bbs/models:headline raw-t)))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-t))
+ (posts (get-flat-posts posts-assoc))
+ (truncated-ids (if (> (length posts) 6)
+ (mapcar #'car (butlast (cdr posts) 5))
+ nil))
+ (selected-posts (if (> (length posts) 6) (cons (car posts) (last posts 5)) posts)))
+ `(,id (cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:truncated . ,truncated-ids)
+ (cl-bbs/models:posts ,selected-posts))))
+ (subseq sorted-threads 0 frontpage-count)))
+ (list-path (merge-pathnames "list" board-dir))
+ (index-path (merge-pathnames "index" board-dir)))
+ (write-sexp-file list-path list-entries)
+ (write-sexp-file index-path index-entries)))))
+
+(defun delete-thread-file (board thread-id)
+ (let ((filepath (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (when (probe-file filepath)
+ (delete-file filepath)
+ (regenerate-board-index board))))
+
+(defun get-next-handler-thread-id (board)
+ (let* ((board-dir (merge-pathnames (format nil "sexp/~a/" board) *base-dir*))
+ (files (and (probe-file board-dir)
+ (uiop:directory-files board-dir)))
+ (max-id 0))
+ (dolist (file files)
+ (let ((id (handler-case (parse-integer (pathname-name file))
+ (error () nil))))
+ (when (and id (> id max-id))
+ (setf max-id id))))
+ (1+ max-id)))
+
+(defun shame-thread-file (board thread-id)
+ (let* ((source-file (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*))
+ (target-board "shame")
+ (target-dir (merge-pathnames (format nil "sexp/~a/" target-board) *base-dir*)))
+ (when (and (probe-file source-file) (string-not-equal board target-board))
+ (ensure-directories-exist target-dir)
+ (let* ((new-id (get-next-handler-thread-id target-board))
+ (target-file (merge-pathnames (format nil "~D" new-id) target-dir)))
+ (uiop:copy-file source-file target-file)
+ (delete-file source-file)
+ (regenerate-board-index board)
+ (regenerate-board-index target-board)))))
+
+(defun delete-thread-comment (board thread-id comment-id)
+ (let ((thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (when (probe-file thread-path)
+ (let* ((thread-data (read-sexp-file thread-path))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-thread)))
+ (when posts-assoc
+ (let* ((actual-posts (get-flat-posts posts-assoc))
+ (new-posts (remove-if (lambda (p) (= (car p) comment-id)) actual-posts)))
+ (if (null new-posts)
+ (delete-file thread-path)
+ (progn
+ (setf (cdr posts-assoc) (list new-posts))
+ (write-sexp-file thread-path thread-data)))
+ (regenerate-board-index board)))))))
+
+(defun edit-thread-comment (board thread-id comment-id new-content &optional new-headline)
+ (let ((thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (when (probe-file thread-path)
+ (let* ((thread-data (read-sexp-file thread-path))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-thread)))
+ (when posts-assoc
+ (let* ((actual-posts (get-flat-posts posts-assoc))
+ (new-posts (mapcar (lambda (p)
+ (if (= (car p) comment-id)
+ (cons (car p)
+ (mapcar (lambda (kv)
+ (if (eq (car kv) 'cl-bbs/models:content)
+ (cons (car kv) new-content)
+ kv))
+ (cdr p)))
+ p))
+ actual-posts)))
+ (setf (cdr posts-assoc) (list new-posts))
+ (when (and (= comment-id 1) new-headline)
+ (let ((headline-assoc (assoc 'cl-bbs/models:headline raw-thread)))
+ (if headline-assoc
+ (setf (cdr headline-assoc) new-headline)
+ (setf raw-thread (append raw-thread (list (cons 'cl-bbs/models:headline new-headline)))))))
+ (write-sexp-file thread-path thread-data)
+ (regenerate-board-index board)))))))
+
+(defun get-date ()
+ (multiple-value-bind (second minute hour date month year)
+ (get-decoded-time)
+ (format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D"
+ year month date hour minute second)))
+
+(defun get-next-thread-number (threads)
+ (if (null threads)
+ 1
+ (1+ (apply #'max (mapcar #'car threads)))))
+
+(defun create-thread (path headline date message)
+ (let ((thread `((cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:posts . ((1 (cl-bbs/models:date . ,date)
+ (cl-bbs/models:vip . nil)
+ (cl-bbs/models:content . ,message)))))))
+ (write-sexp-file path thread)))
+
+(defun add-thread-to-list (path thread-number headline date)
+ (let ((threads (read-sexp-file path)))
+ (write-sexp-file path
+ (cons `(,thread-number (cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:date . ,date)
+ (cl-bbs/models:messages . 1))
+ threads))))
+
+(defun add-thread-to-index (path thread-number headline date message)
+ (let ((threads (read-sexp-file path))
+ (thread `(,thread-number
+ (cl-bbs/models:headline . ,headline)
+ (cl-bbs/models:truncated . nil)
+ (cl-bbs/models:posts
+ ((1 (cl-bbs/models:date . ,date)
+ (cl-bbs/models:vip . nil)
+ (cl-bbs/models:content . ,message)))))))
+ (write-sexp-file path (cons thread threads))))
+
+(defun update-thread-data (thread-data new-post)
+ (let* ((raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-thread)))
+ (if posts-assoc
+ (let* ((posts-list (if (listp (cdr posts-assoc))
+ (if (listp (cadr posts-assoc))
+ (cadr posts-assoc)
+ (cdr posts-assoc))
+ (cdr posts-assoc)))
+ (actual-posts (if (listp (car posts-list)) posts-list (list posts-list)))
+ (updated-posts (append actual-posts (list new-post))))
+ (setf (cdr posts-assoc) (list updated-posts))
+ thread-data)
+ (append thread-data (list `(cl-bbs/models:posts (,new-post)))))))
+
+(defvar *app* (make-instance 'ningle:<app>))
+
+(defun normalize-path (path)
+ (if (stringp path)
+ (let ((len (length path)))
+ (if (and (> len 1)
+ (char= (char path (1- len)) #\/))
+ (subseq path 0 (1- len))
+ path))
+ path))
+
+(defun get-body-params (params env)
+ (let ((has-theme (assoc "theme" params :test #'string=))
+ (has-epistula (assoc "epistula" params :test #'string=))
+ (has-action (assoc "action" params :test #'string=)))
+ (if (or has-theme has-epistula has-action)
+ params
+ (let ((content-len (getf env :content-length)))
+ (if (and content-len (> content-len 0) (getf env :raw-body))
+ (let ((body-bytes (make-array content-len :element-type '(unsigned-byte 8)))
+ (stream (getf env :raw-body)))
+ (let ((bytes-read (read-sequence body-bytes stream)))
+ (let ((body-str (flexi-streams:octets-to-string
+ body-bytes :start 0 :end bytes-read :external-format :utf-8)))
+ (quri:url-decode-params body-str))))
+ params)))))
+
+(defun handle-request (env)
+ "Accepts a Clack request environment plist, normalizes it, and dispatches it through ningle endpoints."
+ (let ((normalized-env (copy-list env)))
+ (setf (getf normalized-env :path-info) (normalize-path (getf env :path-info)))
+ (unless (hash-table-p (getf normalized-env :headers))
+ (setf (getf normalized-env :headers) (make-hash-table :test 'equal)))
+ (when (getf normalized-env :request-method)
+ (setf (getf normalized-env :request-method)
+ (intern (string-upcase (symbol-name (getf normalized-env :request-method))) :keyword)))
+ (let ((cl-bbs/views:*preferences* (cl-bbs/views:make-preferences
+ :theme (get-theme-from-env normalized-env)
+ :syntax-theme (get-syntax-theme-from-env normalized-env)
+ :default-board (or (get-default-board-from-env normalized-env) "")
+ :search-hide-input (get-search-hide-input-from-env normalized-env)
+ :search-local-only (get-search-local-only-from-env normalized-env)
+ :search-position (get-search-position-from-env normalized-env))))
+ (lack.component:call *app* normalized-env))))
+
+(defun render-main-index ()
+ (let ((index-file (pathname (or (uiop:getenv "SBBS_INDEX_FILE")
+ (merge-pathnames "src/static/index.html"
+ (asdf:system-source-directory :cl-bbs/server))))))
+ (if (probe-file index-file)
+ (let* ((html-content (uiop:read-file-string index-file))
+ (boards-list-html (generate-boards-html-list))
+ (dynamic-html (cl-ppcre:regex-replace-all "<!--BOARDS-LIST-PLACEHOLDER-->"
+ html-content
+ (lambda (match &rest regs)
+ (declare (ignore match regs))
+ boards-list-html))))
+ `(200 (:content-type "text/html; charset=utf-8"
+ :cache-control "no-store, no-cache, must-revalidate, max-age=0"
+ :pragma "no-cache"
+ :expires "0")
+ (,dynamic-html)))
+ `(200 (:content-type "text/plain"
+ :cache-control "no-store, no-cache, must-revalidate, max-age=0"
+ :pragma "no-cache"
+ :expires "0") ("SchemeBBS clone root")))))
+
+;; 1. GET /
+(setf (ningle:route *app* "/" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (let* ((env (lack.request:request-env ningle:*request*))
+ (default-board (get-default-board-from-env env)))
+ (if default-board
+ `(303 (:location ,(format nil "/~a/" default-board)) ("Redirecting..."))
+ (render-main-index)))))
+
+;; 1b. GET /index.html
+(setf (ningle:route *app* "/index.html" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (render-main-index)))
+
+;; 2. GET /about
+(setf (ningle:route *app* "/about" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (let ((about-file (pathname (merge-pathnames "src/static/about.html"
+ (asdf:system-source-directory :cl-bbs/server)))))
+ (if (probe-file about-file)
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(uiop:read-file-string about-file)))
+ `(404 (:content-type "text/plain") ("About page not found"))))))
+
+;; 3. GET /admin (Admin Control Panel)
+(setf (ningle:route *app* "/admin" :method :GET)
+ (lambda (params)
+ (let ((env (lack.request:request-env ningle:*request*)))
+ (if (not (authenticate-admin env))
+ `(401 (:content-type "text/plain"
+ :www-authenticate "Basic realm=\"cl-bbs Admin\"")
+ ("Unauthorized"))
+ (let* ((sexp-dir (merge-pathnames "sexp/" *base-dir*))
+ (paths (and (probe-file sexp-dir) (uiop:subdirectories sexp-dir)))
+ (boards (sort (mapcar (lambda (path) (car (last (pathname-directory path)))) paths) #'string<))
+ (board (cdr (assoc "board" params :test #'string=)))
+ (thread-id-str (cdr (assoc "thread" params :test #'string=)))
+ (threads nil)
+ (comments nil)
+ (headline nil))
+ (when (and board (member board boards :test #'string=))
+ (let ((list-path (merge-pathnames (format nil "sexp/~a/list" board) *base-dir*)))
+ (when (probe-file list-path)
+ (setf threads (read-sexp-file list-path)))))
+ (when (and board thread-id-str)
+ (let ((thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id-str) *base-dir*)))
+ (when (probe-file thread-path)
+ (let* ((thread-data (read-sexp-file thread-path))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (posts-assoc (assoc 'cl-bbs/models:posts raw-thread)))
+ (setf headline (cdr (assoc 'cl-bbs/models:headline raw-thread)))
+ (when posts-assoc
+ (setf comments (get-flat-posts posts-assoc)))))))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-moderation boards board threads thread-id-str comments
+ cl-bbs/views:*preferences* headline))))))))
+
+;; 4. POST /admin/action (Admin Actions)
+(setf (ningle:route *app* "/admin/action" :method :POST)
+ (lambda (params)
+ (let* ((env (lack.request:request-env ningle:*request*))
+ (parsed-params (get-body-params params env)))
+ (if (not (authenticate-admin env))
+ `(401 (:content-type "text/plain"
+ :www-authenticate "Basic realm=\"cl-bbs Admin\"")
+ ("Unauthorized"))
+ (let ((action (cdr (assoc "action" parsed-params :test #'string=)))
+ (board (cdr (assoc "board" parsed-params :test #'string=)))
+ (thread-id-str (cdr (assoc "thread" parsed-params :test #'string=)))
+ (comment-id-str (cdr (assoc "comment" parsed-params :test #'string=)))
+ (content (cdr (assoc "content" parsed-params :test #'string=))))
+ (cond
+ ((string= action "delete-board")
+ (delete-board-dir board)
+ `(303 (:location "/admin") ("Redirecting...")))
+ ((string= action "create-board")
+ (let ((sanitized (sanitize-board-name board)))
+ (if sanitized
+ (progn
+ (ensure-board-dirs sanitized)
+ `(303 (:location "/admin") ("Redirecting...")))
+ `(400 (:content-type "text/plain") ("Invalid Board Name")))))
+ ((string= action "delete-thread")
+ (delete-thread-file board thread-id-str)
+ `(303 (:location ,(format nil "/admin?board=~a" board)) ("Redirecting...")))
+ ((string= action "shame-thread")
+ (shame-thread-file board thread-id-str)
+ `(303 (:location ,(format nil "/admin?board=~a" board)) ("Redirecting...")))
+ ((string= action "delete-comment")
+ (let ((cid (and comment-id-str (parse-integer comment-id-str :junk-allowed t))))
+ (when cid
+ (delete-thread-comment board thread-id-str cid)))
+ `(303 (:location ,(format nil "/admin?board=~a&thread=~a" board thread-id-str)) ("Redirecting...")))
+ ((string= action "edit-comment")
+ (let ((cid (and comment-id-str (parse-integer comment-id-str :junk-allowed t)))
+ (headline (cdr (assoc "headline" parsed-params :test #'string=))))
+ (when cid
+ (edit-thread-comment board thread-id-str cid content headline)))
+ `(303 (:location ,(format nil "/admin?board=~a&thread=~a" board thread-id-str)) ("Redirecting...")))
+ (t
+ `(400 (:content-type "text/plain") ("Invalid Action")))))))))
+
+;; 5. GET /sw.js
+(setf (ningle:route *app* "/sw.js" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (let ((sw-file (merge-pathnames "src/static/sw.js"
+ (asdf:system-source-directory :cl-bbs/server))))
+ (if (probe-file sw-file)
+ `(200 (:content-type "application/javascript")
+ (,(uiop:read-file-string sw-file)))
+ `(404 (:content-type "text/plain") ("Service worker not found"))))))
+
+;; 6. GET /manifest.json
+(setf (ningle:route *app* "/manifest.json" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (let ((manifest-file (merge-pathnames "src/static/manifest.json"
+ (asdf:system-source-directory :cl-bbs/server))))
+ (if (probe-file manifest-file)
+ `(200 (:content-type "application/json")
+ (,(uiop:read-file-string manifest-file)))
+ `(404 (:content-type "text/plain") ("Manifest not found"))))))
+
+;; 6b. GET /search
+(setf (ningle:route *app* "/search" :method :GET)
+ (lambda (params)
+ (let* ((query (cdr (assoc "q" params :test #'string=)))
+ (board-filter (cdr (assoc "board" params :test #'string=)))
+ (results (if query (search-posts query board-filter) nil)))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-search-results (or query "") results cl-bbs/views:*preferences*))))))
+
+;; 6b-api. POST /api/colorize
+(setf (ningle:route *app* "/api/colorize" :method :POST)
+ (lambda (params)
+ (let* ((env (lack.request:request-env ningle:*request*))
+ (parsed-params (get-body-params params env))
+ (code (cdr (assoc "code" parsed-params :test #'string=))))
+ (if code
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(handler-case (colorize:html-colorization :common-lisp code)
+ (error () (cl-who:escape-string code)))))
+ `(400 (:content-type "text/plain") ("No code provided"))))))
+
+;; 6c. GET /playground (Global Playground)
+(setf (ningle:route *app* "/playground" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-playground nil)))))
+
+;; 6d. GET /:board/playground (Board-scoped Playground)
+(setf (ningle:route *app* "/:board/playground" :method :GET)
+ (lambda (params)
+ (let ((board (cdr (assoc :board params))))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-playground board))))))
+
+;; 7. GET /:board/list
+(setf (ningle:route *app* "/:board/list" :method :GET)
+ (lambda (params)
+ (let* ((board (cdr (assoc :board params)))
+ (list-path (merge-pathnames (format nil "sexp/~a/list" board) *base-dir*)))
+ (if (probe-file list-path)
+ (let ((list-data (read-sexp-file list-path)))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-list board list-data cl-bbs/views:*preferences*))))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-list board nil cl-bbs/views:*preferences*)))))))
+
+;; 8. GET /:board/preferences
+(setf (ningle:route *app* "/:board/preferences" :method :GET)
+ (lambda (params)
+ (let ((board (cdr (assoc :board params))))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-preferences board cl-bbs/views:*preferences*))))))
+
+;; 9. POST /:board/preferences
+(setf (ningle:route *app* "/:board/preferences" :method :POST)
+ (lambda (params)
+ (let* ((env (lack.request:request-env ningle:*request*))
+ (parsed-params (get-body-params params env))
+ (board (cdr (assoc :board params)))
+ (theme (cdr (assoc "theme" parsed-params :test #'string=)))
+ (syntax-theme (cdr (assoc "syntax_theme" parsed-params :test #'string=)))
+ (default-board (let ((val (cdr (assoc "default_board" parsed-params :test #'string=))))
+ (if val (string-trim '(#\Space #\Tab #\Newline #\Return) val) "")))
+ (search-hide-input (let ((val (cdr (assoc "search_hide_input" parsed-params :test #'string=))))
+ (if (member val '("yes" "no") :test #'string=) val "no")))
+ (search-local-only (let ((val (cdr (assoc "search_local_only" parsed-params :test #'string=))))
+ (if (member val '("yes" "no") :test #'string=) val "no")))
+ (search-position (let ((val (cdr (assoc "search_position" parsed-params :test #'string=))))
+ (if (member val '("top" "bottom") :test #'string=) val "top"))))
+ `(303 (:location ,(format nil "/~a/preferences" board)
+ :set-cookie ,(format nil "theme=~a; Path=/; Max-Age=31536000" (or theme "default"))
+ :set-cookie ,(format nil "syntax_theme=~a; Path=/; Max-Age=31536000" (or syntax-theme "simple"))
+ :set-cookie ,(format nil "default_board=~a; Path=/; Max-Age=31536000" (or default-board ""))
+ :set-cookie ,(format nil "search_hide_input=~a; Path=/; Max-Age=31536000" search-hide-input)
+ :set-cookie ,(format nil "search_local_only=~a; Path=/; Max-Age=31536000" search-local-only)
+ :set-cookie ,(format nil "search_position=~a; Path=/; Max-Age=31536000" search-position))
+ ("Redirecting...")))))
+
+;; RSS Feed (All boards)
+(setf (ningle:route *app* "/rss" :method :GET)
+ (lambda (params)
+ (declare (ignore params))
+ (let ((env (lack.request:request-env ningle:*request*))
+ (all-threads (cl-bbs/rss:get-all-boards-rss-threads 20)))
+ (list 200 '(:content-type "application/rss+xml; charset=utf-8")
+ (list (cl-bbs/rss:generate-rss "all" all-threads env))))))
+
+;; RSS Feed (Specific board)
+(setf (ningle:route *app* "/:board/rss" :method :GET)
+ (lambda (params)
+ (let* ((board (cdr (assoc :board params)))
+ (env (lack.request:request-env ningle:*request*))
+ (list-path (merge-pathnames (format nil "sexp/~a/list" board) *base-dir*))
+ (threads (when (probe-file list-path) (read-sexp-file list-path))))
+ (list 200 '(:content-type "application/rss+xml; charset=utf-8")
+ (list (cl-bbs/rss:generate-rss board threads env))))))
+
+;; 10. POST /:board/post (New Thread)
+(setf (ningle:route *app* "/:board/post" :method :POST)
+ (lambda (params)
+ (block out
+ (let* ((board (cdr (assoc :board params)))
+ (env (lack.request:request-env ningle:*request*))
+ (parsed-params (get-body-params params env))
+ (epistula (cdr (assoc "epistula" parsed-params :test #'string=)))
+ (titulus (cdr (assoc "titulus" parsed-params :test #'string=)))
+ (date (get-date))
+ (list-path (merge-pathnames (format nil "sexp/~a/list" board) *base-dir*))
+ (index-path (merge-pathnames (format nil "sexp/~a/index" board) *base-dir*))
+ (threads (read-sexp-file list-path))
+ (thread-number (get-next-thread-number threads))
+ (thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-number) *base-dir*)))
+ (cond
+ ((or (null epistula)
+ (string= "" (string-trim '(#\Space #\Tab #\Newline #\Return) epistula)))
+ `(400 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page "Post body cannot be empty" cl-bbs/views:*preferences*))))
+ ((and titulus (> (length titulus) *headline-limit*))
+ `(400 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page (format nil "Headline exceeds maximum length of ~D characters" *headline-limit*)
+ cl-bbs/views:*preferences*))))
+ ((and epistula (> (length epistula) *body-limit*))
+ `(400 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page (format nil "Post body exceeds maximum length of ~D characters" *body-limit*)
+ cl-bbs/views:*preferences*))))
+ (t
+ (progn
+ (when (is-board-locked board)
+ (return-from out
+ `(403 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page "This board is read-only"
+ cl-bbs/views:*preferences*)))))
+ (unless (probe-file (merge-pathnames (format nil "sexp/~a/" board) *base-dir*))
+ (return-from out
+ `(403 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page "Only administrators can create new boards"
+ cl-bbs/views:*preferences*)))))
+ (ensure-board-dirs board)
+ (create-thread thread-path titulus date epistula)
+ (add-thread-to-list list-path thread-number titulus date)
+ (add-thread-to-index index-path thread-number titulus date epistula)
+ `(303 (:location ,(format nil "/~a/" board)) ("Redirecting...")))))))))
+
+;; 11. GET /:board
+(setf (ningle:route *app* "/:board" :method :GET)
+ (lambda (params)
+ (let* ((board (cdr (assoc :board params)))
+ (index-path (merge-pathnames (format nil "sexp/~a/index" board) *base-dir*)))
+ (if (probe-file index-path)
+ (let ((index-data (read-sexp-file index-path)))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-index board index-data cl-bbs/views:*preferences*))))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-index board nil cl-bbs/views:*preferences*)))))))
+
+;; 12. GET /:board/:thread_id
+(setf (ningle:route *app* "/:board/:thread_id" :method :GET)
+ (lambda (params)
+ (let* ((board (cdr (assoc :board params)))
+ (thread-id (cdr (assoc :thread_id params)))
+ (thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (if (probe-file thread-path)
+ (let ((thread-data (read-sexp-file thread-path)))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-thread board thread-id thread-data nil cl-bbs/views:*preferences*))))
+ `(404 (:content-type "text/plain") ("Thread not found"))))))
+
+;; 13. POST /:board/:thread_id/post (Reply to Thread)
+(setf (ningle:route *app* "/:board/:thread_id/post" :method :POST)
+ (lambda (params)
+ (block out
+ (let* ((board (cdr (assoc :board params)))
+ (thread-id (cdr (assoc :thread_id params)))
+ (env (lack.request:request-env ningle:*request*))
+ (parsed-params (get-body-params params env))
+ (epistula (cdr (assoc "epistula" parsed-params :test #'string=)))
+ (date (get-date))
+ (thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (cond
+ ((or (null epistula)
+ (string= "" (string-trim '(#\Space #\Tab #\Newline #\Return) epistula)))
+ `(400 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page "Post body cannot be empty" cl-bbs/views:*preferences*))))
+ ((and epistula (> (length epistula) *body-limit*))
+ `(400 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page (format nil "Post body exceeds maximum length of ~D characters" *body-limit*)
+ cl-bbs/views:*preferences*))))
+ (t
+ (progn
+ (when (is-board-locked board)
+ (return-from out
+ `(403 (:content-type "text/html; charset=utf-8")
+ (,(render-error-page "This board is read-only" cl-bbs/views:*preferences*)))))
+ (if (probe-file thread-path)
+ (let* ((thread-data (read-sexp-file thread-path))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (posts-assoc (let ((assoc-res (assoc 'cl-bbs/models:posts raw-thread)))
+ (if assoc-res
+ assoc-res
+ (cadr (if (consp thread-data)
+ thread-data
+ (list thread-data))))))
+ (posts-list (if (listp (cdr posts-assoc))
+ (if (listp (cadr posts-assoc))
+ (cadr posts-assoc)
+ (cdr posts-assoc))
+ (cdr posts-assoc)))
+ (posts (if (listp (car posts-list)) posts-list (list posts-list)))
+ (new-post `(,(1+ (reduce #'max posts :key #'car :initial-value 0))
+ (cl-bbs/models:date . ,date)
+ (cl-bbs/models:vip . nil)
+ (cl-bbs/models:content . ,epistula)))
+ (new-thread-data (update-thread-data thread-data new-post)))
+ (write-sexp-file thread-path new-thread-data)
+ (let* ((list-path (merge-pathnames (format nil "sexp/~a/list" board) *base-dir*))
+ (threads-list (read-sexp-file list-path))
+ (str-id (if (stringp thread-id) (parse-integer thread-id) thread-id))
+ (target-thread-assoc (assoc str-id threads-list)))
+ (when target-thread-assoc
+ (let* ((rem-threads (remove str-id threads-list :key #'car :test #'equal))
+ (raw-new-thread (if (and (consp new-thread-data)
+ (consp (car new-thread-data))
+ (consp (caar new-thread-data)))
+ (car new-thread-data)
+ new-thread-data))
+ (messages-count (length (cadr (assoc 'cl-bbs/models:posts raw-new-thread))))
+ (headline-val (if (stringp (cdr (assoc 'cl-bbs/models:headline
+ (cdr target-thread-assoc))))
+ (cdr (assoc 'cl-bbs/models:headline
+ (cdr target-thread-assoc)))
+ (cdr (assoc 'headline (cdr target-thread-assoc)))))
+ (updated-entry `(,str-id
+ (cl-bbs/models:headline . ,headline-val)
+ (cl-bbs/models:date . ,date)
+ (cl-bbs/models:messages . ,messages-count))))
+ (write-sexp-file list-path (cons updated-entry rem-threads))))
+ (let* ((index-path (merge-pathnames (format nil "sexp/~a/index" board) *base-dir*))
+ (index-list (read-sexp-file index-path))
+ (target-index-assoc (assoc str-id index-list)))
+ (when target-index-assoc
+ (let* ((rem-index (remove str-id index-list :key #'car :test #'equal))
+ (raw-new-thread (if (and (consp new-thread-data)
+ (consp (car new-thread-data))
+ (consp (caar new-thread-data)))
+ (car new-thread-data)
+ new-thread-data))
+ (headline-val (if (stringp (cdr (assoc 'cl-bbs/models:headline
+ (cdr target-index-assoc))))
+ (cdr (assoc 'cl-bbs/models:headline
+ (cdr target-index-assoc)))
+ (cdr (assoc 'headline (cdr target-index-assoc)))))
+ (actual-posts (get-flat-posts (assoc 'cl-bbs/models:posts raw-new-thread)))
+ (truncated-ids (if (> (length actual-posts) 6)
+ (mapcar #'car (butlast (cdr actual-posts) 5))
+ nil))
+ (updated-entry `(,str-id
+ (cl-bbs/models:headline . ,headline-val)
+ (cl-bbs/models:truncated . ,truncated-ids)
+ (cl-bbs/models:posts
+ ,(if (> (length actual-posts) 6)
+ (cons (car actual-posts) (last actual-posts 5))
+ actual-posts)))))
+ (write-sexp-file index-path (cons updated-entry rem-index))))))
+ `(303 (:location ,(format nil "/~a/" board)) ("Redirecting...")))
+ `(404 (:content-type "text/plain") ("Thread not found"))))))))))
+
+;; 14. GET /:board/:thread_id/:range (Thread Comments with Range)
+(setf (ningle:route *app* "/:board/:thread_id/:range" :method :GET)
+ (lambda (params)
+ (let* ((board (cdr (assoc :board params)))
+ (thread-id (cdr (assoc :thread_id params)))
+ (range-str (cdr (assoc :range params)))
+ (thread-path (merge-pathnames (format nil "sexp/~a/~a" board thread-id) *base-dir*)))
+ (if (probe-file thread-path)
+ (let ((thread-data (read-sexp-file thread-path)))
+ `(200 (:content-type "text/html; charset=utf-8")
+ (,(render-thread board thread-id thread-data range-str cl-bbs/views:*preferences*))))
+ `(404 (:content-type "text/plain") ("Thread not found"))))))
diff --git a/server/main.lisp b/server/main.lisp
new file mode 100644
index 0000000..755d68a
--- /dev/null
+++ b/server/main.lisp
@@ -0,0 +1,61 @@
+(in-package :cl-bbs/server)
+
+(defvar *server* nil)
+(defvar *app* nil)
+
+;; Initialize colorize and HyperSpec lookup paths safely
+(defun init-colorize ()
+ (setf colorize:*debug* nil)
+ (handler-case
+ (let* ((base-dir (asdf:system-source-directory :cl-bbs/server))
+ (local-clhs-dir (and base-dir (merge-pathnames "src/HyperSpec/" base-dir)))
+ (local-map-file (and local-clhs-dir (merge-pathnames "Data/Map_Sym.txt" local-clhs-dir)))
+ (mop-map-file (and local-clhs-dir (merge-pathnames "Mop_Sym.txt" local-clhs-dir))))
+ (if (and local-map-file (probe-file local-map-file))
+ (progn
+ (setf clhs-lookup::*hyperspec-pathname* local-clhs-dir)
+ (setf clhs-lookup::*hyperspec-map-file* local-map-file)
+ (setf clhs-lookup::*mop-map-file* mop-map-file))
+ (setf clhs-lookup::*hyperspec-map-file* #p"nonexistent-map-sym.txt")))
+ (error (e)
+ (declare (ignore e))
+ (setf clhs-lookup::*hyperspec-map-file* #p"nonexistent-map-sym.txt"))))
+
+
+(defun make-real-ip-middleware (app)
+ (lambda (env)
+ (let ((x-forwarded-for (gethash "x-forwarded-for" (getf env :headers))))
+ ;; If X-Forwarded-For exists, update REMOTE_ADDR to the first IP in the list
+ (when x-forwarded-for
+ (let ((real-ip (first (uiop:split-string x-forwarded-for :separator '(#\,)))))
+ (setf (getf env :remote-addr) (string-trim " " real-ip)))))
+ (funcall app env)))
+
+(defun build-app ()
+ (lack:builder
+ (:static :path "/static/"
+ :root (merge-pathnames "src/static/" (asdf:system-source-directory :cl-bbs/server)))
+ (lambda (app) (make-real-ip-middleware app))
+ :accesslog
+ (lambda (env)
+ (cl-bbs/handlers:handle-request env))))
+
+(defun start-app (host port &key (async t))
+ "Starts the Hunchentoot server running the cl-bbs application on the specified PORT."
+ (init-colorize)
+ (when *server*
+ (stop-app))
+ (setf *app* (build-app))
+ (setf *server* (clack:clackup *app* :address host
+ :port port
+ :server :hunchentoot
+ :use-thread async))
+ (format t "cl-bbs running on port ~a~%" port)
+ t)
+
+(defun stop-app ()
+ "Stops the currently running Hunchentoot server instance if one exists."
+ (when *server*
+ (clack:stop *server*)
+ (setf *server* nil))
+ t)
diff --git a/server/models.lisp b/server/models.lisp
new file mode 100644
index 0000000..8ee7f4d
--- /dev/null
+++ b/server/models.lisp
@@ -0,0 +1,35 @@
+(defpackage :cl-bbs/models
+ (:use :cl)
+ (:export #:thread
+ #:post
+ #:board
+ #:headline
+ #:posts
+ #:truncated
+ #:content
+ #:date
+ #:messages
+ #:vip
+ #:name))
+
+(in-package :cl-bbs/models)
+
+(defclass post ()
+ ((id :initarg :id :accessor post-id)
+ (date :initarg :date :accessor post-date)
+ (vip :initarg :vip :accessor post-vip :initform nil)
+ (content :initarg :content :accessor post-content))
+ (:documentation "Represents a single post on a message board."))
+
+(defclass thread ()
+ ((id :initarg :id :accessor thread-id)
+ (headline :initarg :headline :accessor thread-headline)
+ (date :initarg :date :accessor thread-date)
+ (messages :initarg :messages :accessor thread-messages :initform 1)
+ (truncated :initarg :truncated :accessor thread-truncated :initform nil)
+ (posts :initarg :posts :accessor thread-posts :initform nil))
+ (:documentation "Represents a thread consisting of a series of posts."))
+
+(defclass board ()
+ ((name :initarg :name :accessor board-name))
+ (:documentation "Represents a message board."))
diff --git a/server/rss.lisp b/server/rss.lisp
new file mode 100644
index 0000000..aef3013
--- /dev/null
+++ b/server/rss.lisp
@@ -0,0 +1,122 @@
+(defpackage #:cl-bbs/rss
+ (:use #:cl)
+ (:local-nicknames (#:models #:cl-bbs/models)
+ (#:storage #:cl-bbs/storage))
+ (:export #:generate-rss
+ #:get-all-boards-rss-threads))
+
+(in-package #:cl-bbs/rss)
+
+(defun get-url-scheme (env headers)
+ (if (string= "https" (gethash "x-forwarded-proto" headers))
+ "https"
+ (let ((url-scheme (getf env :url-scheme)))
+ (if url-scheme
+ (string-downcase (string url-scheme))
+ "http"))))
+
+(defun get-request-base-url (env)
+ "Construct the base URL from the request environment."
+ (let* ((headers (getf env :headers))
+ (scheme (get-url-scheme env headers))
+ (host (gethash "host" headers)))
+ (if host
+ (format nil "~a://~a" scheme host)
+ "")))
+
+(defun get-tz-offset-string ()
+ "Returns the basic timezone offset of the machine like '-0300' or '+0000'."
+ (multiple-value-bind (sec min hr date month year day-of-week dst-p tz)
+ (get-decoded-time)
+ (declare (ignore sec min hr date month year day-of-week dst-p))
+ (let* ((offset-hours (- tz))
+ (sign (if (>= offset-hours 0) #\+ #\-))
+ (abs-hours (abs offset-hours)))
+ (format nil "~c~2,'0d00" sign (truncate abs-hours)))))
+
+(defun convert-to-rfc822 (iso-8601-string)
+ "Convert simple ISO 8601 string to RFC1123/RFC822 retaining literal parsing and appending manual offset."
+ (let ((clean-string (cl-ppcre:regex-replace-all " " iso-8601-string "T")))
+ (handler-case
+ (let* ((parsed (local-time:parse-timestring clean-string))
+ (utc-str (local-time:format-rfc1123-timestring nil parsed :timezone local-time:+utc-zone+)))
+ (cl-ppcre:regex-replace "(?:GMT|\\+0000)$" utc-str (get-tz-offset-string)))
+ (error ()
+ (let ((now-utc (local-time:format-rfc1123-timestring nil (local-time:now) :timezone local-time:+utc-zone+)))
+ (cl-ppcre:regex-replace "(?:GMT|\\+0000)$" now-utc (get-tz-offset-string)))))))
+
+
+
+(defun get-all-boards-rss-threads (limit)
+ "Fetch latest threads from all boards combining them for RSS."
+ (let ((all-threads nil)
+ (sexp-base (merge-pathnames "sexp/" storage:*base-dir*)))
+ (when (probe-file sexp-base)
+ (loop for board-dir in (uiop:subdirectories sexp-base) do
+ (let ((board-name (car (last (pathname-directory board-dir))))
+ (list-path (merge-pathnames "list" board-dir)))
+ (when (probe-file list-path)
+ (let ((board-threads (storage:read-sexp-file list-path)))
+ (loop for thread in board-threads do
+ ;; thread is (ID (models:headline . "...") (models:date . "..."))
+ (push (cons (car thread) (cons `(models:board . ,board-name) (cdr thread))) all-threads)))))))
+ ;; Sort by date descending
+ (setf all-threads (sort all-threads
+ (lambda (a b)
+ (string> (cdr (assoc 'models:date (cdr a)))
+ (cdr (assoc 'models:date (cdr b)))))))
+ ;; Take top LIMIT
+ (if (> (length all-threads) limit)
+ (subseq all-threads 0 limit)
+ all-threads)))
+
+(defun generate-rss (board threads env)
+ "Generate an RSS feed for the given board and threads."
+ (let* ((now (local-time:now))
+ (utc-str (local-time:format-rfc1123-timestring nil now))
+ (rfc822-date utc-str)
+ (base-url (get-request-base-url env))
+ (request-url (format nil "~a~a" base-url (getf env :request-uri))))
+ (with-output-to-string (s)
+ (format s "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>~%")
+ (format s "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">~%")
+ (format s " <channel>~%")
+ (if (string= board "all")
+ (progn
+ (format s " <title>cl-bbs - all boards</title>~%")
+ (format s " <description>Latest threads from all boards on cl-bbs.</description>~%"))
+ (progn
+ (format s " <title>cl-bbs - /~a/</title>~%" board)
+ (format s " <description>Latest threads from /~a/.</description>~%" board)))
+ (format s " <link>~a</link>~%" request-url)
+ (format s " <pubDate>~a</pubDate>~%" rfc822-date)
+ (format s " <generator>cl-bbs RSS generator</generator>~%")
+
+ (loop for t-entry in threads do
+ (let* ((id (car t-entry))
+ (thread-data (cdr t-entry))
+ (board-val (if (string= board "all")
+ (or (cdr (assoc 'models:board thread-data)) board)
+ board))
+ (headline (or (cdr (assoc 'models:headline thread-data)) "Untitled"))
+ (date (or (cdr (assoc 'models:date thread-data)) rfc822-date)) ; ISO 8601
+ (pub-date (if (string= date rfc822-date) rfc822-date (convert-to-rfc822 date)))
+ (thread-url (if (not (string= base-url ""))
+ (format nil "~a/~a/~a" base-url board-val id)
+ (format nil "/~a/~a" board-val id)))
+ (thread-path (merge-pathnames (format nil "sexp/~a/~a" board-val id) storage:*base-dir*))
+ (thread-full-data (when (probe-file thread-path) (storage:read-sexp-file thread-path)))
+ (posts (cdr (assoc 'models:posts thread-full-data)))
+ (first-post (when posts (cdar (car posts))))
+ (content (if first-post (cdr (assoc 'models:content first-post)) "")))
+ (format s " <item>~%")
+ (format s " <title><![CDATA[~a]]></title>~%" headline)
+ (format s " <link>~a</link>~%" thread-url)
+ (format s " <description><![CDATA[~a]]></description>~%" headline) ;; Fallback to headline
+ (format s " <content:encoded><![CDATA[~a]]></content:encoded>~%" content)
+ (format s " <pubDate>~a</pubDate>~%" pub-date)
+ (format s " <guid>~a</guid>~%" thread-url)
+ (format s " </item>~%")))
+
+ (format s " </channel>~%")
+ (format s "</rss>~%"))))
diff --git a/server/storage.lisp b/server/storage.lisp
new file mode 100644
index 0000000..48c57b3
--- /dev/null
+++ b/server/storage.lisp
@@ -0,0 +1,43 @@
+(defpackage :cl-bbs/storage
+ (:use :cl)
+ (:export #:*base-dir*
+ #:ensure-board-dirs
+ #:read-sexp-file
+ #:write-sexp-file
+ #:is-board-locked))
+
+(in-package :cl-bbs/storage)
+
+(defvar *base-dir*
+ (pathname (or (uiop:getenv "SBBS_DATADIR")
+ (merge-pathnames "data/" (asdf:system-source-directory :cl-bbs/server)))))
+
+(defun ensure-board-dirs (board-name)
+ "Ensures that directories for storing the board S-expressions and HTML exist."
+ (let ((sexp-dir (merge-pathnames (format nil "sexp/~a/" board-name) *base-dir*))
+ (html-dir (merge-pathnames (format nil "html/~a/" board-name) *base-dir*)))
+ (ensure-directories-exist sexp-dir)
+ (ensure-directories-exist html-dir)))
+
+(defun read-sexp-file (path)
+ "Reads a safe S-expression from the specified file path, or returns NIL if file does not exist."
+ (with-open-file (stream path :direction :input :if-does-not-exist nil)
+ (if stream
+ (let ((*read-eval* nil))
+ (read stream nil nil))
+ nil)))
+
+(defun write-sexp-file (path data)
+ "Writes the given data as a pretty-printed S-expression to the specified file path."
+ (with-open-file (stream path :direction :output :if-exists :supersede :if-does-not-exist :create)
+ (write data :stream stream :pretty t)
+ (terpri stream)))
+
+(defun is-board-locked (board-name)
+ "Checks if a board is locked by examining the SBBS_LOCKED_BOARDS environment variable.
+BOARD-NAME can be a string or a symbol."
+ (let ((locked-env (uiop:getenv "SBBS_LOCKED_BOARDS"))
+ (board-str (if (symbolp board-name) (string-downcase (symbol-name board-name)) board-name)))
+ (when (and locked-env board-str)
+ (let ((locked-boards (cl-ppcre:split "," locked-env)))
+ (member board-str locked-boards :test #'string=)))))
diff --git a/server/views.lisp b/server/views.lisp
new file mode 100644
index 0000000..e3f035a
--- /dev/null
+++ b/server/views.lisp
@@ -0,0 +1,1009 @@
+# (defpackage :cl-bbs/views
+ (:use :cl)
+ (:import-from :cl-who
+ #:with-html-output-to-string
+ #:htm
+ #:str
+ #:esc
+ #:fmt)
+ (:import-from :cl-bbs/storage
+ #:is-board-locked)
+ (:export #:render-index
+ #:render-list
+ #:render-thread
+ #:render-preferences
+ #:render-moderation
+ #:render-error-page
+ #:render-search-results
+ #:render-playground
+ #:preferences
+ #:make-preferences
+ #:preferences-theme
+ #:preferences-syntax-theme
+ #:preferences-default-board
+ #:preferences-search-hide-input
+ #:preferences-search-local-only
+ #:preferences-search-position
+ #:*preferences*))
+
+(in-package :cl-bbs/views)
+
+(defstruct preferences
+ (theme "dark")
+ (syntax-theme "simple")
+ (default-board "")
+ (search-hide-input "no")
+ (search-local-only "no")
+ (search-position "top"))
+
+(defvar *preferences* (make-preferences))
+
+(defun get-git-commit-hash ()
+ "Gets the git commit hash from environmental dynamics (APP_COMMIT_HASH) with a fallback to uiop:run-program."
+ (let ((env-hash (uiop:getenv "APP_COMMIT_HASH")))
+ (if (and env-hash (string/= env-hash ""))
+ env-hash
+ (or (handler-case
+ (string-trim '(#\Space #\Tab #\Newline #\Return)
+ (uiop:run-program '("git" "rev-parse" "--short" "HEAD")
+ :output :string))
+ (error () nil))
+ "unknown"))))
+
+(defun render-footer-html (&optional board)
+ "Renders the common footer HTML with cl-bbs version hash and a GitHub link."
+ (let ((hash (get-git-commit-hash)))
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:p :class "footer"
+ "cl-bbs version:"
+ (:a :href (format nil "https://github.com/ryukinix/cl-bbs/commit/~a" hash)
+ :target "_blank"
+ (cl-who:esc hash))
+ " - "
+ (:a :href (if board (format nil "/~a/rss" board) "/rss")
+ :target "_blank"
+ "RSS Feed")))))
+
+(defun render-board-name (board)
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:h1
+ (cl-who:esc
+ (if (is-board-locked board)
+ (concatenate 'string board " 🔒")
+ board)))))
+
+(defun get-hash-hue (id-val)
+ (let ((id-num (cond ((integerp id-val) id-val)
+ ((stringp id-val) (or (handler-case (parse-integer id-val :junk-allowed t)
+ (error () nil))
+ 0))
+ (t 0))))
+ (mod (* id-num 137) 360)))
+
+(defmacro layout (title class prefs &body body)
+ `(cl-who:with-html-output-to-string (s nil :prologue "<!DOCTYPE html>" :indent t)
+ (:html
+ (:head
+ (:meta :charset "utf-8")
+ (:meta :name "viewport" :content "width=device-width, initial-scale=1.0")
+ (:title (cl-who:esc ,title))
+ (:link :rel "manifest" :href "/manifest.json")
+ (:link :rel "icon" :href "/static/favicon.ico" :type "image/png")
+ (:link :rel "stylesheet"
+ :href (format nil "/static/styles/themes/~a.css" (or (preferences-theme ,prefs) "default"))
+ :type "text/css")
+ (:link :rel "stylesheet"
+ :href (format nil "/static/styles/syntax/~a.css" (or (preferences-syntax-theme ,prefs) "simple"))
+ :type "text/css")
+ (:script "if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/sw.js');
+ });
+}")
+ (:script "
+function validatePostForm(form, errorId) {
+ const content = form.epistula.value.trim();
+ const errorEl = document.getElementById(errorId);
+ if (!content) {
+ if (errorEl) {
+ errorEl.textContent = 'Post body cannot be empty!';
+ errorEl.style.display = 'block';
+ } else {
+ alert('Post body cannot be empty!');
+ }
+ return false;
+ }
+ if (errorEl) {
+ errorEl.style.display = 'none';
+ }
+ return true;
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelectorAll('textarea[name=\"epistula\"]').forEach(function(ta) {
+ ta.addEventListener('keydown', function(e) {
+ if (e.ctrlKey && e.key === 'Enter') {
+ e.preventDefault();
+ ta.form.requestSubmit();
+ }
+ });
+ });
+});
+")
+ (:script :src "/static/jscl-snippets.js" :defer t))
+ (:body :class ,class
+ (cl-who:str (render-boards-header))
+ (:hr)
+ (cl-who:str (progn ,@body))
+ (when (show-search-at-bottom-p)
+ (cl-who:htm
+ (:hr)
+ (cl-who:str (render-search-form))))))))
+
+(defun render-error-page (error-message &optional (prefs *preferences*))
+ "Renders an HTML error page displaying the given ERROR-MESSAGE, using the specified layout PREFS."
+ (layout "Error - cl-bbs" "error-page" prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:h1 "Error")
+ (:hr)
+ (:div :class "error-container"
+ (:p :class "error-title" (cl-who:esc error-message))
+ (:p "We were unable to process your post because it does not meet the validation requirements.")
+ (:p (:button :class "error-back-button" :onclick "history.back();" "← Go Back and Edit Post")))
+ (:hr)
+ (cl-who:str (render-footer-html)))))
+
+(defun board-view-p (path)
+ "Checks if the given PATH represents a board-specific view."
+ (and path
+ (string/= path "/")
+ (string/= path "/index.html")
+ (not (uiop:string-prefix-p "/search" path))
+ (not (uiop:string-prefix-p "/admin" path))
+ (not (uiop:string-prefix-p "/about" path))
+ (not (uiop:string-prefix-p "/sw.js" path))
+ (not (uiop:string-prefix-p "/manifest.json" path))
+ (not (uiop:string-prefix-p "/playground" path))))
+
+(defun get-current-board-from-path (path)
+ "Extracts the board name from the request PATH."
+ (when (and path (string/= path "") (char= (char path 0) #\/))
+ (let ((parts (cl-ppcre:split "/" path)))
+ (when (>= (length parts) 2)
+ (let ((b (second parts)))
+ (and (string/= b "") b))))))
+
+(defun show-search-at-top-p ()
+ "Determines whether the search form should be rendered at the top header."
+ (let* ((env (and (boundp 'ningle:*request*) ningle:*request* (lack.request:request-env ningle:*request*)))
+ (path (and env (getf env :path-info)))
+ (is-board (board-view-p path)))
+ (and (not (and is-board (string= (preferences-search-hide-input *preferences*) "yes")))
+ (string= (preferences-search-position *preferences*) "top"))))
+
+(defun show-search-at-bottom-p ()
+ "Determines whether the search form should be rendered at the bottom of the page."
+ (and (not (string= (preferences-search-hide-input *preferences*) "yes"))
+ (string= (preferences-search-position *preferences*) "bottom")))
+
+(defun render-search-form ()
+ "Renders the search form as a standalone block, with board filter if local search is configured."
+ (let* ((env (and (boundp 'ningle:*request*) ningle:*request* (lack.request:request-env ningle:*request*)))
+ (path (and env (getf env :path-info)))
+ (board (and (board-view-p path) (get-current-board-from-path path))))
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:form :action "/search" :method "GET" :style "margin: 1em 2%; display: inline-flex;"
+ (when (and (string= (preferences-search-local-only *preferences*) "yes") board)
+ (cl-who:htm (:input :type "hidden" :name "board" :value board)))
+ (:input :type "text" :name "q" :placeholder "Search posts..."
+ :style "padding: 2px 5px; font-size: 0.85em; margin-right: 5px;")
+ (:input :type "submit" :value "Search")))))
+
+(defun render-boards-header ()
+ (let* ((sexp-dir (merge-pathnames "sexp/" cl-bbs/storage:*base-dir*))
+ (paths (and (probe-file sexp-dir) (uiop:subdirectories sexp-dir)))
+ (boards (sort (mapcar (lambda (path)
+ (car (last (pathname-directory path))))
+ paths)
+ #'string<))
+ (env (and (boundp 'ningle:*request*) ningle:*request* (lack.request:request-env ningle:*request*)))
+ (path (and env (getf env :path-info)))
+ (board (and (board-view-p path) (get-current-board-from-path path))))
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:div :style "display: flex; justify-content: space-between; align-items: center; margin: 0.5em 2% 1em 2%;"
+ (:p :class "boards" :style "font-size: 0.9em; margin: 0;"
+ "[ "
+ (loop for board in boards
+ for i from 0
+ unless (zerop i)
+ do (cl-who:str " | ")
+ do (cl-who:htm (:a :href (format nil "/~a/" board) (cl-who:esc board))))
+ " ]")
+ (when (show-search-at-top-p)
+ (cl-who:htm
+ (:form :action "/search" :method "GET" :style "margin: 0; display: inline-flex;"
+ (when (and (string= (preferences-search-local-only *preferences*) "yes") board)
+ (cl-who:htm (:input :type "hidden" :name "board" :value board)))
+ (:input :type "text" :name "q" :placeholder "Search posts..."
+ :style "padding: 2px 5px; font-size: 0.85em; margin-right: 5px;")
+ (:input :type "submit" :value "Search"))))))))
+
+(defun render-menu (board selected)
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:p :class "nav"
+ (if (string= selected "front")
+ (cl-who:str "front")
+ (cl-who:htm (:a :href (format nil "/~a" board) "front")))
+ " - "
+ (if (string= selected "list")
+ (cl-who:str "list")
+ (cl-who:htm (:a :href (format nil "/~a/list" board) "list")))
+ " - "
+ (if (string= selected "front")
+ (cl-who:htm (:a :href "#newthread" "new"))
+ (cl-who:htm (:a :href (format nil "/~a#newthread" board) "new")))
+ " - "
+ (:a :href (format nil "/~a/preferences" board) "preferences")
+ " - "
+ (if (string= selected "playground")
+ (cl-who:str "λ")
+ (cl-who:htm (:a :href (format nil "/~a/playground" board) "λ")))
+ " - "
+ (:a :href "/index.html" "?"))))
+
+(defun render-thread-form (board)
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:div :class "newthread-form"
+ (:h2 :id "newthread" "New thread")
+ (:p :id "newthread-error" :style "color: red; font-weight: bold; display: none;")
+ (:form :action (format nil "/~a/post" board)
+ :method "POST"
+ :onsubmit "return validatePostForm(this, 'newthread-error');"
+ (:p (:input :type "text" :name "titulus" :size 35 :placeholder "Headline"))
+ (:p (:textarea :name "epistula"
+ :rows 5
+ :cols 50
+ :placeholder "Message"))
+ (:p (:input :type "text" :name "name" :style "display:none")
+ (:input :type "text" :name "message" :style "display:none")
+ (:input :type "submit" :value "Post"))))))
+
+(defun unescape-html (string)
+ (let ((s string))
+ (setf s (cl-ppcre:regex-replace-all "&quot;" s "\""))
+ (setf s (cl-ppcre:regex-replace-all "&lt;" s "<"))
+ (setf s (cl-ppcre:regex-replace-all "&gt;" s ">"))
+ (setf s (cl-ppcre:regex-replace-all "&#39;" s "'"))
+ (setf s (cl-ppcre:regex-replace-all "&#039;" s "'"))
+ (setf s (cl-ppcre:regex-replace-all "&apos;" s "'"))
+ (setf s (cl-ppcre:regex-replace-all "&#[xX]([0-9a-fA-F]+);" s
+ (lambda (match-string hex-str)
+ (declare (ignore match-string))
+ (string (code-char (parse-integer hex-str :radix 16))))
+ :simple-calls t))
+ (setf s (cl-ppcre:regex-replace-all "&#([0-9]+);" s
+ (lambda (match-string dec-str)
+ (declare (ignore match-string))
+ (string (code-char (parse-integer dec-str :radix 10))))
+ :simple-calls t))
+ (setf s (cl-ppcre:regex-replace-all "&amp;" s "&"))
+ s))
+
+(defun format-text (text &optional thread-id)
+ (let* ((escaped (cl-who:escape-string text))
+ ;; 1. Extract code blocks
+ (code-blocks '())
+ (code-block-placeholder-format "<!--CODEBLOCK-PLACEHOLDER-~a-->")
+ (placeholder-idx 0)
+ (processed escaped))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "(?s)```\\n*(.*?)\\n*```"
+ processed
+ (lambda (match-string &optional content &rest others)
+ (declare (ignore match-string others))
+ (let ((placeholder (format nil code-block-placeholder-format (incf placeholder-idx))))
+ (push (cons placeholder (or content "")) code-blocks)
+ placeholder))
+ :simple-calls t))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "(?m)^&gt;(?!&gt;)\\s*(.*?)$"
+ processed
+ "<blockquote>\\1</blockquote>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "\\*\\*(.*?)\\*\\*"
+ processed
+ "<b>\\1</b>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "__(.*?)__"
+ processed
+ "<i>\\1</i>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "`([^`]+)`"
+ processed
+ "<code>\\1</code>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "~~(.*?)~~"
+ processed
+ "<del>\\1</del>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "&gt;&gt;(\\d+)"
+ processed
+ (lambda (match-string &optional num &rest others)
+ (declare (ignore match-string others))
+ (let ((num-val (or num "")))
+ (if thread-id
+ (format nil "<a href=\"#t~ap~a\">&gt;&gt;~a</a>" thread-id num-val num-val)
+ (format nil "<a href=\"#t~a\">&gt;&gt;~a</a>" num-val num-val))))
+ :simple-calls t))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "https?://[\\w\\-\\.\\/\\?\\=\\&\\%#\\+:\\;]+"
+ processed
+ "<a href=\"\\&\" target=\"_blank\">\\&</a>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ (concatenate 'string
+ "<a href=\"(https?://[\\w\\-\\.\\/\\?\\=\\&\\%#\\+:\\;]+"
+ "\\.(?:png|jpg|jpeg|gif|webp|bmp))\" "
+ "target=\"_blank\">.*?</a>")
+ processed
+ (concatenate 'string
+ "<br /><a href=\"\\1\" target=\"_blank\">"
+ "<img src=\"\\1\" style=\"max-width:300px; "
+ "max-height:300px; display:block; margin:0.5em 0;\" "
+ "alt=\"preview\" /></a><br />")))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "image\\+<a href=\"(https?://[\\w\\-\\.\\/\\?\\=\\&\\%#\\+:\\;]+)\" target=\"_blank\">.*?</a>"
+ processed
+ (concatenate 'string
+ "<br /><a href=\"\\1\" target=\"_blank\">"
+ "<img src=\"\\1\" style=\"max-width:300px; "
+ "max-height:300px; display:block; margin:0.5em 0;\" "
+ "alt=\"preview\" /></a><br />")))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "\\r\\n"
+ processed
+ (string #\Newline)))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "\\n\\n+"
+ processed
+ "</p><p>"))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "\\n"
+ processed
+ "<br />"))
+ (setf processed (format nil "<p>~a</p>" processed))
+ (dolist (pair code-blocks)
+ (let ((placeholder (car pair))
+ (content (cdr pair)))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ placeholder
+ processed
+ (lambda (match-string &optional regs)
+ (declare (ignore match-string regs))
+ (multiple-value-bind (match-start match-end reg-starts reg-ends)
+ (cl-ppcre:scan "^(?i)(lisp|cl|common-lisp)\\r?\\n" content)
+ (declare (ignore reg-starts reg-ends))
+ (if match-start
+ (let* ((escaped-code (subseq content match-end))
+ (raw-code (unescape-html escaped-code))
+ (colorized-code (handler-case (colorize:html-colorization :common-lisp raw-code)
+ (error () (cl-who:escape-string raw-code)))))
+ ;; Note: colorize already wraps the result in <span class="..."><span class="paren1">...
+ ;; We wrap it in <pre class="lisp-code-block"> but keep a data-raw-code attribute
+ ;; or just use content for JS execution. JSCL needs the raw text. To avoid JSCL trying
+ ;; to parse HTML, we'll embed the raw code in a hidden div, or rely on JS `textContent`
+ ;; which extracts raw text from nested HTML elements. `textContent` works well.
+ (format nil "</p><pre class=\"lisp-code-block\">~a</pre><p>" colorized-code))
+ (format nil "</p><pre>~a</pre><p>" content))))
+ :simple-calls t))))
+ (setf processed
+ (cl-ppcre:regex-replace-all
+ "<p>\\s*</p>"
+ processed
+ ""))
+ processed))
+
+(defun render-post-form (board thread-id)
+ (let ((error-id (format nil "reply-error-~a" thread-id)))
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:p :id error-id :style "color: red; font-weight: bold; display: none;")
+ (:form :action (format nil "/~a/~a/post" board thread-id)
+ :method "POST"
+ :onsubmit (format nil "return validatePostForm(this, '~a');" error-id)
+ (:p (:textarea :name "epistula"
+ :rows 8
+ :cols 78
+ :placeholder "Message")
+ (:br)
+ (:input :type "text" :name "name" :class "name" :style "display:none")
+ (:input :type "text" :name "message" :class "message" :style "display:none")
+ (:input :type "submit" :value "Post"))))))
+
+(defun render-frontpage-thread (board thread-data index &optional (prefs *preferences*))
+ (let* ((thread-id (car thread-data))
+ (props (cdr thread-data))
+ (headline (cdr (assoc 'cl-bbs/models:headline props)))
+ (posts (second (assoc 'cl-bbs/models:posts props)))
+ (next-post-number (if posts (1+ (reduce #'max posts :key #'car :initial-value 0)) 1))
+ (truncated (cdr (assoc 'cl-bbs/models:truncated props)))
+ (theme (preferences-theme prefs)))
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:pre :class "jump"
+ (:a :id (format nil "d~a" index)
+ :href (if (= index 10) "#d1" (format nil "#d~a" (1+ index))) "↓")
+ (cl-who:str "&nbsp;"))
+ (let ((heading-style (if (string= theme "colored")
+ (format nil (concatenate 'string
+ "border-left: 5px solid hsl(~D, 80%, 45%); "
+ "padding-left: 10px; margin-left: 2%;")
+ (get-hash-hue thread-id))
+ "")))
+ (cl-who:htm
+ (:h2 :style heading-style
+ (:a :href (format nil "/~a/~a" board thread-id) (cl-who:esc headline)))))
+ (:dl
+ (let ((prev-id nil))
+ (dolist (post posts)
+ (let* ((post-id (car post))
+ (post-data (cdr post))
+ (content (cdr (assoc 'cl-bbs/models:content post-data)))
+ (date (cdr (assoc 'cl-bbs/models:date post-data))))
+ (when (and prev-id (> post-id (1+ prev-id)))
+ ;; Instead of hard assumption based just on IDs, actually check via truncated list if
+ ;; the missing IDs are meant to be rendered as collapsed (i.e. they actually exist in the background).
+ ;; Find the maximum contiguous subsegment of truncated IDs bridging prev-id and post-id.
+ (let* ((missing-ids (loop for id from (1+ prev-id) to (1- post-id) collect id))
+ (actual-missing (if (listp truncated)
+ (remove-if-not (lambda (id) (member id truncated)) missing-ids)
+ missing-ids)))
+ (when (>= (length actual-missing) 2)
+ (let ((fst (car actual-missing))
+ (lst (car (last actual-missing))))
+ (cl-who:htm
+ (:dt :class "collapsed" :style "margin: 0.5em 2%; margin-left: 0; padding-left: 0;"
+ (:a :href (format nil "/~a/~a#t~ap~a" board thread-id thread-id fst)
+ (cl-who:str (format nil "~D" fst)))
+ (when (> lst fst)
+ (cl-who:htm
+ (cl-who:str "...")
+ (:a :href (format nil "/~a/~a#t~ap~a" board thread-id thread-id lst)
+ (cl-who:str (format nil "~D" lst)))))))))))
+ (setf prev-id post-id)
+ (let ((post-style (if (string= theme "colored")
+ (let ((hue (get-hash-hue post-id)))
+ (format nil (concatenate 'string
+ "background-color: hsl(~D, 85%, 96%); "
+ "border-left: 4px solid hsl(~D, 85%, 45%); "
+ "padding: 0.5em 1em; "
+ "margin: 0.3em 2% 1.2em 2%; "
+ "border-radius: 0 4px 4px 0;")
+ hue hue))
+ "")))
+ (cl-who:htm
+ (:dt :style "margin: 0.5em 2%; margin-left: 0; padding-left: 0;"
+ (:a :href (format nil "/~a/~a#t~ap~a" board thread-id thread-id post-id)
+ :id (format nil "t~ap~a" thread-id post-id)
+ (cl-who:str (format nil "~a" post-id)))
+ " "
+ (:samp (cl-who:esc date)))
+ (:dd :style post-style (cl-who:str (format-text content thread-id))))))))
+ (unless (is-board-locked board)
+ (cl-who:htm
+ (:dt :style "margin: 0.5em 2%; margin-left: 0; padding-left: 0;"
+ (:a :href (format nil "#t~ap~a" thread-id next-post-number)
+ :id (format nil "t~ap~a" thread-id next-post-number)
+ (cl-who:str (format nil "~a" next-post-number))))
+ (:dd (cl-who:str (render-post-form board thread-id))))))
+ (:hr))))
+
+(defun render-index (board threads &optional (prefs *preferences*))
+ "Renders the board index (frontpage) HTML with the list of active THREADS
+and the new thread form, using layout PREFS."
+ (layout (format nil "/~a/" board) nil prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (cl-who:str (render-board-name board))
+ (cl-who:str (render-menu board "front"))
+ (:hr)
+ (loop for t-data in threads
+ for i from 1
+ do (cl-who:htm (cl-who:str (render-frontpage-thread board t-data i prefs))))
+ (unless (is-board-locked board)
+ (cl-who:htm (cl-who:str (render-thread-form board))))
+ (:hr)
+ (cl-who:str (render-footer-html board)))))
+
+(defun render-list (board threads &optional (prefs *preferences*))
+ "Renders the board thread-list HTML page, showing all THREADS in tabular format, using layout PREFS."
+ (layout (format nil "/~a/" board) nil prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (cl-who:str (render-board-name board))
+ (cl-who:str (render-menu board "list"))
+ (:hr)
+ (:table :summary "Thread list"
+ (:thead (:tr (:th "#") (:th "headline") (:th "posts") (:th "last update")))
+ (:tbody
+ (loop for t-data in threads
+ for i from 1
+ do (let* ((thread-id (car t-data))
+ (props (cdr t-data))
+ (headline (cdr (assoc 'cl-bbs/models:headline props)))
+ (messages (cdr (assoc 'cl-bbs/models:messages props)))
+ (date (cdr (assoc 'cl-bbs/models:date props))))
+ (cl-who:htm
+ (:tr (:td (cl-who:str (format nil "~a" i)))
+ (:td (:a :href (format nil "/~a/~a" board thread-id) (cl-who:esc headline)))
+ (:td (cl-who:str (format nil "~a" messages)))
+ (:td (:samp (cl-who:esc date)))))))))
+ (:hr)
+ (cl-who:str (render-footer-html board)))))
+
+(defun render-thread (board thread-id thread-data &optional range-string (prefs *preferences*))
+ "Renders a single thread page HTML for THREAD-ID under BOARD with THREAD-DATA (comments),
+optionally filtered by RANGE-STRING, using layout PREFS."
+ (let* ((theme (preferences-theme prefs))
+ (raw-thread (if (and (consp thread-data)
+ (consp (car thread-data))
+ (consp (caar thread-data)))
+ (car thread-data)
+ thread-data))
+ (headline (if (consp (car raw-thread))
+ (cdr (assoc 'cl-bbs/models:headline raw-thread))
+ (cdr (assoc 'cl-bbs/models:headline (list raw-thread)))))
+ (posts-assoc (if (consp (car raw-thread)) (assoc 'cl-bbs/models:posts raw-thread) (cadr thread-data)))
+ (posts-list (if (and posts-assoc (listp (cdr posts-assoc)) (not (keywordp (cdr posts-assoc))))
+ (if (listp (cadr posts-assoc)) (cadr posts-assoc) (cdr posts-assoc))
+ (cdr posts-assoc)))
+ (posts (if (listp (car posts-list)) posts-list (list posts-list)))
+ (next-post-number (if posts (1+ (reduce #'max posts :key #'car :initial-value 0)) 1))
+ (filter-func (if (and range-string (string/= range-string ""))
+ (let ((allowed-ids (make-hash-table :test #'eql)))
+ (dolist (part (cl-ppcre:split "," range-string))
+ (let ((subparts (cl-ppcre:split "-" part)))
+ (cond
+ ((= (length subparts) 1)
+ (let ((id (parse-integer (first subparts) :junk-allowed t)))
+ (when id
+ (setf (gethash id allowed-ids) t))))
+ ((= (length subparts) 2)
+ (let ((start (parse-integer (first subparts) :junk-allowed t))
+ (end (parse-integer (second subparts) :junk-allowed t)))
+ (when (and start end (<= start end))
+ (loop for id from start to end
+ do (setf (gethash id allowed-ids) t))))))))
+ (lambda (id) (gethash id allowed-ids)))
+ (lambda (id) (declare (ignore id)) t))))
+ (layout (format nil "/~a/" board) "thread" prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (cl-who:str (render-board-name board))
+ (cl-who:str (render-menu board "thread"))
+ (:hr)
+ (let ((heading-style (if (string= theme "colored")
+ (format nil "border-left: 5px solid hsl(~D, 80%, 45%); padding-left: 10px;"
+ (get-hash-hue thread-id))
+ "")))
+ (cl-who:htm
+ (:h2 :style heading-style (cl-who:esc headline))))
+ (:dl
+ (loop for post in posts
+ for post-id = (car post)
+ for post-data = (cdr post)
+ for content = (cdr (assoc 'cl-bbs/models:content post-data))
+ for date = (cdr (assoc 'cl-bbs/models:date post-data))
+ when (funcall filter-func post-id)
+ do (let ((post-style (if (string= theme "colored")
+ (let ((hue (get-hash-hue post-id)))
+ (format nil (concatenate 'string
+ "background-color: hsl(~D, 85%, 96%); "
+ "border-left: 4px solid hsl(~D, 85%, 45%); "
+ "padding: 0.5em 1em; margin: 0.3em 0 1.2em 0; "
+ "border-radius: 0 4px 4px 0;")
+ hue hue))
+ "")))
+ (cl-who:htm
+ (:dt (:a :href (format nil "/~a/~a#t~ap~a" board thread-id thread-id post-id)
+ :id (format nil "t~ap~a" thread-id post-id)
+ (cl-who:str (format nil "~a" post-id)))
+ " "
+ (:samp (cl-who:esc date)))
+ (:dd :style post-style (cl-who:str (format-text content thread-id))))))
+ (unless (is-board-locked board)
+ (cl-who:htm
+ (:dt (:a :href (format nil "#t~ap~a" thread-id next-post-number)
+ :id (format nil "t~ap~a" thread-id next-post-number)
+ (cl-who:str (format nil "~a" next-post-number))))
+ (:dd (cl-who:str (render-post-form board thread-id))))))
+ (:hr)
+ (cl-who:str (render-footer-html board))))))
+
+(defun render-preferences (board &optional (prefs *preferences*))
+ "Renders the board preferences HTML page, allowing users to choose a custom
+stylesheet THEME, default-board and search configuration."
+ (let* ((sexp-dir (merge-pathnames "sexp/" cl-bbs/storage:*base-dir*))
+ (paths (and (probe-file sexp-dir) (uiop:subdirectories sexp-dir)))
+ (boards (sort (mapcar (lambda (path)
+ (car (last (pathname-directory path))))
+ paths)
+ #'string<))
+ (theme (preferences-theme prefs))
+ (syntax-theme (preferences-syntax-theme prefs))
+ (default-board (preferences-default-board prefs))
+ (search-hide-input (preferences-search-hide-input prefs))
+ (search-local-only (preferences-search-local-only prefs))
+ (search-position (preferences-search-position prefs)))
+ (layout (format nil "/~a/ - Preferences" board) "preferences" prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (cl-who:str (render-board-name board))
+ (cl-who:str (render-menu board "preferences"))
+ (:hr)
+ (:h2 "Preferences")
+ (:form :action (format nil "/~a/preferences" board) :method "POST" :class "preferences-form"
+ (:div :style "margin-bottom: 2em;"
+ (:h3 :style "margin-bottom: 0.5em;" "Style Theme")
+ (:p :style "color: #555; font-size: 0.9em; margin-bottom: 0.8em;"
+ "Customize the look and feel of the textboard.")
+ (:div :class "theme-selector-container"
+ (dolist (item '("default" "dark" "no" "colored" "matrix"))
+ (cl-who:htm
+ (:label :class "theme-option-label" :style "margin-right: 15px;"
+ (:input :type "radio"
+ :name "theme"
+ :value item
+ :checked (and theme (string= theme item))
+ :onchange "updateThemePreview(this.value)")
+ (:span :class "theme-option-text" (cl-who:str item)))))))
+
+ (:div :style "margin-bottom: 2em;"
+ (:h3 :style "margin-bottom: 0.5em;" "Syntax Theme")
+ (:p :style "color: #555; font-size: 0.9em; margin-bottom: 0.8em;"
+ "Choose syntax highlighting color scheme for Lisp code.")
+ (:div :class "syntax-theme-selector-container"
+ (dolist (item '("simple" "colorful"))
+ (cl-who:htm
+ (:label :class "theme-option-label" :style "margin-right: 15px;"
+ (:input :type "radio"
+ :name "syntax_theme"
+ :value item
+ :checked (and syntax-theme (string= syntax-theme item)))
+ (:span :class "theme-option-text" (cl-who:str item)))))))
+
+ (:div :style "margin-bottom: 2em;"
+ (:h3 :style "margin-bottom: 0.5em;" "Default Board")
+ (:p :style "color: #555; font-size: 0.9em; margin-bottom: 0.8em;"
+ "Select the board you land on when visiting the root domain.")
+ (:div :class "board-selector-container"
+ (:select :name "default_board" :style "padding: 4px; font-size: 0.95em;"
+ (:option :value ""
+ :selected (or (null default-board) (string= default-board ""))
+ "None (Main Page)")
+ (dolist (item boards)
+ (cl-who:htm
+ (:option :value item
+ :selected (and default-board (string= default-board item))
+ (cl-who:str (format nil "/~a/" item))))))))
+
+ (:div :style "margin-bottom: 2em;"
+ (:h3 :style "margin-bottom: 0.5em;" "Search Settings")
+ (:p :style "color: #555; font-size: 0.9em; margin-bottom: 0.8em;"
+ "Configure how the search bar behaves on board indices and thread views.")
+ (:div :class "search-preferences-container" :style "line-height: 1.8em;"
+ (:div :style "margin-bottom: 0.8em;"
+ (:label :style "font-weight: bold;"
+ "Hide search input in board view: ")
+ (:br)
+ (:select :name "search_hide_input" :style "padding: 4px; font-size: 0.95em;"
+ (:option :value "no" :selected (string= search-hide-input "no") "No")
+ (:option :value "yes" :selected (string= search-hide-input "yes") "Yes")))
+ (:div :style "margin-bottom: 0.8em;"
+ (:label :style "font-weight: bold;"
+ "Only make local searches in the current board: ")
+ (:br)
+ (:select :name "search_local_only"
+ :style "padding: 4px; font-size: 0.95em;"
+ (:option :value "no"
+ :selected (string= search-local-only "no")
+ "No (Global)")
+ (:option :value "yes"
+ :selected (string= search-local-only "yes")
+ "Yes (Local)")))
+ (:div :style "margin-bottom: 0.8em;"
+ (:label :style "font-weight: bold;"
+ "Placement of search input: ")
+ (:br)
+ (:select :name "search_position"
+ :style "padding: 4px; font-size: 0.95em;"
+ (:option :value "top"
+ :selected (string= search-position "top")
+ "Top (Header)")
+ (:option :value "bottom"
+ :selected (string= search-position "bottom")
+ "Bottom (Footer)")))))
+
+ (:p :style "margin-top: 2em;"
+ (:input :type "submit" :value "Save Preferences")))
+ (:script "
+function updateThemePreview(themeValue) {
+ // Find all stylesheet links
+ const links = document.querySelectorAll('link[rel=\"stylesheet\"]');
+ for (const link of links) {
+ if (link.href.includes('/static/styles/themes/')) {
+ link.href = '/static/styles/themes/' + themeValue + '.css';
+ }
+ }
+}
+")
+ (:hr)
+ (cl-who:str (render-footer-html))))))
+
+(defun render-moderation (boards &optional board threads thread comments (prefs *preferences*) headline)
+ "Renders the admin/moderation control panel HTML page showing BOARDS and allowing deletions/edits."
+ (layout "cl-bbs Moderation Panel" "moderation" prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:h1 "Moderation Panel")
+ (:p (:a :href "/" "Back to Home"))
+ (:hr)
+ (:h2 "Boards")
+ (:ul
+ (dolist (b boards)
+ (cl-who:htm
+ (:li (:strong (:a :href (format nil "/admin?board=~a" b) (cl-who:esc b)))
+ " &nbsp; "
+ (:form :action "/admin/action" :method "POST" :style "display:inline;"
+ (:input :type "hidden" :name "action" :value "delete-board")
+ (:input :type "hidden" :name "board" :value b)
+ (:input :type "submit" :value "Delete Board" :class "delete-button"
+ :onclick (concatenate 'string
+ "return confirm('Are you sure you want to "
+ "delete the ENTIRE board? "
+ "This cannot be undone.');")))))))
+ (:h2 "Create Board")
+ (:p "Enter a board name below to create a new board. Board names must be in "
+ (:strong "kebab-case")
+ " (only lowercase letters, numbers, and hyphens; no spaces or underlines).")
+ (:div :style "margin: 1em 0;"
+ (:form :action "/admin/action" :method "POST" :onsubmit "return validateCreateBoard()"
+ (:input :type "hidden" :name "action" :value "create-board")
+ (:input :type "text" :name "board" :id "new-board-name" :placeholder "board-name"
+ :style "padding: 6px; font-size: 1em; border: 1px solid #bababa;
+ border-radius: 4px; font-family: monospace;")
+ " "
+ (:input :type "submit" :value "Create Board"))
+ (:p :id "board-error" :style "color: red; font-size: 0.9em; margin: 0.5em 0; display: none;"))
+ (:script :type "text/javascript"
+ "function validateCreateBoard() {
+ const input = document.getElementById('new-board-name');
+ const error = document.getElementById('board-error');
+ const boardName = input.value.trim();
+
+ // Regex for kebab-case (lowercase alphanumeric and hyphens only, no start/end hyphens)
+ const kebabRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+
+ if (!boardName) {
+ error.textContent = 'Please enter a board name.';
+ error.style.display = 'block';
+ return false;
+ }
+
+ if (!kebabRegex.test(boardName)) {
+ error.textContent = 'Invalid board name! Must contain only lowercase ' +
+ 'alphanumeric characters and hyphens (e.g. \"lisp-board\", no spaces, ' +
+ 'underlines or capitals).';
+ error.style.display = 'block';
+ return false;
+ }
+
+ error.style.display = 'none';
+ return true;
+}")
+ (when board
+ (cl-who:htm
+ (:hr)
+ (:h2 (cl-who:fmt "Threads in /~a/" board))
+ (if threads
+ (cl-who:htm
+ (:table :border 1 :cellpadding 5
+ (:thead (:tr (:th "ID") (:th "Headline") (:th "Date") (:th "Actions")))
+ (:tbody
+ (dolist (t-data threads)
+ (let* ((tid (car t-data))
+ (props (cdr t-data))
+ (headline (cdr (assoc 'cl-bbs/models:headline props))))
+ (cl-who:htm
+ (:tr (:td (cl-who:str (format nil "~a" tid)))
+ (:td (:a :href (format nil "/admin?board=~a&thread=~a" board tid)
+ (cl-who:esc headline)))
+ (:td (cl-who:str (format nil "~a" (cdr (assoc 'cl-bbs/models:date props)))))
+ (:td (:form :action "/admin/action" :method "POST" :style "display:inline;"
+ (:input :type "hidden" :name "action" :value "delete-thread")
+ (:input :type "hidden" :name "board" :value board)
+ (:input :type "hidden" :name "thread" :value tid)
+ (:input :type "submit" :value "Delete Thread" :class "delete-button"
+ :onclick
+ (concatenate 'string
+ "return confirm('Are you sure you want "
+ "to delete this thread?');")))
+ (unless (string-equal board "shame")
+ (cl-who:htm
+ (:form :action "/admin/action" :method "POST"
+ :style "display:inline; margin-left: 5px;"
+ (:input :type "hidden" :name "action" :value "shame-thread")
+ (:input :type "hidden" :name "board" :value board)
+ (:input :type "hidden" :name "thread" :value tid)
+ (:input :type "submit" :value "Shame" :class "shame-button"
+ :onclick
+ (concatenate 'string
+ "return confirm('Are you sure you want "
+ "to move this thread to the shame "
+ "board?');")))))))))))))
+ (cl-who:htm (:p "No threads found on this board.")))))
+ (when (and board thread)
+ (cl-who:htm
+ (:hr)
+ (:h2 (cl-who:fmt "Comments in Thread #~a (~a)" thread board))
+ (if comments
+ (cl-who:htm
+ (:dl
+ (dolist (p comments)
+ (let* ((pid (car p))
+ (pdata (cdr p))
+ (content (cdr (assoc 'cl-bbs/models:content pdata)))
+ (date (cdr (assoc 'cl-bbs/models:date pdata))))
+ (cl-who:htm
+ (:dt "No." (cl-who:str (format nil "~a" pid)) " " (:samp (cl-who:esc date))
+ " &nbsp; "
+ (:form :action "/admin/action" :method "POST" :style "display:inline;"
+ (:input :type "hidden" :name "action" :value "delete-comment")
+ (:input :type "hidden" :name "board" :value board)
+ (:input :type "hidden" :name "thread" :value thread)
+ (:input :type "hidden" :name "comment" :value pid)
+ (:input :type "submit" :value "Delete Comment" :class "delete-button"
+ :onclick
+ "return confirm('Are you sure you want to delete this comment?');")))
+ (:dd
+ (:div :class "comment-preview"
+ (cl-who:str (format-text content thread)))
+ (:form :action "/admin/action" :method "POST" :style "margin-top: 0.5em;"
+ (:input :type "hidden" :name "action" :value "edit-comment")
+ (:input :type "hidden" :name "board" :value board)
+ (:input :type "hidden" :name "thread" :value thread)
+ (:input :type "hidden" :name "comment" :value pid)
+ (when (and (= pid 1) headline)
+ (cl-who:htm
+ (:p (:label :for "headline" "Thread Headline: ")
+ (:br)
+ (:input :type "text" :name "headline" :id "headline"
+ :size 60 :value headline))))
+ (:textarea :name "content" :rows 3 :cols 60 (cl-who:str content))
+ (:br)
+ (:input :type "submit" :value "Save Changes"))))))))
+ (cl-who:htm (:p "No comments found."))))))))
+
+(defun render-search-results (query results &optional (prefs *preferences*))
+ "Renders the search results page HTML, showing matching posts for the given QUERY, using layout PREFS."
+ (let ((theme (preferences-theme prefs)))
+ (layout (format nil "Search: ~a" query) "search-results" prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (:h1 "Search Results")
+ (:p :style "margin: 0.5em 2%;"
+ (:button :class "lisp-btn" :onclick "history.back();" "← Go Back")
+ " - Query: "
+ (:strong (cl-who:esc query)))
+ (:hr)
+ (if (null results)
+ (cl-who:htm (:p :style "margin: 2em; text-align: center;" "No results found matching your query."))
+ (cl-who:htm
+ (:dl :style "margin: 1em 2%;"
+ (dolist (match results)
+ (let* ((board (getf match :board))
+ (thread-id (getf match :thread-id))
+ (headline (getf match :headline))
+ (post-id (getf match :post-id))
+ (date (getf match :date))
+ (content (getf match :content))
+ (post-style (if (string= theme "colored")
+ (let ((hue (get-hash-hue post-id)))
+ (format nil (concatenate 'string
+ "background-color: hsl(~D, 85%, 96%); "
+ "border-left: 4px solid hsl(~D, 85%, 45%); "
+ "padding: 0.5em 1em; "
+ "margin: 0.3em 0 1.2em 0; "
+ "border-radius: 0 4px 4px 0;")
+ hue hue))
+ "")))
+ (cl-who:htm
+ (:dt :style "margin-top: 1.5em; font-size: 0.95em;"
+ "[" (:a :href (format nil "/~a/" board) (cl-who:esc board)) "] "
+ (:a :href (format nil "/~a/~a" board thread-id) (:strong (cl-who:esc headline)))
+ " - Post "
+ (:a :href (format nil "/~a/~a#t~ap~a" board thread-id thread-id post-id)
+ (cl-who:str (format nil "#~a" post-id)))
+ " "
+ (:samp (cl-who:esc date)))
+ (:dd :style post-style
+ (cl-who:str (format-text content thread-id)))))))))
+ (:hr)
+ (cl-who:str (render-footer-html))))))
+
+(defun render-playground (&optional board (prefs *preferences*))
+ "Renders the interactive Common Lisp playground view."
+ (layout (if board (format nil "/~a/ - Lisp Playground" board) "Lisp Playground")
+ "playground-page"
+ prefs
+ (cl-who:with-html-output-to-string (s nil :indent t)
+ (when board
+ (cl-who:htm (cl-who:str (render-menu board "playground"))))
+ (:h2 "Common Lisp Playground")
+ (:p :style "margin: 0.5em 2%; font-size: 0.95em;"
+ "Write and execute Common Lisp code directly in your browser using "
+ (:a :href "https://github.com/jscl-project/jscl" :target "_blank" "JSCL")
+ ". Everything runs completely client-side in a sandboxed environment.")
+ (:div :class "playground-container" :style "margin: 1.5em 2%;"
+ (:div :style "margin-bottom: 1em; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;"
+ (:span "Load Example: ")
+ (:select :id "playground-examples" :style "padding: 4px;"
+ (:option :value "" "-- Select Example --")
+ (:option :value "hello" "Hello World")
+ (:option :value "fib" "Fibonacci Numbers")
+ (:option :value "loop" "Loop Macro")
+ (:option :value "clos" "Common Lisp Object System (CLOS)")))
+ (:div :id "example-data-hello" :style "display:none;"
+ (cl-who:str (colorize:html-colorization :common-lisp "(format t \"Hello, World!~%\")")))
+ (:div :id "example-data-fib" :style "display:none;"
+ (cl-who:str (colorize:html-colorization :common-lisp "(defun fib (n)
+ (if (< n 2)
+ n
+ (+ (fib (- n 1)) (fib (- n 2)))))
+
+(format t \"Fibonacci of 10 is: ~a~%\" (fib 10))")))
+ (:div :id "example-data-loop" :style "display:none;"
+ (cl-who:str (colorize:html-colorization :common-lisp "(loop for x from 1 to 5
+ do (format t \"Square of ~d is ~d~%\" x (* x x)))")))
+ (:div :id "example-data-clos" :style "display:none;"
+ (cl-who:str (colorize:html-colorization :common-lisp "(defclass person ()
+ ((name :accessor person-name :initarg :name)
+ (age :accessor person-age :initarg :age)))
+
+(defmethod introduce ((p person))
+ (format t \"Hi, I am ~a and I am ~a years old.~%\"
+ (person-name p)
+ (person-age p)))
+
+(let ((p (make-instance 'person :name \"Alice\" :age 30)))
+ (introduce p))")))
+ (:pre :id "playground-editor"
+ :class "lisp-code-block"
+ :contenteditable "true"
+ :spellcheck "false"
+ :style (concatenate 'string
+ "min-height: 200px; width: 96%; max-width: 800px; "
+ "font-family: monospace; font-size: 1.1em; padding: 10px; "
+ "border: 1px solid currentColor; background: transparent; "
+ "color: inherit; margin-bottom: 1em; outline: none; "
+ "overflow: auto; white-space: pre-wrap;")
+ "")
+ (:div :style "display: flex; gap: 10px; margin-bottom: 1em;"
+ (:button :id "playground-run" :class "lisp-btn" "Run Code")
+ (:button :id "playground-clear" :class "lisp-btn" "Clear Output"))
+ (:h3 "Output Console")
+ (:pre :id "playground-output"
+ :style (concatenate 'string
+ "display: none; padding: 10px; width: 96%; max-width: 800px; "
+ "border: 1px dashed currentColor; "
+ "background-color: rgba(128, 128, 128, 0.05); white-space: pre-wrap; "
+ "word-break: break-all; font-family: monospace; font-size: 1.1em; "
+ "line-height: 1.4em;")
+ ""))
+ (:hr)
+ (cl-who:str (render-footer-html)))))
diff --git a/static/about.html b/static/about.html
new file mode 100644
index 0000000..e19b02a
--- /dev/null
+++ b/static/about.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>cl-bbs - About</TITLE>
+<LINK rel="stylesheet" href="/static/styles/about.css" type="text/css">
+</HEAD>
+<BODY>
+<H1>About CL-BBS</H1>
+<HR>
+<P><A href="/">Back to Home</A></P>
+
+<H2>A Bit of History</H2>
+<P><DEL>/prog/</DEL> was a <A href="https://en.wikipedia.org/wiki/Textboard">textboard</A> about ``programming'', hosted at world4ch.org and later dis.4chan.org. It was an odd and fascinating place that went mostly unmoderated for years. Unfortunately the owners were made to remember its existence thanks to imbecilic spammers; and without consideration for the small community that inhabited the place, the formers decided to pull the plug on a sad day of July 2014. <P> If you weren't lucky enough to have been a part of it, an archive of all the posts is available at <A href="https://archive.org/details/prog-20140418.db">archive.org</A>. There's a <A href="http://world4search.readsicp.org/">search engine</A> for this archive which has also been entirely restored as web pages at <A href="https://archive.tinychan.org/prog/">tinychan.org</A>.</P>
+<P>Back then, a somewhat recurrent joke was that the BBS script, <CODE>shiichan</CODE>, should be rewritten in Scheme. Alyssa P. Hacker fulfilled this dream in MIT Scheme. Our project provides a complete, production-grade Common Lisp port of this textboard engine, preserving all original aesthetics and capabilities.</P>
+
+<H2>More History</H2>
+<P>If you're interested in the history of textboards as a whole, here are a few interesting reads: </P>
+<UL>
+<LI><A href="https://wakaba.c3.cx/shii/shiichan">Shii's blurb</A></LI>
+<LI><A href="http://tanasinn.info/wiki/Main_Page">Tanasinn wiki</A></LI>
+<LI><A href="https://github.com/bibanon/bibanon/wiki/2channel">bibanon</A></LI>
+</UL>
+
+<H2>Other Textboard Scripts</H2>
+<UL>
+<LI><A href="https://github.com/j416dy/2ch">bbs.cgi</A> - The original <A href="https://5ch.net/">2ch</A> Perl script</LI>
+<LI><A href="https://wakaba.c3.cx/shii/shiichan3960.zip">Shiichan</A> - The PHP script that was used at world4ch</LI>
+<LI><A href="http://wakaba.c3.cx/s/web/wakaba_kareha">Kareha</A> - Popular Perl script used by <A href="http://4-ch.net/4ch">4-ch.net</A> among others</LI>
+<LI><A href="http://tanami.org/pub/software/TrueScript/Tablecat.zip">Tablecat</A> - Tablecat's Perl script (the site is currently offline)</LI>
+<LI><A href="https://github.com/desuwa/hivebbs">HiveBBS</A> - A Ruby script seen at <A href="http://bbs.neet.tv/">bbs.neet.tv</A></LI>
+<LI><A href="https://github.com/z411/weabot">Weabot</A> - Python script used by Bienvenido a Internet</LI>
+</UL>
+
+<H2>Bonus Track</H2>
+<P>Listening to this tune will make you enjoy writing Common Lisp code.</P>
+<P><A href="https://www.youtube.com/watch?v=WZCs4Eyalxc"><IMG src="/static/img/snake.png" alt="The SICP snake"></A></P>
+</BODY>
+</HTML>
diff --git a/static/art.ico b/static/art.ico
new file mode 100644
index 0000000..9e0ba21
--- /dev/null
+++ b/static/art.ico
Binary files differ
diff --git a/static/errors/400.html b/static/errors/400.html
new file mode 100644
index 0000000..e78aa17
--- /dev/null
+++ b/static/errors/400.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>400 bad request</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ _ _ ___ ___
+| || | / _ \ / _ \
+| || |_| | | | | | |
+|__ _| |_| | |_| |
+ |_| \___/ \___/
+
+ error 400
+ bad request
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/403.html b/static/errors/403.html
new file mode 100644
index 0000000..02b5256
--- /dev/null
+++ b/static/errors/403.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>403 forbidden</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ _ _ ___ _____
+ | || | / _ \___ /
+ | || |_| | | ||_ \
+ |__ _| |_| |__) |
+ |_| \___/____/
+
+ error 403
+ forbidden
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/404.html b/static/errors/404.html
new file mode 100644
index 0000000..5ecb584
--- /dev/null
+++ b/static/errors/404.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>404 not found</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ _ _ ___ _ _
+ | || | / _ \| || |
+ | || |_| | | | || |_
+ |__ _| |_| |__ _|
+ |_| \___/ |_|
+
+ error 404
+ page not found
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/405.html b/static/errors/405.html
new file mode 100644
index 0000000..9f22a74
--- /dev/null
+++ b/static/errors/405.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>405 method not allowed</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ _ _ ___ ____
+ | || | / _ \| ___|
+ | || |_| | | |___ \
+ |__ _| |_| |___) |
+ |_| \___/|____/
+
+ error 405
+ method not allowed
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/429.html b/static/errors/429.html
new file mode 100644
index 0000000..1a625f6
--- /dev/null
+++ b/static/errors/429.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>Too Many Requests</TITLE>
+<STYLE type="text/css">
+</STYLE>
+</HEAD>
+<BODY>
+<H1>Cool Down</H1>
+<P>Easy on the post button. Please wait 5 seconds before submitting another POST request</P>
+</BODY>
+</HTML>
diff --git a/static/errors/500.html b/static/errors/500.html
new file mode 100644
index 0000000..fdd75e4
--- /dev/null
+++ b/static/errors/500.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>500 internal server error</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ ____ ___ ___
+ | ___| / _ \ / _ \
+ |___ \| | | | | | |
+ ___) | |_| | |_| |
+ |____/ \___/ \___/
+
+ error 500
+ internal server error
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/502.html b/static/errors/502.html
new file mode 100644
index 0000000..bfd6a7e
--- /dev/null
+++ b/static/errors/502.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>502 bad gateway</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ ____ ___ ____
+| ___| / _ \___ \
+|___ \| | | |__) |
+ ___) | |_| / __/
+|____/ \___/_____|
+
+ error 502
+ bad gateway
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/503.html b/static/errors/503.html
new file mode 100644
index 0000000..50abd13
--- /dev/null
+++ b/static/errors/503.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>503 service unavailable</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ ____ ___ _____
+| ___| / _ \___ /
+|___ \| | | ||_ \
+ ___) | |_| |__) |
+|____/ \___/____/
+
+ error 503
+ service unavailable
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/errors/504.html b/static/errors/504.html
new file mode 100644
index 0000000..9e892a3
--- /dev/null
+++ b/static/errors/504.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>504 gateway timeout</TITLE>
+</HEAD>
+<BODY>
+<PRE>
+ ____ ___ _ _
+| ___| / _ \| || |
+|___ \| | | | || |_
+ ___) | |_| |__ _|
+|____/ \___/ |_|
+
+ error 504
+ gateway timeout
+</PRE>
+</BODY>
+</HTML>
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..4dd119b
--- /dev/null
+++ b/static/favicon.ico
Binary files differ
diff --git a/static/img/cloudflare.png b/static/img/cloudflare.png
new file mode 100644
index 0000000..ad7ff2e
--- /dev/null
+++ b/static/img/cloudflare.png
Binary files differ
diff --git a/static/img/freebsd.png b/static/img/freebsd.png
new file mode 100644
index 0000000..b00e7d4
--- /dev/null
+++ b/static/img/freebsd.png
Binary files differ
diff --git a/static/img/gnu.png b/static/img/gnu.png
new file mode 100644
index 0000000..a4109c4
--- /dev/null
+++ b/static/img/gnu.png
Binary files differ
diff --git a/static/img/i2p.png b/static/img/i2p.png
new file mode 100644
index 0000000..36a9980
--- /dev/null
+++ b/static/img/i2p.png
Binary files differ
diff --git a/static/img/mit-scheme.png b/static/img/mit-scheme.png
new file mode 100644
index 0000000..076d4a9
--- /dev/null
+++ b/static/img/mit-scheme.png
Binary files differ
diff --git a/static/img/nginx.png b/static/img/nginx.png
new file mode 100644
index 0000000..389f308
--- /dev/null
+++ b/static/img/nginx.png
Binary files differ
diff --git a/static/img/nocookie.png b/static/img/nocookie.png
new file mode 100644
index 0000000..916b894
--- /dev/null
+++ b/static/img/nocookie.png
Binary files differ
diff --git a/static/img/nojs.png b/static/img/nojs.png
new file mode 100644
index 0000000..f00d277
--- /dev/null
+++ b/static/img/nojs.png
Binary files differ
diff --git a/static/img/snake.png b/static/img/snake.png
new file mode 100644
index 0000000..8f039ec
--- /dev/null
+++ b/static/img/snake.png
Binary files differ
diff --git a/static/img/src/freebsd.svg b/static/img/src/freebsd.svg
new file mode 100644
index 0000000..0f331ee
--- /dev/null
+++ b/static/img/src/freebsd.svg
@@ -0,0 +1 @@
+<?xml version="1.0" ?><svg enable-background="new 0 0 515.91 728.5" height="512" id="Layer_1" version="1.1" viewBox="0 0 512.00003 512" width="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs7"/><g id="g4064" transform="translate(248.78395,68.722659)"><path d="m -733.62329,72.267944 c 0,203.804876 -165.21649,369.021366 -369.02141,369.021366 -203.8048,0 -369.0213,-165.21649 -369.0213,-369.021366 0,-203.804874 165.2165,-369.021364 369.0213,-369.021364 203.80492,0 369.02141,165.21649 369.02141,369.021364 z" id="path4942" style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(0.69372682,0,0,0.69372682,772.15023,137.14313)"/><path d="m 7.2159984,-36.722673 c -123.7116984,0 -224.0000084,100.28815 -224.0000084,223.999993 0,123.71173 100.28831,224.00003 224.0000084,224.00003 123.7118116,0 224.0000216,-100.2883 224.0000216,-224.00003 0,-123.711843 -100.28821,-223.999993 -224.0000216,-223.999993 z m -48.3350004,21.944941 c 1.7136,-0.0851 2.9057,0.53928 3.22,2.17 -2.0447,8.1113813 -17.6023,14.6857813 -18.48,25.025052 -0.3569,4.22443 2.5222,7.72318 5.705,11.34 9.6922,-6.22003 30.1909,-8.57852 45.1500004,-5.98506 15.05761,2.60413 34.4186096,16.877507 36.7150096,23.869997 7.3784,-0.28929 18.8254,-1.86412 24.92,-7.98 1.0136,-8.534727 -2.1897,-23.7059167 -6.23,-30.7649277 -1.4959,-2.60411996 -2.4583,-7.69216 5.355,-4.65507 18.1613,11.515971 28.198,30.7202617 29.365,37.6250577 3.9921,23.60187 -10.9797,43.73252 -41.09,59.01 5.2062,29.802533 -10.4076,41.546963 -18.655,48.930003 -5.0056,4.47519 -11.1918,8.96941 -12.6,15.47 -1.1756,5.40108 2.599,10.19491 4.2,16.03 3.6757,10.75211 13.8759,33.57367 1.435,48.47494 3.7605,14.61208 7.0623,32.033 18.095,42.97999 15.7683,15.63421 38.0534,18.93944 61.635,25.30496 11.410002,3.07574 33.774302,9.71498 39.620002,11.76 23.1959,6.29574 24.5469,31.18599 3.115,35.14003 1.4092,2.36478 3.535,4.0319 5.6,7.07012 -17.5063,2.60602 -34.8666,2.20539 -44.415,2.76493 -13.963702,1.30391 -45.070802,1.89819 -45.745002,-2.90493 14.3227,-6.79974 44.675402,-14.71781 45.745002,-26.4951 5.6519,-2.421 7.8049,7.37307 9.975,13.16004 9.1134,-0.5768 20.9515,-3.60718 25.445,-8.08499 0.8314,-2.01521 -1.5211,-4.2644 -3.045,-5.8101 -4.2813,-4.34023 -9.9319,-6.72886 -16.065,-8.71495 -32.474102,-7.62989 -53.292002,-11.63557 -64.645002,-13.82505 -15.2977,-2.96174 -35.3511,-15.26975 -38.64,-20.54495 0,6.36529 4.6432,17.09109 8.26,25.34 1.6213,3.69264 4.3634,7.20989 3.64,11.55 -1.1583,3.90432 -14.5089,8.27927 -22.3300096,10.99 -9.7991,3.37591 -23.5285004,5.8582 -30.4150004,6.37004 -16.7337,1.24298 -51.378,5.60034 -73.185,-3.08004 -4.031498,-1.72727 -6.030498,-4.17413 -6.859998,-7.49 -8.5936,0.6636 -34.2297,2.52135 -41.965,-5.77505 -2.02531,-2.1699 -4.12091,-5.42058 -3.46501,-9.24 1.9965,-11.64118 37.00701,-14.3388 58.380008,-16.24 9.1047,-0.82051 21.4806,-0.67862 27.335,-7.7 3.3371,-4.00255 -2.6787,-14.20464 -2.1,-15.4699 1.1767,-1.67832 6.0196,2.14357 7.91,2.24 -2.7971,-3.38475 -5.4329,-6.62054 -8.365,-10.15 -7.8124,-9.40363 -10.848,-23.01331 -3.325,-35.45505 -3.1249,-3.75189 -7.686,-6.36519 -5.95,-13.02 -3.1538,-0.0862 -8.2483,-3.07092 -7.525,-6.54495 -8.2464,2.31661 -17.063698,0.0594 -16.484998,-6.89505 -15.5861,-4.43666 -14.7131,-18.76482 -6.65,-26.63494 -3.9543,-3.42396 -8.882,-8.94589 -12.74,-11.55 -0.029,-0.0291 -8.6725,7.62272 -11.865,7.80495 -15.89471,6.28845 -38.77181,-11.88063 -40.18001,-19.63495 4.8803,0.23139 7.3057,1.40034 10.99,1.68 3.7037,0.27967 9.2625,0.1736 11.095,0 -0.5787,3.03901 -4.0781,4.60701 -4.165,7.10495 6.92501,3.35641 25.14811,1.60685 26.18001,-3.64 -3.5204,-3.51938 -8.4892,-8.59432 -11.865,-11.97 -2.0157,-0.0582 -2.3169,1.70564 -3.185,4.16505 -1.8807,-2.02541 -7.59591,-14.8988 -9.66001,-19.07505 6.69361,1.97725 13.68951,3.79411 20.05501,5.91505 -1.2828,1.17757 -5.0128,3.63552 -3.325,4.9 6.5104,3.18282 13.5371,7.44498 17.395,8.4 0.492,-1.36965 2.3122,-4.40137 2.1,-7.31505 -0.3472,-4.73458 -3.1309,-8.68527 -5.6,-12.49495 -2.1219,1.35039 -3.7462,4.95791 -6.09,4.06 -0.868,-5.20811 -1.4715,-7.11233 -2.87,-10.43 -1.5624,-3.70372 -5.5112,-6.81307 -6.09,-10.43 2.7778,-2.27729 18.713,10.0286 23.275,17.08 3.1828,4.91792 5.4562,8.56364 5.215,14.91 -0.019,3.81943 -1.9055,6.95688 -2.87,10.43 2.7488,2.60411 5.9549,5.34912 8.26,7.77 3.3274,-4.91792 10.695398,-6.74061 19.529998,-4.44505 4.4752,1.50561 6.916,6.69244 7.35,11.13 0.299,3.13453 -1.406,6.71708 -3.605,8.74999 0.3472,0.58845 0.616,1.11104 1.05,1.68 2.961,-1.39932 8.2639,-1.7762 11.9,1.99506 5.594,5.70875 15.5611,-1.19236 15.05,-13.61505 -0.8198,-1.1564 -1.6652,-2.3436 -2.485,-3.5 -7.4071,-2.79788 -17.403,-8.61751 -21.56,-13.44 -8.1113,-4.64778 -14.8171,-5.47983 -21.665,-11.58495 -9.403798,-8.3814 -12.764498,-25.60857 -6.369998,-34.125063 2.3534,-3.14406 6.219798,-6.07443 9.064998,-10.67494 2.093,-3.38632 3.6812,-6.9039 3.71,-10.67506 0.029,-4.57172 3.6695,-14.62876 4.2,-16.90494 -15.672898,-27.24668 0.8664,-44.940667 22.085,-61.24998766 5.93,-4.55875004 18.9751,-13.87008134 25.095,-14.17506134 z m 15.12,45.115053 c -3.5085,-0.0437 -6.9385,1.253726 -9.415,4.269996 6.0287,-6.234026 27.3694004,1.1359 23.835,15.05 5.7045004,-9.34394 -4.7639,-19.199706 -14.42,-19.319996 z m -25.69,8.504936 c -9.3748,0 -12.418,3.64807 -17.115,7.80506 -5.131,4.53309 -9.1849,10.92325 -12.985,17.39494 -3.9929,6.79964 -7.9109,15.5306 -6.58,22.71506 0.512,2.75632 2.1597,5.91908 4.445,8.22494 -0.1994,-0.24091 -0.3981,-0.46838 -0.595,-0.73494 -3.1538,-8.08405 3.9714,-20.15406 8.855,-21.77 l 0.63,-0.14 -0.42,1.4 c -0.6752,2.77771 -0.8275,6.35712 -0.49,7.91 0.1254,-0.84 2.1933,-5.78693 3.64,-8.96 l 0.245,-0.59506 0.28,0 c 3.7614,0.4629 6.9667,3.3852 7.42,7.73506 -0.1663,1.86323 -0.5367,3.68592 -1.015,5.42494 1.8434,-4.89742 3.2051,-10.28003 4.515,-14.38494 2.7198,-8.50674 6.5808,-16.7123 9.59,-24.92 0.8005,-2.18938 2.9171,-7.10506 -0.42,-7.10506 z m 20.125,5.04 c -6.0846,0.23543 -13.5139,6.69525 -16.59,12.53 -3.5854,5.97005 -7.8756,13.23012 -10.955,20.68506 0.1525,-0.16229 0.2926,-0.335 0.455,-0.49 2.6331,-2.13158 5.2163,-3.24576 7.56,-3.53506 l 1.225,-0.0347 -0.1052,0.21 c -1.2249,2.47878 -1.9038,7.23117 -1.19,10.11506 0.54,-3.12502 2.39,-7.3574 3.8849,-9.34506 0.9215,-0.55138 1.3564,-0.14706 2.52,0.45506 8.9496,6.50216 2.4723,38.163113 -10.15,36.854953 4.615,2.61453 10.6538,3.30366 15.82,3.11505 7.8123,-0.2894 19.614,-2.85453 24.7451,0.28 -2.3245,-2.28524 -9.6989,-2.39668 -12.11,-2.31 4.9188,-5.93163 9.2379,-15.228763 10.6749,-21.980003 3.3757,-15.96325 1.0572,-40.1427 -12.6,-46.02506 -0.9961,-0.42974 -2.0582,-0.5684 -3.1849,-0.52494 z m -46.2,56.420003 c -0.8394,0.037 -1.5689,0.1064 -2.135,0.28 -5.1213,1.41782 -9.1231,5.54334 -9.345,10.11506 -0.2604,5.51679 5.0805,10.88484 10.115,13.16 4.62,1.62132 9.86,-1.84452 15.155,-1.01505 2.0448,0.51117 3.4153,2.66739 5.46,2.97505 0.463,0.0795 0.9431,0.12992 1.435,0.14 1.4756,0.0381 3.0316,-0.24024 4.025,-1.22505 1.6782,-1.65883 2.2706,-4.73704 1.47,-6.96495 -0.3375,-0.95389 -1.8749,-2.32411 -2.415,-1.89 -0.5594,0.5796 0.8475,2.15903 1.05,3.22 0.3377,1.78517 0.112,3.07653 -1.19,4.34 -1.2924,1.26437 -2.4604,0.86868 -4.235,0.56 -1.8999,-0.3183 -3.3632,-2.48169 -5.215,-2.8 -4.4849,-1.4467 -10.7319,2.49727 -15.4,0.38495 -4.803,-2.10347 -8.2888,-5.97844 -8.26,-11.23495 0.328,-4.65841 3.6203,-6.78517 7.7,-7.87505 3.5878,-0.79084 8.6533,-0.38091 11.865,2.31 0.7523,1.32137 1.8974,0.47029 0.875,-1.01495 -1.7161,-2.49985 -7.3175,-3.62489 -10.955,-3.46506 z m -11.9,25.65506 c -0.8077,0.037 -1.4641,0.33858 -1.645,0.91 -0.5787,1.87018 3.9998,1.17119 5.215,2.69495 3.0381,3.81942 7.6579,9.16753 11.13,12.49505 7.2723,6.96259 18.7865,13.92563 27.65,13.51 1.2635,-0.0582 2.4766,-0.27832 3.605,-0.66505 7.8605,-1.77476 14.2902,-11.46992 22.575,-13.37 5.3239,-2.43052 7.6435,0.72307 11.2700004,-0.28 0.8006,-0.22087 2.0929,-0.39458 2.17,-1.22495 0.088,-0.76182 -0.9917,-1.29897 -1.715,-1.54 -4.1376004,-0.81995 -8.5139004,-0.0157 -12.2850004,0.91 -8.7961,2.36287 -13.9086,12.35898 -22.155,13.82495 -16.348,2.92241 -33.0545,-12.68389 -42.14,-25.93495 -0.5968,-0.80169 -2.329,-1.39193 -3.675,-1.33 z m 37.065,33.00495 c 0.2581,0.66796 0.4629,1.35923 0.63,2.13505 6.4957,4.02987 21.5349,1.70162 29.12,-1.64505 0.3217,-0.52159 -22.0805,6.11933 -29.75,-0.49 z m 0.91,21.97999 c -1.4548,0.0594 -2.9368,0.25816 -4.445,0.59506 -1.5269,0.54566 -8.7748,3.03162 -6.37,4.44494 2.5511,-5.03216 15.757,-5.10888 22.715,-1.82 9.0468,4.28042 14.9181,12.47468 22.47,13.33505 6.2788,0.72331 10.2720004,-1.94364 10.5700004,-2.31 0.1833,-0.23161 0.7,-1.77004 -0.63,-1.36505 -2.0071,0.61836 -3.0362004,3.065 -10.6750004,2.27505 -6.6549,-0.69529 -14.1901,-10.68244 -21.665,-13.05505 -4.0026,-1.3263 -7.2977,-2.03526 -10.535,-2.1 -0.4855,-0.0112 -0.95,-0.019 -1.435,0 z m 43.8550004,52.29 c -3.7431004,0.74177 -7.8635004,0.95502 -11.7600004,1.22505 l -1.925,1.78495 23.6600004,18.62 c 12.2384096,5.53616 12.4648096,-2.95927 6.75501,-8.50495 l -16.73001,-13.12505 z m -54.7400004,42.63 c 0.2894,1.15842 0.1926,2.64231 -0.28,4.37505 6.9347,0.47411 13.4102,1.59131 19.39,3.39495 1.1767,-1.19593 1.293,-2.80191 0.56,-4.76 -6.2498,-1.79177 -12.8415,-2.74164 -19.67,-3.01 z m 29.4,3.60495 c -0.9165,0 -1.8487,0.028 -2.765,0.0706 -0.2315,1.21329 -0.067,2.17178 0.28,3.00989 6.4621,-1.30212 16.3977,0.58352 20.545,3.25505 0.2026,-0.93565 0.3448,-1.8639 -0.07,-2.8 -5.2492,-2.37832 -11.5741,-3.5103 -17.99,-3.53505 z m 37.4150004,1.85505 c -3.43349999,0 -6.23,3.28597 -6.23,7.38495 0,4.09909 2.79540001,7.45505 6.23,7.45505 3.44331,0 6.1950096,-3.35596 6.1950096,-7.45505 0,-4.09898 -2.7516996,-7.38495 -6.1950096,-7.38495 z m -68.1800004,1.68 c -0.569,1.11944 -1.8011,1.83993 -3.325,2.38 6.0377,1.10141 11.7023,2.38728 16.38,4.27 2.1894,-0.60009 3.5695,-1.52801 4.235,-2.69495 -4.7067,-1.80376 -10.4903,-3.10576 -17.29,-3.95505 z m 28.35,2.8 c -0.7523,1.10913 -0.8422,2.21681 -0.63,3.32495 8.1885,-0.15434 13.0354,0.76394 18.34,2.90505 1.2345,-0.72329 2.2809,-1.9824 2.03,-3.32505 -6.491,-1.99539 -13.0657,-2.99231 -19.74,-2.90495 z m -35.35,1.15505 c -1.2152,0.41284 -3.261,0.80876 -5.46,1.22495 3.8387,1.47695 8.0323,2.90808 12.845,4.2 2.0737,-0.12476 4.4826,-0.25905 5.775,-0.77 -4.7162,-1.43628 -9.0223,-3.02321 -13.16,-4.65495 z m -12.53,2.1 -6.37,0.94495 c 4.9285,1.19437 9.4663,2.55013 13.44,4.13 2.7391,-0.0862 4.3759,-0.50019 5.215,-1.15495 -3.5975,-1.47469 -7.5301,-2.83997 -12.285,-3.92 z m -24.779998,1.43495 c -14.2068,1.98924 -37.7727,3.89625 -41.65001,12.28505 -2.498,4.00288 0.2059,8.48423 4.30501,11.72495 9.2398,5.28628 23.9188,5.0466 38.955,3.85 -0.8294,-3.75905 0.098,-8.57013 2.73,-11.55 -13.9851,2.24784 -39.9169,0.2828 -38.885,-2.1 0.018,-0.047 0.085,-0.41037 0.1053,-0.59505 -0.077,3.13454 31.5217,4.55828 40.53,0.98 1.8518,-1.90076 4.737898,-4.40788 7.805098,-5.91495 -2.6525,-3.7996 -7.047198,-6.79739 -13.895098,-8.68 z m 67.304998,0.98 c -2.093,0.52125 -3.6153,1.07128 -4.165,1.78505 5.8544,0.45395 11.2438,1.13827 15.96,2.1699 2.7295,-0.36411 4.3685,-0.93622 5.005,-1.68 -5.0346,-1.01058 -10.3958,-1.85215 -16.8,-2.27495 z m -10.185,2.87 c -2.5269,0.4134 -5.2155,0.84527 -7.945,1.26 6.1631,0.60749 12.0345,1.35329 15.96,3.18495 3.26,-0.51218 5.0803,-1.24746 5.495,-2.13495 -2.604,-1.17455 -7.289,-1.89492 -13.51,-2.31 z m -19.775,2.24 c -7.9473,0.6552 -16.3967,1.16155 -21.875,3.74495 -8.487398,3.98272 -14.470798,9.44238 -12.319998,18.2 1.5914,6.51021 9.396398,9.03896 17.989998,9.97509 48.1375,5.20756 85.8501004,-1.26373 110.81001,-15.29509 l 0.595,-2.8 c -0.328,-1.97748 -0.7966,-3.90466 -1.925,-5.88 -20.2928096,10.14641 -42.10521,14.50792 -64.92501,14.31505 -14.3226,4.26295 -49.6685,1.00464 -58.589998,-5.00495 12.162198,4.06046 45.078498,8.78954 58.204998,3.08 -1.4476,-12.78144 -13.9702,-19.16857 -27.965,-20.33505 z" id="path8303" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g></svg> \ No newline at end of file
diff --git a/static/img/src/gnu.svg b/static/img/src/gnu.svg
new file mode 100644
index 0000000..06403cb
--- /dev/null
+++ b/static/img/src/gnu.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="534.68079"
+ height="522.76312"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ version="1.0"
+ sodipodi:docname="heckert_gnu.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ objecttolerance="10"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.35"
+ inkscape:cx="375"
+ inkscape:cy="520"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1680"
+ inkscape:window-height="975"
+ inkscape:window-x="0"
+ inkscape:window-y="25" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <cc:license
+ rdf:resource="http://artlibre.org/licence.php/lalgb.html" />
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Aurelio A. Hecker &lt;aurium@gmail.com&gt;</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:title>GNU Head</dc:title>
+ </cc:Work>
+ <cc:License
+ rdf:about="http://artlibre.org/licence.php/lalgb.html">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#ShareAlike" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Notice" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Attribution" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-126.94531,-19.552058)">
+ <g
+ id="Layer_1"
+ transform="translate(126.95974,19.57999)">
+ <g
+ id="g2490">
+ <path
+ id="path2492"
+ d="M 112.993,304.372 C 109.359,304.619 104.628,306.247 101.454,311.936 C 98.863,316.582 92.793,317.323 89.959,316.387 C 85.238,314.827 79.204,313.745 73.359,317.91 C 67.514,322.074 61.202,318.925 62.309,311.334 C 63.416,303.744 66.158,296.983 73.436,292.796 C 89.331,283.652 78.284,277.288 87.008,268.111 C 95.749,258.916 93.818,256.102 93.528,243.614 C 63.985,239.176 47.241,230.661 28.544,207.301 C 9.847,183.94 2.599,164.711 0.661,135.423 C -1.277,106.135 0.403,87.154 13.057,62.843 C 25.711,38.532 39.034,26.829 62.122,14.69 C 85.21,2.553 107.832,-0.354 128.742,0 C 149.652,0.353 163.585,1.885 175.4,8.626 C 187.214,15.367 198.022,23.85 204.912,23.911 C 211.803,23.972 216.494,22.167 219.843,19.601 C 220.737,42.304 200.304,44.276 182.024,44.756 C 163.743,45.235 152.51,37.673 135.673,40.486 C 118.836,43.299 91.269,50.961 77.84,74.336 C 64.411,97.711 63.953,105.974 65.069,125.576 C 66.185,145.179 83.057,159.804 92.581,159.963 C 102.104,160.122 104.23,157.364 112.566,151.927 C 120.903,146.489 163.462,93.805 177.984,81.971 C 192.507,70.137 206.838,66.483 222.312,66.293 C 237.786,66.103 241.74,66.628 254.22,71.743 C 266.7,76.857 274.369,86.276 284.431,92.034 C 289.574,89.007 292.769,84.487 297.488,81.038 C 314.365,68.703 323.134,65.379 343.193,67.331 C 354.722,68.453 369.259,75.039 377.933,82.581 C 393.378,96.01 406.138,114.706 416.449,128.909 C 424.478,139.97 433.926,153.204 447.414,157.341 C 462.141,161.856 473.223,142.782 474.85,132.202 C 475.772,126.207 476.975,120.953 476.985,114.355 C 476.995,107.756 476.321,99.748 474.419,92.871 C 472.517,85.994 469.259,79.151 465.731,73.477 C 459.989,64.242 450.594,54.578 440.819,49.606 C 435.333,46.816 422.426,41.743 414.29,41.565 C 404.321,41.349 396.726,44.698 387.509,45.326 C 376.353,46.087 356.425,48.595 347.679,39.976 C 342.113,34.491 341.413,30.832 340.462,23.68 C 342.921,25.706 346.36,27.038 349.421,27.297 C 356.407,27.887 362.948,22.183 368.587,18.878 C 373.232,16.155 379.81,11.289 385.911,8.83 C 401.587,2.51 420.843,2.692 437.391,4.385 C 450.655,5.741 467.852,11.642 479.485,18.225 C 491.173,24.837 502.436,35.986 510.686,46.496 C 521.065,59.72 526.739,71.066 530.498,87.559 C 535.303,108.621 536.635,140.261 530.826,161.028 C 526.248,177.393 511.047,202.532 498.107,214.855 C 485.345,227.01 470.609,232.877 453.851,236.086 C 454.507,239.468 454.615,242.622 455.482,245.395 C 456.714,249.331 459.07,253.719 463.91,252.331 C 465.84,251.778 466.873,251.299 469.418,253.525 C 471.961,255.751 470.637,264.35 463.48,268.209 C 459.851,270.166 456.021,270.591 450.209,270.707 C 444.397,270.823 435.326,270.848 429.016,269.743 C 422.707,268.639 418.938,263.632 411.95,262.848 C 401.151,261.64 392.882,253.838 384.223,248.258 C 380.61,245.929 379.391,245.905 377.309,244.728 C 378.434,248.172 379.934,251.749 380.682,255.06 C 381.432,258.371 381.112,261.438 381.51,264.088 C 381.906,266.736 382.641,268.795 383.069,270.943 C 384.799,272.49 385.352,274.843 388.258,275.583 C 391.166,276.322 395.719,275.171 398.694,274.771 C 401.669,274.371 403.663,272.75 405.997,273.194 C 408.333,273.637 410.546,275.309 412.503,276.876 C 414.46,278.443 416.142,278.672 417.726,282.582 C 419.31,286.492 420.763,294.499 420.98,299.851 C 421.197,305.203 420.597,310.034 419.011,314.281 C 417.425,318.528 416.031,320.411 411.736,324.617 C 407.441,328.824 400.447,334.997 393.468,339.297 C 394.753,341.2 395.323,342.942 397.327,345.007 C 399.331,347.073 403.509,349.388 405.368,351.567 C 407.225,353.746 408.167,355.309 408.194,357.753 C 408.223,360.196 408.104,363.372 406.12,366.036 C 401.179,372.669 395.335,373.857 388.229,376.977 C 389.424,384.439 390.399,389.77 388.672,397.221 C 387.967,400.257 386.401,407.141 382.656,408.907 C 378.91,410.673 371.687,409.818 367.115,410.544 C 362.543,411.271 359.984,411.961 356.42,412.669 C 354.254,415.8 351.029,419.255 349.924,422.061 C 348.817,424.868 349.151,425.874 350.08,428.74 C 351.009,431.606 354.172,435.779 355.068,439.135 C 355.964,442.491 356.138,446.104 355.302,449.469 C 354.464,452.834 353.031,455.276 350.163,458.264 C 347.296,461.253 341.655,463.6 338.845,466.022 C 336.033,468.445 333.234,468.736 334.001,472.745 C 335.056,478.251 343.101,487.181 348.921,489.307 C 352.409,490.582 357.091,490.18 361.327,488.392 C 361.04,493.095 356.97,496.265 354.311,499.388 C 361.512,497.329 368.495,493.387 373.325,488.162 C 371.827,492.7 366.673,499.985 358.964,506.382 C 351.255,512.779 351.302,511.47 347.47,514.015 C 354.462,515.42 360.964,515.04 366.38,514.067 C 363.411,515.704 358.544,518.006 352.097,518.683 C 345.65,519.359 341.675,518.881 335.865,517.47 C 330.054,516.058 324.75,510.776 317.709,507.135 C 318.395,509.747 318.883,512.888 321.844,515.574 C 324.805,518.26 329.285,519.626 334.047,521.949 C 327.985,521.261 321.615,521.053 315.658,519.74 C 311.248,518.767 308.285,517.735 305.058,516.137 C 301.833,514.539 299.343,512.208 296.486,510.244 C 297.855,512.859 298.261,515.935 300.591,518.091 C 302.921,520.247 306.708,521.05 309.771,522.531 C 306.576,522.461 303.503,523.154 300.191,522.321 C 296.877,521.488 293.769,520.179 290.324,517.641 C 286.879,515.103 283.326,511.978 280.386,508.783 C 277.448,505.588 273.82,501.446 272.722,498.5 C 271.622,495.553 273.382,493.492 273.712,490.991 C 271.683,492.324 269.187,493.058 267.628,494.993 C 266.069,496.928 264.687,498.695 265.377,502.068 C 266.068,505.441 269.181,509.564 271.382,513.316 C 264.814,507.945 261.146,505.877 259.583,499.398 C 258.12,493.332 261.905,487.826 264.782,483.239 C 266.211,480.961 268.349,478.841 269.685,476.642 C 266.718,473.739 263.327,471.244 260.784,467.933 C 258.242,464.623 257.174,460.143 254.579,456.976 C 251.983,453.808 248.491,451.803 245.45,449.22 C 248.123,454.962 252.516,462.005 253.47,466.449 C 254.423,470.893 252.401,473.613 250.824,476.012 C 249.246,478.411 246.7,478.946 244.285,480.42 C 241.87,481.895 238.114,480.732 236.675,482.921 C 235.237,485.109 234.851,488.401 236.226,490.569 L 241.866,499.471 C 239.788,498.422 236.208,495.371 234.594,493.207 C 232.98,491.043 230.504,489.044 230.165,486.118 C 229.826,483.193 229.661,480.654 231.556,478.048 C 233.452,475.443 238.642,474.571 240.965,472.568 C 243.288,470.565 244.591,469.261 244.458,466.672 C 244.326,464.083 241.797,462.231 239.623,460.599 C 232.986,455.621 224.472,451.034 218.836,445.034 C 215.652,441.644 214.124,438.907 213.484,436.568 C 212.845,434.23 213.521,432.406 215.103,429.53 C 216.685,426.653 222.11,422.812 222.77,419.687 C 223.429,416.563 220.689,415.892 218.216,414.79 C 215.743,413.688 211.046,415.355 208.752,413.438 C 206.459,411.521 209.426,407.493 206.841,405.284 C 204.257,403.075 198.342,404.308 195.289,401.932 C 192.236,399.555 190.974,396.605 189.922,393.157 C 188.87,389.709 188.751,385.932 189.464,383.435 C 190.177,380.938 192.319,380.501 193.611,379.207 C 194.903,377.913 196.024,376.84 194.302,374.91 C 192.58,372.981 186.937,375.018 183.693,372.541 C 180.449,370.064 182.435,364.131 180.141,361.63 C 177.848,359.129 173.732,360.108 171.56,359.309 C 169.389,358.51 168.537,361.329 167.205,356.871 C 165.873,352.412 165.837,339.121 164.045,334.161 C 162.253,329.201 158.734,333.826 157.1,328.896 C 155.466,323.967 157.015,316.084 156.279,308.639 L 152.944,288.382 C 141.31,296.197 133.63,298.651 121.131,300.859 C 120.088,331.441 117.215,349.45 127.595,383.379 C 137.975,417.307 158.982,437.303 187.321,466.028 C 149.926,438.012 132.073,415.236 120.899,386.058 C 109.723,356.876 111.869,332.472 112.993,304.372 z M 311.701,292.08 C 314.578,292.209 318.074,292.939 320.094,294.251 C 322.115,295.563 324.26,296.635 326.094,298.502 C 330.67,303.162 334.102,308.782 335.619,315.24 C 336.586,319.357 336.73,324.637 337.082,329.471 C 335.697,325.003 334.766,320.344 332.793,316.341 C 329.682,310.029 324.397,303.868 317.834,301.031 C 315.668,300.093 314.223,300.267 313.031,300.433 C 313.564,301.328 314.457,302.078 314.496,303.389 C 314.535,304.7 314.242,307.265 312.019,308.343 C 308.435,310.082 304.451,308.859 301.683,306.412 C 298.892,303.946 297.941,299.774 299.402,296.189 C 301.143,291.917 307.777,291.906 311.701,292.08 z M 275.629,286 C 275.99,278.655 278.641,272.461 285.223,268.859 C 288.627,266.997 293.871,264.589 298.69,264.023 C 303.319,263.48 314.579,263.877 321.352,266.968 C 326.553,269.342 330.096,273.038 334.663,275.739 C 339.229,278.441 342.477,280.252 346.825,283.282 C 353.499,287.933 357.411,293.054 362.038,296.124 C 364.263,297.599 367.393,297.38 370.347,298.282 C 368.05,298.964 365.048,299.716 362.57,298.924 C 359.48,297.936 355.16,295.111 352.763,293.086 C 348.527,289.508 346.331,287.762 341.708,285.063 C 338.948,283.455 334.403,281.56 331.37,279.582 C 328.335,277.604 323.067,273.264 320.151,271.795 C 316.444,269.929 309.77,268.891 305.643,269.012 C 299.957,269.178 291.727,270.116 287.327,274.108 C 282.956,278.076 281.296,281.567 282.282,286.72 C 283.928,284.824 285.024,282.442 286.673,281.032 C 290.089,278.114 294.009,277.973 298.261,278.526 C 303.038,279.148 305.443,281.907 307.021,286.442 C 305.959,285.662 304.007,283.949 302.466,283.418 C 298.849,282.17 296.114,282.554 292.726,284.371 C 288.726,286.517 289.814,289.693 289.238,293.225 C 288.974,294.854 288.359,296.166 287.92,297.637 C 286.416,297.239 285.025,296.613 283.887,295.485 C 283.934,297.135 283.694,298.578 284.032,300.434 C 284.368,302.289 285.272,304.493 285.891,306.522 C 283.106,303.736 280.256,301.142 278.457,297.545 C 276.656,293.946 275.436,289.952 275.629,286 z M 280.449,232.54 C 282.502,229.493 283.103,224.819 284.369,221.019 C 283.863,225.832 284.836,231.125 282.486,234.611 C 280.136,238.097 276.009,237.486 272.59,238.5 C 275.25,236.472 278.395,235.587 280.449,232.54 z M 231.298,303.908 C 233.934,301.774 241.048,303.48 246.369,303.861 C 242.545,304.84 238.721,305.82 234.897,306.799 C 234.541,308.1 234.702,309.591 233.829,310.7 C 232.955,311.81 231.295,312.221 230.029,312.98 C 230.154,309.562 228.662,306.041 231.298,303.908 z M 268.789,155.688 C 271.965,157.398 275.996,159.659 278.801,159.729 C 281.606,159.799 282.34,157.895 283.99,156.796 C 286.586,165.261 289.566,174.562 293.986,182.243 C 295.838,185.461 297.019,183.965 298.427,187.199 C 299.833,190.432 300.458,197.011 301.792,201.142 C 303.128,205.273 304.87,208.313 306.409,211.898 C 302.501,206.361 297.452,199.301 295.274,194.693 C 293.098,190.086 294.491,189.685 292.729,185.297 C 290.967,180.909 287.405,174.069 284.743,168.455 C 282.837,168.708 280.477,169.055 279.028,168.608 C 277.579,168.161 277.868,166.62 276.559,165.624 C 275.248,164.628 273.045,165.014 271.209,163.424 C 269.373,161.835 268.955,158.574 268.789,155.688 z M 251.688,197.457 C 254.677,197.195 258.389,197.996 261.188,197.837 C 263.987,197.678 265.985,196.952 268.384,196.509 C 267.939,192.293 268.361,184.547 264.469,181.732 C 261.955,179.914 251.63,181.283 248.872,181.878 C 242.666,183.216 235.439,186.92 229.437,189.236 C 235.189,185.218 240.158,181.494 247.057,179.42 C 251.204,178.173 260.882,176.552 265.162,177.708 C 267.116,178.235 267.511,179.044 268.696,180.7 C 272.77,186.388 272.758,191.304 273.815,197.93 C 274.854,199.549 275.961,200.629 276.803,202.299 C 277.643,203.969 278.119,206.597 278.354,208.745 C 277.288,207.199 277.94,205.26 274.987,204.109 C 272.032,202.958 267.508,204.033 263.527,204.449 C 266.429,206.543 267.871,208.56 267.982,212.987 C 268.112,218.185 263.711,221.926 259.116,222.282 C 252.737,222.777 249.448,221.376 246.868,215.924 C 245.738,213.536 246.557,210.663 246.463,208.335 C 238.798,208.868 238.923,208.618 233.321,213.289 C 236.366,217.75 238.284,220.315 243.316,222.984 C 246.138,224.482 249.333,225.938 252.83,226.533 C 256.328,227.128 262.065,227.703 265.739,227.139 C 269.414,226.575 271.496,225.526 274.22,223.007 C 276.945,220.487 277.195,216.879 278.259,214.027 C 278.017,216.772 277.702,220.268 276.683,222.259 C 273.583,228.312 268.222,230.106 261.821,230.126 C 255.677,230.146 248.077,228.729 242.361,226.359 C 237.791,224.465 230.215,219.101 228.417,214.441 C 227.188,211.257 228.161,207.35 228.032,203.804 C 234.36,199.596 235.926,200.631 243.413,199.399 C 246.545,198.883 248.699,197.72 251.688,197.457 z M 214.827,168.956 C 215.005,166.435 215.284,166.489 215.513,165.255 C 209.748,163.713 207.436,167.453 204.056,174.399 C 202.791,176.998 204.043,178.545 202.686,180.256 C 201.331,181.967 199.147,181.727 196.946,183.37 C 194.745,185.012 192.564,188.157 190.506,189.858 C 186.283,193.349 180.575,195.48 175.677,197.829 C 179.022,195.361 185.513,191.618 187.613,188.032 C 188.727,186.131 188.562,182.546 190.296,180.837 C 192.029,179.127 192.636,177.944 194.874,177.508 C 197.112,177.072 196.212,174.478 196.934,172.416 C 197.656,170.354 199.094,168.89 200.624,166.83 C 202.154,164.77 204.084,161.677 207.105,160.554 C 210.126,159.43 214.837,160.645 217.124,159.998 C 219.41,159.351 218.097,157.689 219.758,156.691 C 221.421,155.693 224.654,156.133 226.875,155.066 C 230.812,153.173 232.695,150.013 235.063,146.63 C 235.407,153.912 235.323,159.155 232.706,166.006 C 229.759,173.717 226.772,178.259 218.408,183.001 C 215.526,184.634 212.658,185.201 210.269,186.058 C 211.662,184.158 213.656,183.333 214.449,180.358 C 215.24,177.381 214.649,171.478 214.827,168.956 z M 349.865,204.463 C 345.888,204.722 344.945,204.199 341.763,206.443 C 343.488,209.127 345.212,211.811 346.935,214.495 C 346.783,209.197 347.299,208.135 349.865,204.463 z M 339.445,200.613 C 344.953,198.267 349.752,196.369 355.744,195.994 C 358.146,195.843 361.875,196.215 364.301,196.325 C 366.367,193.283 369,190.884 369.965,187.412 C 370.604,185.109 370.289,174.42 369.158,171.645 C 365.863,163.569 362.66,163.228 358.125,158.602 C 354.834,155.247 354.703,150.138 353.719,144.413 C 353.358,142.31 352.326,139.649 351.842,137.479 C 345.914,136.48 339.539,136.151 332.719,136.492 C 336.026,133.32 339.223,130.422 340.172,125.66 C 341.119,120.898 340.348,115.043 337.641,110.773 C 334.934,106.503 330.61,102.783 326.641,99.304 C 322.67,95.825 316.979,92.953 313.217,90.107 C 318.11,88.355 323.106,85.781 327.899,84.852 C 340.385,82.43 351.143,86.134 363.186,93.415 C 370.188,97.65 375.979,101.673 383.299,109.336 C 390.617,116.998 399.694,131.053 406.779,139.388 C 413.864,147.723 419.584,154.315 425.765,159.286 C 431.947,164.258 437.542,165.674 443.429,168.867 L 452.605,178.611 L 460.031,169.422 L 471.98,170.773 L 474.449,157.361 L 485.416,154.356 L 483.627,136.276 L 491.512,130.68 L 485.586,119.431 L 491.607,107.139 L 483.757,93.088 L 486.784,79.559 L 477.696,71.814 L 476.51,57.437 L 463.832,52.601 L 458.746,40.093 L 444.492,39.933 L 437.5,30.648 L 427.904,31.32 L 417.207,30.155 L 411.158,25.689 L 402.379,31.517 L 391.756,25.978 L 370.639,33.995 L 384.405,22.023 L 398.667,19.745 L 409.593,16.96 L 424.349,21.806 L 437.337,18.516 L 449.86,25.919 L 463.112,25.186 L 469.622,36.867 L 482.817,38.39 L 489.956,51.377 L 498.255,55.908 L 500.458,72.545 L 507.849,79.555 L 504.599,94.112 L 513.306,105.136 L 505.142,116.948 L 513.872,124.331 L 502.712,134.222 L 514.015,145.993 L 500.365,150.395 L 509.256,163.02 L 498.106,164.944 L 505.418,178.826 L 492.852,178.659 L 499.182,189.057 L 485.399,187.398 L 488.696,202.357 L 478.28,193.194 L 474.712,211.324 L 467.524,198.306 L 463.631,214.543 L 454.758,202.57 L 455,219.596 L 445.193,204.287 C 447.261,212.973 446.847,222.484 452.552,230.673 C 470.228,228.281 481.781,223.827 495.323,210.796 C 507.657,198.928 522.399,174.183 526.391,158.488 C 530.946,140.578 531.086,107.638 526.6,89.404 C 522.282,71.848 516.67,63.462 506.643,49.136 C 497.059,38.957 487.633,28.204 475.469,20.945 C 457.613,10.29 434.717,7.417 414.367,7.694 C 403.099,7.848 395.551,10.012 388.814,12.462 C 377.033,16.746 367.611,26.091 355.634,30.709 C 351.728,32.214 348.597,32.261 345.521,30.618 C 348.533,36.644 351.81,39.347 359.322,41.54 C 364.756,43.126 377.722,42.123 386.648,41.299 C 396.23,40.414 401.228,37.565 414.269,37.744 C 427.005,37.919 437.097,42.757 442.615,45.912 C 454,52.422 462.164,59.644 469.258,71.084 C 473.154,77.366 475.883,84.454 477.805,91.687 C 479.727,98.92 480.569,107.021 480.692,114.102 C 480.815,121.183 480.274,127.504 478.542,133.876 C 474.491,148.794 465.554,163.974 447.196,161.629 C 435.962,160.193 419.026,139.107 413.354,131.371 C 401.909,115.758 390.133,98.481 375.752,85.479 C 366.984,77.554 354.979,72.1 343.19,71.021 C 321.073,68.997 314.514,73.198 297.866,86.92 C 301.253,88 304.559,88.596 308.03,90.16 C 315.733,93.631 322.778,98.117 328.923,103.871 C 335.362,109.902 338.251,113.719 338.48,122.355 C 338.632,128.09 333.914,132.688 329.589,136.824 C 326.943,139.357 323.441,141.676 321.298,143.642 C 318.458,146.251 314.941,149.105 318.509,153.261 C 319.851,154.823 322.181,156.001 324.146,157.46 C 327.265,159.775 325.837,168.379 321.728,170.661 C 316.47,173.578 310.187,173.252 305.851,169.347 C 311.324,168.528 314.8,167.802 314.658,163.325 C 314.576,160.73 311.277,158.574 308.039,157.098 C 300.23,153.538 294.332,154.063 286.56,148.471 C 278.71,142.82 278.537,139.726 276.415,130.854 C 272.63,131.25 269.145,131.337 264.084,133.013 C 259.023,134.688 253.43,138.676 248.835,139.663 C 237.263,142.153 234.957,141.415 228.624,139.776 C 217.474,136.89 202.956,141.831 188.534,146.444 C 186.136,147.211 176.437,146.47 169.908,148.761 C 159.824,152.299 145.675,160.867 137.503,167.749 C 125.121,178.178 108.849,209.389 99.655,245.213 C 98.689,256.103 101.786,263.44 94.926,268.759 C 88.066,274.078 88.256,287.705 85.93,291.527 C 83.604,295.348 80.911,293.659 75.904,299.026 C 70.897,304.393 68.348,303.496 68.474,313.644 C 72.789,310.807 79.08,307.509 84.604,307.31 C 89.496,307.133 89.548,313.574 102.104,301.161 C 108.819,294.52 121.72,294.317 130.671,291.388 C 139.623,288.459 155.754,282.423 171.91,264.361 C 188.066,246.299 195.797,227.284 198.93,199.794 C 199.429,204.668 200.295,210.165 198.208,223.61 C 196.121,237.055 191.623,252.156 201.099,270.244 L 207.003,281.513 C 203.488,276.716 196.622,268.891 194.555,263.315 L 189.787,250.452 C 179.12,267.2 176.668,270.196 161.85,282.195 C 161.26,289.639 161.115,297.403 163.548,304.526 C 165.981,311.648 162.103,319.858 163.954,324.549 C 165.805,329.24 170.086,326.57 171.484,331.319 C 172.882,336.068 170.754,345.457 172.922,350.086 C 175.09,354.715 180.409,351.906 182.987,353.401 C 185.565,354.897 186.408,356.947 187.787,358.914 C 189.165,360.88 187.248,365.528 190.052,367.037 C 192.855,368.547 197.228,367.268 200.145,368.717 C 203.062,370.166 203.505,373.649 202.806,376.77 C 202.106,379.891 198.989,381.323 197.62,384.496 C 196.252,387.67 196.405,389.879 198.049,392.793 C 199.693,395.708 203.333,397.313 206.961,398.784 C 210.589,400.255 215.687,399.429 219.431,401.463 C 223.175,403.497 226.54,407.55 228.517,410.496 C 230.494,413.441 231.043,415.519 230.942,418.616 C 230.84,421.712 229.584,425.279 227.931,428.27 C 226.278,431.261 222.631,434.901 228.325,440.613 C 234.019,446.324 233.809,443.862 237.071,447.567 C 236.716,441.158 234.068,435.937 236.004,431.112 C 237.941,426.287 243.756,425.448 246.821,423.278 C 249.886,421.107 251.651,418.125 254.067,415.549 C 254.032,418.864 254.387,422.428 254.01,424.924 C 253.242,429.999 251.744,434.504 259.307,435.318 C 262.714,435.684 265.949,433.475 269.534,432.149 C 267.656,435.512 264.39,438.216 263.899,442.241 C 263.408,446.264 263.998,450.684 266.784,454.677 C 269.571,458.668 274.383,461.632 277.909,463.978 C 281.434,466.325 283.323,466.513 286.032,467.782 C 286.165,471.738 285.386,475.925 285.837,479.651 C 286.288,483.377 287.462,486.675 289.616,489.755 C 291.772,492.836 295.229,494.603 298.485,496.881 C 296.7,492.724 293.079,488.78 293.128,484.41 C 293.177,480.041 294.487,476.944 298.749,473.067 C 303.011,469.189 312.989,465.826 317.694,462.056 C 322.399,458.285 323.917,455.975 325.606,452.314 C 327.295,448.653 326.825,442.755 327.137,439.464 C 328.686,444.074 328.907,445.507 329.41,450.33 C 329.916,455.154 326.599,461.92 325.195,465.491 C 331.015,462.68 334.709,458.187 336.574,453.796 C 337.822,454.536 337.193,456.565 337.652,460.475 C 339.726,458.411 342.084,456.823 344.172,454.875 C 346.262,452.929 348.551,451.217 349.588,448.5 C 350.623,445.783 350.199,442.894 349.428,439.737 C 348.657,436.58 345.129,432.919 343.229,429.466 C 341.331,426.014 343.565,422.625 343.436,418.465 C 339.635,417.812 336.407,415.876 332.038,416.505 C 327.667,417.133 322.661,422.038 318.194,422.095 C 313.727,422.152 311.219,417.967 307.557,416.817 C 303.895,415.667 300.19,415.793 296.508,415.281 C 301.172,410.813 319.274,408.685 331.656,406.666 C 342.551,404.889 353.363,403.862 364.402,403.707 C 369.332,403.637 375.668,405.265 379.091,404.281 C 382.517,403.299 381.786,400.966 382.802,398.423 C 385.644,391.315 384.243,384.985 382.697,377.827 C 372.787,376.665 362.461,375.243 352.476,376.01 C 342.714,376.76 331.154,381.401 321.085,381.715 C 312.331,381.987 299.368,380.038 291.851,375.456 C 282.665,369.857 275.558,357.832 273.035,353.971 C 268.926,347.682 263.743,335.344 260.589,329.703 C 257.437,324.062 256.742,322.484 254.119,320.129 C 251.496,317.775 247.903,317.593 244.796,316.324 C 248.378,316.69 252.289,315.556 255.543,317.422 C 260.432,320.227 265.904,331.853 268.946,336.912 C 272.169,326.108 271.417,321.86 280.743,315.768 C 273.227,324.192 273.04,330.573 271.208,340.491 C 277.458,350.749 286.147,367.089 297.517,371.623 C 301.624,373.262 308.677,374.29 313.117,374.839 C 326.935,376.55 340.263,372.171 353.834,368.82 C 363.754,366.37 373.82,367.357 385.885,368.066 C 389.981,368.306 399.639,365.842 401.639,361.699 C 402.719,359.464 402.639,356.778 401.088,354.396 C 399.535,352.014 395.399,350.862 393.182,348.718 C 390.965,346.574 389.69,344.029 387.944,341.684 C 378.729,343.452 371.149,344.131 364.309,344.317 C 369.215,342.296 373.922,341.143 379.028,339.146 C 384.134,337.149 389.932,333.871 394.053,331.151 C 398.174,328.43 400.805,326.852 403.787,323.686 C 406.767,320.518 410.59,317.255 412.787,313.383 C 414.986,309.511 415.326,305.18 415.449,300.388 C 415.572,295.597 415.365,288.874 413.517,285.014 C 411.671,281.155 408.111,279.984 405.251,279.071 C 402.39,278.158 400.431,280.467 397.456,280.779 C 394.479,281.091 391.698,282.967 387.394,280.943 C 383.09,278.919 377.988,274.799 373.636,268.684 C 369.286,262.568 366.175,252.572 361.74,244.882 C 354.095,231.626 344.568,219.243 335.457,206.96 C 334.707,203.931 333.58,199.745 333.205,195.636 C 332.551,188.476 332.428,181.038 334.65,174.058 C 336.353,168.711 338.83,163.063 344.429,160.87 C 346.669,159.992 349.503,159.644 351.505,160.386 C 353.509,161.127 355.474,163.388 354.687,166 C 353.822,168.872 351.832,168.95 350.48,170.4 C 348.185,172.86 347.267,174.665 346.4,177.732 C 348.88,177.692 352.232,177.503 354.269,178.039 C 356.927,178.739 359.402,180.501 360.751,184.267 C 357.579,181.454 356.003,180.163 352.044,180.063 C 350.23,180.018 348.026,180.667 346.019,180.969 C 345.269,183.409 344.828,186.401 343.769,188.289 C 342.71,190.177 340.648,189.724 339.874,191.93 C 339.1,194.132 339.055,197.718 339.445,200.613 z M 367.16,197.306 C 368.375,197.785 368.838,197.89 370.377,199.061 C 371.916,200.232 374.039,201.401 375.258,204.199 C 376.479,206.998 376.422,211.326 376.162,213.089 C 375.092,212.775 373.633,212.987 372.523,212.038 C 371.416,211.089 370.755,208.94 369.335,208.02 C 367.913,207.101 366.728,207.302 365.157,207.103 C 365.964,209.399 366.356,211.696 365.981,213.778 C 365.167,218.323 362.495,220.625 358.169,221.169 C 356.28,221.406 353.28,220.784 351.636,220.485 C 352.398,222.187 353.689,223.678 354.452,225.38 C 358.11,224.851 360.821,224.926 362.788,224.006 C 364.755,223.086 366.864,221.025 369.388,220.152 C 371.913,219.279 374.165,219.293 376.927,218.862 C 374.751,220.737 373.323,222.613 371.15,224.488 C 372.744,229.121 374.338,233.753 375.931,238.385 C 379.31,240.499 382.48,241.613 386.738,244.223 C 390.998,246.834 396.105,251.504 401.652,254.051 C 407.199,256.598 410.502,257.984 416.298,259.077 C 422.093,260.17 426.704,264.864 432.407,265.627 C 438.11,266.39 448.762,266.544 453.157,266.115 C 458.147,265.628 464.63,263.053 465.212,257.582 C 461.028,258.614 454.638,257.688 451.411,249.815 C 449.979,246.319 450.681,239.89 448.489,235.756 C 446.298,231.621 443.243,230.294 439.593,225.2 C 435.943,220.106 429.972,212.071 426.62,205.246 C 423.27,198.422 422.481,190.692 419.647,184.577 C 416.813,178.463 412.538,173.524 409.225,168.811 C 401.991,158.511 396.157,151.472 384.239,146.276 C 378.507,143.777 371.981,143.111 365.852,141.528 C 363.358,142.645 359.938,143.879 359.331,146.796 C 358.724,149.713 359.89,154.021 362.007,156.211 C 365.55,159.872 370.8,163.829 372.864,169.648 C 374.19,173.382 374.575,185.9 373.426,189.297 C 372.234,192.824 369.463,194.957 367.16,197.306 z M 368.314,226.935 C 367.207,227.89 366.363,228.871 364.582,229.664 C 362.803,230.457 360.373,230.662 358.012,231.252 C 361.323,236.406 364.801,241.614 367.973,247.033 C 371.147,252.451 373.897,258.384 376.926,264.058 C 376.783,260.806 376.975,257.339 376.502,254.3 C 376.029,251.262 374.652,248.603 373.287,244.042 C 371.924,239.481 370.246,233.276 368.314,226.935 z M 279.229,113.611 C 278.762,118.681 279.62,126.607 280.67,131.571 C 281.928,137.513 283.139,140.594 287.961,144.174 C 296.791,150.729 302.428,149.624 309.221,153.425 C 312.453,155.234 316.549,158.266 317.744,161.471 C 318.939,164.676 316.955,168.156 314.504,169.524 C 318.791,170.162 323.598,165.329 321.592,160.367 C 320.604,157.925 315.901,156.837 314.459,154.373 C 313.02,151.91 313.023,149.374 313.701,146.872 C 314.926,142.362 323.849,138.264 327.002,134.699 C 331.854,129.213 333.717,124.624 330.969,117.559 C 327.11,107.634 312.201,99.67 303.278,95.775 C 299.192,93.992 296.901,93.865 293.714,92.91 C 300.239,96.734 307.118,99.78 310.419,108.328 C 313.175,115.467 310.788,118.157 310.88,124.373 C 310.923,127.255 314.718,128.696 316.403,130.517 C 317.87,132.1 317.751,133.275 316.579,135.46 C 316.45,133.489 315.608,132.018 314.095,130.947 C 311.597,129.176 308.906,129.557 307.575,125.926 C 305.555,120.406 309.561,115.178 305.057,108.98 C 301.688,104.343 297.207,99.153 291.162,99.017 C 288.301,98.952 286.15,99.384 283.094,100.288 L 279.02,95.385 L 278.926,95.321 C 270.639,87.758 264.969,81.525 253.344,76.327 C 241.719,71.13 235.99,70.343 222.661,70.639 C 209.332,70.934 196.104,72.049 180.666,85.362 C 165.229,98.675 125.442,149.654 115.793,156.106 C 106.145,162.558 104.008,164.744 91.952,164.747 C 79.896,164.75 61.08,147.697 60.343,126.385 C 59.605,105.072 59.309,97.497 74.124,71.822 C 88.94,46.146 115.78,38.262 135.694,35.6 C 155.609,32.938 167.576,39.952 182.307,39.985 C 197.037,40.019 211.595,37.476 215.888,25.761 C 213.787,27.012 210.972,28.422 203.056,28.007 C 195.14,27.592 184.164,18.427 171.817,12.286 C 159.47,6.146 145.73,5.427 128.107,5.094 C 110.484,4.761 88.494,6.108 66.708,17.869 C 44.922,29.63 30.051,42.109 17.415,65.127 C 4.779,88.146 2.891,106.94 4.928,134.811 C 6.965,162.682 15.561,183.41 32.112,203.809 C 48.663,224.208 65.204,234.189 95.03,237.9 L 101.309,210.204 L 90.1,226.29 L 90.377,208.399 L 80.238,220.98 L 75.791,203.918 L 67.577,217.597 L 63.497,198.547 L 51.594,208.174 L 55.362,192.455 L 39.612,194.198 L 46.847,183.272 L 32.486,183.447 L 40.842,168.86 L 28.1,166.837 L 38.26,153.57 L 22.66,148.945 L 35.576,136.577 L 22.823,126.184 L 32.798,118.426 L 23.47,106.014 L 33.42,94.431 L 29.706,79.135 L 38.155,71.77 L 40.67,54.289 L 50.152,49.527 L 58.31,35.881 L 73.389,34.28 L 80.829,22.006 L 95.974,22.777 L 110.286,14.998 L 125.129,18.455 L 141.989,13.363 L 154.476,16.289 L 170.772,18.684 L 186.505,31.264 L 162.372,22.84 L 150.232,28.661 L 140.197,22.536 L 133.288,27.229 L 121.063,28.453 L 110.1,27.745 L 102.108,37.501 L 85.817,37.669 L 80.006,50.812 L 65.52,55.894 L 64.165,71.001 L 53.777,79.139 L 57.238,93.355 L 48.267,108.12 L 55.149,121.035 L 48.376,132.855 L 57.387,138.735 L 55.343,157.733 L 67.875,160.891 L 70.698,174.984 L 84.354,173.565 L 92.838,183.221 L 103.324,172.982 C 122.44,163.45 130.312,158.118 145.21,142.005 C 162.169,123.662 171.889,106.703 194.479,94.144 C 210.681,85.136 223.896,81.894 242.096,85.039 C 259.184,87.991 264.906,98.241 277.491,109.039 C 278.825,106.624 279.096,105.984 281.294,104.206 C 280.604,107.342 279.508,110.595 279.229,113.611 z"
+ style="fill-rule:evenodd" />
+
+ </g>
+
+ </g>
+ </g>
+</svg>
diff --git a/static/img/src/mit-scheme.svg b/static/img/src/mit-scheme.svg
new file mode 100644
index 0000000..f2e251d
--- /dev/null
+++ b/static/img/src/mit-scheme.svg
@@ -0,0 +1,599 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="289.93768"
+ height="289.93768"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="mit-gnu-scheme.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="130.85897"
+ inkscape:cy="121.20607"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1280"
+ inkscape:window-height="780"
+ inkscape:window-x="-4"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ inkscape:object-nodes="false"
+ inkscape:snap-smooth-nodes="false"
+ inkscape:snap-nodes="false"
+ fit-margin-top="20"
+ fit-margin-left="20"
+ fit-margin-right="20"
+ fit-margin-bottom="20" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-18.549603,-255.80548)">
+ <g
+ style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text2996"
+ transform="translate(0.08464721,0)">
+ <path
+ d="m 96.512577,316.36743 5.643283,6.07255 c 1.03398,1.11262 1.03507,1.43638 0.0383,2.48756 l 0.23669,0.2547 3.75345,-3.48813 -0.23669,-0.25469 c -1.04087,0.84237 -1.40295,0.82911 -2.28744,-0.12266 l -5.531163,-5.9519 c -0.884488,-0.95176 -0.872169,-1.28798 0.04507,-2.29027 l -0.236694,-0.2547 -2.667629,2.47906 3.328535,9.52274 -9.387674,-3.89192 -2.654224,2.46661 0.236694,0.25469 c 1.161511,-0.95449 1.485271,-0.95558 2.394674,0.023 l 5.057778,5.4425 c 1.28313,1.38073 1.332151,1.80984 0.294269,2.92425 l 0.236694,0.2547 3.150215,-2.92754 -0.236694,-0.25469 c -1.082027,0.90561 -1.596442,0.80907 -2.854656,-0.54485 l -5.020406,-5.40229 10.216361,4.24599 0.18768,-0.1744 -3.70647,-10.87031"
+ style="font-size:18.30000114px"
+ id="path3852"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 117.17324,313.97734 c -0.71512,0.41202 -0.96897,0.36158 -2.25404,-0.40827 l -9.56493,-6.10894 -0.30427,0.2034 1.82917,9.58533 c 0.51119,2.87208 0.49135,3.23755 -0.21856,3.75615 l 0.19324,0.28906 3.0123,-2.01372 -0.19324,-0.28906 c -0.73025,0.48818 -1.16674,0.4938 -1.45151,0.0678 -0.12204,-0.18256 -0.21878,-0.42606 -0.27493,-0.67469 l -0.4901,-2.24783 3.98598,-2.66462 1.60011,1.04353 c 0.45716,0.28872 0.82808,0.61309 0.97047,0.82608 0.25425,0.38034 0.077,0.67494 -0.71917,1.22918 l 0.19324,0.28906 3.87948,-2.59343 -0.19324,-0.28905 m -9.87522,1.36259 -1.04726,-5.35333 4.56162,3.00399 -3.51436,2.34934"
+ style="font-size:18.30000114px"
+ id="path3854"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 119.49238,299.4529 -0.34404,0.17125 c 0.12203,0.40942 0.0315,0.67933 -0.24698,0.81797 -0.16382,0.0815 -0.47495,0.15464 -0.81869,0.16222 -0.72032,0.0724 -1.39172,0.24303 -1.91596,0.50399 -0.70445,0.35065 -1.28665,0.98797 -1.59079,1.73217 -0.32059,0.79328 -0.27298,1.62814 0.15107,2.48003 0.65239,1.31061 1.82989,1.86922 4.14587,1.92246 1.48943,0.0354 2.67567,0.28303 3.39512,0.7017 0.25346,0.13958 0.50662,0.44284 0.70233,0.83602 0.54637,1.09763 0.10236,2.25897 -1.09356,2.85427 -1.45805,0.72578 -2.93867,0.33849 -4.75269,-1.25245 l -0.3768,0.18756 2.2203,3.22847 0.36042,-0.17941 c -0.13856,-0.31942 -0.0561,-0.64664 0.1896,-0.76896 0.18021,-0.0897 0.49133,-0.1628 0.85968,-0.20306 0.74486,-0.0641 1.46541,-0.25928 2.12071,-0.58547 1.884,-0.9378 2.69782,-2.9578 1.85788,-4.64521 -0.66869,-1.34337 -2.0998,-1.95973 -4.84129,-2.04646 -2.19315,-0.0939 -3.24003,-0.47227 -3.68039,-1.35693 -0.44851,-0.90104 -0.0703,-1.86608 0.94539,-2.37168 0.73722,-0.36696 1.58023,-0.3982 2.44719,-0.0938 0.76882,0.27144 1.2102,0.58322 1.95345,1.41932 l 0.40957,-0.20387 -2.09739,-3.31009"
+ style="font-size:18.30000114px"
+ id="path3856"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 130.23512,295.19981 -0.36084,0.13223 c 0.0761,0.42039 -0.0436,0.67866 -0.33576,0.7857 -0.17183,0.063 -0.48912,0.10128 -0.83159,0.0709 -0.72391,-0.008 -1.41004,0.0879 -1.95989,0.28942 -0.73885,0.27076 -1.38785,0.83991 -1.77227,1.546 -0.4062,0.75304 -0.45103,1.58805 -0.1236,2.48155 0.50373,1.3746 1.61237,2.05978 3.90833,2.36834 1.47642,0.19958 2.62808,0.57665 3.29691,1.07217 0.2365,0.1667 0.45464,0.49606 0.60576,0.90844 0.42187,1.15123 -0.14761,2.25647 -1.40194,2.71612 -1.52926,0.5604 -2.95808,0.012 -4.5854,-1.76941 l -0.3952,0.14482 1.85036,3.45382 0.37802,-0.13852 c -0.10245,-0.33277 0.0156,-0.64889 0.27333,-0.74334 0.18901,-0.0693 0.5063,-0.10757 0.87684,-0.10693 0.74739,0.0185 1.48507,-0.0959 2.17238,-0.3478 1.976,-0.72411 3.00782,-2.64194 2.35927,-4.41175 -0.51632,-1.40897 -1.87065,-2.17953 -4.58581,-2.56834 -2.16938,-0.33545 -3.1681,-0.82703 -3.50812,-1.75489 -0.34631,-0.94504 0.13609,-1.86244 1.20141,-2.25283 0.77321,-0.28335 1.61452,-0.22134 2.44259,0.17686 0.73415,0.35464 1.13842,0.71324 1.78485,1.62627 l 0.42956,-0.15742 -1.71919,-3.52137"
+ style="font-size:18.30000114px"
+ id="path3858"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 148.88084,302.81057 c -0.81501,0.13004 -1.0342,-0.008 -1.96044,-1.18493 l -6.75901,-9.11717 -0.35679,0.0816 -1.70778,9.60769 c -0.54618,2.86565 -0.69499,3.20003 -1.54313,3.43151 l 0.0775,0.33895 3.53223,-0.80767 -0.0775,-0.33895 c -0.8563,0.1958 -1.26612,0.0455 -1.38034,-0.45404 -0.0489,-0.21408 -0.0525,-0.47607 -0.0164,-0.72838 l 0.34337,-2.27487 4.67397,-1.06873 1.12302,1.54535 c 0.32421,0.43272 0.55514,0.868 0.61225,1.11775 0.10198,0.44599 -0.16866,0.65805 -1.11008,0.89209 l 0.0775,0.33895 4.5491,-1.04018 -0.0775,-0.33895 m -9.71223,-2.24705 0.92979,-5.37498 3.19116,4.4327 -4.12095,0.94228"
+ style="font-size:18.30000114px"
+ id="path3860"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 159.85951,293.91339 -0.52728,-4.10535 -0.38281,0.0337 c -0.0756,0.39246 -0.34801,0.63692 -0.7126,0.66907 -0.16406,0.0145 -0.44232,-0.0161 -0.72541,-0.10139 -0.91894,-0.21292 -1.84327,-0.2784 -2.70004,-0.20286 -1.4948,0.13179 -2.95802,0.83029 -3.99984,1.93254 -1.17641,1.24272 -1.71965,2.99912 -1.54287,5.00434 0.14947,1.69532 0.83456,3.21483 1.85586,4.17193 1.19822,1.08847 2.93639,1.63333 4.74109,1.47422 2.05991,-0.1816 3.79068,-1.17926 4.77249,-2.75387 l -0.35706,-0.2992 c -1.23486,1.41321 -2.38978,2.06616 -3.90281,2.19956 -1.14844,0.10125 -2.20143,-0.17334 -3.04636,-0.79694 -1.08136,-0.80485 -1.79111,-2.39566 -1.9695,-4.41911 -0.2909,-3.2995 1.21639,-5.5818 3.85964,-5.81483 1.03907,-0.0916 2.02074,0.20763 2.8142,0.87252 0.63476,0.53191 0.9532,1.01822 1.40403,2.17259 l 0.41927,-0.037"
+ style="font-size:18.30000114px"
+ id="path3862"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 165.61664,295.62919 0.17301,-3.54598 c 0.0642,-1.31604 0.27508,-1.50729 1.68875,-1.56657 l 0.017,-0.34728 -5.08136,-0.24792 -0.0169,0.34728 c 1.40118,0.19662 1.59243,0.40749 1.52822,1.72352 l -0.38615,7.91449 c -0.0749,1.53537 -0.26839,1.7458 -1.69856,1.76763 l -0.0169,0.34728 5.08136,0.24792 0.0169,-0.34728 c -1.36552,-0.17656 -1.59154,-0.42577 -1.52822,-1.72353 l 0.18371,-3.76532 5.53831,0.27022 -0.1739,3.56426 c -0.0749,1.53537 -0.2684,1.74579 -1.69856,1.76762 l -0.0169,0.34729 5.08135,0.24792 0.017,-0.34729 c -1.36552,-0.17655 -1.59155,-0.42576 -1.52823,-1.72352 l 0.39596,-8.11554 c 0.0642,-1.31604 0.27508,-1.50729 1.68875,-1.56657 l 0.0169,-0.34729 -5.08136,-0.24792 -0.0169,0.34729 c 1.40118,0.19662 1.59243,0.40749 1.52823,1.72352 l -0.17301,3.54598 -5.53832,-0.27021"
+ style="font-size:18.30000114px"
+ id="path3864"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 187.47669,295.52361 c 0.35485,-1.83256 0.62611,-2.07828 2.13413,-1.97267 l 0.0661,-0.34136 -4.16818,-0.8071 -0.0661,0.34136 c 1.3805,0.37915 1.59142,0.83007 1.24353,2.6267 l -0.9393,4.85089 c -0.23657,1.22171 -0.49453,1.88008 -0.88636,2.3634 -0.65207,0.76845 -1.90976,1.10276 -3.25723,0.84184 -1.27561,-0.247 -2.07246,-0.84866 -2.39404,-1.787 -0.21207,-0.63754 -0.20912,-1.32665 0.0101,-2.45853 l 1.11325,-5.74921 c 0.25048,-1.29357 0.48638,-1.45293 1.94804,-1.30038 l 0.0661,-0.34136 -5.08445,-0.98453 -0.0661,0.34136 c 1.39846,0.38263 1.55782,0.61853 1.30386,1.93007 l -1.08541,5.60548 c -0.60185,3.10816 0.56815,4.86319 3.62241,5.4546 1.95833,0.3792 3.42756,0.0113 4.36481,-1.07473 0.5773,-0.67109 0.89903,-1.46623 1.16691,-2.84963 l 0.90799,-4.6892"
+ style="font-size:18.30000114px"
+ id="path3866"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 199.32106,296.069 -0.36502,-0.12019 c -0.20117,0.37689 -0.45533,0.50513 -0.75082,0.40783 -0.17382,-0.0572 -0.44642,-0.22406 -0.69613,-0.46041 -0.563,-0.45512 -1.16034,-0.80594 -1.71657,-0.98909 -0.74742,-0.24611 -1.60952,-0.20245 -2.34911,0.11275 -0.78579,0.33852 -1.33905,0.96554 -1.63667,1.8694 -0.45788,1.39055 -0.0136,2.61578 1.5955,4.28234 1.03401,1.0726 1.70321,2.08288 1.92027,2.88648 0.082,0.27748 0.0488,0.67112 -0.0886,1.08828 -0.38347,1.16459 -1.51585,1.67799 -2.78473,1.26018 -1.54699,-0.50939 -2.32726,-1.82598 -2.4981,-4.23275 l -0.39978,-0.13164 -0.69192,3.85668 0.3824,0.12591 c 0.12613,-0.32453 0.41484,-0.49919 0.67557,-0.41334 0.1912,0.063 0.4638,0.22978 0.754,0.4602 0.57466,0.47822 1.22415,0.84622 1.91943,1.07516 1.99892,0.65819 3.99807,-0.20559 4.58758,-1.99593 0.46933,-1.42532 -0.11466,-2.86994 -2.0027,-4.85955 -1.49315,-1.60912 -1.97136,-2.61431 -1.66229,-3.55294 0.31479,-0.956 1.26233,-1.37613 2.34001,-1.02128 0.78218,0.25756 1.40348,0.8282 1.8058,1.65427 0.35569,0.73365 0.45023,1.26571 0.39065,2.38282 l 0.43455,0.14309 0.8367,-3.82827"
+ style="font-size:18.30000114px"
+ id="path3868"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 208.59199,310.21602 -0.46482,-0.21564 c -1.66177,1.4078 -2.56043,1.47505 -4.93431,0.37377 l -0.44822,-0.20794 c -0.81342,-0.37736 -1.47214,-0.78382 -1.53444,-0.95394 -0.0599,-0.0883 -0.008,-0.28631 0.12254,-0.56852 l 1.90222,-4.10035 2.5565,1.186 c 1.36124,0.63151 1.51014,0.96284 1.08673,2.4408 l 0.38182,0.17713 1.7867,-3.85133 -0.38181,-0.17713 c -0.43196,0.62671 -0.61388,0.84492 -0.9043,0.99261 -0.34673,0.18209 -0.87316,0.0993 -1.65339,-0.2627 l -2.55649,-1.18601 1.70969,-3.68533 c 0.21564,-0.46481 0.36145,-0.51821 0.80967,-0.31027 l 2.24108,1.03967 c 1.87586,0.87025 2.12556,1.28869 1.71465,2.91367 l 0.41501,0.19253 1.05149,-2.39699 -8.81492,-4.0894 -0.14632,0.31541 c 1.18223,0.6695 1.29022,1.00203 0.75113,2.16407 l -3.41938,7.37066 c -0.5391,1.16204 -0.88703,1.30324 -2.13737,0.82404 l -0.14633,0.31541 8.96432,4.15872 2.04855,-2.45894"
+ style="font-size:18.30000114px"
+ id="path3870"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 217.47652,305.95538 -4.82937,7.77173 c -0.82099,1.32119 -1.09821,1.38592 -2.43661,0.66197 l -0.18352,0.29532 4.53869,2.82035 0.18352,-0.29533 c -1.23593,-0.85418 -1.32797,-1.19147 -0.62289,-2.32614 l 4.93561,-7.94271 0.83935,0.52157 c 1.77195,1.10109 1.94005,1.59337 1.23509,3.5253 l 0.37304,0.23181 1.54872,-2.70034 -8.76651,-5.44752 -1.73524,2.58443 0.37304,0.23181 c 1.42585,-1.46242 1.96686,-1.5356 3.70773,-0.45382 l 0.83935,0.52157"
+ style="font-size:18.30000114px"
+ id="path3872"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 227.78013,313.11696 -5.72905,7.13447 c -0.97394,1.21286 -1.25693,1.24378 -2.49854,0.3641 l -0.21771,0.27111 4.16653,3.34576 0.2177,-0.27111 c -1.12422,-0.99664 -1.17503,-1.34255 -0.33859,-2.38418 l 5.85509,-7.29142 0.77052,0.61874 c 1.62665,1.30622 1.73433,1.81514 0.80212,3.64826 l 0.34246,0.27499 1.86226,-2.49447 -8.04768,-6.46236 -2.03349,2.35697 0.34246,0.27499 c 1.59138,-1.28031 2.13728,-1.28789 3.73539,-0.005 l 0.77053,0.61873"
+ style="font-size:18.30000114px"
+ id="path3874"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 240.3112,323.19732 -0.27037,-0.27311 c -0.35061,0.24411 -0.63529,0.24268 -0.85417,0.0216 -0.12874,-0.13005 -0.2956,-0.40263 -0.41043,-0.72671 -0.2943,-0.66143 -0.66664,-1.24561 -1.07863,-1.66177 -0.55362,-0.55921 -1.3412,-0.91255 -2.14321,-0.96834 -0.85378,-0.056 -1.63173,0.25061 -2.30799,0.9201 -1.04039,1.02999 -1.20217,2.32321 -0.52747,4.53937 0.43281,1.4256 0.5691,2.62972 0.39679,3.44409 -0.0532,0.28442 -0.26192,0.61982 -0.57404,0.92881 -0.87132,0.86262 -2.11329,0.80462 -3.05316,-0.14474 -1.14586,-1.15743 -1.24171,-2.68486 -0.29886,-4.90584 l -0.29612,-0.29911 -2.3708,3.11962 0.28325,0.28611 c 0.25997,-0.23162 0.59654,-0.25581 0.78966,-0.0607 0.14162,0.14306 0.30848,0.41564 0.46207,0.75286 0.29417,0.68731 0.70513,1.3105 1.22012,1.8307 1.48061,1.49556 3.65387,1.63588 4.99337,0.30977 1.0664,-1.05574 1.2036,-2.60789 0.42746,-5.23865 -0.59758,-2.11225 -0.56611,-3.22495 0.13616,-3.9202 0.71526,-0.70812 1.7502,-0.65116 2.54844,0.15515 0.57937,0.58522 0.87302,1.37605 0.85547,2.29471 -0.017,0.81515 -0.17492,1.33197 -0.73621,2.29966 l 0.32188,0.32513 2.48679,-3.02846"
+ style="font-size:18.30000114px"
+ id="path3876"
+ inkscape:connector-curvature="0" />
+ </g>
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#000000;stroke-width:1.10108805;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path3002"
+ sodipodi:cx="141.42136"
+ sodipodi:cy="420.3855"
+ sodipodi:rx="98.884468"
+ sodipodi:ry="98.884468"
+ d="m 240.30582,420.3855 c 0,54.61238 -44.27208,98.88447 -98.88446,98.88447 -54.612388,0 -98.884472,-44.27209 -98.884472,-98.88447 0,-54.61239 44.272084,-98.88447 98.884472,-98.88447 54.61238,0 98.88446,44.27208 98.88446,98.88447 z"
+ transform="matrix(0.9081926,0,0,0.9081926,35.080614,18.983332)" />
+ <path
+ transform="matrix(0.93601302,0,0,0.93601302,31.146212,7.2880297)"
+ d="m 240.30582,420.3855 c 0,54.61238 -44.27208,98.88447 -98.88446,98.88447 -54.612388,0 -98.884472,-44.27209 -98.884472,-98.88447 0,-54.61239 44.272084,-98.88447 98.884472,-98.88447 54.61238,0 98.88446,44.27208 98.88446,98.88447 z"
+ sodipodi:ry="98.884468"
+ sodipodi:rx="98.884468"
+ sodipodi:cy="420.3855"
+ sodipodi:cx="141.42136"
+ id="path3004"
+ style="fill:none;stroke:#000000;stroke-width:1.06836116;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#000000;stroke-width:0.85900283;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path3006"
+ sodipodi:cx="141.42136"
+ sodipodi:cy="420.3855"
+ sodipodi:rx="98.884468"
+ sodipodi:ry="98.884468"
+ d="m 240.30582,420.3855 c 0,54.61238 -44.27208,98.88447 -98.88446,98.88447 -54.612388,0 -98.884472,-44.27209 -98.884472,-98.88447 0,-54.61239 44.272084,-98.88447 98.884472,-98.88447 54.61238,0 98.88446,44.27208 98.88446,98.88447 z"
+ transform="matrix(1.1641405,0,0,1.1641405,-1.1158837,-88.613449)" />
+ <path
+ transform="matrix(1.1947542,0,0,1.1947542,-5.4453173,-101.75922)"
+ d="m 240.30582,420.3855 c 0,54.61238 -44.27208,98.88447 -98.88446,98.88447 -54.612388,0 -98.884472,-44.27209 -98.884472,-98.88447 0,-54.61239 44.272084,-98.88447 98.884472,-98.88447 54.61238,0 98.88446,44.27208 98.88446,98.88447 z"
+ sodipodi:ry="98.884468"
+ sodipodi:rx="98.884468"
+ sodipodi:cy="420.3855"
+ sodipodi:cx="141.42136"
+ id="path3008"
+ style="fill:none;stroke:#000000;stroke-width:1.67398441;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:type="arc" />
+ <g
+ style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text3878"
+ transform="translate(0.24000768,0)">
+ <path
+ d="m 62.225868,436.39514 c -1.303653,0.42923 -1.58664,0.27194 -2.119528,-1.17098 l -0.330259,0.10874 1.699765,5.16247 0.330259,-0.10874 c -0.417149,-1.4425 -0.265369,-1.80074 1.00352,-2.21852 l 7.717634,-2.54107 c 1.268889,-0.41778 1.574981,-0.24883 2.12525,1.18837 l 0.330259,-0.10874 -1.699764,-5.16247 -0.33026,0.10873 c 0.399554,1.50609 0.277028,1.77763 -1.009243,2.20114 l -7.717633,2.54107"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3932"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 66.36363,452.21675 8.748146,-4.01572 c 0.997887,-0.45806 1.689597,-0.57422 2.050859,-0.35747 0.273197,0.15649 0.446605,0.3588 0.803239,0.96025 l 0.315998,-0.14505 -1.794092,-3.90839 -0.315998,0.14505 c 0.240214,0.6549 0.280571,0.91828 0.245404,1.23646 -0.08015,0.43951 -0.610175,0.86404 -1.641325,1.33737 l -5.604801,2.5728 5.110368,-10.09817 -1.305488,-2.84398 -0.315998,0.14505 c 0.328281,0.71515 0.302111,1.00907 -0.17419,2.03315 l -7.334472,3.36678 c -1.713039,0.78635 -2.135921,0.69856 -2.869367,-0.63604 l -0.315998,0.14505 1.794092,3.9084 0.315998,-0.14506 c -0.521325,-1.31116 -0.268586,-1.76949 1.41119,-2.54056 l 6.519531,-2.9927 -5.772882,11.55004 0.129786,0.28274"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3934"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 82.837191,455.57813 -0.198475,-0.32908 c -0.398226,0.15469 -0.674566,0.0863 -0.835236,-0.18012 -0.09451,-0.1567 -0.192501,-0.46091 -0.227808,-0.80292 -0.130307,-0.71212 -0.354639,-1.36754 -0.657077,-1.869 -0.406402,-0.67383 -1.088655,-1.20266 -1.854989,-1.4457 -0.816577,-0.25548 -1.644861,-0.1406 -2.459726,0.35087 -1.253639,0.75609 -1.715335,1.97487 -1.581359,4.28758 0.08501,1.48742 -0.06602,2.68979 -0.425217,3.4407 -0.118656,0.2639 -0.400481,0.54072 -0.776572,0.76755 -1.049923,0.63323 -2.243328,0.28446 -2.933266,-0.85948 -0.841157,-1.39467 -0.574705,-2.90174 0.864534,-4.83831 l -0.217378,-0.36042 -3.03861,2.47377 0.207927,0.34476 c 0.30719,-0.16391 0.639992,-0.10818 0.78176,0.12688 0.103964,0.17237 0.201952,0.47658 0.271832,0.84048 0.124088,0.73724 0.376773,1.43967 0.754821,2.06649 1.086889,1.80211 3.166019,2.45014 4.780079,1.47666 1.284979,-0.77499 1.783746,-2.2512 1.648792,-4.99074 -0.08349,-2.19357 0.209063,-3.26759 1.055269,-3.77796 0.861877,-0.51981 1.854308,-0.2208 2.440283,0.75077 0.425304,0.70517 0.524516,1.54291 0.29117,2.43162 -0.208468,0.78822 -0.483585,1.25334 -1.256924,2.06169 l 0.23628,0.39177 3.12989,-2.35786"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3936"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 85.758958,461.53415 -7.301321,5.51482 c -1.241223,0.93751 -1.52318,0.89821 -2.511652,-0.25866 l -0.27745,0.20956 3.220653,4.26398 0.27745,-0.20957 c -0.846018,-1.24153 -0.810599,-1.58936 0.255393,-2.39452 l 7.46195,-5.63614 0.5956,0.78854 c 1.257377,1.6647 1.237181,2.1845 -0.115426,3.73362 l 0.264711,0.35046 2.416271,-1.96266 -6.220713,-8.23588 -2.548627,1.78742 0.264711,0.35046 c 1.856397,-0.85177 2.387532,-0.72547 3.62285,0.91003 l 0.5956,0.78854"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3938"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 84.359504,474.62961 c -1.016656,0.92204 -1.339144,0.89334 -2.412498,-0.20843 l -0.257553,0.23358 3.651291,4.02597 0.257553,-0.23359 c -0.967433,-1.14842 -0.97436,-1.53743 0.01519,-2.43488 l 6.018613,-5.4585 c 0.989545,-0.89745 1.337883,-0.86749 2.424792,0.22199 l 0.257553,-0.23359 -3.651291,-4.02596 -0.257553,0.23358 c 0.977204,1.21368 0.975621,1.51158 -0.02748,2.42133 l -6.018613,5.4585"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3940"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 97.388188,474.6043 -6.172999,6.75401 c -1.049408,1.14818 -1.333805,1.16096 -2.516698,0.20378 l -0.234574,0.25665 3.944339,3.60503 0.234574,-0.25665 c -1.058272,-1.0664 -1.086887,-1.41485 -0.18563,-2.40093 l 6.308805,-6.9026 0.729432,0.66669 c 1.539913,1.40744 1.614863,1.9222 0.567483,3.69204 l 0.3242,0.2963 2.01777,-2.37044 -7.61852,-6.96314 -2.179868,2.22228 0.324192,0.29631 c 1.669904,-1.17607 2.215166,-1.14876 3.728061,0.23399 l 0.729433,0.66668"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3942"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 109.98865,486.83598 c 1.06701,-1.53156 1.41457,-1.64627 2.75043,-0.93863 l 0.19876,-0.2853 -3.48356,-2.42692 -0.19875,0.28529 c 1.10843,0.90604 1.11853,1.40375 0.0725,2.90529 l -2.82444,4.05413 c -0.71134,1.02104 -1.21396,1.5184 -1.76803,1.80149 -0.90754,0.43829 -2.19281,0.23427 -3.31896,-0.5503 -1.06608,-0.74272 -1.550776,-1.61567 -1.464528,-2.60384 0.06447,-0.66878 0.346418,-1.29758 1.005458,-2.24354 l 3.34748,-4.80491 c 0.75318,-1.0811 1.03342,-1.1312 2.30787,-0.39943 l 0.19876,-0.2853 -4.24934,-2.96042 -0.19876,0.28529 c 1.12345,0.9165 1.17355,1.19674 0.4099,2.29285 l -3.263791,4.68479 c -1.80973,2.59764 -1.451281,4.67624 1.101321,6.45458 1.63667,1.14024 3.12895,1.39927 4.42589,0.7862 0.79972,-0.37958 1.41606,-0.97614 2.22155,-2.13232 l 2.73029,-3.919"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3944"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 116.63567,488.31502 -4.18445,8.13713 c -0.71136,1.38331 -0.98244,1.47027 -2.37504,0.85702 l -0.15901,0.30921 4.75208,2.44372 0.15901,-0.30921 c -1.30101,-0.75134 -1.42006,-1.08008 -0.80913,-2.26809 l 4.27651,-8.31615 0.87881,0.45192 c 1.85527,0.95406 2.06266,1.43111 1.51639,3.41376 l 0.39058,0.20086 1.32507,-2.81684 -9.17868,-4.72006 -1.52036,2.71641 0.39058,0.20085 c 1.30279,-1.57304 1.83611,-1.68977 3.65883,-0.75245 l 0.87881,0.45192"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3946"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 129.64881,502.75031 -0.48052,-0.17789 c -1.54426,1.53579 -2.4347,1.67447 -4.88882,0.76593 l -0.46336,-0.17154 c -0.84092,-0.31132 -1.52995,-0.66398 -1.60561,-0.82858 -0.0667,-0.0833 -0.0312,-0.28474 0.0768,-0.57649 l 1.5693,-4.23894 2.6429,0.97843 c 1.40726,0.52099 1.58209,0.83939 1.27785,2.34641 l 0.39472,0.14612 1.474,-3.98151 -0.39472,-0.14613 c -0.38062,0.65916 -0.54457,0.89117 -0.82229,1.06155 -0.33112,0.20915 -0.86247,0.16855 -1.66907,-0.13007 l -2.6429,-0.97843 1.41046,-3.80989 c 0.1779,-0.48053 0.31899,-0.54538 0.78236,-0.37384 l 2.31683,0.85772 c 1.93926,0.71794 2.22152,1.11514 1.94146,2.7677 l 0.42904,0.15884 0.85706,-2.47319 -9.11286,-3.37367 -0.12071,0.32607 c 1.23184,0.57312 1.36599,0.89598 0.92125,2.0973 l -2.82093,7.61979 c -0.44474,1.20132 -0.7803,1.3698 -2.06487,0.9918 l -0.12072,0.32608 9.26732,3.43086 1.846,-2.61443"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3948"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 144.20071,496.6515 c -3.43718,-0.63476 -6.36316,1.50464 -7.03116,5.12177 -0.3124,1.69159 -0.039,3.43555 0.74005,4.65877 0.83437,1.32648 2.30638,2.32409 3.89,2.61655 3.54515,0.65471 6.45784,-1.41271 7.12584,-5.02984 0.32902,-1.78158 0.0896,-3.40759 -0.70073,-4.67012 -0.90442,-1.45107 -2.29642,-2.37808 -4.024,-2.69713 m -0.11964,0.64785 c 0.8278,0.15287 1.59578,0.62967 2.13728,1.32517 0.80892,1.06125 1.05439,2.85588 0.65891,4.99736 -0.19608,1.06175 -0.65934,2.26025 -1.19207,3.12956 -0.24505,0.41998 -0.63406,0.81338 -1.1244,1.15084 -0.74616,0.51353 -1.61579,0.6879 -2.56956,0.51176 -0.8278,-0.15288 -1.57779,-0.62635 -2.10462,-1.30053 -0.7829,-1.00062 -1.04109,-2.92786 -0.6722,-4.92538 0.33899,-1.83556 1.16524,-3.48809 2.07725,-4.19431 0.84474,-0.6442 1.79965,-0.87726 2.78941,-0.69447"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3950"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 158.70002,506.56074 0.33909,-4.23204 -0.41956,-0.0336 c -0.33582,1.44178 -0.6861,1.6891 -2.14542,1.57217 l -2.64502,-0.21193 0.32447,-4.04962 c 0.0409,-0.51076 0.1409,-0.6129 0.63342,-0.57344 l 2.48085,0.19878 c 2.06129,0.16516 2.44069,0.47094 2.61925,2.13752 l 0.45604,0.0365 0.15428,-2.61293 -9.68626,-0.7761 -0.0278,0.34659 c 1.3411,0.21761 1.55778,0.49199 1.45547,1.7689 l -0.63286,7.89858 c -0.12131,1.51405 -0.34097,1.73511 -1.73464,1.71524 l -0.0278,0.34659 5.10763,0.40924 0.0278,-0.34658 c -1.39875,-0.18551 -1.63367,-0.46136 -1.52843,-1.77475 l 0.31862,-3.97665 2.64503,0.21193 c 1.47756,0.11839 1.76574,0.41686 1.86625,1.91196 l 0.41956,0.0336"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3952"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 170.66566,499.44404 0.51616,9.13543 c 0.0877,1.55302 -0.10188,1.76535 -1.6132,1.94239 l 0.0196,0.34715 5.33509,-0.30144 -0.0196,-0.34715 c -1.50234,0.0116 -1.77156,-0.2115 -1.84692,-1.54527 l -0.52751,-9.33641 0.98662,-0.0557 c 2.08288,-0.11768 2.50342,0.18849 3.03721,2.17453 l 0.4385,-0.0248 -0.28512,-3.09985 -10.30477,0.58223 0.0659,3.11224 0.4385,-0.0248 c 0.32598,-2.0163 0.7266,-2.38719 2.77293,-2.50281 l 0.98663,-0.0557"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3954"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 189.25774,505.44432 -0.50544,0.0842 c -0.57779,2.09989 -1.28183,2.66243 -3.86315,3.09244 l -0.48738,0.0812 c -0.88451,0.14735 -1.6577,0.18339 -1.80511,0.0781 -0.0993,-0.0391 -0.16846,-0.23166 -0.21958,-0.53853 l -0.74275,-4.45866 2.77989,-0.4631 c 1.4802,-0.24658 1.79006,-0.057 2.27432,1.40214 l 0.41517,-0.0692 -0.69764,-4.18789 -0.41518,0.0692 c -0.003,0.76115 -0.0302,1.04395 -0.18663,1.32975 -0.18355,0.34596 -0.66493,0.57457 -1.51333,0.7159 l -2.7799,0.4631 -0.66757,-4.00738 c -0.0842,-0.50543 0.006,-0.63178 0.49345,-0.71297 l 2.43691,-0.40596 c 2.03979,-0.33981 2.48203,-0.13519 3.05954,1.4383 l 0.45128,-0.0752 -0.48417,-2.57231 -9.58521,1.59678 0.0571,0.34297 c 1.35383,-0.11422 1.6306,0.0994 1.84109,1.36299 l 1.33516,8.01475 c 0.21049,1.26359 0.003,1.57646 -1.29982,1.88623 l 0.0571,0.34298 9.74767,-1.62384 0.30411,-3.18598"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3956"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 199.70377,497.11224 -1.33431,-3.91812 -0.36842,0.10932 c 0.004,0.39965 -0.2142,0.69347 -0.56508,0.79758 -0.15789,0.0468 -0.43667,0.0723 -0.73106,0.0451 -0.94295,-0.0257 -1.8618,0.0943 -2.68637,0.33892 -1.43861,0.42683 -2.7334,1.40274 -3.53484,2.6904 -0.90536,1.4521 -1.08794,3.28151 -0.51535,5.21135 0.4841,1.6316 1.45808,2.98423 2.64953,3.71877 1.39099,0.82805 3.20285,1.01583 4.93971,0.5005 1.98248,-0.5882 3.4799,-1.91056 4.12845,-3.64915 l -0.40948,-0.2221 c -0.92869,1.63083 -1.93044,2.5007 -3.38659,2.93274 -1.10528,0.32794 -2.19186,0.26856 -3.14405,-0.17429 -1.21999,-0.57337 -2.23234,-1.99097 -2.81013,-3.93836 -0.94216,-3.17547 0.0804,-5.71223 2.6243,-6.467 1.00002,-0.29671 2.02162,-0.19896 2.9316,0.29459 0.72798,0.39484 1.13689,0.808 1.80857,1.84946 l 0.40352,-0.11972"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3958"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 204.74518,496.82159 -1.47617,-3.22876 c -0.54785,-1.1983 -0.44848,-1.46507 0.77977,-2.16747 l -0.14457,-0.31622 -4.62677,2.11533 0.14457,0.31622 c 1.33478,-0.4694 1.60156,-0.37003 2.14941,0.82827 l 3.29474,7.20645 c 0.63916,1.39801 0.56404,1.67382 -0.69607,2.35055 l 0.14457,0.31622 4.62677,-2.11533 -0.14457,-0.31622 c -1.29388,0.47082 -1.60916,0.35338 -2.14941,-0.82828 l -1.56747,-3.42847 5.04285,-2.30555 1.48377,3.24539 c 0.63916,1.39802 0.56404,1.67383 -0.69607,2.35055 l 0.14457,0.31622 4.62677,-2.11533 -0.14457,-0.31622 c -1.29388,0.47083 -1.60916,0.35339 -2.14941,-0.82827 l -3.37844,-7.38952 c -0.54785,-1.1983 -0.44848,-1.46508 0.77977,-2.16748 l -0.14457,-0.31622 -4.62677,2.11533 0.14457,0.31622 c 1.33478,-0.4694 1.60156,-0.37002 2.14941,0.82828 l 1.47617,3.22875 -5.04285,2.30556"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3960"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 226.55605,492.39247 -5.23993,-8.0746 c -0.59771,-0.92106 -0.81279,-1.58866 -0.65062,-1.9775 0.11529,-0.29298 0.29036,-0.49385 0.83385,-0.9338 l -0.18928,-0.29167 -3.60747,2.34103 0.18927,0.29167 c 0.61323,-0.33249 0.86799,-0.41056 1.18791,-0.42183 0.44648,0.0157 0.94327,0.47866 1.5609,1.43042 l 3.35714,5.17327 -10.73165,-3.59456 -2.62501,1.70348 0.18927,0.29166 c 0.66009,-0.42835 0.9547,-0.44501 2.03695,-0.12199 l 4.39317,6.76977 c 1.02606,1.58115 1.00043,2.01228 -0.21393,2.93122 l 0.18927,0.29167 3.60748,-2.34103 -0.18928,-0.29167 c -1.22187,0.70566 -1.71196,0.52194 -2.7181,-1.02851 l -3.90504,-6.01757 12.26413,4.03989 0.26097,-0.16935"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3962"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 225.92131,476.73893 c -2.63453,2.29704 -2.77863,5.91887 -0.36133,8.69133 1.13048,1.29657 2.66626,2.16693 4.10863,2.31752 1.55768,0.17145 3.25432,-0.36096 4.46813,-1.41928 2.71729,-2.36919 2.90949,-5.93585 0.4922,-8.70831 -1.19061,-1.36554 -2.61286,-2.18921 -4.09308,-2.35536 -1.6988,-0.19408 -3.29039,0.31957 -4.61455,1.4741 m 0.43295,0.49656 c 0.63449,-0.55321 1.48546,-0.85814 2.36687,-0.8497 1.33413,0.0265 2.8922,0.95021 4.32334,2.59161 0.70955,0.81381 1.36006,1.92189 1.70956,2.87968 0.17652,0.45307 0.24269,1.00235 0.20205,1.5962 -0.0618,0.90369 -0.46607,1.69316 -1.19712,2.33056 -0.63449,0.55321 -1.47167,0.84611 -2.32725,0.83944 -1.27047,-0.009 -2.94031,-1.00538 -4.27524,-2.53644 -1.22668,-1.40692 -2.00703,-3.08162 -1.99295,-4.23501 0.0208,-1.06214 0.4321,-1.95489 1.19074,-2.61634"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3964"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 244.44439,470.90738 c 0.31007,0.69586 0.45242,1.16759 0.5008,1.57965 0.10381,0.95333 -0.32285,1.92261 -1.33682,3.05351 l -0.81851,0.91288 c -0.89181,0.99464 -1.13238,1.09847 -1.60926,0.67089 l -6.44472,-5.77842 c -0.96739,-0.86737 -0.98711,-1.22916 -0.0901,-2.36663 l -0.25888,-0.23212 -3.44506,3.84231 0.25888,0.23211 c 0.98577,-0.93496 1.33534,-0.94106 2.28911,-0.0859 l 6.04958,5.42414 c 0.95377,0.85516 0.98711,1.22916 0.16343,2.28488 l 0.25888,0.23212 6.5725,-7.33036 -1.78439,-2.77969 -0.30541,0.34063"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3966"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 241.78368,459.08055 c -2.02413,2.84957 -1.31702,6.40463 1.68173,8.53473 1.40241,0.99616 3.09918,1.48312 4.53675,1.29214 1.55457,-0.19766 3.0796,-1.11217 4.01218,-2.42506 2.08771,-2.93908 1.44029,-6.45175 -1.55847,-8.58185 -1.477,-1.04915 -3.05246,-1.51729 -4.53048,-1.33258 -1.69706,0.20868 -3.12435,1.08038 -4.14171,2.51262 m 0.53709,0.38151 c 0.48749,-0.68628 1.24352,-1.18181 2.10244,-1.37978 1.30331,-0.28635 3.03424,0.24732 4.80962,1.50842 0.88024,0.62526 1.77189,1.55044 2.33574,2.39989 0.2776,0.39921 0.47042,0.91778 0.56982,1.50466 0.15126,0.89309 -0.0571,1.75521 -0.61875,2.54592 -0.48749,0.68629 -1.23292,1.16689 -2.06633,1.36054 -1.23735,0.28831 -3.09391,-0.2897 -4.74994,-1.46603 -1.52176,-1.08094 -2.67219,-2.52664 -2.9283,-3.65133 -0.22822,-1.03754 -0.0372,-2.00173 0.5457,-2.82229"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3968"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 256.89225,445.36781 -2.24939,4.08857 0.28861,0.15878 c 0.4274,-0.66298 0.58136,-0.82892 0.81548,-0.95075 0.27662,-0.16112 0.69832,-0.0544 1.38777,0.32488 l 2.59745,1.42902 c 0.51307,0.28228 0.40789,1.4985 -0.22723,2.65292 -1.43785,2.61348 -4.1478,3.21124 -7.13005,1.5705 -1.50716,-0.82918 -2.71923,-2.08085 -3.11675,-3.21857 -0.39752,-1.13771 -0.28351,-2.36997 0.31632,-3.46026 0.49399,-0.89788 1.23086,-1.51593 2.1521,-1.82368 0.71357,-0.23401 1.28762,-0.25238 2.4862,-0.11513 l 0.20289,-0.36877 -3.45367,-1.733 -0.19406,0.35274 c 0.25171,0.26381 0.3174,0.67591 0.13216,1.01261 -0.0882,0.16034 -0.27745,0.39042 -0.54847,0.61727 -0.63746,0.5892 -1.1715,1.25618 -1.56845,1.97769 -1.76423,3.20672 -0.66871,6.79625 2.60215,8.59576 1.5713,0.86447 3.01199,1.09315 4.45196,0.71571 1.69733,-0.46559 3.15345,-1.66962 4.12377,-3.43332 0.75862,-1.37889 1.30009,-3.69183 1.09175,-4.64192 l -3.25482,-1.79069 c -0.94599,-0.52045 -1.04857,-0.82753 -0.61711,-1.80158 l -0.28861,-0.15878"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3970"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 256.80459,430.75455 -1.50216,3.71554 0.32235,0.13032 c 0.37365,-0.87537 0.62598,-1.10891 1.03316,-0.94429 0.16966,0.0686 0.33895,0.23573 0.53171,0.49131 l 2.85343,3.97629 -4.78158,0.98822 c -0.34691,0.0769 -0.67648,0.0621 -0.86311,-0.0134 -0.30539,-0.12347 -0.36205,-0.42272 -0.17649,-0.97935 0.0274,-0.0679 0.096,-0.23752 0.15451,-0.43101 l -0.32235,-0.13032 -1.92056,4.75045 0.32235,0.13033 c 0.36317,-0.80065 0.69672,-0.94214 3.77483,-1.67159 l 4.156,-0.90557 2.95207,1.19349 c 1.47603,0.59675 1.5771,0.835 1.02078,2.40633 l 0.32235,0.13033 2.09891,-5.19157 -0.32235,-0.13032 c -0.6444,1.49624 -0.94402,1.65146 -2.23343,1.13016 l -3.29138,-1.33068 -2.81914,-4.06112 c -1.07459,-1.63853 -1.2695,-2.23055 -0.98755,-3.12324 l -0.32235,-0.13033"
+ style="font-size:18.29999924px;letter-spacing:1.14999998px"
+ id="path3972"
+ inkscape:connector-curvature="0" />
+ </g>
+ <text
+ xml:space="preserve"
+ style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="107.81197"
+ y="369.00021"
+ id="text3997"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3999"
+ x="107.81197"
+ y="369.00021"
+ style="font-size:16px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4001">
+ <path
+ d="m 149.45659,432.41812 1.19531,0 c -3e-5,3.4974 -0.57555,6.04297 -1.72656,7.63672 -1.12894,1.59375 -2.5456,2.39062 -4.25,2.39063 -1.39456,-10e-6 -2.73375,-0.52019 -4.01758,-1.56055 -1.28388,-1.0625 -2.43492,-3.88476 -3.45312,-8.4668 l -2.85547,-12.88281 -9.89453,22.44531 -6.2754,0 14.21094,-30.61328 c -0.75262,-3.9622 -1.66017,-6.89514 -2.72265,-8.79883 -1.06252,-1.9036 -2.37957,-2.85542 -3.95118,-2.85547 -1.26172,5e-5 -2.36849,0.48702 -3.32031,1.46094 -0.92969,0.95186 -1.44987,2.43493 -1.56055,4.44922 l -1.19531,0 c 0.0664,-3.25387 0.7194,-5.85478 1.95899,-7.80273 1.23957,-1.97001 2.78905,-2.95504 4.64843,-2.95508 1.19531,4e-5 2.32421,0.49809 3.38672,1.49414 1.08463,0.974 2.01431,2.65629 2.78907,5.04687 0.79685,2.36853 2.02537,7.29366 3.68554,14.77539 l 2.35742,10.52539 c 0.95181,4.36069 1.9479,7.28256 2.98829,8.76563 1.06247,1.46094 2.32419,2.19141 3.78515,2.19141 2.47914,0 3.88474,-1.7487 4.2168,-5.2461"
+ style="font-size:68px;fill:#000000"
+ id="path3421" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 103.7963,407.88687 0,-65.67901 c 0,0 26.38888,21.40432 59.81481,0 33.42593,21.40432 59.81481,0 59.81481,0 l 0,65.67901 c 2.34568,60.40123 -53.07098,63.33333 -59.81481,73.30247 -6.74383,-9.96914 -62.16049,-12.90124 -59.81481,-73.30247 z"
+ id="path4120"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4169"
+ y="399.02515"
+ x="155.29356"
+ style="font-size:12.12077999px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ xml:space="preserve"><tspan
+ style="font-size:8.08052063px"
+ y="399.02515"
+ x="155.29356"
+ id="tspan4171"
+ sodipodi:role="line">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:12.12077999px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4173">
+ <path
+ d="m 176.32544,431.05326 0.60368,0 c -2e-5,1.7663 -0.29068,3.05189 -0.87197,3.85679 -0.57015,0.80489 -1.28562,1.20734 -2.14639,1.20734 -0.7043,0 -1.38063,-0.26271 -2.02901,-0.78813 -0.6484,-0.53659 -1.22971,-1.96193 -1.74394,-4.276 l -1.4421,-6.50624 -4.99706,11.33561 -3.16928,0 7.17698,-15.4607 c -0.38009,-2.00104 -0.83843,-3.48227 -1.37503,-4.4437 -0.5366,-0.96138 -1.20175,-1.44208 -1.99547,-1.4421 -0.63721,2e-5 -1.19616,0.24596 -1.67686,0.73782 -0.46953,0.48072 -0.73223,1.22972 -0.78813,2.247 l -0.60367,0 c 0.0335,-1.64331 0.36332,-2.95685 0.98935,-3.94063 0.62603,-0.99492 1.40856,-1.49239 2.34761,-1.49241 0.60367,2e-5 1.1738,0.25155 1.71041,0.75459 0.54777,0.4919 1.01729,1.34151 1.40856,2.54883 0.40244,1.19618 1.02288,3.68353 1.86132,7.46205 l 1.19058,5.31567 c 0.48069,2.20229 0.98375,3.67793 1.50918,4.42692 0.53658,0.73783 1.17379,1.10674 1.91162,1.10673 1.25205,1e-5 1.96192,-0.88314 2.12962,-2.64944"
+ style="font-size:34.34220886px;fill:#000000"
+ id="path3413" />
+ </g>
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4177"
+ d="m 153.26552,418.66418 0,-33.17004 c 0,0 13.32724,10.80988 30.20842,0 16.88118,10.80988 30.20842,0 30.20842,0 l 0,33.17004 c 1.18465,30.50458 -26.80257,31.98539 -30.20842,37.02013 -3.40585,-5.03474 -31.39307,-6.51555 -30.20842,-37.02013 z"
+ style="fill:none;stroke:#000000;stroke-width:1.01006508;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <text
+ xml:space="preserve"
+ style="font-size:6.06039047px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="180.00874"
+ y="414.60709"
+ id="text4181"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4183"
+ x="180.00874"
+ y="414.60709"
+ style="font-size:4.04026031px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:6.06039047px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4185">
+ <path
+ d="m 190.52468,430.62114 0.30184,0 c -10e-6,0.88315 -0.14534,1.52595 -0.43598,1.9284 -0.28508,0.40244 -0.64281,0.60367 -1.0732,0.60367 -0.35215,0 -0.69032,-0.13136 -1.0145,-0.39407 -0.3242,-0.26829 -0.61486,-0.98096 -0.87197,-2.138 l -0.72106,-3.25312 -2.49853,5.66781 -1.58463,0 3.58849,-7.73035 c -0.19005,-1.00053 -0.41922,-1.74114 -0.68752,-2.22185 -0.2683,-0.48069 -0.60088,-0.72104 -0.99773,-0.72105 -0.31861,1e-5 -0.59808,0.12298 -0.83843,0.36891 -0.23477,0.24036 -0.36612,0.61486 -0.39407,1.1235 l -0.30183,0 c 0.0168,-0.82166 0.18166,-1.47843 0.49467,-1.97032 0.31302,-0.49746 0.70428,-0.74619 1.17381,-0.74621 0.30183,2e-5 0.5869,0.12578 0.8552,0.3773 0.27388,0.24595 0.50865,0.67076 0.70428,1.27442 0.20122,0.59809 0.51144,1.84176 0.93066,3.73102 l 0.59529,2.65784 c 0.24035,1.10114 0.49188,1.83896 0.75459,2.21346 0.26829,0.36891 0.5869,0.55337 0.95581,0.55336 0.62603,1e-5 0.98096,-0.44157 1.06481,-1.32472"
+ style="font-size:17.17110634px;fill:#000000"
+ id="path3405" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.50503254;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 178.99472,424.4266 0,-16.58502 c 0,0 6.66362,5.40494 15.10422,0 8.44059,5.40494 15.10421,0 15.10421,0 l 0,16.58502 c 0.59232,15.25229 -13.40129,15.99269 -15.10421,18.51006 -1.70293,-2.51737 -15.69654,-3.25777 -15.10422,-18.51006 z"
+ id="path4189"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4193"
+ y="421.62961"
+ x="191.38618"
+ style="font-size:3.12381101px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ xml:space="preserve"><tspan
+ style="font-size:2.08254075px"
+ y="421.62961"
+ x="191.38618"
+ id="tspan4195"
+ sodipodi:role="line">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:3.12381101px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4197">
+ <path
+ d="m 196.8066,429.88402 0.15558,0 c -1e-5,0.45522 -0.0749,0.78654 -0.22473,0.99398 -0.14694,0.20745 -0.33133,0.31117 -0.55318,0.31117 -0.18151,0 -0.35582,-0.0677 -0.52292,-0.20312 -0.16711,-0.1383 -0.31692,-0.50564 -0.44945,-1.10203 l -0.37167,-1.67681 -1.28786,2.92145 -0.8168,0 1.84968,-3.98459 c -0.098,-0.51571 -0.21608,-0.89746 -0.35437,-1.14524 -0.1383,-0.24777 -0.30973,-0.37166 -0.51428,-0.37166 -0.16423,0 -0.30828,0.0634 -0.43217,0.19015 -0.12101,0.12389 -0.18872,0.31693 -0.20312,0.5791 l -0.15558,0 c 0.009,-0.42352 0.0936,-0.76205 0.25498,-1.01559 0.16134,-0.25641 0.36302,-0.38462 0.60503,-0.38463 0.15558,1e-5 0.30252,0.0648 0.44081,0.19448 0.14118,0.12677 0.26218,0.34574 0.36303,0.65689 0.10371,0.30829 0.26362,0.94933 0.4797,1.92315 l 0.30684,1.36997 c 0.12389,0.56758 0.25354,0.94789 0.38895,1.14092 0.13829,0.19016 0.30252,0.28523 0.49267,0.28523 0.32268,0 0.50564,-0.2276 0.54886,-0.68282"
+ style="font-size:8.85079765px;fill:#000000"
+ id="path3397" />
+ </g>
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4201"
+ d="m 190.86351,426.69106 0,-8.5487 c 0,0 3.43475,2.78596 7.78542,0 4.35068,2.78596 7.78543,0 7.78543,0 l 0,8.5487 c 0.30531,7.86175 -6.90766,8.24339 -7.78543,9.54096 -0.87776,-1.29757 -8.09073,-1.67921 -7.78542,-9.54096 z"
+ style="fill:none;stroke:#000000;stroke-width:0.26031759;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <text
+ xml:space="preserve"
+ style="font-size:1.55697834px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="197.95447"
+ y="425.82547"
+ id="text4205"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4207"
+ x="197.95447"
+ y="425.82547"
+ style="font-size:1.03798556px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:1.55697834px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4209">
+ <path
+ d="m 200.65614,429.93964 0.0775,0 c 0,0.22689 -0.0373,0.39203 -0.11201,0.49542 -0.0732,0.1034 -0.16514,0.15509 -0.27571,0.15509 -0.0905,0 -0.17735,-0.0337 -0.26064,-0.10124 -0.0833,-0.0689 -0.15796,-0.25202 -0.22402,-0.54927 l -0.18524,-0.83576 -0.6419,1.45612 -0.40711,0 0.92192,-1.98601 c -0.0488,-0.25705 -0.1077,-0.44732 -0.17663,-0.57082 -0.0689,-0.12349 -0.15437,-0.18524 -0.25633,-0.18524 -0.0818,0 -0.15365,0.0316 -0.2154,0.0948 -0.0603,0.0618 -0.0941,0.15797 -0.10124,0.28864 l -0.0776,0 c 0.004,-0.21109 0.0467,-0.37982 0.12709,-0.50619 0.0804,-0.12781 0.18094,-0.19171 0.30156,-0.19171 0.0775,0 0.15079,0.0323 0.21971,0.0969 0.0704,0.0632 0.13068,0.17232 0.18094,0.32741 0.0517,0.15366 0.1314,0.47317 0.2391,0.95854 l 0.15293,0.68283 c 0.0617,0.28289 0.12637,0.47245 0.19387,0.56866 0.0689,0.0948 0.15078,0.14216 0.24556,0.14216 0.16083,0 0.25201,-0.11344 0.27356,-0.34033"
+ style="font-size:4.41143894px;fill:#000000"
+ id="path3389" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.1297482;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 197.69396,428.3482 0,-4.26086 c 0,0 1.71196,1.38858 3.88043,0 2.16848,1.38858 3.88044,0 3.88044,0 l 0,4.26086 c 0.15217,3.91848 -3.44294,4.1087 -3.88044,4.75544 -0.4375,-0.64674 -4.0326,-0.83696 -3.88043,-4.75544 z"
+ id="path4213"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4217"
+ y="427.87085"
+ x="201.08595"
+ style="font-size:0.7686348px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ xml:space="preserve"><tspan
+ style="font-size:0.51242322px"
+ y="427.87085"
+ x="201.08595"
+ id="tspan4219"
+ sodipodi:role="line">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.7686348px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4221">
+ <path
+ d="m 202.41969,429.90191 0.0383,0 c 0,0.11201 -0.0184,0.19353 -0.0553,0.24457 -0.0362,0.051 -0.0815,0.0766 -0.13612,0.0766 -0.0447,0 -0.0875,-0.0167 -0.12867,-0.05 -0.0411,-0.034 -0.078,-0.12442 -0.11059,-0.27116 l -0.0915,-0.41259 -0.31688,0.71884 -0.20098,0 0.45512,-0.98043 c -0.0241,-0.1269 -0.0532,-0.22083 -0.0872,-0.2818 -0.034,-0.061 -0.0762,-0.0914 -0.12655,-0.0914 -0.0404,0 -0.0759,0.0156 -0.10633,0.0468 -0.0298,0.0305 -0.0464,0.078 -0.05,0.14249 l -0.0383,0 c 0.002,-0.10421 0.023,-0.18751 0.0627,-0.24989 0.0397,-0.0631 0.0893,-0.0946 0.14887,-0.0946 0.0383,0 0.0744,0.0159 0.10846,0.0478 0.0347,0.0312 0.0645,0.0851 0.0893,0.16163 0.0255,0.0759 0.0649,0.23359 0.11803,0.47321 l 0.0755,0.33709 c 0.0305,0.13965 0.0624,0.23323 0.0957,0.28073 0.034,0.0468 0.0744,0.0702 0.12122,0.0702 0.0794,0 0.12442,-0.056 0.13505,-0.16801"
+ style="font-size:2.17779851px;fill:#000000"
+ id="path3381" />
+ </g>
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4225"
+ d="m 200.95735,429.11626 0,-2.10347 c 0,0 0.84514,0.68551 1.91565,0 1.07052,0.68551 1.91566,0 1.91566,0 l 0,2.10347 c 0.0751,1.93444 -1.69968,2.02834 -1.91566,2.34762 -0.21598,-0.31928 -1.99078,-0.41318 -1.91565,-2.34762 z"
+ style="fill:none;stroke:#000000;stroke-width:0.0640529;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <text
+ xml:space="preserve"
+ style="font-size:0.38810664px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="202.64885"
+ y="428.82535"
+ id="text4229"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4231"
+ x="202.64885"
+ y="428.82535"
+ style="font-size:0.25873777px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.38810664px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4233">
+ <path
+ d="m 203.32228,429.85089 0.0193,0 c 0,0.0566 -0.009,0.0977 -0.0279,0.12349 -0.0182,0.0258 -0.0412,0.0387 -0.0687,0.0387 -0.0225,0 -0.0442,-0.008 -0.065,-0.0252 -0.0208,-0.0172 -0.0394,-0.0628 -0.0558,-0.13691 l -0.0462,-0.20833 -0.16001,0.36296 -0.10148,0 0.22981,-0.49505 c -0.0122,-0.0641 -0.0268,-0.1115 -0.044,-0.14228 -0.0172,-0.0308 -0.0385,-0.0462 -0.0639,-0.0462 -0.0204,0 -0.0383,0.008 -0.0537,0.0236 -0.015,0.0154 -0.0234,0.0394 -0.0252,0.0719 l -0.0193,0 c 0.001,-0.0526 0.0116,-0.0947 0.0317,-0.12618 0.02,-0.0319 0.0451,-0.0478 0.0752,-0.0478 0.0193,0 0.0376,0.008 0.0548,0.0242 0.0175,0.0157 0.0326,0.043 0.0451,0.0816 0.0129,0.0383 0.0327,0.11795 0.0596,0.23894 l 0.0381,0.17021 c 0.0154,0.0705 0.0315,0.11776 0.0483,0.14175 0.0172,0.0236 0.0376,0.0354 0.0612,0.0354 0.0401,0 0.0628,-0.0283 0.0682,-0.0848"
+ style="font-size:1.09963548px;fill:#000000"
+ id="path3373" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.03234222;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 202.58391,429.45419 0,-1.0621 c 0,0 0.42674,0.34613 0.96727,0 0.54054,0.34613 0.96728,0 0.96728,0 l 0,1.0621 c 0.0379,0.97676 -0.85822,1.02417 -0.96728,1.18539 -0.10905,-0.16122 -1.0052,-0.20863 -0.96727,-1.18539 z"
+ id="path4237"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4241"
+ y="429.34482"
+ x="203.45729"
+ style="font-size:0.18822911px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ xml:space="preserve"><tspan
+ style="font-size:0.12548608px"
+ y="429.34482"
+ x="203.45729"
+ id="tspan4243"
+ sodipodi:role="line">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.18822911px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4245">
+ <path
+ d="m 203.7839,429.84221 0.009,0 c 0,0.0274 -0.005,0.0474 -0.0135,0.0599 -0.009,0.0125 -0.02,0.0187 -0.0333,0.0187 -0.0109,0 -0.0214,-0.004 -0.0315,-0.0122 -0.0101,-0.008 -0.0191,-0.0305 -0.0271,-0.0664 l -0.0224,-0.10104 -0.0776,0.17604 -0.0492,0 0.11145,-0.2401 c -0.006,-0.0311 -0.013,-0.0541 -0.0213,-0.069 -0.008,-0.0149 -0.0187,-0.0224 -0.031,-0.0224 -0.01,0 -0.0186,0.004 -0.026,0.0114 -0.007,0.007 -0.0114,0.0191 -0.0122,0.0349 l -0.009,0 c 5.2e-4,-0.0255 0.006,-0.0459 0.0154,-0.0612 0.01,-0.0154 0.0219,-0.0232 0.0365,-0.0232 0.009,0 0.0182,0.004 0.0266,0.0117 0.009,0.008 0.0158,0.0208 0.0219,0.0396 0.006,0.0186 0.0159,0.0572 0.0289,0.11588 l 0.0185,0.0825 c 0.007,0.0342 0.0153,0.0571 0.0234,0.0688 0.008,0.0114 0.0182,0.0172 0.0297,0.0172 0.0195,0 0.0305,-0.0137 0.0331,-0.0411"
+ style="font-size:0.53331584px;fill:#000000"
+ id="path3365" />
+ </g>
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4249"
+ d="m 203.4258,429.6498 0,-0.51511 c 0,0 0.20697,0.16787 0.46912,0 0.26216,0.16787 0.46912,0 0.46912,0 l 0,0.51511 c 0.0184,0.47372 -0.41623,0.49671 -0.46912,0.5749 -0.0529,-0.0782 -0.48751,-0.10118 -0.46912,-0.5749 z"
+ style="fill:none;stroke:#000000;stroke-width:0.01568576;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <text
+ xml:space="preserve"
+ style="font-size:0.08842224px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="203.86418"
+ y="429.59842"
+ id="text4253"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4255"
+ x="203.86418"
+ y="429.59842"
+ style="font-size:0.05894816px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.08842224px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4257">
+ <path
+ d="m 204.01761,429.83205 0.004,0 c 0,0.0129 -0.002,0.0223 -0.006,0.0281 -0.004,0.006 -0.009,0.009 -0.0157,0.009 -0.005,0 -0.0101,-0.002 -0.0148,-0.006 -0.005,-0.004 -0.009,-0.0143 -0.0127,-0.0312 l -0.0105,-0.0475 -0.0365,0.0827 -0.0231,0 0.0524,-0.11279 c -0.003,-0.0146 -0.006,-0.0254 -0.01,-0.0324 -0.004,-0.007 -0.009,-0.0105 -0.0146,-0.0105 -0.005,0 -0.009,0.002 -0.0122,0.005 -0.003,0.003 -0.005,0.009 -0.006,0.0164 l -0.004,0 c 2.5e-4,-0.012 0.003,-0.0216 0.007,-0.0288 0.005,-0.007 0.0103,-0.0109 0.0171,-0.0109 0.004,0 0.009,0.002 0.0125,0.006 0.004,0.004 0.007,0.01 0.0103,0.0186 0.003,0.009 0.007,0.0269 0.0136,0.0544 l 0.009,0.0388 c 0.004,0.0161 0.007,0.0268 0.011,0.0323 0.004,0.005 0.009,0.008 0.0139,0.008 0.009,0 0.0143,-0.006 0.0155,-0.0193"
+ style="font-size:0.25052968px;fill:#000000"
+ id="path3362" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.00736852;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 203.84939,429.74168 0,-0.24198 c 0,0 0.0972,0.0789 0.22038,0 0.12315,0.0789 0.22037,0 0.22037,0 l 0,0.24198 c 0.009,0.22254 -0.19553,0.23334 -0.22037,0.27007 -0.0249,-0.0367 -0.22902,-0.0475 -0.22038,-0.27007 z"
+ id="path4261"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4265"
+ y="429.71896"
+ x="204.06689"
+ style="font-size:0.04007544px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ xml:space="preserve"><tspan
+ style="font-size:0.02671696px"
+ y="429.71896"
+ x="204.06689"
+ id="tspan4267"
+ sodipodi:role="line">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.04007544px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4269">
+ <path
+ d="m 204.13644,429.82485 0.002,0 c 0,0.006 -9.6e-4,0.0101 -0.003,0.0127 -0.002,0.003 -0.004,0.004 -0.007,0.004 -0.002,0 -0.005,-8.7e-4 -0.007,-0.003 -0.002,-0.002 -0.004,-0.006 -0.006,-0.0141 l -0.005,-0.0215 -0.0165,0.0375 -0.0105,0 0.0237,-0.0511 c -0.001,-0.007 -0.003,-0.0115 -0.005,-0.0147 -0.002,-0.003 -0.004,-0.005 -0.007,-0.005 -0.002,0 -0.004,8.1e-4 -0.006,0.002 -0.002,0.002 -0.002,0.004 -0.003,0.007 l -0.002,0 c 1.1e-4,-0.005 0.001,-0.01 0.003,-0.013 0.002,-0.003 0.005,-0.005 0.008,-0.005 0.002,0 0.004,8.3e-4 0.006,0.002 0.002,0.002 0.003,0.004 0.005,0.008 0.001,0.004 0.003,0.0122 0.006,0.0247 l 0.004,0.0176 c 0.002,0.007 0.003,0.0122 0.005,0.0146 0.002,0.002 0.004,0.004 0.006,0.004 0.004,0 0.006,-0.003 0.007,-0.009"
+ style="font-size:0.11354708px;fill:#000000"
+ id="path3359" />
+ </g>
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4273"
+ d="m 204.06019,429.78388 0,-0.10967 c 0,0 0.0441,0.0357 0.0999,0 0.0558,0.0357 0.0999,0 0.0999,0 l 0,0.10967 c 0.004,0.10086 -0.0886,0.10576 -0.0999,0.1224 -0.0113,-0.0166 -0.1038,-0.0215 -0.0999,-0.1224 z"
+ style="fill:none;stroke:#000000;stroke-width:0.00333962;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <text
+ xml:space="preserve"
+ style="font-size:0.02003768px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ x="204.14943"
+ y="429.76575"
+ id="text4277"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4279"
+ x="204.14943"
+ y="429.76575"
+ style="font-size:0.01335845px">(Y F) = (F (Y F))</tspan></text>
+ <g
+ style="font-size:0.02003768px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Nimbus Roman No9 L;-inkscape-font-specification:Nimbus Roman No9 L"
+ id="text4281">
+ <path
+ d="m 204.18419,429.81871 9.9e-4,0 c 0,0.003 -4.8e-4,0.005 -0.001,0.006 -9.4e-4,0.001 -0.002,0.002 -0.004,0.002 -0.001,0 -0.002,-4.4e-4 -0.003,-0.001 -0.001,-8.9e-4 -0.002,-0.003 -0.003,-0.007 l -0.002,-0.0108 -0.008,0.0187 -0.005,0 0.0119,-0.0256 c -6.3e-4,-0.003 -0.001,-0.006 -0.002,-0.007 -8.8e-4,-0.002 -0.002,-0.002 -0.003,-0.002 -0.001,0 -0.002,4.1e-4 -0.003,0.001 -7.7e-4,8e-4 -0.001,0.002 -0.001,0.004 l -0.001,0 c 6e-5,-0.003 6e-4,-0.005 0.002,-0.007 0.001,-0.002 0.002,-0.002 0.004,-0.002 0.001,0 0.002,4.2e-4 0.003,0.001 9e-4,8.1e-4 0.002,0.002 0.002,0.004 6.7e-4,0.002 0.002,0.006 0.003,0.0123 l 0.002,0.009 c 7.9e-4,0.004 0.002,0.006 0.002,0.007 8.9e-4,0.001 0.002,0.002 0.003,0.002 0.002,0 0.003,-0.001 0.004,-0.004"
+ style="font-size:0.05677343px;fill:#000000"
+ id="path3356" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.00166981;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 204.14607,429.79822 0,-0.0548 c 0,0 0.022,0.0179 0.0499,0 0.0279,0.0179 0.0499,0 0.0499,0 l 0,0.0548 c 0.002,0.0504 -0.0443,0.0529 -0.0499,0.0612 -0.006,-0.008 -0.0519,-0.0108 -0.0499,-0.0612 z"
+ id="path4285"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#000000;stroke-width:0.79445159;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path3010"
+ sodipodi:cx="141.42136"
+ sodipodi:cy="420.3855"
+ sodipodi:rx="98.884468"
+ sodipodi:ry="98.884468"
+ d="m 240.30582,420.3855 c 0,54.61238 -44.27208,98.88447 -98.88446,98.88447 -54.612388,0 -98.884472,-44.27209 -98.884472,-98.88447 0,-54.61239 44.272084,-98.88447 98.884472,-98.88447 54.61238,0 98.88446,44.27208 98.88446,98.88447 z"
+ transform="matrix(1.2587299,0,0,1.2587299,-14.49285,-128.37748)" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ id="path4287"
+ sodipodi:cx="76.721085"
+ sodipodi:cy="343.48764"
+ sodipodi:rx="3.1819806"
+ sodipodi:ry="3.1819806"
+ d="m 79.903065,343.48764 c 0,1.75736 -1.424621,3.18198 -3.18198,3.18198 -1.75736,0 -3.181981,-1.42462 -3.181981,-3.18198 0,-1.75736 1.424621,-3.18198 3.181981,-3.18198 1.757359,0 3.18198,1.42462 3.18198,3.18198 z"
+ transform="translate(4.2426407,-4.9497475)" />
+ <path
+ transform="matrix(-1,0,0,1,322.79424,-4.9497475)"
+ d="m 79.903065,343.48764 c 0,1.75736 -1.424621,3.18198 -3.18198,3.18198 -1.75736,0 -3.181981,-1.42462 -3.181981,-3.18198 0,-1.75736 1.424621,-3.18198 3.181981,-3.18198 1.757359,0 3.18198,1.42462 3.18198,3.18198 z"
+ sodipodi:ry="3.1819806"
+ sodipodi:rx="3.1819806"
+ sodipodi:cy="343.48764"
+ sodipodi:cx="76.721085"
+ id="path4297"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ </g>
+</svg>
diff --git a/static/img/src/nginx.svg b/static/img/src/nginx.svg
new file mode 100644
index 0000000..07790fe
--- /dev/null
+++ b/static/img/src/nginx.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 73 83" style="enable-background:new 0 0 73 83;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#00AB4E;}
+ .st1{fill:#00853E;}
+ .st2{fill:#004C2C;}
+ .st3{fill:#FFFFFF;}
+</style>
+<path class="st0" d="M36.5,0.5c-0.9,0-1.7,0.2-2.5,0.7L2.8,19.2c-1.5,0.9-2.5,2.5-2.5,4.3v36.1c0,0.9,0.2,1.7,0.7,2.5l35.5-20.5
+ C36.5,41.5,36.5,14.9,36.5,0.5z"/>
+<path class="st1" d="M1,62c0.4,0.7,1,1.4,1.8,1.8L34,81.9c1.5,0.9,3.4,0.9,4.9,0l31.3-18.1c0.8-0.4,1.4-1.1,1.8-1.8L36.5,41.5
+ C36.5,41.5,13.5,54.8,1,62z"/>
+<path class="st2" d="M72,62c0.4-0.7,0.7-1.6,0.7-2.5l0-36.1c0-1.8-0.9-3.4-2.5-4.3L39,1.1c-0.8-0.4-1.6-0.7-2.5-0.7l0,41
+ C36.5,41.5,59.5,54.8,72,62z"/>
+<path class="st3" d="M44.7,57.1c1.4,1.7,3.6,2.5,5.6,2.5c2.7,0,4.7-1.8,4.7-4l0-28.2c0-2.2-1.8-4-4-4h-0.1c-2.3,0-4.1,1.8-4.1,4.1
+ l0,20.1c0,0.1-0.1,0.1-0.2,0.1L28.3,25.9c-1.4-1.7-3.6-2.5-5.6-2.5c-2.7,0-4.7,1.8-4.7,4l0,28.2c0,2.2,1.8,4,4,4h0.1
+ c2.3,0,4.1-1.8,4.1-4.1l0-20.1c0-0.1,0.1-0.1,0.2-0.1L44.7,57.1z"/>
+</svg>
diff --git a/static/img/src/sicp-snake.png b/static/img/src/sicp-snake.png
new file mode 100644
index 0000000..66bdd5e
--- /dev/null
+++ b/static/img/src/sicp-snake.png
Binary files differ
diff --git a/static/img/tux.png b/static/img/tux.png
new file mode 100644
index 0000000..bdc5b1d
--- /dev/null
+++ b/static/img/tux.png
Binary files differ
diff --git a/static/img/valid-html.png b/static/img/valid-html.png
new file mode 100644
index 0000000..e6a4dc2
--- /dev/null
+++ b/static/img/valid-html.png
Binary files differ
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..d2d107f
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML PUBLIC "ISO/IEC 15445:2000//DTD HTML//EN">
+<HTML lang="EN">
+<HEAD>
+<META content="text/html; charset=UTF-8" http-equiv="content-type">
+<META name="viewport" content="width=device-width, initial-scale=1.0">
+<LINK rel="icon" href="/static/favicon.ico" type="image/png">
+<TITLE>kiwi larp</TITLE>
+<LINK rel="stylesheet" href="/static/styles/about.css" type="text/css">
+</HEAD>
+<BODY>
+<div style="background:#222;border:1px solid #666;padding:8px;margin:10px 0;text-align:center;">
+ Best tilde to exist
+ <a href="https://catto.garden" target="_blank" rel="noopener noreferrer">
+ catto.garden
+ </a>
+</div>
+<H1>kiwi larp</H1>
+<HR>
+<P>an epic and cool textboard running on <a href="https://github.com/ryukinix/cl-bbs">cl-bbs </a> larp til you can't no more</P>
+<p> larping since 28/7/26 <br>
+irc on ==> irc.larp.nz :> <br>
+Follow <a href="https://bsky.app/profile/kawars.xyz">Bluesky account</a> or <a href="https://snac.larp.nz/chers/">My snac account </a> For updates and <a href="https://s.kawars.xyz/irc/"> IRC posting bot </a> <br>
+if you want a snac account message me on irc(chers) or xmpp(chersbobers@catto.garden)/chers@larp.nz <br>
+you can also get an xmpp account too <br>
+Thank you xenia for the name!
+
+
+<h1> RULES: </h1>
+<ul>
+1. Don't be a bigot (No homophobia, racisim, transphobia etc) <br>
+2. Don't post illegal content this servers at my grandmas house
+</ul>
+<PRE style="font-family: monospace; line-height: 1.0; letter-spacing: 0;">
+
+ ░░██ ░░██
+ ████ ████
+ ████▓▓████
+ ██ ██ ██
+ ██████
+ ████
+ ██████
+ ██████░░
+ ██████████
+ ██████████
+░░░░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░
+░░░░░░░░░░░░░░░░░░░░░░░░░░██████████░░░░░░░░░░░░░░░░░░░░░░
+░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░
+░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██░░░░░░░░░░░░░░░░░░░░
+
+</PRE>
+
+<H2>larpboards</H2>
+<UL>
+<!--BOARDS-LIST-PLACEHOLDER-->
+</UL>
+<DIV style="margin: 0.5em 2%; padding-bottom: 1em;">
+ <A href="/admin" style="text-decoration: none; font-weight: bold; padding: 6px 14px; border: 1px solid #882200; border-radius: 4px; background-color: #fff9f5; color: #882200; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: background-color 0.2s; display: inline-block;">Admin Moderation Panel</A>
+</DIV>
+
+<H2>larpy features</H2>
+<P>One newline is a line break (&lt;BR&gt;), two or more newlines will start a new paragraph.</P>
+<TABLE id="toggled-element" summary="Formatting help">
+<THEAD><TR><TH>input</TH><TH>output</TH></TR></THEAD>
+<TBODY><TR><TD><SAMP>**bold**</SAMP></TD><TD><B>bold</B></TD></TR>
+<TR><TD><SAMP>__italic__</SAMP></TD><TD><I>italic</I></TD></TR>
+<TR><TD><SAMP>`monospaced`</SAMP></TD><TD><CODE>monospaced</CODE></TD></TR>
+<TR><TD><SAMP>~~spoiler~~</SAMP></TD><TD><DEL>spoiler</DEL></TD></TR>
+<TR><TD><SAMP>http://c2.com</SAMP></TD><TD><A href="http://c2.com">http://c2.com</A></TD></TR>
+<TR><TD><SAMP>https://example.com/pic.png</SAMP></TD><TD>direct link auto-renders as clickable <B>image preview</B></TD></TR>
+<TR><TD><SAMP>image+https://example.com/anyurl</SAMP></TD><TD>prefix renders as clickable <B>image preview</B> for any image URL (even without standard extensions)</TD></TR>
+<TR><TD><SAMP>link to post &gt;&gt;7</SAMP></TD><TD>link to post <A href="#">&gt;&gt;7</A></TD></TR>
+<TR><TD><SAMP>&gt;quoted text</SAMP></TD><TD><BLOCKQUOTE><P>quoted text</P></BLOCKQUOTE></TD></TR>
+<TR><TD><KBD>```<BR>;;;This is a block of code<BR>(lambda (h)<BR>&nbsp;&nbsp;((lambda (f) (f f))<BR>&nbsp;&nbsp;&nbsp;(lambda (f) (h (lambda (x) ((f f) x))))))<BR>```</KBD></TD>
+<TD><PRE>;;;This is a block of code
+(lambda (h)
+ ((lambda (f) (f f))
+ (lambda (f) (h (lambda (x) ((f f) x))))))</PRE></TD></TR>
+<TR><TD><KBD>```lisp<BR>;;;Executable snippet<BR>(format t "Hello")<BR>```</KBD></TD>
+<TD><PRE class="lisp-code-block"><SPAN class=""><SPAN class="paren1">(<SPAN class="">format t <SPAN class="string">"Hello"</SPAN></SPAN>)</SPAN></SPAN></PRE><P style="font-size: 0.85em; font-style: italic;">Renders as a syntax-highlighted, client-side executable Common Lisp snippet via JSCL. You can also try out the <B>interactive Lisp Playground</B> by clicking the <B>λ</B> link in the navigation menu!</P></TD></TR>
+<TR><TD><KBD>Ctrl+Enter</KBD></TD><TD>In multiline inputs (post, reply, playground): submit or evaluate without leaving the keyboard.</TD></TR></TBODY></TABLE>
+
+<HR>
+<h1> all of this ^ was from the og site template </h1>
+<P style="font-size: 0.9em; text-align: center; margin-top: 3em; padding-top: 1.5em; border-top: 1px dashed #aaa;"><A href="/about" style="text-decoration: none; font-weight: bold; color: #882200;">About cl-bbs</A> | <A href="/rss" style="text-decoration: none; font-weight: bold; color: #882200;">RSS Feed</A></P>
+</BODY>
+</HTML>
diff --git a/static/jscl-snippets.js b/static/jscl-snippets.js
new file mode 100644
index 0000000..2240528
--- /dev/null
+++ b/static/jscl-snippets.js
@@ -0,0 +1,319 @@
+// Common Lisp Snippet Executor and Playground using JSCL
+// Loaded dynamically and runs completely client-side.
+
+let jsclLoading = false;
+let jsclLoaded = false;
+const jsclCallbacks = [];
+
+function ensureJSCL(callback) {
+ if (window.jscl) {
+ callback();
+ return;
+ }
+ jsclCallbacks.push(callback);
+ if (jsclLoading) return;
+ jsclLoading = true;
+
+ const script = document.createElement('script');
+ script.src = "https://cdn.jsdelivr.net/gh/jscl-project/jscl-project.github.io/jscl.js";
+ script.onload = () => {
+ jsclLoaded = true;
+ while (jsclCallbacks.length > 0) {
+ const cb = jsclCallbacks.shift();
+ cb();
+ }
+ };
+ script.onerror = () => {
+ // Try unpkg as a fallback CDN
+ const fallbackScript = document.createElement('script');
+ fallbackScript.src = "https://unpkg.com/jscl/dist/jscl.js";
+ fallbackScript.onload = () => {
+ jsclLoaded = true;
+ while (jsclCallbacks.length > 0) {
+ const cb = jsclCallbacks.shift();
+ cb();
+ }
+ };
+ fallbackScript.onerror = (err) => {
+ console.error("Failed to load JSCL from both CDNs:", err);
+ alert("Failed to load JSCL Common Lisp compiler. Please check your internet connection.");
+ };
+ document.head.appendChild(fallbackScript);
+ };
+ document.head.appendChild(script);
+}
+
+function initInlineSnippets() {
+ const blocks = document.querySelectorAll('pre.lisp-code-block:not(#playground-editor)');
+ blocks.forEach((block) => {
+ // Wrap the pre element
+ const container = document.createElement('div');
+ container.className = 'lisp-snippet-container';
+ container.style.margin = '1em 0';
+
+ block.parentNode.insertBefore(container, block);
+ container.appendChild(block);
+
+ // Create controls
+ const controls = document.createElement('div');
+ controls.className = 'lisp-snippet-controls';
+ controls.style.margin = '5px 0';
+ controls.style.display = 'flex';
+ controls.style.gap = '10px';
+
+ const runBtn = document.createElement('button');
+ runBtn.textContent = 'Run';
+ runBtn.className = 'lisp-btn lisp-btn-run';
+
+ const editBtn = document.createElement('button');
+ editBtn.textContent = 'Edit';
+ editBtn.className = 'lisp-btn lisp-btn-edit';
+
+ controls.appendChild(runBtn);
+ controls.appendChild(editBtn);
+ container.appendChild(controls);
+
+ // Create output panel
+ const outputPanel = document.createElement('pre');
+ outputPanel.className = 'lisp-output-panel';
+ outputPanel.style.display = 'none';
+ outputPanel.style.padding = '0.5em';
+ outputPanel.style.marginTop = '5px';
+ outputPanel.style.border = '1px dashed currentColor';
+ outputPanel.style.backgroundColor = 'rgba(128, 128, 128, 0.05)';
+ outputPanel.style.whiteSpace = 'pre-wrap';
+ outputPanel.style.wordBreak = 'break-all';
+ container.appendChild(outputPanel);
+
+ let isEditing = false;
+
+ editBtn.addEventListener('click', () => {
+ if (!isEditing) {
+ block.contentEditable = 'true';
+ block.spellcheck = false;
+ block.style.outline = '1px solid currentColor';
+ block.style.padding = '5px';
+ block.focus();
+ editBtn.textContent = 'View';
+ isEditing = true;
+ } else {
+ block.contentEditable = 'false';
+ block.style.outline = 'none';
+ block.style.padding = '';
+ editBtn.textContent = 'Edit';
+ isEditing = false;
+
+ // Fetch colorized version from backend API
+ const code = block.textContent;
+ fetch('/api/colorize', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: 'code=' + encodeURIComponent(code)
+ })
+ .then(response => response.text())
+ .then(html => {
+ block.innerHTML = html;
+ })
+ .catch(err => console.error("Failed to colorize snippet:", err));
+ }
+ });
+
+ block.addEventListener('keydown', (e) => {
+ if (e.ctrlKey && e.key === 'Enter') {
+ e.preventDefault();
+ runBtn.click();
+ }
+ });
+
+ runBtn.addEventListener('click', () => {
+ runBtn.disabled = true;
+ const originalText = runBtn.textContent;
+ runBtn.textContent = 'Running...';
+ outputPanel.style.display = 'block';
+ outputPanel.textContent = 'Initializing JSCL and executing...';
+
+ ensureJSCL(() => {
+ try {
+ const code = block.textContent;
+ const wrappedCode = `
+(let ((out (make-string-output-stream)))
+ (let ((*standard-output* out))
+ (let ((val (progn
+${code}
+ )))
+ (format nil "~A|==SEPARATOR==|~S" (get-output-stream-string out) val))))
+`;
+ const resRaw = window.jscl.evaluateString(wrappedCode);
+ let resStr = resRaw;
+ if (Array.isArray(resRaw)) {
+ resStr = resRaw.join('');
+ } else if (typeof resRaw !== 'string') {
+ resStr = String(resRaw);
+ }
+
+ let stdout = "";
+ let returnValue = "";
+ if (typeof resStr === 'string' && resStr.includes('|==SEPARATOR==|')) {
+ const parts = resStr.split('|==SEPARATOR==|');
+ stdout = parts[0];
+ returnValue = parts[1];
+ } else {
+ returnValue = String(resStr);
+ }
+
+ let displayResult = "";
+ if (stdout) {
+ displayResult += stdout;
+ if (!stdout.endsWith('\n')) {
+ displayResult += '\n';
+ }
+ }
+ displayResult += `=> ${returnValue}`;
+
+ outputPanel.textContent = displayResult;
+ outputPanel.style.color = '';
+ } catch (err) {
+ outputPanel.textContent = `Error: ${err.message || err}`;
+ outputPanel.style.color = 'red';
+ } finally {
+ runBtn.disabled = false;
+ runBtn.textContent = originalText;
+ }
+ });
+ });
+ });
+}
+
+function initPlayground() {
+ const editor = document.getElementById('playground-editor');
+ const runBtn = document.getElementById('playground-run');
+ const clearBtn = document.getElementById('playground-clear');
+ const examplesSelect = document.getElementById('playground-examples');
+ const outputPanel = document.getElementById('playground-output');
+
+ if (!editor || !runBtn || !clearBtn || !examplesSelect || !outputPanel) return;
+
+ const examples = {
+ hello: `(format t "Hello, World!~%")`,
+ fib: `(defun fib (n)
+ (if (< n 2)
+ n
+ (+ (fib (- n 1)) (fib (- n 2)))))
+
+(format t "Fibonacci of 10 is: ~a~%" (fib 10))`,
+ loop: `(loop for x from 1 to 5
+ do (format t "Square of ~d is ~d~%" x (* x x)))`,
+ clos: `(defclass person ()
+ ((name :accessor person-name :initarg :name)
+ (age :accessor person-age :initarg :age)))
+
+(defmethod introduce ((p person))
+ (format t "Hi, I am ~a and I am ~a years old.~%"
+ (person-name p)
+ (person-age p)))
+
+(let ((p (make-instance 'person :name "Alice" :age 30)))
+ (introduce p))`
+ };
+
+ examplesSelect.addEventListener('change', () => {
+ const key = examplesSelect.value;
+ const exampleData = document.getElementById('example-data-' + key);
+ if (exampleData) {
+ editor.innerHTML = exampleData.innerHTML;
+ } else {
+ editor.innerHTML = "";
+ }
+ });
+
+ clearBtn.addEventListener('click', () => {
+ outputPanel.textContent = '';
+ outputPanel.style.display = 'none';
+ });
+
+ editor.addEventListener('keydown', (e) => {
+ if (e.ctrlKey && e.key === 'Enter') {
+ e.preventDefault();
+ runBtn.click();
+ }
+ });
+
+ runBtn.addEventListener('click', () => {
+ runBtn.disabled = true;
+ const originalText = runBtn.textContent;
+ runBtn.textContent = 'Running...';
+ outputPanel.style.display = 'block';
+ outputPanel.textContent = 'Initializing JSCL and executing...';
+
+ ensureJSCL(() => {
+ try {
+ const code = editor.textContent;
+ const wrappedCode = `
+(let ((out (make-string-output-stream)))
+ (let ((*standard-output* out))
+ (let ((val (progn
+${code}
+ )))
+ (format nil "~A|==SEPARATOR==|~S" (get-output-stream-string out) val))))
+`;
+ const resRaw = window.jscl.evaluateString(wrappedCode);
+ let resStr = resRaw;
+ if (Array.isArray(resRaw)) {
+ resStr = resRaw.join('');
+ } else if (typeof resRaw !== 'string') {
+ resStr = String(resRaw);
+ }
+
+ let stdout = "";
+ let returnValue = "";
+ if (typeof resStr === 'string' && resStr.includes('|==SEPARATOR==|')) {
+ const parts = resStr.split('|==SEPARATOR==|');
+ stdout = parts[0];
+ returnValue = parts[1];
+ } else {
+ returnValue = String(resStr);
+ }
+
+ let displayResult = "";
+ if (stdout) {
+ displayResult += stdout;
+ if (!stdout.endsWith('\n')) {
+ displayResult += '\n';
+ }
+ }
+ displayResult += `=> ${returnValue}`;
+
+ outputPanel.textContent = displayResult;
+ outputPanel.style.color = '';
+
+ // Dynamically update the editor syntax highlighting using backend colorize API
+ fetch('/api/colorize', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: 'code=' + encodeURIComponent(code)
+ })
+ .then(response => response.text())
+ .then(html => {
+ editor.innerHTML = html;
+ })
+ .catch(err => console.error("Failed to colorize playground code:", err));
+
+ } catch (err) {
+ outputPanel.textContent = `Error: ${err.message || err}`;
+ outputPanel.style.color = 'red';
+ } finally {
+ runBtn.disabled = false;
+ runBtn.textContent = originalText;
+ }
+ });
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ initInlineSnippets();
+ initPlayground();
+});
diff --git a/static/larp.ico b/static/larp.ico
new file mode 100644
index 0000000..0195309
--- /dev/null
+++ b/static/larp.ico
Binary files differ
diff --git a/static/lisp.html b/static/lisp.html
new file mode 100644
index 0000000..5de8bf2
--- /dev/null
+++ b/static/lisp.html
@@ -0,0 +1,14 @@
+<pre>
+
+This is not SAX
+Not a JSON API
+
+┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
+│ ● │ ●─┼──┤ ● │ ●─┼──┤ ● │ ●─┼──┤ ● │ ╱ │
+└─┼─┴───┘ └─┼─┴───┘ └─┼─┴───┘ └─┼─┴───┘
+ │ │ │ │
+┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐
+│ L │ │ I │ │ S │ │ P │
+└───┘ └───┘ └───┘ └───┘
+
+</pre></body></html>
diff --git a/static/manifest.json b/static/manifest.json
new file mode 100644
index 0000000..f3faaf6
--- /dev/null
+++ b/static/manifest.json
@@ -0,0 +1,18 @@
+{
+ "name": "cl-bbs Textboard",
+ "short_name": "cl-bbs",
+ "description": "An anonymous textboard bulletin board system",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#882200",
+ "orientation": "any",
+ "icons": [
+ {
+ "src": "/static/schemebbs.png",
+ "sizes": "64x64",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ]
+}
diff --git a/static/schemebbs.png b/static/schemebbs.png
new file mode 100644
index 0000000..b102724
--- /dev/null
+++ b/static/schemebbs.png
Binary files differ
diff --git a/static/styles/about.css b/static/styles/about.css
new file mode 100644
index 0000000..d305c5f
--- /dev/null
+++ b/static/styles/about.css
@@ -0,0 +1,82 @@
+@import url("common.css");
+
+body {
+ width: 98%;
+ font-family: serif;
+ margin: 2em auto;
+ max-width: 800px;
+ padding: 0 1em;
+ box-sizing: border-box;
+ background-color: #1c2023;
+ color: #95aec7;
+}
+img {
+ max-width: 100%;
+ height: auto;
+}
+del {
+ text-decoration: none;
+ background-color: #95aec7;
+}
+del:hover {
+ background-color: transparent;
+}
+th {
+ text-align: left;
+ font-weight: normal;
+ text-decoration: underline;
+ padding-bottom: 1em;
+ color: #666666;
+}
+td {
+ padding: 0.2em 2em 0.2em 0;
+}
+pre {
+ background-color: #1f2427;
+ color: #95aec7;
+ padding: 0.4em;
+}
+blockquote {
+ color: #9aae86;
+ border-left: 2px solid #859774;
+ padding-left: 0.5em;
+ margin-left: 0;
+}
+h1 {
+ margin: 0;
+ padding: 0;
+ font-size: 2em;
+ font-weight: bold;
+ color: #c7ae95;
+}
+h2 {
+ margin: 0;
+ padding: 0;
+ font-size: 1.5em;
+ font-weight: bold;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ color: #c7ae95;
+}
+h2 samp {
+ font-size: 0.66em;
+}
+hr {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ height: 1px;
+ background-color: #716356;
+ color: #716356;
+}
+a, a:visited, a:active {
+ color: #c7ae95;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:target {
+ background-color: #22272a;
+ text-decoration: underline;
+}
diff --git a/static/styles/common.css b/static/styles/common.css
new file mode 100644
index 0000000..8e385c2
--- /dev/null
+++ b/static/styles/common.css
@@ -0,0 +1,248 @@
+/* Structural and Shared Core Styles for cl-bbs */
+
+/* Base alignment margins and layouts */
+h1 {
+ margin: 0.5em 2% 0.3em 2%;
+}
+
+h2 {
+ margin: 0.5em 2%;
+}
+
+.nav {
+ margin: 0.5em 2% 0.5em 2%;
+}
+
+.preferences-form {
+ margin: 1.5em 2% 2em 2%;
+ padding: 0;
+ max-width: 600px;
+}
+
+.preferences-form p {
+ margin: 0.5em 0;
+}
+
+.theme-options-title {
+ font-weight: bold;
+ margin-bottom: 0.5em;
+}
+
+.theme-selector-container {
+ display: flex !important;
+ flex-direction: column !important;
+ align-items: flex-start !important;
+ gap: 0.8em;
+ width: 100%;
+ margin: 1em 0;
+}
+
+.theme-option-label {
+ display: flex !important;
+ width: auto !important;
+ align-items: center;
+ gap: 0.5em;
+ cursor: pointer;
+ user-select: none;
+ clear: both;
+}
+
+.theme-option-text {
+ text-transform: capitalize;
+}
+
+.preferences-form input[type=submit] {
+ cursor: pointer;
+}
+
+/* Base mobile responsive layout changes */
+@media (max-width: 768px) {
+ .preferences-form {
+ margin: 1.5em 0;
+ }
+}
+
+/* Error page styling */
+.error-container {
+ margin: 2em 2%;
+ padding: 1.5em;
+ border-left: 5px solid red;
+ background-color: #fff8f8;
+ font-family: serif;
+}
+
+.error-container p {
+ margin: 0.5em 0;
+}
+
+.error-container .error-title {
+ color: red;
+ font-size: 1.2em;
+ font-weight: bold;
+ margin-top: 0;
+}
+
+.error-back-button {
+ padding: 8px 16px;
+ font-weight: bold;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+/* Moderation Panel Styles (Theme-Adaptive) */
+body.moderation {
+ max-width: 900px;
+ margin: 0 auto !important;
+ padding: 1em 2% !important;
+}
+
+body.moderation h1 {
+ margin-left: 0 !important;
+}
+
+body.moderation h2 {
+ margin-left: 0 !important;
+ border-bottom: 1px solid currentColor;
+ padding-bottom: 0.3em;
+}
+
+body.moderation ul {
+ padding-left: 1.5em;
+ margin-top: 1em;
+}
+
+body.moderation li {
+ margin-bottom: 0.8em;
+ display: flex;
+ align-items: center;
+ gap: 1em;
+}
+
+body.moderation form {
+ margin: 0;
+}
+
+body.moderation table {
+ width: 100% !important;
+ margin: 1.5em 0 !important;
+ border-collapse: collapse;
+}
+
+body.moderation th {
+ padding: 8px !important;
+ font-weight: bold;
+}
+
+body.moderation td {
+ padding: 8px !important;
+ vertical-align: middle;
+}
+
+body.moderation dd form {
+ margin-top: 1em;
+ padding: 1em;
+ border: 1px dashed currentColor;
+ opacity: 0.9;
+}
+
+body.moderation dd form p {
+ margin: 0.3em 0;
+}
+
+body.moderation input[type=submit].delete-button {
+ color: #ff3333 !important;
+ border-color: #ff3333 !important;
+ cursor: pointer;
+}
+
+body.moderation input[type=submit].delete-button:hover {
+ background-color: #ff3333 !important;
+ color: white !important;
+}
+
+body.moderation input[type=submit].shame-button {
+ color: #ff9900 !important;
+ border-color: #ff9900 !important;
+ cursor: pointer;
+}
+
+body.moderation input[type=submit].shame-button:hover {
+ background-color: #ff9900 !important;
+ color: white !important;
+}
+
+body.moderation .comment-preview {
+ margin-bottom: 0.5em;
+ padding: 0.5em;
+ background-color: #fafafa;
+ border-left: 3px solid #ccc;
+}
+
+/* Global button reset — all submit inputs and bare buttons share the canonical style */
+input[type=submit],
+button {
+ padding: 4px 10px;
+ font-size: 0.85em;
+ font-family: inherit;
+ background: transparent;
+ color: inherit;
+ border: 1px solid currentColor;
+ cursor: pointer;
+ border-radius: 3px;
+ user-select: none;
+ margin: 0.4em 0;
+}
+
+input[type=submit]:hover,
+button:hover {
+ background-color: rgba(128, 128, 128, 0.15);
+}
+
+input[type=submit]:active,
+button:active {
+ background-color: rgba(128, 128, 128, 0.3);
+}
+
+input[type=submit]:disabled,
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Lisp Interactive Snippets & Playground Styles */
+.lisp-btn {
+ padding: 4px 10px;
+ font-size: 0.85em;
+ font-family: inherit;
+ background: transparent;
+ color: inherit;
+ border: 1px solid currentColor;
+ cursor: pointer;
+ border-radius: 3px;
+ user-select: none;
+}
+
+.lisp-btn:hover {
+ background-color: rgba(128, 128, 128, 0.15);
+}
+
+.lisp-btn:active {
+ background-color: rgba(128, 128, 128, 0.3);
+}
+
+.lisp-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+@media (max-width: 768px) {
+ #playground-editor {
+ width: 100% !important;
+ }
+ #playground-output {
+ width: 100% !important;
+ }
+}
+
+/* Syntax highlighting is now handled via separate syntax theme CSS files */
+
diff --git a/static/styles/syntax/colorful.css b/static/styles/syntax/colorful.css
new file mode 100644
index 0000000..217f235
--- /dev/null
+++ b/static/styles/syntax/colorful.css
@@ -0,0 +1,13 @@
+/* Colorful Syntax Highlighting for Lisp (Colorize output) */
+.lisp-code-block .string { color: #d14; }
+.lisp-code-block .comment { color: #998; font-style: italic; }
+.lisp-code-block .symbol { color: #008080; }
+.lisp-code-block .keyword { color: #000080; font-weight: bold; }
+.lisp-code-block .character { color: #099; }
+.lisp-code-block .special { color: #0086B3; }
+.lisp-code-block .paren1 { color: #aa0000; }
+.lisp-code-block .paren2 { color: #00aa00; }
+.lisp-code-block .paren3 { color: #0000aa; }
+.lisp-code-block .paren4 { color: #aaaa00; }
+.lisp-code-block .paren5 { color: #00aaaa; }
+.lisp-code-block .paren6 { color: #aa00aa; }
diff --git a/static/styles/syntax/simple.css b/static/styles/syntax/simple.css
new file mode 100644
index 0000000..ddf0ff5
--- /dev/null
+++ b/static/styles/syntax/simple.css
@@ -0,0 +1,19 @@
+/* Simple Syntax Highlighting for Lisp (Colorize output) */
+.lisp-code-block {
+ background-color: #121214 !important; /* Rich dark background to improve contrast */
+ color: #b0b0b0 !important; /* Slightly lighter/clearer gray for base code */
+ padding: 10px !important;
+}
+
+.lisp-code-block .string { color: #b0b0b0; }
+.lisp-code-block .comment { color: #808080; font-style: italic; }
+.lisp-code-block .symbol { color: #1e90ff; font-style: normal !important; } /* Explicitly remove italic */
+.lisp-code-block .keyword { color: #5c87ff; font-weight: bold; } /* Vibrant royalblue for dark contrast */
+.lisp-code-block .character { color: #b0b0b0; }
+.lisp-code-block .special { color: #b0b0b0; }
+.lisp-code-block .paren1 { color: #b0b0b0; }
+.lisp-code-block .paren2 { color: #b0b0b0; }
+.lisp-code-block .paren3 { color: #b0b0b0; }
+.lisp-code-block .paren4 { color: #b0b0b0; }
+.lisp-code-block .paren5 { color: #b0b0b0; }
+.lisp-code-block .paren6 { color: #b0b0b0; }
diff --git a/static/styles/themes/colored.css b/static/styles/themes/colored.css
new file mode 100644
index 0000000..487d414
--- /dev/null
+++ b/static/styles/themes/colored.css
@@ -0,0 +1,6 @@
+@import url("default.css");
+
+/* Extra styles for random theme */
+dd {
+ transition: background-color 0.3s ease;
+}
diff --git a/static/styles/themes/default.css b/static/styles/themes/default.css
new file mode 100644
index 0000000..79bcd8b
--- /dev/null
+++ b/static/styles/themes/default.css
@@ -0,0 +1,288 @@
+@import url("../common.css");
+
+body {
+ margin: 0;
+ padding: 0;
+ background-color: #1c2023;
+ color: #95aec7;
+ font-family: serif;
+ font-size: 100%;
+}
+/* Board Name */
+h1 {
+ margin: 0.5em 2% 0.3em 2%;
+ padding: 0;
+ font-size: 2em;
+ font-weight: bold;
+}
+/* Headlines */
+h2 {
+ margin: 0.5em 2%;
+ padding: 0.5em 0 0 0;
+ font-size: 1.5em;
+ font-weight: bold;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+}
+/* Links */
+h2 samp {
+ font-size: 0.66em;
+}
+hr {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ height: 1px;
+ background-color: #716356;
+ color: #716356;
+}
+p.newthread {
+ margin: 1em 2% 2em 2%;
+ padding: 1em 2%;
+ background-color: #202427;
+}
+/* boardlist */
+p.boardlist {
+ margin: 0.8em 2% 0 2%;
+ font-size: 0.8em;
+ color: #999;
+}
+
+/* menu */
+p.nav {
+ margin: 0.5em 2% 0.5em 2%;
+ padding: 0;
+ font-size: .9em;
+ font-style: italic;
+}
+/* thread navigation */
+pre.jump {
+ margin: 0 2% -1em 0;
+ padding: 0;
+ font-size: 1em;
+ font-family: serif;
+ font-weight: bold;
+ word-spacing: 1em;
+ text-align: right;
+}
+pre.jump a {
+text-decoration: none;
+}
+a, a:visited, a:active {
+ color: #c7ae95;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:target {
+ background-color: #22272a;
+ text-decoration: underline;
+}
+dl {
+ margin: 0.5em 2% 0 2%;
+ padding: 0;
+}
+dt {
+ margin: 0;
+ padding: 0;
+}
+/* post number */
+dt a {
+ font-size: 1.4em;
+}
+/* post date */
+dt code {
+ font-size: 1em;
+ font-family: monospace;
+ color: #8bafd2;
+}
+dt samp {
+ font-size: 1em;
+ font-family: monospace;
+ color: #8bafd2;
+ margin-left: 1%;
+}
+
+dd {
+ margin: 0 2%;
+ padding: 0;
+ line-height: 1.4em;
+}
+dd p {
+ margin: 1em 0;
+ padding: 0;
+ background-color: transparent;
+ word-wrap: break-word;
+}
+
+dd pre, dd code {
+ font-size: 1em;
+ font-family: monospace;
+}
+
+dd pre {
+ margin: 1.2em 0;
+ padding: 2px;
+ color: #95aec7;
+ background-color: #1f2427;
+ line-height: 1.4em;
+ overflow: auto;
+}
+blockquote {
+ border-left: solid 2px #859774;
+ margin: 1em 0 1em 0;
+ padding: 0 0 0 1em;
+ background-color: inherit;
+ color: #9aae86;
+}
+blockquote:hover {
+ background-color: #252b2f;
+}
+
+/* spoilers */
+del {
+ background-color: #95aec7;
+ text-decoration: none;
+}
+del:hover {
+ background-color: transparent;
+}
+/* thread list */
+table {
+ margin: 1em 2%;
+ padding: 0;
+ border-collapse: collapse;
+}
+th {
+ color: #666666;
+ background-color: #1c2023;
+ padding: 0.1em 0.5em;
+ font-family: monospace;
+ font-weight: normal;
+ text-align: left;
+ font-size: 1em;
+}
+td {
+ border: 0;
+ padding: 0.1em 0.5em;
+ line-height: 1em;
+}
+
+tr:nth-child(odd) {
+ background: #252a2e;
+}
+tr:hover {
+ background-color: #536883;
+}
+td p, td pre, td blockquote {
+ margin: .5em 0;
+}
+
+/* forms */
+
+textarea {
+ margin: 0.2em 0 0.2em 0;
+ border: 1px solid #334a68;
+ padding: 2px;
+ max-width: 98%;
+ color: #95aec7;
+ background-color: #293035;
+ font-size: 1em;
+ font-family: monospace;
+}
+input[type=text] {
+ margin: 0.2em 0 0.5em 0;
+ border: 1px solid #334a68;
+ background-color: #293035;
+ color: #95aec7;
+ padding: 2px;
+ max-width: 98%;
+ font-size: 1em;
+ font-family: monospace;
+}
+input[type=checkbox] {
+ border: 1px solid #334a68;
+ background-color: #293035 !important;
+ vertical-align: middle;
+ margin: 0 2px;
+}
+
+fieldset.comment {
+ display: none;
+}
+/* preferences */
+fieldset {
+ border: none;
+}
+ul {
+ margin: 0 0 1em 0;
+ padding: 0 0 1em 1em;
+}
+/* flash messages */
+p.flash {
+ background: transparent;
+ color: red;
+}
+
+/* New Thread Form Styling */
+.newthread-form {
+ margin: 1.5em 2% 2em 2%;
+ padding: 1.5em 2%;
+ background-color: #202427;
+ border: 1px solid #334a68;
+ border-radius: 4px;
+ max-width: 600px;
+}
+.newthread-form h2 {
+ margin: 0 0 1em 0;
+ padding: 0;
+ color: #c7ae95;
+}
+.newthread-form p {
+ margin: 0.5em 0;
+}
+.newthread-form input[type=text], .newthread-form textarea {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 8px;
+ font-size: 1em;
+ border: 1px solid #334a68;
+ background-color: #293035;
+ color: #95aec7;
+ border-radius: 4px;
+}
+p.footer {
+font-size: 0.9em;
+font-family: monospace;
+background: transparent;
+text-align: center;
+}
+
+/* Dark Error page styling */
+.error-container {
+ background-color: #202427;
+ color: #95aec7;
+ border-left: 5px solid #ff3333;
+ font-family: inherit;
+}
+
+.error-container .error-title {
+ color: #ff5555;
+}
+
+.error-back-button {
+ background-color: #293035;
+ color: #95aec7;
+ border: 1px solid #334a68;
+}
+
+.error-back-button:hover {
+ background-color: #536883;
+}
+
+/* Dark Moderation Panel styling overrides */
+body.moderation .comment-preview {
+ background-color: #202427;
+ border-left: 3px solid #334a68;
+}
diff --git a/static/styles/themes/light.css b/static/styles/themes/light.css
new file mode 100644
index 0000000..0e4ee47
--- /dev/null
+++ b/static/styles/themes/light.css
@@ -0,0 +1,287 @@
+@import url("../common.css");
+
+body {
+ margin: 0;
+ padding: 0;
+ background-color: white;
+ color: #000000;
+ font-family: serif;
+ font-size: 100%;
+}
+/* Board Name */
+h1 {
+ margin: 0.5em 2% 0.3em 2%;
+ padding: 0;
+ font-size: 2em;
+ font-weight: bold;
+}
+/* Headlines */
+h2 {
+ margin: 0.5em 2%;
+ padding: 0.5em 0 0 0;
+ font-size: 1.5em;
+ font-weight: bold;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+}
+/* Links */
+h2 samp {
+ font-size: 0.66em;
+}
+hr {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ height: 1px;
+ background-color: #aaaaaa;
+ color: #aaaaaa;
+}
+p.newthread {
+ margin: 1em 2% 2em 2%;
+ padding: 1em 2%;
+ background-color: #efefef;
+}
+/* menu */
+p.nav {
+ margin: 0.5em 2% 0.5em 2%;
+ padding: 0;
+ font-size: .9em;
+ font-style: italic;
+ background-color: white;
+}
+/* thread navigation */
+pre.jump {
+ margin: 0 2% -1em 0;
+ padding: 0;
+ background-color: white;
+ font-size: 1em;
+ font-family: serif;
+ font-weight: bold;
+ word-spacing: 1em;
+ text-align: right;
+}
+pre.jump a {
+text-decoration: none;
+}
+a, a:visited, a:active {
+ color: #882200;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:target {
+ background-color: #f3f3f3;
+ text-decoration: underline;
+}
+dl {
+ margin: 0.5em 2% 0 2%;
+ padding: 0;
+}
+dt {
+ margin: 0;
+ padding: 0;
+}
+/* post number */
+dt a {
+ font-size: 1.4em;
+}
+/* post date */
+dt code {
+ font-size: 1em;
+ font-family: monospace;
+ color: #555555;
+}
+dt samp {
+ font-size: 1em;
+ font-family: monospace;
+ margin-left: 1%;
+}
+
+dd {
+ margin: 0 2%;
+ padding: 0;
+ line-height: 1.4em;
+}
+dd p {
+ margin: 1em 0;
+ padding: 0;
+ background-color: transparent;
+ word-wrap: break-word;
+}
+
+dd pre, dd code {
+ font-size: 1em;
+ font-family: monospace;
+}
+
+dd pre {
+ margin: 1.2em 0;
+ padding: 2px;
+ background-color: #f7f7f7;
+ line-height: 1.4em;
+ overflow: auto;
+}
+blockquote {
+ border-left: solid 2px #cccccc;
+ margin: 1em 0 1em 0;
+ padding: 0 0 0 1em;
+ background-color: inherit;
+ color: #555555;
+}
+blockquote:hover {
+ background-color: #fafafa;
+}
+
+/* spoilers */
+del {
+ background-color: #200200;
+ text-decoration: none;
+}
+del:hover {
+ background-color: transparent;
+}
+/* thread list */
+table {
+ margin: 1em 2%;
+ padding: 0;
+ border-collapse: collapse;
+}
+th {
+ color: #666666;
+ padding: 0.1em 0.5em;
+ font-family: monospace;
+ font-weight: normal;
+ text-align: left;
+ font-size: 1em;
+}
+td {
+ border: 0;
+ padding: 0.1em 0.5em;
+// background-color: #fafafa;
+ line-height: 1em;
+}
+
+tr:nth-child(odd) {
+ background: #fafafa;
+}
+tr:hover {
+ background-color: yellow;
+}
+td p, td pre, td blockquote {
+ margin: .5em 0;
+}
+
+/* forms */
+
+textarea {
+ margin: 0.2em 0 0.2em 0;
+ border: 1px solid #bababa;
+ padding: 2px;
+ max-width: 98%;
+ //overflow: hidden;
+
+ font-size: 1em;
+ font-family: monospace;
+}
+input[type=text] {
+ margin: 0.2em 0 0.5em 0;
+ border: 1px solid #bababa;
+ padding: 2px;
+ max-width: 98%;
+ font-size: 1em;
+ font-family: monospace;
+}
+input[type=checkbox] {
+ border: 1px solid #bababa;
+ vertical-align: middle;
+ margin: 0 2px;
+}
+
+fieldset.comment {
+ display: none;
+}
+/* preferences */
+fieldset {
+ border: none;
+}
+ul {
+ margin: 0 0 1em 0;
+ padding: 0 0 1em 1em;
+}
+/* flash messages */
+p.flash {
+ background: transparent;
+ color: red;
+}
+
+p.footer {
+font-size: 0.9em;
+font-family: monospace;
+background: transparent;
+text-align: center;
+}
+
+/* New Thread Form Styling */
+.newthread-form {
+ margin: 1.5em 2% 2em 2%;
+ padding: 1.5em 2%;
+ background-color: #fcfcfc;
+ border: 1px solid #e2e2e2;
+ border-radius: 4px;
+ max-width: 600px;
+}
+.newthread-form h2 {
+ margin: 0 0 1em 0;
+ padding: 0;
+}
+.newthread-form p {
+ margin: 0.5em 0;
+}
+.newthread-form input[type=text], .newthread-form textarea {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 8px;
+ font-size: 1em;
+ border: 1px solid #bababa;
+ border-radius: 4px;
+}
+/* Mobile Responsiveness Rules */
+@media (max-width: 768px) {
+ body {
+ padding: 0.5em 1em;
+ }
+ h1 {
+ font-size: 1.8em;
+ margin: 0.5em 0 0.3em 0;
+ }
+ h2 {
+ font-size: 1.3em;
+ margin: 0 0 0.5em 0;
+ }
+ p.nav {
+ margin: 0.5em 0;
+ line-height: 1.5em;
+ }
+ dl {
+ margin: 0.5em 0;
+ }
+ dd {
+ margin: 0 0 1em 0;
+ }
+ textarea, input[type=text] {
+ width: 100% !important;
+ max-width: 100% !important;
+ box-sizing: border-box;
+ }
+ table {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ margin: 1em 0;
+ }
+ th, td {
+ padding: 0.2em 0.4em;
+ font-size: 0.9em;
+ }
+}
diff --git a/static/styles/themes/matrix.css b/static/styles/themes/matrix.css
new file mode 100644
index 0000000..b1e3766
--- /dev/null
+++ b/static/styles/themes/matrix.css
@@ -0,0 +1,423 @@
+@import url("../common.css");
+
+body {
+ margin: 0;
+ padding: 0;
+ background-color: #0d0d0d;
+ color: #00ff41;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 100%;
+ animation: pulse-glow 4s infinite alternate;
+ text-shadow: 0 0 3px #00ff41;
+}
+
+@keyframes pulse-glow {
+ 0% {
+ text-shadow: 0 0 1px #00ff41, 0 0 3px #00ff41;
+ }
+ 100% {
+ text-shadow: 0 0 2px #00ff41, 0 0 4px #008f11, 0 0 8px #003b00;
+ }
+}
+
+/* Board Name */
+h1 {
+ margin: 0.5em 2% 0.3em 2%;
+ padding: 0;
+ font-size: 2em;
+ font-weight: bold;
+ color: #d1ffdf;
+ text-shadow: 0 0 5px #00ff41, 0 0 10px #00ff41;
+ animation: glitch 1.5s ease-in-out infinite alternate;
+}
+
+@keyframes glitch {
+ 0% { transform: translate(0) }
+ 20% { transform: translate(-2px, 1px) }
+ 40% { transform: translate(-1px, -1px) }
+ 60% { transform: translate(2px, 1px) }
+ 80% { transform: translate(1px, -1px) }
+ 100% { transform: translate(0) }
+}
+
+/* Headlines */
+h2 {
+ margin: 0.5em 2%;
+ padding: 0.5em 0 0 0;
+ font-size: 1.5em;
+ font-weight: bold;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ color: #a4ffbd;
+ text-shadow: 0 0 4px #00ff41;
+}
+
+/* Links */
+h2 samp {
+ font-size: 0.66em;
+}
+hr {
+ border: 0;
+ margin: 1em 0;
+ padding: 0;
+ height: 1px;
+ background: linear-gradient(to right, #0d0d0d, #00ff41, #0d0d0d);
+}
+
+p.newthread {
+ margin: 1em 2% 2em 2%;
+ padding: 1em 2%;
+ background-color: #051405;
+ border: 1px solid #00ff41;
+}
+
+/* boardlist */
+p.boardlist {
+ margin: 0.8em 2% 0 2%;
+ font-size: 0.8em;
+ color: #008f11;
+}
+
+div.newthread-form {
+ border: 1px solid #008f11;
+ padding: 1em;
+ margin: 1em 2%;
+ background-color: rgba(0, 59, 0, 0.2);
+ box-shadow: inset 0 0 5px #003b00;
+ transition: all 0.3s ease;
+}
+
+div.newthread-form:hover {
+ box-shadow: inset 0 0 10px #008f11;
+ border-color: #00ff41;
+}
+
+
+/* menu */
+p.nav {
+ margin: 0.5em 2% 0.5em 2%;
+ padding: 0;
+ font-size: .9em;
+ font-style: italic;
+ font-weight: bold;
+ background-color: #0d0d0d;
+}
+
+/* thread navigation */
+pre.jump {
+ margin: 0 2% -1em 0;
+ padding: 0;
+ background-color: transparent;
+ font-size: 1em;
+ font-family: inherit;
+ font-weight: bold;
+ word-spacing: 1em;
+ text-align: right;
+}
+pre.jump a {
+ text-decoration: none;
+ color: #d1ffdf;
+ transition: text-shadow 0.2s;
+}
+
+pre.jump a:hover {
+ text-shadow: 0 0 8px #00ff41;
+}
+
+a, a:visited, a:active {
+ color: #d1ffdf;
+ text-decoration: none;
+ font-weight: bold;
+ transition: color 0.3s, text-shadow 0.3s;
+}
+a:hover {
+ color: #ffffff;
+ text-shadow: 0 0 5px #00ff41, 0 0 10px #00ff41;
+ text-decoration: underline dashed #00ff41;
+}
+a:target {
+ background-color: #003b00;
+ text-decoration: underline dashed #00ff41;
+}
+dl {
+ margin: 0.5em 2% 0 2%;
+ padding: 0;
+}
+dt {
+ margin: 0;
+ padding: 0;
+}
+/* post number */
+dt a {
+ font-size: 1.4em;
+ color: #008f11;
+ text-shadow: none;
+}
+dt a:hover {
+ color: #00ff41;
+}
+
+/* post date */
+dt code {
+ font-size: 1em;
+ font-family: inherit;
+ color: #008f11;
+ text-shadow: none;
+}
+dt samp {
+ font-size: 1em;
+ font-family: inherit;
+ margin-left: 1%;
+ color: #00ff41;
+ text-shadow: none;
+ font-weight: bold;
+}
+
+dd {
+ margin: 0 2% 1em 2%;
+ padding: 1em;
+ line-height: 1.4em;
+ background: rgba(0, 255, 65, 0.03);
+ border-left: 2px solid #008f11;
+ transition: background 0.3s, border-color 0.3s;
+}
+
+dd:hover {
+ background: rgba(0, 255, 65, 0.08);
+ border-left: 2px solid #00ff41;
+}
+
+dd p {
+ margin: 1em 0;
+ padding: 0;
+ background-color: transparent;
+ word-wrap: break-word;
+}
+
+dd pre, dd code {
+ font-size: 1em;
+ font-family: monospace;
+ text-shadow: none !important;
+}
+
+dd pre {
+ margin: 1.2em 0;
+ padding: 10px;
+ background-color: rgba(0, 0, 0, 0.8);
+ border: 1px dotted #008f11;
+ line-height: 1.4em;
+ overflow: auto;
+ color: #00ff41;
+}
+blockquote {
+ border-left: dashed 2px #008f11;
+ margin: 1em 0 1em 0;
+ padding: 0 0 0 1em;
+ background-color: rgba(0, 59, 0, 0.3);
+ color: #8cffab;
+ font-style: italic;
+}
+blockquote:hover {
+ background-color: rgba(0, 89, 0, 0.4);
+}
+
+/* spoilers */
+del {
+ background-color: #000;
+ color: #000;
+ text-decoration: none;
+ transition: all 0.5s ease;
+ user-select: none;
+}
+del:active, del:hover {
+ background-color: transparent;
+ color: #00ff41;
+ text-shadow: 0 0 3px #00ff41;
+}
+
+/* thread list */
+table {
+ margin: 1em 2%;
+ padding: 0;
+ border-collapse: collapse;
+ width: 96%;
+}
+th {
+ color: #00ff41;
+ padding: 0.5em;
+ font-family: inherit;
+ font-weight: bold;
+ text-align: left;
+ font-size: 1em;
+ border-bottom: 1px solid #008f11;
+ text-transform: uppercase;
+}
+td {
+ border: 0;
+ border-bottom: 1px dashed #003b00;
+ padding: 0.8em 0.5em;
+ line-height: 1.2em;
+}
+
+tr:nth-child(odd) {
+ background: rgba(0, 255, 65, 0.05);
+}
+tr:hover {
+ background-color: rgba(0, 255, 65, 0.2);
+}
+td p, td pre, td blockquote {
+ margin: .5em 0;
+}
+
+/* forms */
+
+textarea, input[type=text] {
+ margin: 0.2em 0 0.2em 0;
+ border: 1px solid #008f11;
+ padding: 5px;
+ max-width: 98%;
+ font-size: 1em;
+ font-family: inherit;
+ background-color: black;
+ color: #00ff41;
+}
+
+textarea:focus, input[type=text]:focus {
+ outline: none;
+ border-color: #00ff41;
+ box-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
+}
+
+input[type=submit], button, .lisp-btn {
+ font-family: inherit;
+ font-weight: bold;
+ font-size: 1em;
+ margin: 0.6em 0;
+ border: 1px solid #00ff41;
+ padding: 5px 15px;
+ background-color: black;
+ color: #00ff41;
+ cursor: pointer;
+ text-transform: uppercase;
+ transition: all 0.2s;
+ box-shadow: 0 0 3px #008f11;
+}
+
+input[type=submit]:hover, button:hover, .lisp-btn:hover {
+ background-color: #00ff41;
+ color: black;
+ box-shadow: 0 0 8px #00ff41;
+}
+
+input[type=submit]:active, button:active, .lisp-btn:active {
+ transform: scale(0.95);
+}
+
+input[type=submit]:disabled, button:disabled, .lisp-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+input[type=checkbox] {
+ border: 1px solid #00ff41;
+ vertical-align: middle;
+ margin: 0 2px;
+ background-color: black;
+}
+
+fieldset.comment {
+ display: none;
+}
+/* preferences */
+fieldset {
+ border: 1px solid #008f11;
+ padding: 1em;
+ margin-top: 1em;
+}
+
+legend {
+ color: #00ff41;
+ font-weight: bold;
+ padding: 0 5px;
+}
+
+select {
+ background-color: black;
+ color: #00ff41;
+ border: 1px solid #008f11;
+ padding: 3px;
+ font-family: inherit;
+}
+
+select:focus {
+ outline: none;
+ border-color: #00ff41;
+ box-shadow: 0 0 5px rgba(0, 255, 65, 0.4);
+}
+
+
+ul {
+ margin: 0 0 1em 0;
+ padding: 0 0 1em 2em;
+ list-style-type: square;
+}
+
+li {
+ margin-bottom: 0.5em;
+}
+
+/* flash messages */
+p.flash {
+ background: rgba(255, 0, 0, 0.2);
+ color: #ff3333;
+ border: 1px solid #ff0000;
+ padding: 10px;
+ text-shadow: none;
+}
+
+p.footer {
+ font-size: 0.8em;
+ font-family: inherit;
+ background: transparent;
+ text-align: center;
+ color: #00aa1a;
+ text-shadow: none;
+ margin-top: 3em;
+ border-top: 1px solid #003b00;
+ padding-top: 1em;
+}
+
+/* Matrix Error page styling */
+.error-container {
+ background-color: #0d0d0d;
+ color: #00ff41;
+ border-left: 5px solid #00ff41;
+ box-shadow: 0 0 5px #003b00;
+ font-family: inherit;
+}
+
+.error-container .error-title {
+ color: #ff3333;
+ text-shadow: 0 0 3px #ff3333;
+}
+
+.error-back-button {
+ background-color: black;
+ color: #00ff41;
+ border: 1px solid #00ff41;
+ box-shadow: 0 0 3px #008f11;
+}
+
+.error-back-button:hover {
+ background-color: #00ff41;
+ color: black;
+ box-shadow: 0 0 8px #00ff41;
+}
+
+/* Matrix Moderation Panel styling overrides */
+body.moderation .comment-preview {
+ background-color: rgba(0, 59, 0, 0.2);
+ border-left: 3px solid #008f11;
+ box-shadow: inset 0 0 3px #003b00;
+}
+\n.lisp-code-block {\n text-shadow: none !important;\n}
diff --git a/static/styles/themes/no.css b/static/styles/themes/no.css
new file mode 100644
index 0000000..1bf0411
--- /dev/null
+++ b/static/styles/themes/no.css
@@ -0,0 +1 @@
+@import url("../common.css");
diff --git a/static/sw.js b/static/sw.js
new file mode 100644
index 0000000..edf8149
--- /dev/null
+++ b/static/sw.js
@@ -0,0 +1,31 @@
+const CACHE_NAME = 'cl-bbs-v2';
+const ASSETS = [
+ '/static/styles/themes/default.css',
+ '/static/styles/themes/dark.css',
+ '/static/styles/themes/no.css',
+ '/static/styles/themes/colored.css',
+ '/static/styles/themes/matrix.css',
+ '/static/favicon.ico',
+ '/static/schemebbs.png'
+];
+
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => {
+ return cache.addAll(ASSETS);
+ })
+ );
+});
+
+self.addEventListener('fetch', (event) => {
+ event.respondWith(
+ caches.match(event.request).then((cachedResponse) => {
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+ return fetch(event.request).catch(() => {
+ // Fallback or offline page
+ });
+ })
+ );
+});
diff --git a/static/userscripts/highlight.user.js b/static/userscripts/highlight.user.js
new file mode 100644
index 0000000..94d8ba5
--- /dev/null
+++ b/static/userscripts/highlight.user.js
@@ -0,0 +1,22 @@
+// ==UserScript==
+// @name Syntax Highlighting for textboard.org
+// @namespace http://textboard.org/
+// @description Syntax Highlighting of code blocks with highlight.js
+// @version 1
+// @match *://textboard.org/*
+// @resource css https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/haskell.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/lisp.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/scheme.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/javascript.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/lua.min.js
+// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/go.min.js
+// @grant GM_addStyle
+// @grant GM_getResourceText
+// ==/UserScript==
+GM_addStyle(GM_getResourceText('css'));
+(function () {
+ 'use strict';
+ hljs.initHighlighting();
+})();
diff --git a/static/userscripts/localjump.user.js b/static/userscripts/localjump.user.js
new file mode 100644
index 0000000..8c6274e
--- /dev/null
+++ b/static/userscripts/localjump.user.js
@@ -0,0 +1,17 @@
+// ==UserScript==
+// @name localjump
+// @author Anon
+// @namespace http://textboard.org
+// @description Change single post quote links inside full thread views from separate single post views to local scroll jumps within the thread
+// @version 1
+// @match *://textboard.org/*
+// @grant none
+// ==/UserScript==
+(function() {
+ 'use strict';
+ Array.from (document.getElementsByTagName ("a")).filter (e => e.hasAttribute ("href")).forEach (e => {
+ var h = e.getAttribute ("href")
+ var s = h.replace (/^\/([^\/]+)\/(\d+)\/(\d+)$/, "/$1/$2#t$2p$3")
+ if (h != s) { e.setAttribute ("href", s); }
+})
+})();
diff --git a/static/userscripts/unvip.user.js b/static/userscripts/unvip.user.js
new file mode 100644
index 0000000..e5017cb
--- /dev/null
+++ b/static/userscripts/unvip.user.js
@@ -0,0 +1,18 @@
+// ==UserScript==
+// @name UnVIP
+// @author Anon
+// @namespace http://textboard.org
+// @description View the thread list in update order regardless of VIPs
+// @version 1
+// @match *://textboard.org/*
+// @grant none
+// ==/UserScript==
+(function() {
+ 'use strict';
+ const tbody = document.getElementsByTagName ("tbody") [0]
+ const rows = Array.from (tbody.children)
+ const upd = e => e.children [3].children [0].textContent
+ rows.sort ((a, b) => {var u = upd (a); var v = upd (b); return u < v ? 1 : u > v ? -1 : 0; })
+ tbody.innerHTML = ""
+ rows.forEach (e => tbody.appendChild (e))
+})();
diff --git a/static/userscripts/wordfilter.user.js b/static/userscripts/wordfilter.user.js
new file mode 100644
index 0000000..476706f
--- /dev/null
+++ b/static/userscripts/wordfilter.user.js
@@ -0,0 +1,29 @@
+// ==UserScript==
+// @name Word Filter for textboard.org
+// @namespace http://textboard.org
+// @description Replace words with other words
+// @version 1
+// @match *://textboard.org/*
+// @grant none
+// ==/UserScript==
+(function() {
+ 'use strict';
+ var replacements, regex, key, textnodes, node, s;
+ replacements = {
+ "nigger": "mujina",
+ "faggot": "baku",
+ };
+ regex = {};
+ for (key in replacements) {
+ regex[key] = new RegExp(key, 'gi');
+ }
+ textnodes = document.evaluate( "//body//text()", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0; i < textnodes.snapshotLength; i++) {
+ node = textnodes.snapshotItem(i);
+ s = node.data;
+ for (key in replacements) {
+ s = s.replace(regex[key], replacements[key]);
+ }
+ node.data = s;
+ }
+})();