Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f74c522d7 | |||
24f44ec093 | |||
adb7f61436 | |||
b18faec312 | |||
7432f2e77d | |||
de34c5922c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,4 +4,3 @@ www/src/output
|
||||
srv/srv_config.hy
|
||||
**/__pycache__
|
||||
**/log
|
||||
www/src/scripts/builtins/**/builtin
|
||||
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1747542820,
|
||||
"narHash": "sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "292fa7d4f6519c074f0a50394dbbe69859bb6043",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
39
flake.nix
Normal file
39
flake.nix
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
devShells.x86_64-linux.default = mkShell {
|
||||
buildInputs = with python312Packages; [
|
||||
hy
|
||||
(buildPythonPackage rec {
|
||||
pname = "hyrule";
|
||||
version = "1.0.0";
|
||||
|
||||
src = fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-SZyFjXNs6thVWhKbajYelY0DoRYqrcCQcdKMNC5t6Og=";
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [
|
||||
pip
|
||||
wheel
|
||||
hy
|
||||
];
|
||||
})
|
||||
bleach
|
||||
pyaml
|
||||
requests
|
||||
requests_toolbelt
|
||||
validators
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
hy>=1
|
||||
hyrule>=1
|
||||
bleach>=6.2.0
|
||||
pyaml>=25.1.0
|
||||
requests>=2.32.3
|
||||
requests-toolbelt>=1
|
||||
validators>=0.34.0
|
@ -1,57 +0,0 @@
|
||||
(import yaml [safe-load])
|
||||
|
||||
(setv ones-names [
|
||||
"hena"
|
||||
"di"
|
||||
"tri"
|
||||
"tetra"
|
||||
"penta"
|
||||
"hexa"
|
||||
"hepta"
|
||||
"octa"
|
||||
"ennea"])
|
||||
|
||||
(setv tens-names [
|
||||
"decagon"
|
||||
"icosi"
|
||||
"triconta"
|
||||
"tetraconta"
|
||||
"pentaconta"
|
||||
"hexaconta"
|
||||
"heptaconta"
|
||||
"octaconta"
|
||||
"enneaconta"])
|
||||
|
||||
(defn n-gon-name [n]
|
||||
(when (> 10 n)
|
||||
(return (+ (get ones-names (- n 1)) "gon")))
|
||||
|
||||
(when (= n 11)
|
||||
(return "undecagon"))
|
||||
|
||||
(when (= n 12)
|
||||
(return "dodecagon"))
|
||||
|
||||
(when (> 20 n)
|
||||
(return (+ (get ones-names (- n 1)) "decagon")))
|
||||
|
||||
(when (= 20 n)
|
||||
(return "icosagon"))
|
||||
|
||||
(let [[modc r] (divmod n 10)]
|
||||
(+ (get tens-names (- modc 1)) "kai" (get ones-names (- r 1)) "gon")))
|
||||
|
||||
(defn get-members []
|
||||
(with [f (open "www/src/data/arpa-n-gon.yaml" "r")]
|
||||
(return (get (safe-load f) "members"))))
|
||||
|
||||
(defn get-member-by-domain [members domain]
|
||||
(for [[idx member] (enumerate members)]
|
||||
(when (= (get member "arpa-domain") domain)
|
||||
(return #(idx member)))))
|
||||
|
||||
(defn next-member [members domain]
|
||||
(get members (% (+ (get (get-member-by-domain members domain) 0) 1) (len members))))
|
||||
|
||||
(defn prev-member [members domain]
|
||||
(get members (% (- (get (get-member-by-domain members domain) 0) 1) (len members))))
|
@ -1,50 +0,0 @@
|
||||
(import urllib.parse [quote-plus urlparse])
|
||||
(import os [mkdir])
|
||||
(import os.path [isdir :as dir? isfile :as file? abspath])
|
||||
(import datetime [datetime])
|
||||
(import bleach [clean])
|
||||
(import validators [domain :as domain?])
|
||||
(require hyrule.destructure [setv+])
|
||||
(import subprocess [check-output])
|
||||
|
||||
(defn create-comment [request]
|
||||
(setv now (datetime.now))
|
||||
|
||||
(setv url-comment-dir (quote-plus (. request (get "route") (get "parameters") (get "route")) :safe ""))
|
||||
|
||||
(when (not (dir? "./www/data"))
|
||||
(mkdir "./www/data"))
|
||||
|
||||
(when (not (dir? f"./www/data/comments"))
|
||||
(mkdir "./www/data/comments"))
|
||||
|
||||
(when (not (dir? f"./www/data/comments/{url-comment-dir}"))
|
||||
(mkdir f"./www/data/comments/{url-comment-dir}"))
|
||||
|
||||
(setv+ {{comment "comment" name "name" site "site" commit "commit"} "body"} request)
|
||||
|
||||
(when (!= (. (check-output "git log -1 --format=%h" :shell True :executable "/bin/bash") (decode) (strip)) commit)
|
||||
(print (. (check-output "git log -1 --format=%h" :shell True :executable "/bin/bash") (decode) (strip)) commit)
|
||||
(return (dict
|
||||
:code 303
|
||||
:headers {"Location" (. request (get "headers") (get "Referer"))})))
|
||||
|
||||
(when (.startswith site "//")
|
||||
(setv site (.replace site "//" "")))
|
||||
|
||||
(setv [protocol domain path #* _] (urlparse
|
||||
(if (in "://" site)
|
||||
site
|
||||
(+ "http://" site))))
|
||||
|
||||
(setv site-valid? (= (domain? domain) True))
|
||||
|
||||
(when (not protocol)
|
||||
(setv protocol "//"))
|
||||
|
||||
(with [f (open f"./www/data/comments/{url-comment-dir}/{(.strftime now "%Y-%m-%d_%H:%M:%S_%f")}" "w")]
|
||||
(.write f f"<div class=\"comment\"><span style=\"font-weight: bold\">{(if (and site site-valid?) f"<a href=\"{(clean protocol)}://{(clean domain)}{(clean path)}\">" "")}{(clean name)}{(if (and site site-valid?) "</a>" "")}</span> at {(.strftime now "%Y-%m-%d %H:%M:%S")}:<br><pre>{(clean comment)}</pre></div>"))
|
||||
|
||||
(return (dict
|
||||
:code 303
|
||||
:headers {"Location" (. request (get "headers") (get "Referer"))})))
|
@ -1,54 +1,32 @@
|
||||
(import bleach [clean])
|
||||
(import mimetypes [guess-type])
|
||||
(import subprocess [check-output])
|
||||
(import re)
|
||||
(import re [sub])
|
||||
(import os [environ :as hy-env])
|
||||
(import asyncio)
|
||||
(import aiofiles)
|
||||
|
||||
(setv (get hy-env "PATH") (+ (get hy-env "PATH") ":./www/site/scripts"))
|
||||
|
||||
(defn :async execute-bash [data]
|
||||
|
||||
(defn :async process-match [match]
|
||||
(let [command (.group match 1)
|
||||
process (await (asyncio.create-subprocess-shell
|
||||
command
|
||||
:stdout asyncio.subprocess.PIPE
|
||||
:stderr asyncio.subprocess.PIPE
|
||||
:shell True
|
||||
:executable "/bin/bash"
|
||||
:env hy-env))]
|
||||
(let [[stdout stderr] (await (.communicate process))]
|
||||
(.decode stdout :errors "ignore"))))
|
||||
|
||||
(setv matches (list (re.finditer r"\$\[(.*?)\]" data)))
|
||||
(setv replacements (await (asyncio.gather #* (lfor match matches (process-match match)))))
|
||||
(defn execute-bash [data]
|
||||
(sub r"\$\[(.*?)\]" (fn [sequence]
|
||||
(. (check-output (.group sequence 1) :shell True :executable "bash" :env hy-env) (decode) (strip)))
|
||||
data))
|
||||
|
||||
(setv result (list data))
|
||||
(for [match (reversed matches)]
|
||||
(setv (cut result (match.start) (match.end)) (get replacements (.index matches match))))
|
||||
(.join "" result))
|
||||
(defn parse-html-file [path #** kwargs]
|
||||
(with [f (open path "r")]
|
||||
(setv data (.read f)))
|
||||
|
||||
(defn :async parse-html-file [path [no-exec False] #** kwargs]
|
||||
(with [:async f (aiofiles.open path "r")]
|
||||
(setv data (await (.read f))))
|
||||
|
||||
(for [[k v] (.items kwargs)]
|
||||
(setv data (.replace data f"{"{"}{k}{"}"}" (str v))))
|
||||
|
||||
(when no-exec
|
||||
(return data))
|
||||
|
||||
(await (execute-bash data)))
|
||||
(execute-bash data))
|
||||
|
||||
(defn :async send-raw-file [path]
|
||||
(defn send-raw-file [path]
|
||||
(setv [mime-type _] (guess-type path))
|
||||
|
||||
|
||||
(when (not mime-type)
|
||||
(setv mime-type "text/plain"))
|
||||
|
||||
(with [:async f (aiofiles.open path "rb")]
|
||||
(setv data (await (.read f))))
|
||||
|
||||
|
||||
(with [f (open path "rb")]
|
||||
(setv data (.read f)))
|
||||
|
||||
(return #({"Content-Type" mime-type "Cache-Control" "max-age=300, stale-while-revalidate=3600"} data)))
|
||||
|
@ -1,141 +1,117 @@
|
||||
(require hyrule.destructure [defn+])
|
||||
(require hyrule.destructure [defn+ setv+])
|
||||
(require hyrule.argmove [doto])
|
||||
(require hyrule.oop [meth])
|
||||
(import hyrule.collections [assoc])
|
||||
(import content.file-io [parse-html-file send-raw-file])
|
||||
(import content.comments [create-comment])
|
||||
(import content.arpa-n-gon :as arpa-n-gon)
|
||||
(import re)
|
||||
(import functools [lru-cache])
|
||||
(import urllib.parse [quote-plus urlparse])
|
||||
(import os [mkdir])
|
||||
(import os.path [isdir :as dir? isfile :as file? abspath])
|
||||
(import asyncio)
|
||||
(import datetime [datetime])
|
||||
(import bleach [clean])
|
||||
(import validators [domain :as domain?])
|
||||
|
||||
(defn :async error [code message]
|
||||
(defmacro unless [test #* body]
|
||||
`(when (not ~test) (do ~@body)))
|
||||
|
||||
(defn error [code message]
|
||||
(return (dict
|
||||
:code code
|
||||
:body (await (parse-html-file "./www/site/html/error.html" :code code :message message)))))
|
||||
:body (parse-html-file "./www/site/html/error.html"))))
|
||||
|
||||
(defclass always []
|
||||
(meth __init__ [@value])
|
||||
(meth __getitem__ [_] @value))
|
||||
(defn+ match-request [{method "method" {path "path"} "route" :as request}]
|
||||
(when (.startswith path "/assets/")
|
||||
(when (!= method "GET")
|
||||
(return (error 405 "method not allowed")))
|
||||
|
||||
(defclass method-map []
|
||||
(meth __init__ [] (setv @methods {}))
|
||||
(setv [headers data] (send-raw-file (+ "./www/site" path)))
|
||||
|
||||
(meth __getattr__ [attr]
|
||||
(if (in attr @methods)
|
||||
(get @methods attr)
|
||||
(fn [value] (setv (get @methods attr) value))))
|
||||
|
||||
(meth __getitem__ [item]
|
||||
(setv item (.get @methods item None))
|
||||
(if item
|
||||
item
|
||||
(fn [#* _] (error 405 "method not allowed")))))
|
||||
(return (dict
|
||||
:code 200
|
||||
:headers headers
|
||||
:body data)))
|
||||
|
||||
(defclass router []
|
||||
(meth __init__ []
|
||||
(setv @route-map {})
|
||||
(setv @route-cache {}))
|
||||
|
||||
(meth add-route [route]
|
||||
(when (not-in route @route-map)
|
||||
(setv (get @route-map route) (method-map)))
|
||||
|
||||
(return (get @route-map route)))
|
||||
|
||||
(meth get-route-by-path [path]
|
||||
(when (.startswith path "/html/")
|
||||
(when (!= method "GET")
|
||||
(return (error 405 "method not allowed")))
|
||||
|
||||
(when (file? (+ "./www/site/html" path))
|
||||
(unless (= path "/html/view-thought.html")
|
||||
(return (dict
|
||||
:code 200
|
||||
:body (parse-html-file f"./www/site/html{path}"))))
|
||||
|
||||
(return (dict
|
||||
:code 200
|
||||
:body (parse-html-file "./www/site/html/html/view-thought.html" #** (dfor [k v] (.items (. request (get "route") (get "parameters"))) k (quote-plus v)))))))
|
||||
|
||||
(when (= path "/style.css")
|
||||
(when (!= method "GET")
|
||||
(return (error 405 "method not allowed")))
|
||||
|
||||
(setv [headers data] (send-raw-file "./www/site/assets/style.css"))
|
||||
(return (dict
|
||||
:code 200
|
||||
:headers headers
|
||||
:body data)))
|
||||
|
||||
(when (= path "/")
|
||||
(when (!= method "GET")
|
||||
(return (error 405 "method not allowed")))
|
||||
|
||||
(return (dict
|
||||
:code 200
|
||||
:body (parse-html-file "./www/site/html/home.html" :route "/"))))
|
||||
|
||||
(when (= path "/test.html")
|
||||
(when (!= method "GET")
|
||||
(return (error 405 "method not allowed")))
|
||||
|
||||
(return (dict
|
||||
:code 200
|
||||
:body (parse-html-file "./www/site/html/test.html" :route "/test.html"))))
|
||||
|
||||
|
||||
(when (= path "/comment")
|
||||
(cond
|
||||
(in path @route-map) (get @route-map path)
|
||||
(in (when (.endswith path "/") (get path (slice 0 -1))) @route-map) (get @route-map (get path (slice 0 -1)))
|
||||
True (do
|
||||
(while (> (.count path "/") 1)
|
||||
(setv path (.join "/" (get (.split path "/") (slice 0 -1))))
|
||||
(when (in (+ path "/*") @route-map)
|
||||
(return (get @route-map (+ path "/*")))))
|
||||
(= method "POST") (do
|
||||
(setv now (datetime.now))
|
||||
|
||||
(setv url-comment-dir (quote-plus (. request (get "route") (get "parameters") (get "route")) :safe ""))
|
||||
|
||||
(when (not (dir? "./www/data"))
|
||||
(mkdir "./www/data"))
|
||||
|
||||
(when (not (dir? f"./www/data/comments"))
|
||||
(mkdir "./www/data/comments"))
|
||||
|
||||
(when (not (dir? f"./www/data/comments/{url-comment-dir}"))
|
||||
(mkdir f"./www/data/comments/{url-comment-dir}"))
|
||||
|
||||
(setv+ {{comment "comment" name "name" site "site"} "body"} request)
|
||||
|
||||
(when (.startswith site "//")
|
||||
(setv site (.replace site "//" "")))
|
||||
|
||||
(setv [protocol domain path #* _] (urlparse
|
||||
(if (in "://" site)
|
||||
site
|
||||
(+ "http://" site))))
|
||||
|
||||
(setv site-valid? (= (domain? domain) True))
|
||||
|
||||
(when (not protocol)
|
||||
(setv protocol "//"))
|
||||
|
||||
(print protocol domain path site-valid?)
|
||||
|
||||
|
||||
(with [f (open f"./www/data/comments/{url-comment-dir}/{(.strftime now "%Y-%m-%d_%H:%M:%S_%f")}" "w")]
|
||||
(.write f f"<div class=\"comment\"><span style=\"font-weight: bold\">{(if (and site site-valid?) f"<a href=\"{(clean protocol)}://{(clean domain)}{(clean path)}\">" "")}{(clean name)}{(if (and site site-valid?) "</a>" "")}</span> at {(.strftime now "%Y-%m-%d %H:%M:%S")}:<br><pre>{(clean comment)}</pre></div>"))
|
||||
|
||||
(always (fn [#* _] (error 404 "not-found)")))))))
|
||||
(return (dict
|
||||
:code 301
|
||||
:headers {"Location" (. request (get "headers") (get "Referer"))})))
|
||||
|
||||
(setv routes (router))
|
||||
(= method "GET") (error 405 "method not allowed")))
|
||||
|
||||
(defmacro route [route method]
|
||||
`(. (routes.add-route ~route) ~(hy.models.Symbol method)))
|
||||
|
||||
(defreader route-args
|
||||
(.slurp-space &reader)
|
||||
(if (= (.peekc &reader) "[")
|
||||
`[method path request ~@(.parse-one-form &reader)]
|
||||
'[method path request]))
|
||||
|
||||
(defreader await
|
||||
(.slurp-space &reader)
|
||||
`(await ~(.parse-one-form &reader)))
|
||||
|
||||
(defn if-file-exists [* base-path otherwise]
|
||||
(fn [f]
|
||||
(fn [method path request]
|
||||
(if (file? f"{base-path}{path}")
|
||||
(f method path request)
|
||||
(otherwise method path request)))))
|
||||
|
||||
(defn forward-params [#* params]
|
||||
(fn [f]
|
||||
(fn [method path request]
|
||||
(f method path request #** (dict (lfor param params
|
||||
:if (setx value (. request (get "route") (get "parameters") (get param None)))
|
||||
#(param value)))))))
|
||||
|
||||
(defn require-params [#* params [otherwise (fn [#* _] (error 409 "missing required parameters"))]]
|
||||
(fn [f]
|
||||
(fn [method post request #** kwargs]
|
||||
(if (all (lfor param params (in param (. request (get "route") (get "parameters") (keys)))))
|
||||
(f method post request #** kwargs)
|
||||
(otherwise method post request #** kwargs)))))
|
||||
|
||||
(defn 303-if-not-arpa [[unless (fn [#* _] False)]]
|
||||
(fn [f]
|
||||
(fn [method path request]
|
||||
(if (or (. request (get "headers") (get "Host") (endswith "arpa")) (unless request))
|
||||
(f method path request)
|
||||
(dict
|
||||
:code 303
|
||||
:headers {"Location" f"http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa{(. request (get "route") (get "unparsed_route"))}"})))))
|
||||
|
||||
(defn :async shtml-file-response [file [code 200] [no-exec False] [template-params {}]]
|
||||
(dict
|
||||
:code code
|
||||
:headers {"Connection" "keep-alive" "Keep-Alive" "timeout=5 max=200"}
|
||||
:body #await (parse-html-file f"./www/site/html/{file}" :no-exec no-exec #** template-params)))
|
||||
|
||||
(defn :async raw-file-response [file [code 200]]
|
||||
(dict :code code #** (dict (zip ["headers" "body"] #await (send-raw-file f"./www/site/{file}")))))
|
||||
|
||||
(defn+ :async match-request [{method "method" {path "path"} "route" :as request}]
|
||||
#await ((get (routes.get-route-by-path path) method) method path request))
|
||||
|
||||
(defn [(route "/" "GET")] /home #route-args (shtml-file-response "home.html"))
|
||||
(defn [(route "/html/*" GET) (if-file-exists :base-path "./www/site/html" :otherwise (fn :async [#* _] #await (error 404 "not found")))] /html/* #route-args (shtml-file-response path))
|
||||
(defn [lru-cache (route "/assets/*" "GET") (if-file-exists :base-path "./www/site/" :otherwise (error 404 "not found"))] /assets/* #route-args (raw-file-response path))
|
||||
(defn [(route "/html/view-thought.html" "GET") (forward-params "thought" "filter-tag")] /html/view-thought #route-args [#** template-args] (shtml-file-response "/html/view-thought.html" :template-params template-args))
|
||||
(defn [(route "/comment" "POST")] /comments #route-args (create-comment request))
|
||||
(defn [lru-cache (route "/robots.txt" "GET") ] /robots #route-args (dict :code 200 :headers {"Content-Type" "text/plain"} :body "User-agent *\nDisallow: /\n"))
|
||||
|
||||
;; *.arpa web n-gon
|
||||
(setv members (arpa-n-gon.get-members))
|
||||
|
||||
(defn [(route "/arpa-n-gon" GET) (303-if-not-arpa)] /arpa-n-gon #route-args (shtml-file-response "arpa-n-gon.html" :template-params (dict :n_gon (arpa-n-gon.n-gon-name (len members)) :n_gon_inc (arpa-n-gon.n-gon-name (+ (len members) 1)) :n (len members))))
|
||||
|
||||
(defn [(route "/arpa-n-gon/nav" GET) (303-if-not-arpa :unless (fn [request] (in "from-iframe" (. request (get "route") (get "parameters") (keys))))) (forward-params "current" "style") (require-params "current") ] /arpa-n-gon/nav #route-args [current [style None]]
|
||||
(shtml-file-response "arpa-n-gon-nav.html" :no-exec True :template-params (dict
|
||||
:style style
|
||||
:next (+ "http://" (get (arpa-n-gon.next-member members current) "arpa-domain"))
|
||||
:prev (+ "http://" (get (arpa-n-gon.prev-member members current) "arpa-domain"))
|
||||
:n_gon (arpa-n-gon.n-gon-name (len members)))))
|
||||
|
||||
(defn [(route "/arpa-n-gon/next" GET) (303-if-not-arpa) (forward-params "current" )(require-params "current")] /arpa-n-gon/next #route-args [current] (dict
|
||||
:code 303
|
||||
:headers {"Location" (+ "http://" (get (arpa-n-gon.next-member members current) "arpa-domain"))}))
|
||||
|
||||
(defn [(route "/arpa-n-gon/prev" GET) (303-if-not-arpa) (forward-params "current") (require-params "current")] /arpa-n-gon/next #route-args [current] (dict
|
||||
:code 303
|
||||
:headers {"Location" (+ "http://" (get (arpa-n-gon.prev-member members current) "arpa-domain"))}))
|
||||
(return (dict
|
||||
:code 404
|
||||
:body (parse-html-file "./www/site/html/error.html" :code 404 :message f"{path} not found"))))
|
||||
|
@ -2,27 +2,17 @@
|
||||
(require hyrule.control [branch])
|
||||
(require hyrule.argmove [doto])
|
||||
(require hyrule.collections :readers [s])
|
||||
(require hyrule.oop [meth])
|
||||
(import urllib.parse [urlsplit unquote-plus parse-qs])
|
||||
|
||||
(defclass hashable-dict [dict]
|
||||
;; this is ok because requests are never mutated after being built.
|
||||
(meth __key []
|
||||
(tuple (sorted (@items))))
|
||||
|
||||
(meth __hash__ []
|
||||
(hash (@__key))))
|
||||
|
||||
(defn parse-url-encoded [query]
|
||||
(hashable-dict
|
||||
(dfor pair (.items (parse-qs (unquote-plus query) :keep-blank-values True))
|
||||
(get pair 0)
|
||||
(cond
|
||||
(get pair 1 0) (get pair 1 0)
|
||||
True ""))))
|
||||
(dfor pair (.items (parse-qs (unquote-plus query) :keep-blank-values True))
|
||||
(get pair 0)
|
||||
(cond
|
||||
(get pair 1 0) (get pair 1 0)
|
||||
True "")))
|
||||
|
||||
(defn parse-data [data]
|
||||
(setv request (hashable-dict))
|
||||
(setv request {})
|
||||
|
||||
(setv [head #* body] (.split data b"\r\n\r\n"))
|
||||
(setv [request-line #* headers] (.split head b"\r\n"))
|
||||
@ -32,7 +22,7 @@
|
||||
(assoc "method" http-method)
|
||||
(assoc "route" (let
|
||||
[[_ _ path query _] (urlsplit route)]
|
||||
(hashable-dict
|
||||
(dict
|
||||
:path (.join "/"
|
||||
(do
|
||||
(setv segments [])
|
||||
@ -43,11 +33,10 @@
|
||||
|
||||
segments))
|
||||
|
||||
:parameters (parse-url-encoded query)
|
||||
:unparsed-route route)))
|
||||
:parameters (parse-url-encoded query))))
|
||||
|
||||
(assoc "version" http-version)
|
||||
(assoc "headers" (hashable-dict
|
||||
(assoc "headers" (dict
|
||||
(dfor header
|
||||
(map (fn [x]
|
||||
(.split (.decode x "utf-8") ": ")) headers)
|
||||
@ -65,4 +54,4 @@
|
||||
;; (print "boundary" boundary)
|
||||
;; (print (.split (.join b"\n" body) (.encode boundary "utf-8"))))))
|
||||
else (raise (NotImplementedError f"{(get (.split (. request (get "headers") (get "Content-Type")) ";") 0)} parsing is TODO"))))))
|
||||
|
||||
|
||||
|
@ -7,23 +7,20 @@
|
||||
(.encode (+ (.join "\r\n" (lfor [k v] (.items headers) f"{k}: {v}")) "\r\n\r\n") "utf-8"))
|
||||
|
||||
(defn send-body [body]
|
||||
body)
|
||||
(cond
|
||||
(isinstance body bytes) body
|
||||
(isinstance body str) (.encode body "utf-8")))
|
||||
|
||||
(defn send [[code 200] [headers None] [body ""]]
|
||||
(when (is headers None)
|
||||
(setv headers {}))
|
||||
|
||||
(assoc headers "WHAT...-your-trans-gener..." "that is so cool...")
|
||||
(assoc headers "Connection" "keep-alive")
|
||||
|
||||
(when (not-in "Content-Type" headers)
|
||||
(assoc headers "Content-Type" "text/html"))
|
||||
|
||||
(setv body (cond
|
||||
(isinstance body bytes) body
|
||||
(isinstance body str) (.encode body "utf-8")))
|
||||
|
||||
(+
|
||||
(send-code code)
|
||||
(send-headers (dict #** headers #** {"Content-Length" (len body)}))
|
||||
(send-headers headers)
|
||||
(send-body body)))
|
||||
|
79
srv/main.hy
79
srv/main.hy
@ -4,38 +4,8 @@
|
||||
(import traceback [format-exc])
|
||||
(import http-utils :as http)
|
||||
(import content.router [match-request])
|
||||
(import asyncio)
|
||||
(import concurrent.futures [ThreadPoolExecutor])
|
||||
|
||||
(require hyrule.control [defmain])
|
||||
(require hyrule.oop [meth])
|
||||
|
||||
(setv data-handler-threadpool (ThreadPoolExecutor))
|
||||
|
||||
(defclass http-server-protocol [asyncio.Protocol]
|
||||
|
||||
(meth connection-made [@transport])
|
||||
|
||||
(meth data-received [data]
|
||||
(setv loop (asyncio.get-running-loop))
|
||||
(loop.run-in-executor data-handler-threadpool (fn []
|
||||
(setv thread-loop (asyncio.new-event-loop))
|
||||
(asyncio.set-event-loop thread-loop)
|
||||
|
||||
(setv parsed-request (http.request.parse-data data))
|
||||
(.debug log parsed-request)
|
||||
(.info log (+ (str (cond
|
||||
(in "X-Real-IP" (. parsed-request (get "headers"))) (. parsed-request (get "headers") (get "X-Real-IP"))
|
||||
True (get (.get-extra-info @transport "peername") 1))) f": {(. parsed-request (get "method"))} {(. parsed-request (get "route") (get "path"))}"))
|
||||
|
||||
(setv response-task (asyncio.ensure-future (match-request parsed-request)))
|
||||
(. response-task (add-done-callback (fn [future]
|
||||
(.write @transport (http.response.send #** (future.result)))
|
||||
|
||||
(when (= (. parsed-request (get "headers") (get "Connection")) "close")
|
||||
(@transport.close)))))
|
||||
|
||||
(thread-loop.run-until-complete response-task)))))
|
||||
|
||||
(try
|
||||
(import srv-config [ADDRESS PORT])
|
||||
@ -43,9 +13,46 @@
|
||||
(setv [ADDRESS PORT] ["127.0.0.1" 5000])))
|
||||
|
||||
(defmain []
|
||||
(asyncio.run ((fn :async []
|
||||
(setv loop (asyncio.get-running-loop))
|
||||
|
||||
(with [:async server (await (loop.create-server
|
||||
http-server-protocol ADDRESS PORT))]
|
||||
(await (server.serve-forever)))))))
|
||||
(let [socket (socket AF_INET SOCK_STREAM)]
|
||||
(try
|
||||
(.setsockopt socket SOL_SOCKET SO_REUSEADDR 1)
|
||||
(.bind socket #(ADDRESS PORT))
|
||||
(.listen socket 10)
|
||||
(.debug log "socket bound")
|
||||
|
||||
(while True
|
||||
(try
|
||||
(.start
|
||||
(Thread
|
||||
:target (fn [client-socket address]
|
||||
(try
|
||||
(setv request-data (bytes))
|
||||
(while (setx data (.recv client-socket 1024))
|
||||
(+= request-data data)
|
||||
(when (< (len data) 1024)
|
||||
(break)))
|
||||
|
||||
(setv parsed-request (http.request.parse-data request-data))
|
||||
(.debug log parsed-request)
|
||||
(.info log (+ (str (cond
|
||||
(in "X-Real-IP" (. parsed-request (get "headers"))) (. parsed-request (get "headers") (get "X-Real-IP"))
|
||||
True (get address 0))) f": {(. parsed-request (get "method"))} {(. parsed-request (get "route") (get "path"))}"))
|
||||
|
||||
(setv response (match-request parsed-request))
|
||||
(.sendall client-socket (http.response.send #** response))
|
||||
|
||||
(except [e Exception]
|
||||
(.warn log (format-exc))
|
||||
(.close client-socket))))
|
||||
|
||||
:args #(#* (socket.accept))))
|
||||
|
||||
(except [e Exception]
|
||||
(.warn log (format-exc)))))
|
||||
|
||||
(except [e Exception]
|
||||
(.critical log (format-exc)))
|
||||
|
||||
(finally
|
||||
(.close socket)
|
||||
(.info log "server shut down")))))
|
||||
|
@ -1,11 +1,10 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
mkdir data
|
||||
(cd src; hy build.hy)
|
||||
rm -rf site/html
|
||||
mkdir -p site/{html,assets,scripts}
|
||||
cp -r src/output/* site/html
|
||||
echo 'html generated'
|
||||
(cd ./src/scripts/builtins; ./build.sh)
|
||||
cp -r src/scripts site/
|
||||
echo 'scripts copied'
|
||||
cp -r src/assets site/
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB |
@ -1,43 +0,0 @@
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-weight: italic;
|
||||
}
|
||||
|
||||
.dimgray { color: #525252; }
|
||||
.red { color: #ee5396; }
|
||||
.blue { color: #42be65; }
|
||||
.yellow { color: #ff7eb6; }
|
||||
.green { color: #33b1ff; }
|
||||
.purple { color: #be95ff; }
|
||||
.cyan { color: #3ddbd9; }
|
||||
.white { color: #08bdba; }
|
||||
|
||||
.highlighted .dimgray { color: #525252; }
|
||||
.highlighted .red { color: #98a9ff; }
|
||||
.highlighted .blue { color: #262626; }
|
||||
.highlighted .yellow { color: #393939; }
|
||||
.highlighted .green { color: #dde1e6; }
|
||||
.highlighted .purple { color: #ffffff; }
|
||||
.highlighted .cyan { color: #82cfff; }
|
||||
.highlighted .white { color: #f2f4f8; }
|
||||
|
||||
.bg-dimgray { color: #525252; }
|
||||
.bg-red { color: #ee5396; }
|
||||
.bg-blue { color: #42be65; }
|
||||
.bg-yellow { color: #ff7eb6; }
|
||||
.bg-green { color: #33b1ff; }
|
||||
.bg-purple { color: #be95ff; }
|
||||
.bg-cyan { color: #3ddbd9; }
|
||||
.bg-white { color: #08bdba; }
|
||||
|
||||
.highlighted .bg-dimgray { color: #525252; }
|
||||
.highlighted .bg-red { color: #98a9ff; }
|
||||
.highlighted .bg-blue { color: #262626; }
|
||||
.highlighted .bg-yellow { color: #393939; }
|
||||
.highlighted .bg-green { color: #dde1e6; }
|
||||
.highlighted .bg-purple { color: #ffffff; }
|
||||
.highlighted .bg-cyan { color: #82cfff; }
|
||||
.highlighted .bg-white { color: #f2f4f8; }
|
@ -1,25 +0,0 @@
|
||||
:root {
|
||||
--link: #33b1ff;
|
||||
--visited-link: #be95ff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
font-size: 13px;
|
||||
font-family: 'Liberation Mono', monospace;
|
||||
|
||||
> a {
|
||||
text-underline-offset: 2px;
|
||||
color: var(--link);
|
||||
|
||||
&:visited {
|
||||
color: var(--visited-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -47,20 +47,6 @@
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Liberation Mono', monospace !important;
|
||||
}
|
||||
|
||||
code > * {
|
||||
font-family: monospace !important;
|
||||
> * {
|
||||
font-family: monospace !important;
|
||||
> * {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
@ -100,11 +86,9 @@ body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr 130px;
|
||||
grid-template-rows: 1fr 160px;
|
||||
|
||||
> header {
|
||||
grid-column: 1;
|
||||
@ -173,7 +157,7 @@ body {
|
||||
}
|
||||
|
||||
> footer {
|
||||
height: fit-content;
|
||||
height: 160px;
|
||||
grid-column: 1 / 3;
|
||||
background-color: var(--alt-bg);
|
||||
border: 3px ridge gray;
|
||||
@ -181,27 +165,24 @@ body {
|
||||
padding: 5px;
|
||||
margin-bottom: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 88px;
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-template-columns: 20fr 1fr; /* icky hack */
|
||||
grid-template-rows: min-content;
|
||||
|
||||
> div, > div > div {
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
> footnote > * { margin-top: 2px; }
|
||||
> footnote > iframe {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
float: right;
|
||||
width: 88px;
|
||||
height: 31px;
|
||||
}
|
||||
> footnote > * { margin-top: 2px; }
|
||||
|
||||
a {
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
float: right;
|
||||
width: 88px;
|
||||
height: 31px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -251,15 +232,15 @@ section.comments {
|
||||
flex-direction: column;
|
||||
gap: 2ch;
|
||||
|
||||
div > form {
|
||||
> form {
|
||||
display: grid;
|
||||
grid-template-rows: 12ch 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
|
||||
grid-template-columns: 150px 150px 150px;
|
||||
|
||||
> textarea {
|
||||
resize: none;
|
||||
grid-row: 1;
|
||||
grid-column: 1 / 5;
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
|
||||
> #name {
|
||||
@ -274,10 +255,6 @@ section.comments {
|
||||
grid-row: 2
|
||||
}
|
||||
|
||||
> #commit {
|
||||
grid-row: 2
|
||||
}
|
||||
|
||||
> * {
|
||||
border-radius: 0;
|
||||
background: var(--alt-bg);
|
||||
@ -380,10 +357,6 @@ div.thought-list{
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-rows: 1fr min(min-content, 1fr);
|
||||
|
||||
> div.description {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
> * {
|
||||
white-space: pre-wrap;
|
||||
@ -396,32 +369,3 @@ div.thought-list{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
:root {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
overflow: unset;
|
||||
|
||||
> header {
|
||||
border-bottom: 3px ridge gray;
|
||||
> nav > ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(40%, 1fr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arpa-n-gon-list {
|
||||
> li {
|
||||
background: var(--alt-bg);
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -25,38 +25,22 @@
|
||||
(getattr (stat file) "st_ctime"))
|
||||
|
||||
(defn last-changed-prev-compile [file]
|
||||
(if (file? (-> file (.replace ".hy" ".html") (.replace "./pages/" "../site/html/")))
|
||||
(getattr (stat (-> file (.replace ".hy" ".html") (.replace "./pages/" "../site/html/"))) "st_ctime")
|
||||
False))
|
||||
(getattr (stat (-> file (.replace ".hy" ".html") (.replace "./pages/" "../site/html/"))) "st_ctime"))
|
||||
|
||||
(defmain []
|
||||
(setv last-compile-time (last-changed "../site"))
|
||||
(setv data-dir-changed? (> (last-changed "data") last-compile-time))
|
||||
(setv assets-dir-changed? (> (last-changed "assets") last-compile-time))
|
||||
(setv last-commit-newer (let
|
||||
[last-commit (-> (check-output #[[git log -1 --format="%at"]] :shell True) (.decode) (.strip) (int))]
|
||||
(and (> last-commit last-compile-time) (> last-commit (last-changed "../../srv")))))
|
||||
(setv last-commit-newer (> (-> (check-output #[[git log -1 --format="%at"]] :shell True) (.decode) (.strip) (int)) last-compile-time))
|
||||
|
||||
(print f"last compiled: {last-compile-time :.0f}")
|
||||
|
||||
(when (not (dir? "output"))
|
||||
(mkdir "output"))
|
||||
|
||||
;; only recompile pages when:
|
||||
;; - the source page is newer than the output
|
||||
;; - the `src/data` directory is newer than the output
|
||||
;; - the latest commit is newer than the output
|
||||
;; - the page is view-thought.hy and there are thoughts being recompiled
|
||||
(setv pages-to-build (lfor path (glob "./pages/**/*" :recursive True)
|
||||
:if (not-in "__pycache__" path)
|
||||
:if (or (> (last-changed path) (last-changed-prev-compile path)) data-dir-changed? assets-dir-changed? last-commit-newer)
|
||||
path))
|
||||
(for [path (glob "./pages/**/*" :recursive True)]
|
||||
(when (in "__pycache__" path)
|
||||
(continue))
|
||||
|
||||
(when (any (map (fn [x] (in "/thoughts/" x)) pages-to-build))
|
||||
(when (all (map (fn [x] (not-in "./pages/html/view-thought.hy" x)) pages-to-build))
|
||||
(setv pages-to-build ["./pages/html/view-thought.hy" #* pages-to-build])))
|
||||
|
||||
(for [path pages-to-build]
|
||||
(cond
|
||||
(and (dir? path) (not (dir? (.replace path "pages" "output"))))
|
||||
(do
|
||||
@ -64,13 +48,21 @@
|
||||
(mkdir (.replace path "pages" "output")))
|
||||
|
||||
(file? path)
|
||||
(do
|
||||
(print f"building {path}")
|
||||
(setv page-name (.split (cut path 2 -3) "/"))
|
||||
(enshrine page-name)
|
||||
(with [target (open (+ "./output/" (.join "/" (cut page-name 1 None)) ".html") "w")]
|
||||
(.write target (form->html
|
||||
(hy-eval (hy.read (with [source (open path "r")]
|
||||
(.read source)))
|
||||
:locals local-scope
|
||||
:globals global-scope))))))))
|
||||
;; only recompile pages when:
|
||||
;; - the source page is newer than the output
|
||||
;; - the `src/data` directory is newer than the output
|
||||
;; - the latest commit is newer than the output
|
||||
(if (or (> (last-changed path) (last-changed-prev-compile path)) data-dir-changed? last-commit-newer)
|
||||
(do
|
||||
(print f"building {path}")
|
||||
(setv page-name (.split (cut path 2 -3) "/"))
|
||||
(enshrine page-name)
|
||||
(with [target (open (+ "./output/" (.join "/" (cut page-name 1 None)) ".html") "w")]
|
||||
(.write target (form->html
|
||||
(hy-eval (hy.read (with [source (open path "r")]
|
||||
(.read source)))
|
||||
:locals local-scope
|
||||
:globals global-scope)))))
|
||||
|
||||
(print f"{(.replace path #[[./pages/]] #[[../site/html/]])} more recent, skipping")))))
|
||||
|
||||
|
@ -36,8 +36,8 @@ specific:
|
||||
src: //stella.lifeless.space/res/buttons/entity/0x57e11a.png
|
||||
alt: a dark purple background with a blood-like splatter of red on the upper-left. on the left of the image is a hexagon, every other side has a second stroke inside it, and inside are crosshairs. to the right is individually boxed characters spelling "57e11a", above the text is that in dollcode (▌▖▘▖▖▖▖▌▌▖▘▘▖▘), below the text is that in dollnary (╽╽╽┃┃╿│╽│╽╿╿)
|
||||
|
||||
- host: //www.5snb.club/
|
||||
src: /assets/88x31/522@5snb.club.png
|
||||
- host: //5snb.club/
|
||||
src: //5snb.club/img/buttons/522@5snb.club.png
|
||||
alt: 522@5snb.club
|
||||
|
||||
- host: //girlthi.ng/~thermia/
|
||||
@ -56,35 +56,18 @@ specific:
|
||||
src: //fungal.locahlo.st/medias/88x31/fungal.locahlo.st.png
|
||||
alt: fungal's 88x31 stamp
|
||||
|
||||
- host: https://liv.town/
|
||||
src: https://liv.town/assets/img/livtown-button.gif"
|
||||
- host: //liv.town/
|
||||
src: //liv.town/assets/img/livtown-button.gif"
|
||||
alt: liv.town's animated 88x31 button
|
||||
|
||||
- host: //tasiaiso.vulpecula.zone/
|
||||
src: //tasiaiso.vulpecula.zone/images/badges/tasiaiso.png
|
||||
alt: tasia's 88x31
|
||||
|
||||
# does not 301 to https if requested with http
|
||||
- host: //www.31a05b.net/
|
||||
src: https://www.31a05b.net/a/8831/31a05b.png
|
||||
alt: 31A05B9C's random site
|
||||
|
||||
- host: //bunbun.dev/
|
||||
src: //bunbun.dev/assets/88x31s/bunbun.dev.gif
|
||||
alt: bunbun.dev
|
||||
|
||||
- host: //chloes.website/
|
||||
src: //chloes.website/button.png
|
||||
alt: A pink-purple-blue gradient with the text ChloeCat written in a fancy script font
|
||||
|
||||
- host: https://nyatalie.fyi/
|
||||
src: https://nyatalie.fyi/nat88x31.png
|
||||
alt: Nat's 88x31
|
||||
|
||||
- host: //wolfyja.de
|
||||
src: //wolfyja.de/badges/awoo-now.png
|
||||
alt: awooo now
|
||||
|
||||
general:
|
||||
- src: assets/88x31/e2vial-88x31.gif
|
||||
alt: an estrogen vial spinning next to the text "powered by estrogen"
|
||||
|
@ -1,3 +0,0 @@
|
||||
<a href="http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa/arpa-n-gon/prev?current={domain}">prev</a>
|
||||
<a href="http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa/arpa-n-gon/">*.arpa web n-gon</a>
|
||||
<a href="http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa/arpa-n-gon/next?current={domain}">next</a>
|
@ -1 +0,0 @@
|
||||
<iframe src="http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa/arpa-n-gon/nav?current={url}&style={style-url}"></iframe>
|
@ -1,9 +0,0 @@
|
||||
members:
|
||||
- name: natalie
|
||||
arpa-domain: natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa
|
||||
description: natalieee.net arpa domain
|
||||
|
||||
- name: abbie
|
||||
arpa-domain: 5.d.1.3.2.e.f.1.5.0.7.4.0.1.0.0.2.ip6.arpa
|
||||
description:
|
||||
|
@ -1,8 +0,0 @@
|
||||
(defmacro inherit [#* names]
|
||||
(lfor name names
|
||||
`(setx ~name (get local-scope (str (get '(~name) 0))))))
|
||||
|
||||
(defmacro enshrine [#* names]
|
||||
(lfor name names
|
||||
`(setv (get local-scope (str (get '(~name) 0))) ~name)))
|
||||
|
@ -1,12 +0,0 @@
|
||||
<pre><span class="bold ">Benchmark </span><span class="bold ">1</span>: curl https://natalieee.net/html/view-thought.html
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green "> 1.197 s</span> ± <span class="green "> 0.017 s</span> [User: <span class="blue ">0.021 s</span>, System: <span class="blue ">0.013 s</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan "> 1.163 s</span> … <span class="purple "> 1.319 s</span> 100 runs
|
||||
|
||||
<span class="bold ">Benchmark </span><span class="bold ">2</span>: curl http://test-of.v2.natalieee.net/html/view-thought.html
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green ">176.2 ms</span> ± <span class="green "> 5.1 ms</span> [User: <span class="blue ">20.9 ms</span>, System: <span class="blue ">12.2 ms</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan ">162.2 ms</span> … <span class="purple ">210.5 ms</span> 100 runs
|
||||
|
||||
<span class="bold ">Summary</span>
|
||||
<span class="cyan ">curl http://test-of.v2.natalieee.net/html/view-thought.html</span> ran
|
||||
<span class="bold green "> 6.79</span> ± <span class="green ">0.22</span> times faster than <span class="purple ">curl https://natalieee.net/html/view-thought.html</span>
|
||||
</pre>
|
@ -1,12 +0,0 @@
|
||||
<pre><span class="bold ">Benchmark </span><span class="bold ">1</span>: curl https://natalieee.net/html/view-thought.html?filter-category=python
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green "> 1.083 s</span> ± <span class="green "> 0.031 s</span> [User: <span class="blue ">0.022 s</span>, System: <span class="blue ">0.012 s</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan "> 1.053 s</span> … <span class="purple "> 1.305 s</span> 100 runs
|
||||
|
||||
<span class="bold ">Benchmark </span><span class="bold ">2</span>: curl http://test-of.v2.natalieee.net/html/view-thought.html?filter-tag=python
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green ">176.5 ms</span> ± <span class="green "> 5.6 ms</span> [User: <span class="blue ">20.7 ms</span>, System: <span class="blue ">12.1 ms</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan ">160.8 ms</span> … <span class="purple ">206.0 ms</span> 100 runs
|
||||
|
||||
<span class="bold ">Summary</span>
|
||||
<span class="cyan ">curl http://test-of.v2.natalieee.net/html/view-thought.html?filter-tag=python</span> ran
|
||||
<span class="bold green "> 6.14</span> ± <span class="green ">0.26</span> times faster than <span class="purple ">curl https://natalieee.net/html/view-thought.html?filter-category=python</span>
|
||||
</pre>
|
@ -1,12 +0,0 @@
|
||||
<pre><span class="bold ">Benchmark </span><span class="bold ">1</span>: curl https://natalieee.net/html/view-thought.html?thought=dollcode
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green "> 1.190 s</span> ± <span class="green "> 0.036 s</span> [User: <span class="blue ">0.021 s</span>, System: <span class="blue ">0.013 s</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan "> 1.141 s</span> … <span class="purple "> 1.412 s</span> 100 runs
|
||||
|
||||
<span class="bold ">Benchmark </span><span class="bold ">2</span>: curl http://test-of.v2.natalieee.net/html/view-thought.html?thought=dollcode
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green ">504.0 ms</span> ± <span class="green "> 12.6 ms</span> [User: <span class="blue ">20.6 ms</span>, System: <span class="blue ">13.3 ms</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan ">480.3 ms</span> … <span class="purple ">554.3 ms</span> 100 runs
|
||||
|
||||
<span class="bold ">Summary</span>
|
||||
<span class="cyan ">curl http://test-of.v2.natalieee.net/html/view-thought.html?thought=dollcode</span> ran
|
||||
<span class="bold green "> 2.36</span> ± <span class="green ">0.09</span> times faster than <span class="purple ">curl https://natalieee.net/html/view-thought.html?thought=dollcode</span>
|
||||
</pre>
|
@ -1,4 +0,0 @@
|
||||
#include "bash.h"
|
||||
#include "foo.c"
|
||||
|
||||
PY_FUNC(foo, py_test, _foo);
|
@ -1,62 +0,0 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include <bash/builtins.h>
|
||||
#include <bash/builtins/bashgetopt.h>
|
||||
#include <bash/shell.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
extern char **make_builtin_argv(WORD_LIST*, int*);
|
||||
|
||||
#define DEFINE_BUILTIN(name) \
|
||||
struct builtin name##_struct = { \
|
||||
#name, \
|
||||
name##_builtin_func, \
|
||||
BUILTIN_ENABLED, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
0 \
|
||||
}
|
||||
|
||||
#define WRAP_FUNC_WITH_BUILTIN(name)\
|
||||
int name##_builtin_func(WORD_LIST *list) { \
|
||||
int argc, ret; \
|
||||
char **argv; \
|
||||
argv = make_builtin_argv(list, &argc); \
|
||||
ret = name(argc, argv); \
|
||||
free(argv); \
|
||||
return ret;\
|
||||
}
|
||||
|
||||
#define PY_FUNC(bash_name, modname, function) \
|
||||
int bash_name(int argc, char **argv) { \
|
||||
if (!Py_IsInitialized()) { \
|
||||
dlopen("libpython3.13.so", RTLD_NOW | RTLD_GLOBAL);\
|
||||
PyImport_AppendInittab(#modname, PyInit_##modname); \
|
||||
Py_Initialize(); \
|
||||
} \
|
||||
int ret = 1; \
|
||||
PyObject *mod = NULL, *func = NULL, *result; \
|
||||
\
|
||||
mod = PyImport_ImportModule(#modname); \
|
||||
func = PyObject_GetAttrString(mod, #function); \
|
||||
\
|
||||
PyObject *py_argv = PyTuple_New(argc - 1); \
|
||||
\
|
||||
for (int i = 1; i < argc; i++) { \
|
||||
PyObject *arg_as_str = PyUnicode_FromString(argv[i]); \
|
||||
PyTuple_SetItem(py_argv, i-1, arg_as_str); \
|
||||
} \
|
||||
\
|
||||
result = PyObject_CallObject(func, py_argv); \
|
||||
if (!result) { PyErr_Print(); goto finally; } \
|
||||
ret = (int) PyFloat_AsDouble(result); \
|
||||
\
|
||||
finally: \
|
||||
Py_XDECREF(result); \
|
||||
Py_XDECREF(py_argv); \
|
||||
Py_XDECREF(func); \
|
||||
Py_XDECREF(mod); \
|
||||
return ret; \
|
||||
} \
|
||||
WRAP_FUNC_WITH_BUILTIN(bash_name); \
|
||||
DEFINE_BUILTIN(bash_name);
|
@ -1,4 +0,0 @@
|
||||
<pre><span class="bold ">Benchmark </span><span class="bold ">1</span>: curl http://test-of.v2.natalieee.net/html/view-thought.html?thought=dollcode
|
||||
Time (<span class="bold green ">mean</span> ± <span class="green ">σ</span>): <span class="bold green ">310.4 ms</span> ± <span class="green "> 7.3 ms</span> [User: <span class="blue ">22.5 ms</span>, System: <span class="blue ">10.8 ms</span>]
|
||||
Range (<span class="cyan ">min</span> … <span class="purple ">max</span>): <span class="cyan ">300.2 ms</span> … <span class="purple ">348.1 ms</span> 100 runs
|
||||
</pre>
|
@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
# maybe uses non-posix regex and thus may be non-portable. oops.
|
||||
# the bash regex implementation is actually platform dependent, lol.
|
||||
|
||||
shopt -s extglob
|
||||
|
||||
PY_VERSION=3.13
|
||||
PYFLAGS="$(/usr/bin/python${PY_VERSION}-config --cflags --ldflags)"
|
||||
BASH_INCLUDE_DIR="/usr/include/bash"
|
||||
|
||||
function translate_function_names() {
|
||||
# cython generates garbled names like __pyx_pf_7py_test_2foo for a python function named foo
|
||||
# this function generates a list of #defines that translate these names to what they are in python
|
||||
|
||||
file="${1}"
|
||||
|
||||
grep 'static PyObject \*__pyx_pf_' "${file}" | grep -v 'proto' | while read -r line; do
|
||||
desired_name=$"${line#*$"${file/.c/}"_}"
|
||||
desired_name="${desired_name/#+([0-9])/}"
|
||||
real_name="${line#*\*}"
|
||||
echo "#define ${desired_name%(*} ${real_name%(*}"
|
||||
done
|
||||
}
|
||||
|
||||
py2c() {
|
||||
rm "${1/.py/.c}"
|
||||
cython3 -3 "${1}" -o "${1/.py/.c}"
|
||||
translate_function_names $(basename ${1/.py/.c}) >> "${1/.py/.c}"
|
||||
}
|
||||
|
||||
compile() {
|
||||
gcc -I$BASH_INCLUDE_DIR{/include,/builtins,}\
|
||||
$PYFLAGS $1 -o ${1/.c/} -Wall\
|
||||
-lpython3.13 \
|
||||
-fPIC -shared -O3
|
||||
}
|
||||
|
||||
py2c foo.py
|
||||
compile example_builtin.c
|
@ -1,16 +0,0 @@
|
||||
def _math(*args):
|
||||
import numpy as np
|
||||
matrix_size, iterations, *_ = args
|
||||
matrix_size, iterations = int(matrix_size), int(iterations)
|
||||
|
||||
for _ in range(iterations):
|
||||
a = np.random.rand(matrix_size, matrix_size)
|
||||
b = np.random.rand(matrix_size, matrix_size)
|
||||
|
||||
result = np.dot(a, b)
|
||||
|
||||
print(result.shape)
|
||||
return 0
|
||||
|
||||
import sys
|
||||
_math(*sys.argv[1:])
|
@ -1,3 +0,0 @@
|
||||
def _foo(*_):
|
||||
print("foo")
|
||||
return 0
|
@ -1,16 +0,0 @@
|
||||
def _math(*args):
|
||||
import numpy as np
|
||||
matrix_size, iterations, *_ = args
|
||||
matrix_size, iterations = int(matrix_size), int(iterations)
|
||||
|
||||
for _ in range(iterations):
|
||||
a = np.random.rand(matrix_size, matrix_size)
|
||||
b = np.random.rand(matrix_size, matrix_size)
|
||||
|
||||
result = np.dot(a, b)
|
||||
|
||||
print(result)
|
||||
return 0
|
||||
|
||||
import sys
|
||||
print(sys.argv)
|
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
tmpfile=$(mktemp)
|
||||
|
||||
nvim --headless $1 "+set noswapfile | set nonu | set nornu | set conceallevel=0" "+%s/^$/ /g" "+Lazy! load nvim-treesitter | TSEnable highlight | TOhtml | w! ${tmpfile}" '+qa!' 2>/dev/null
|
||||
|
||||
cat "${tmpfile}" | pup --pre 'style, pre' | sed '/^$/d'
|
||||
|
@ -1,8 +0,0 @@
|
||||
'(html
|
||||
(head
|
||||
(link (:rel stylesheet :href "{style}" :type text/css)))
|
||||
(body
|
||||
(div
|
||||
(a (:href "{prev}" :target _parent) <-)
|
||||
(a (:href "http://natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa/arpa-n-gon" :target _parent) "*.arpa web {n_gon}")
|
||||
(a (:href "{next}" :target _parent) ->))))
|
@ -1,62 +0,0 @@
|
||||
(page
|
||||
:title ".arpa web {n_gon}"
|
||||
:description "the homepage for the .arpa web {n_gon}"
|
||||
`((section
|
||||
(h1 "the *.arpa web {n_gon}")
|
||||
(p "a web {n_gon} (read: webring with {n} members) for anyone with a domain under the arpa tld or any subdomain thereof."))
|
||||
|
||||
(section
|
||||
(h2 "members")
|
||||
(ol (:class "arpa-n-gon-list")
|
||||
~(do
|
||||
(import yaml [safe-load])
|
||||
(with [f (open "data/arpa-n-gon.yaml")]
|
||||
(setv members (get (safe-load f) "members")))
|
||||
|
||||
(hy.models.Expression (lfor member members
|
||||
`(li (div
|
||||
(div "member: " (a (:href ~(get member "arpa-domain") :title ~(get member "arpa-domain")) ~(get member "name")))
|
||||
(div "domain: " ~(get member "arpa-domain")
|
||||
(div "description: " ~(get member "description"))))))))))
|
||||
|
||||
(section
|
||||
(h2 "joining")
|
||||
(p "anyone meeting all of the following criteria is permitted to join this webring:")
|
||||
(ul
|
||||
(li "own a domain under the arpa tld")
|
||||
(li "have a site accessible under that domain")
|
||||
(li "on that site, have at least one instance of page elements complying with the section on acceptable notation of membership")
|
||||
(li "be deemed by natalie to be generally acceptable; this is a low bar, and functionally translates to "don't be an asshole""))
|
||||
|
||||
(p "should $READER align with the above conditions, and is interested in joining this web {n_gon} (thus making it a web {n_gon_inc}), $READER may send an e-mail containing their website's url to "
|
||||
(a (:href "mailto:{n_gon}@8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa") "{n_gon}@8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa")))
|
||||
|
||||
(section
|
||||
(h2 "acceptable notation of membership")
|
||||
(p "there are two main modes of noting one's affiliation with this web {n_gon}, what is appropriate for $READER's website will depend on their preferences regarding loading of external resources.")
|
||||
|
||||
(br)
|
||||
(h3 "1: iframe")
|
||||
(p "the first option is to include an iframe linking to the navigation for the webring. this option is funnier, because requesting that natalieee.net renders the iframe contents allows
|
||||
this server to return html containing the appropriate n-gon for the current member count. in order to conform with the css of some site, a style param can be passed in the iframe's src attribute,
|
||||
which will get loaded as a stylesheet.")
|
||||
|
||||
(figure
|
||||
(figcaption "iframe web {n_gon} html")
|
||||
(code ~(run #[[syntax-hl data/arpa-n-gon-linking-iframe.html]])))
|
||||
|
||||
(br)
|
||||
(h3 "2: anchors")
|
||||
(p "the second option is to include various anchor tags. this option is less funny, because instead of asking natalieee.net for the appropriate n-gon, it simply labels the web {n_gon} as a web n-gon.")
|
||||
|
||||
(figure
|
||||
(figcaption "anchor web {n_gon} html")
|
||||
(code ~(run #[[syntax-hl data/arpa-n-gon-linking-anchors.html]])))
|
||||
|
||||
(p "of the above methods, one must appear anywhere within a site a number of times greater than 0. a valid website can include one of these
|
||||
only once (on a webring page, should the site have a page dedicated to that), or on each page as with this site's footer.")
|
||||
|
||||
(br)
|
||||
(h3 "proper linking")
|
||||
(p "the `current` parameter in any given web {n_gon} related url should be set to the domain of one's site. in this one's case, this parameter gets set to natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa. this is
|
||||
the mechanism that allows the server to properly link to the next or previous site in the {n_gon}."))))
|
@ -9,17 +9,18 @@
|
||||
"including the static site generator and the http server. as such, there will be discontinuities with the prior iteration of the site. "
|
||||
"the historically minded are encouraged to look at the archive.org page for this website prior to early may 2025.")
|
||||
|
||||
(p "if $READER is confused, interested or any adjective, frankly about information found herein, they are encouraged to e-mail natalie. its contact information may be found on " ~(link "/html/about-natalie.html") "."))
|
||||
(p "if $READER is confused, interested or any adjective, frankly about information found herein, they are encouraged to e-mail natalie. its contact information may be found on " ~(link "/html/about-natalie.html") " or below."))
|
||||
|
||||
(section
|
||||
(h2 "site info" ~(run "make-footnote \"as of last build\""))
|
||||
(details
|
||||
(summary "git changelog")
|
||||
(pre ~(run #[[git log --pretty=format:'%ad %s' --date=short | sed 's/</\</g; s/>/\>/g']])))
|
||||
(pre ~(run "git log --pretty=format:'%ad %s' --date=short")))
|
||||
|
||||
(details
|
||||
(summary "file tree of the site on the server")
|
||||
(pre ~(run "tree . | sed 's/^\\./site/; s/─/-/g; s/├/|/g; s/└/\\\\/g; s/│/|/g'"))))
|
||||
(pre ~(run "tree . | sed 's/^\\./site/; s/─/-/g; s/├/|/g; s/└/\\\\/g; s/│/|/g'")))
|
||||
~(run "put-footnotes"))
|
||||
|
||||
(section
|
||||
(h2 "88x31s")
|
||||
@ -32,7 +33,7 @@
|
||||
(with [f (open "data/88x31s.yaml" "r")]
|
||||
(setv 88x31s (safe-load f)))
|
||||
|
||||
(hy.models.Expression [
|
||||
(.join "" (lfor form [
|
||||
`(p (:style "margin-bottom: 4px;") "entities or organizations:")
|
||||
#* (lfor button (get 88x31s "specific")
|
||||
(cond
|
||||
@ -46,10 +47,9 @@
|
||||
(or (in "src" button) (in "alt" button))
|
||||
`(img (:src ~(get button "src") :alt ~(get button "alt")))
|
||||
True ""))
|
||||
])))
|
||||
] (form->html form)))))
|
||||
|
||||
(p "the reader may request their button be displayed here by e-mailing their domain, button url, and alt-text for the button to the following address: "
|
||||
(p "the reader may request their button be displayed here by e-mailing their domain, button, and alt-text for the button to the following e-mail: "
|
||||
(a (:href "mailto:natalieee.net+88x31@8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa") "natalieee.net+88x31@8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa")))
|
||||
|
||||
~(run "put-footnotes")
|
||||
~(comments "/")))
|
||||
|
@ -116,7 +116,7 @@
|
||||
(h2 "contact")
|
||||
(p "modes of communication ordered by likelihood of it seeing the reader's message:"
|
||||
(ol
|
||||
(li "e-mail (note: it is equally likely to perceive messages to any of these addresses):" (ul
|
||||
(li "e-mail (note: it is equally likely to perceive messages to any of thtese addresses):" (ul
|
||||
(li "natalie at natalieee.net")
|
||||
(li "natalie-roentgen-connolly at mail.natalie-roentgen-connolly.website"
|
||||
(li "natalie at 8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa"
|
||||
|
@ -1,235 +0,0 @@
|
||||
(thought
|
||||
:title "natalieee.net version 2"
|
||||
:date "2025-05-14 22:45:34"
|
||||
:tags ["meta" "programming"]
|
||||
:description "yet another site redesign"
|
||||
:content `(
|
||||
(p
|
||||
"it has rewritten its website again. "
|
||||
#[[there was no particular reason for its rewriting of its website, it simply decided it was bored (read: needed a way to procrastinate calculus homework. again.]]
|
||||
~(run #[[make-footnote '100% of site redesigns have occured while taking a calculus class.']]) ") "
|
||||
#[[fortunately, it does think this new iteration of the website is better.]])
|
||||
|
||||
(br)
|
||||
(h2 "major changes")
|
||||
(hr)
|
||||
(ul
|
||||
(li "the site is one repository. this makes everything better.")
|
||||
(li "the static site generator and web server have been rewritten in hy"))
|
||||
|
||||
(p #[[
|
||||
both of the above have major benefits associated with them. firstly, the site is only one repository instead of the prior—and rather ridiculous—three.
|
||||
this makes managing things less horrible as now, when updating something on this site, it only has to push to one repository. one is, on average, less than a range of 1-3 repositories.
|
||||
additionally, this allows the changelog of the website to be autogenerated via git log. this is quite nice as it is better at being descriptive with commit messages than
|
||||
it is with changelog entries for some reason.]])
|
||||
|
||||
(p #[[
|
||||
secondly, rewriting the static site generator in hy has been quite helpful, as this one is much more familiar with hy than it was with common lisp.
|
||||
this site now being written in a lisp that runs on the python interpreter is also very nice, because it means this one can invoke arbitrary arcanities of python
|
||||
at the same time as it makes a website. it is very happy about this, because using python in obscure and unintended ways is very fun.]])
|
||||
|
||||
(p #[[
|
||||
because http servers are trivial, and because the prior http server was already written in python, there are no significant improvements over the old one.
|
||||
the http server's codebase is much simpler now, however, as this one removed features it deemed unecessary, and wrote it in a way that adheres more closely
|
||||
to functional programming ideals than the old server's codebase.]])
|
||||
|
||||
(br)
|
||||
(h2 "implications")
|
||||
(hr)
|
||||
(p #[[
|
||||
it is not going to bother porting many of the blog posts from the old version of its website. rewriting said posts takes a reasonable amount of time,
|
||||
as now they are written in lisp—similar to the rest of the site—instead of simply being fragments of html documents. it will port only the posts
|
||||
that it deems particularly interesting to the new site.]])
|
||||
|
||||
(br)
|
||||
(h2 "blog performance improvements")
|
||||
(hr)
|
||||
(p #[[
|
||||
despite this page (/html/view-thought.html) still being rendered on the server at request time via bash code (as with the old version of this website),
|
||||
it is now much faster. there are a variety of reasons for this: firstly, we now only execute 7 shell scripts to render this page, as opposed
|
||||
to the prior iteration's 14 shell scripts. secondly, useage of awk has been minimized. logic that prior to now was done via awk is now done
|
||||
in bash. this is notable because echo is significantly faster than awk. additionally, thoughts are now indexed via a "database"
|
||||
(text file that gets grepped through), which is substantially faster than listing the thoughts directory and parsing metadata by reading each thought.]])
|
||||
|
||||
(p #[[
|
||||
these changes result in a significant speed improvement for the new site:]] ~(run #[[make-footnote "benchmarks were run on-server to prevent inconsistencies in network request resolution times."]]))
|
||||
|
||||
(figure
|
||||
(figcaption "difference (no url params)")
|
||||
(code ~(run #[[cat data/thoughts/natalieee.net-v2/view-thought-diff]])))
|
||||
|
||||
(figure
|
||||
(figcaption "difference (rendering specific thought)")
|
||||
(code ~(run #[[cat data/thoughts/natalieee.net-v2/view-thought-diff-thought]])))
|
||||
|
||||
(figure
|
||||
(figcaption "difference (filtering for tags)")
|
||||
(code ~(run #[[cat data/thoughts/natalieee.net-v2/view-thought-diff-tag]])))
|
||||
|
||||
(br)
|
||||
(h2 "detailed explanation of performance improvements")
|
||||
(hr)
|
||||
(p #[[
|
||||
to understand the reasons behind these speed increases, we must examine the code for the view-thought page.]])
|
||||
|
||||
(figure
|
||||
(figcaption "www/src/pages/html/view-thought.hy")
|
||||
(code ~(run "syntax-hl pages/html/view-thought.hy | sed -e 's/\\[/\\[/g' -e 's/\\]/\\]/g'")))
|
||||
|
||||
(p #[[
|
||||
this code is quite complicated, and unfortunately it was written by this one, meaning it is probably cognitohazardous. sorry. it would like
|
||||
to note that it is not legally, ethically, or monetarily responsible for any health issues that may arrise from observation of this code.]])
|
||||
|
||||
(p "
|
||||
the reasons that the contents of this file must be enclosed in a do block are rather arcane." ~(run (+ "make-footnote \"because instead of importing pages with python's import machinery "
|
||||
"we simply read them and evaluate them with hy's internal tools, each page, combined with code generating output for that page functionally "
|
||||
"just gets appended to \\`build.hy\\`. thus, view-thought must define its functions inside of a do block, such that all of them are accessible "
|
||||
"to the external scope. it is necessary for them to be accessible to the external scope because thought posts rely on the \\`thought\\` "
|
||||
"function, which is defined in view-thought.\"")))
|
||||
|
||||
(br)
|
||||
(h3 "the `thought` function")
|
||||
(p "
|
||||
it makes the most sense to start with an explanation of `thought` function, as its properties form the basis upon which the functionality of other systems in this
|
||||
file are predicated. understanding the `thought` function requires that we first understand the relevant macros:")
|
||||
|
||||
(figure
|
||||
(figcaption "the relevant macros")
|
||||
(code ~(run #[[syntax-hl data/thoughts/natalieee.net-v2/macros.hy]])))
|
||||
|
||||
(p "
|
||||
the macro `enshrine` takes a list of variable names defined in the scope in which it is called, and saves it to a special predetermined scope that will be accessible
|
||||
to the hy code in each page via the `inherit` macro. the `inherit` macro simply retreives a list of variable names that are expected to be in the afforementioned special scope and, provided
|
||||
they are indeed defined there, defines them in the currenet scope. this is managed by passing the special scope as arguments in the call to `hy.compiler.hy-eval`,
|
||||
which is responsible for executing the code in each hy file within the `www/src/pages/` directory.")
|
||||
|
||||
(p "
|
||||
in `build.hy`, a list of all hy files in `www/src/pages` is enumerated such that the variable `page-name` contains an array of the relative path of each page, split by '/'.
|
||||
prior to evaluating each page, we call `enshrine` on `page-name`. normally, this is irrelevant, as the value is not inherited in the page being evaluated. with pages
|
||||
that call the `thought` function however, we explicitly `inherit` `page-name`, making it accessible within the scope. one should take note of the
|
||||
fact that the value of `page-name` becomes associated with `page-name`'s value in `build.hy` at the time `thought` is called, not at the time it is defined."
|
||||
~(run (+
|
||||
"make-footnote 'this makes `thought` behave as though it also takes an argument containing the path of the current file being parsed. "
|
||||
"because each page is evaluated in `build.hy` as though it is just a form, and not a function called with arguments, "
|
||||
"we must use this this hacky workaround.'")))
|
||||
|
||||
(p "
|
||||
within the `thought` function, we use the `page-name` variable to check if the thought being compiled is already in `www/data/thought-registry`, which stores the metadata
|
||||
associated with each post in a tsv with the following columns: tags, file-name of the post (determined via `(get page-name -1)`), title, date, and description."
|
||||
~(run #[[make-footnote "at some point, this should probably be changed to check that the thought's source file hasn't changed since \`thought-registry\` was last written."]])
|
||||
" if the thought's file name is not present within the `thought-registry` file, the `thought-registry` file is appended to with metadata regarding the thought in the appropriate format.
|
||||
said metadata (other than the file name) is defined in the arguments to `thought`")
|
||||
|
||||
(p "
|
||||
when we are done updating the `thought-registry` (should it need to be updated), we simply clear the footnote counter (it would be silly to have all posts share one set of footnotes),
|
||||
and return a quoted expression containing the content of the thought, an instruction to render the footnotes belonging to the thought, and the comments template with a path specific
|
||||
to the thought. this return value gets evaluated in `build.hy`, where it will output a fragment of an html document to be included within `view-thought.html` as necessary based on url params.")
|
||||
|
||||
(br)
|
||||
|
||||
(dl
|
||||
(dt "but natalie, shouldn't `thought` be defined in `www/src/templates/`?")
|
||||
(dd "yes. it did this instead because it was bored, though."))
|
||||
|
||||
(h3 "bash macros")
|
||||
|
||||
(p #[[
|
||||
prior to the `thought` function, a variety of functions and variables are defined. these can reasonably be thought of compile-time macros for bash code that will be generated in
|
||||
the html output, to be evaluated at page request time. the comparison to macros is appropriate despite these simply being python f-strings, as we are functionally putting arbitrary
|
||||
semantic constructs through a preprocessor, which outputs bash. it is by this mechanism that a suitable abstraction is created for writing efficient(ish) bash code for server side rendering.]])
|
||||
|
||||
(p #[[
|
||||
the afforementioned "bash macros" are not used outside of this file. in fact, the only place they are used is at the end of
|
||||
this file, where we call the page template. prior to making sense of what on earth is going on there, we must understand what some of these
|
||||
macros do: to do so, a distinction between those defined via `defn` and those defined via `setv` should be established:]])
|
||||
|
||||
(br)
|
||||
(dl
|
||||
(dt "macros defined via `defn`")
|
||||
(dd "
|
||||
macros which take an argument or arguments, manifesting as hy forms. a good example being `subshell`, which takes arbitrarily many hy strings
|
||||
(which should be bash expressions) and outputs bash code consisting of all of those strings within a subshell and delineated by ';'.")
|
||||
(dt "macros defined via `setv`")
|
||||
(dd "
|
||||
these take no arguments, but often include references to bash variables that they assume will be defined within the scope in which they are inserted.
|
||||
they are used as a means of expanding formats for bash variables to be rendered in. a good example of this is `thought-tag-format`,
|
||||
which expands to simply be a link to `?filter-tag=${tag}` with the link text `${tag}`."))
|
||||
|
||||
(p #[[note that for the route `/html/view-thought.html`, any url parameter encoded in the requests to this page
|
||||
will be propogated to the html the server returns, such that any instances of `{param-name}` in the html document—assuming that the route contains
|
||||
"?param-name=param-value"—will be replaced with `param-value`. this will occur prior to the server evaluating bash commands in said document. such a system,
|
||||
to give an example, allows for a page at some route that expects an optional `option` parameter, which may be either `1` or `2`. since the value of
|
||||
`{option}` in the corresponding html for that route is substituted prior to bash commands being executed, this allows for conditional logic in bash scripts embedded in the html file corresponding to that route
|
||||
(eg: `test {option} = 1 && echo option 1 || echo option 2`).]])
|
||||
|
||||
(p #[[
|
||||
`test` is used in the above example instead of `[ ... ]` because, as is stated within the about-site page, the way the server knows it is to execute a part of an html document
|
||||
as bash code is by that string being enclosed within $[ ... ]. as such, we may only use the subset of bash code that does not include "[", or "]",
|
||||
else we would prematurely end any bash expression containing "]"]])
|
||||
|
||||
(p "with the above context, the following macros can now be slightly more reasonably explained:")
|
||||
|
||||
(br)
|
||||
(dl
|
||||
(dt "check-param-defined")
|
||||
(dd "
|
||||
this macro takes a param name (intended to be the name of a url parameter possibly passed in the query to the page it is within), and, if it is passed in the url, exits with 0,
|
||||
whilst otherwise exiting with 1. the way that it checks if the url parameter is defined is by checking that `echo '{{' '{param-name}' '}}' | sed 's/ //g'`
|
||||
(which will evaluate to "{param-name}" without being substituted, even if a parameter of that name is in the url) is not equal to `{param-name}` (which will get substituted in the
|
||||
case that a parameter of that name is in the url).")
|
||||
|
||||
(dt "if-param-then-else")
|
||||
(dd "
|
||||
takes url param name `param-name`, bash expression `param-defined`, and bash expression `otherwise`, as well as optional boolean `echo`, which defaults to true.
|
||||
if `param-name` is defined, the `param-defined` expression is executed, otherwise, the `otherwise` expression is executed. if `echo` is true, expressions are executed as
|
||||
`echo expression`, otherwise, they are executed simply as `expression`.")
|
||||
|
||||
(dt "param-if-param-else")
|
||||
(dd "
|
||||
takes url param name `param-name` and expression `otherwise`. calls `if-param-then-else` with arguments `param-name`, `param-name`, and `otherwise`.
|
||||
thus, it evaluates to `param-name` if `check-param-defined` of `param-name` is true, whilst otherwise evaluating to `otherwise`.")
|
||||
|
||||
(dt "get-thought-field")
|
||||
(dd "
|
||||
takes field name `field`, indexes the `thought-registry` file at the line indicated by the current value of the thought= parameter, and evaluates to the value of
|
||||
the column corresponding to `field`."))
|
||||
|
||||
(h3 "the call to `page`")
|
||||
(p "
|
||||
the call to the `page` template is like every other call to the `page` template on this site. the interesting parts are all the bash code.")
|
||||
|
||||
(p "
|
||||
to start simply, the title of the page is set to")
|
||||
(code (pre ~(run "syntax-hl pages/html/view-thought.hy | pup 'pre' --pre | tail -n 32 | head -n1 | sed -e 's/\\[/\\[/g' -e 's/\\]/\\]/g' -e 's/<span class=\"hySexp\"> <\\/span>/<span class=\"hySexp\">\\ <\\/span>/g'")))
|
||||
(p "this sets the title of the page to the value of the `thought` parameter if it passed, and otherwise to the string \"natalie thoughts\"")
|
||||
|
||||
(p "next, the description is set to the following:")
|
||||
(code (pre ~(run "syntax-hl pages/html/view-thought.hy | pup 'pre' --pre | tail -n 31 | head -n6 | sed -e 's/\\[/\\[/g' -e 's/\\]/\\]/g' -e 's/<span class=\"hySexp\"> <\\/span>/<span class=\"hySexp\">\\ <\\/span>/g'")))
|
||||
(p "this sets the page's description to the value of the description column in the `thought-registry` file for the relevant thought if the `thought` parameter is passed. if the `thought` parameter is not passed,
|
||||
the description gets set to 'thoughts filtered for "{filter-tag}"' if the `filter-tag` paremeter is passed, and otherwise "natalie thoughts"")
|
||||
|
||||
(p "finally, we have the complicated part:")
|
||||
(code (pre ~(run "syntax-hl pages/html/view-thought.hy | pup 'pre' --pre | tail -n 23 | sed -e 's/\\[/\\[/g' -e 's/\\]/\\]/g' -e 's/<span class=\"hySexp\"> <\\/span>/<span class=\"hySexp\">\\ <\\/span>/g'")))
|
||||
(p "the first section is very simple—it just sets the value of the heading in the same manner that the description is set. how the class of the relevant div is set is also very self-explanatory.")
|
||||
(p "after setting the heading, the code then checks to see if the url param "thought" is defined. if it is, then it executes a variety of commands inside of a subshell:
|
||||
firstly, it defines the variables `tags` and `date` to be the value of `get-thought-param-field` for the respective fields. following this, we echo `thought-display-meta-format`, which displays the afforementioned metadata—as the name would suggest.
|
||||
this is executed inside of a subshell such that the values defined as variables are accessible to `thought-display-meta-format`, which expects said variables to be defined. finally, we run the `include` command (`www/src/scripts/include`) on the file
|
||||
defined in the file_name column of the post's entry in the `thought-registry`. note that this fails by returning a blank page if there is no entry corresponding to the value of the "thought" param. it should probably fix this.")
|
||||
|
||||
(p "if the "thought" parameter is not passed, a list of posts is rendered by enumerating over lines in the `thought-registry` file. prior to said enumerating, if the "filter-tag" param is passed, then we grep the `thought-registry` file for lines including its
|
||||
value in the list of tags. following this, we sort the lines of the file by the fourth tab-separated column (which contains the publish date) with an at least somewhat silly sort expression."
|
||||
~(run #[[make-footnote '"{{" and "}}" is the syntax to include a regular "{" or "}" in a python f-string']])
|
||||
" this sort expression is "like that" because, in compliance with POSIX or something, when sorting numerically by a certain field, sort will stop at the first non-numeric character in that field.
|
||||
as such, each field in the date is specified. beyond the day, the time of a post is not sorted because it does not write that much. only after sorting do we actually enumerate over posts, for each post reading each column in `thought-registry`
|
||||
in to various variables, and then echoing `thought-list-entry-format`, which—as with `thought-display-meta-format`—expects these variables to be defined.")
|
||||
|
||||
(br)
|
||||
(h3 "performance limitations of this appoach")
|
||||
(p "because we run `include` to render the html fragments containing post contents at request time, the speed a request can be served is highly limited by the amount of time it takes to run said `include` command.
|
||||
it is necessary to run `include`, because the server does not evaluate $[ ... ]. expressions recursively, but only once. as such, simply sending the raw file contents of a blog post would not work,
|
||||
as bash commands meant to render parts of the post would not get executed.")
|
||||
|
||||
(p "currently, `include` calls a hy script that runs the same code that the http server does to parse bash embedded in html. because python is slow, and we are writing and reading potentially large amounts of data to a stream in bash,
|
||||
this is rather slow. since there is no way to have dynamic content in blog posts if they were only evaluated at compile time, some means of making the process of rendering a thought at request time must be found to improve the time
|
||||
it takes to serve a request to this page.")))
|
||||
|
@ -1,170 +0,0 @@
|
||||
(thought
|
||||
:title "bash builtins with python"
|
||||
:date "2025-06-01 05:11:23"
|
||||
:tags ["programming" "shell" "hacks" "python"]
|
||||
:description "fact: doing this decreased the amount of time it took to serve this page by ~200ms"
|
||||
:content `(
|
||||
~(run "syntax-hl data/thoughts/python-bash-builtins/bash.h | pup 'style' --pre")
|
||||
(p ~f"this post was inspired by {(form->html (link #[[https://web.archive.org/web/20160303032434/http://cfajohnson.com/shell/articles/dynamically-loadable/]] #[[this article]]))},
|
||||
{(form->html (link #[[https://git.sakamoto.pl/servfail/ruszt]] #[[this project]]))}, and vaguely just the entirety section 4 of the bash manual.")
|
||||
(p ~f"some example code can be found {(form->html (link #[[//git.natalieee.net/nat/python-bash-builtins]] 'here))}.")
|
||||
(br)
|
||||
(h2 "what are bash builtins?")
|
||||
(hr)
|
||||
(p "until recently, this one was unaware of the concept of loading new bash builtins during a shell session. everyone it discussed such things with were also unaware of this, thus
|
||||
it seems appropriate to provide some explanation. in case the reader has not read the bash manual, or is just unfamiliar with the topic, it will also
|
||||
provide a summary of what a builtin itself is.")
|
||||
|
||||
(p "a bash builtin is a means of running code within bash without creating a new process. many commands one interacts with while using bash are
|
||||
in fact builtins loaded at shell initialization: examples of such commands include `read`, `test`, and `[`. in addition to those built in to
|
||||
the shell, new builtins may be loaded from shared object files via the `enable` builtin. the reason one would want to do this instead of simply
|
||||
calling an external program is that, as previously stated, builtins are executed within the shell, without creating a new process.
|
||||
because while executing a builtin, a new process does not need to be spawned—and then streams for that process read from and written to—there
|
||||
are cases in which bash builtins are vastly more efficient than calling external programs." ~(run #[f[make-footnote 'as is touched on {(form->html (link
|
||||
"https://web.archive.org/web/20160303032434/http://cfajohnson.com/shell/articles/dynamically-loadable/" "here"))}']f]))
|
||||
|
||||
(br)
|
||||
(h2 "why?")
|
||||
(hr)
|
||||
(p "because it is natalie, its first thought upon discovering that one could load new builtins at runtime was something along the lines of "huh, neat, how can python be involved?"
|
||||
we all have our flaws. additionally, at the time of discovering this, it was still writing the previous post, and as such was thinking quite a bit about optimizing the response time for
|
||||
this page. this will be relevant later.")
|
||||
|
||||
(br)
|
||||
(h2 "how?")
|
||||
(hr)
|
||||
(p ~f"one may wonder: python is not a compiled language, so how can we produce a shared object containing python code? thankfully, {(form->html (link #[[//cython.org]] 'cython))} exists.
|
||||
this will allow us to generate c code from python code such that we can then wrap this generated code with other code to create a bash builtin. in this implementation, the bash side of this wrapping process is achieved
|
||||
via the following two macros:" ~(run #[[make-footnote "please note that it is not a c programmer, would not consider itself to know c, and has almost no experience with it. its code will be bad."]]))
|
||||
|
||||
(figure
|
||||
(figcaption "(bash.h) macros to create a shell builtin from a function of type int (int, char**)")
|
||||
(code (pre ~(run "syntax-hl data/thoughts/python-bash-builtins/bash.h | pup 'pre' --pre | tail -n57 | head -n22"))))
|
||||
|
||||
(p "the first macro, `DEFINE_BUILTIN`, takes `name` and creates a builtin struct with `name_struct`. it defines the name of the builtin (as it will appear to `enable`) as `name`,
|
||||
and sets the associated function to `name_builtin_func`, which it assumes will be defined.")
|
||||
|
||||
(p "the second macro, `WRAP_FUNC_WITH_BUILTIN`, takes `name` and creates a function `name_builtin_func`, which wraps the function `name` with logic that parses the arguments
|
||||
passed to the builtin in to an int argc and char** argv. as one will see later, arguments to python functions must be `PyObject`s. having this wrapper function allows arguments
|
||||
from bash to be parsed in to an intermediary form that does not rely on bash's WORD_LIST, and then to be parsed in to `PyObject`s later. this intermediary step exists solely to
|
||||
make the process of calling python functions simpler.")
|
||||
|
||||
(p "the process of obtaining a c function derived from python code that can reasonably be used is a bit more complicated. because the way that cython is being utilized
|
||||
here is not really the intended means of doing so (shockingly), this process becomes rather hacky. in the shell script that builds this code (which is included below), we first invoke
|
||||
cython on a .py file, and then run a function `translate_function_names` on the output.")
|
||||
|
||||
(figure
|
||||
(figcaption "the afforementioned build script")
|
||||
(code ~(run "syntax-hl data/thoughts/python-bash-builtins/build.sh")))
|
||||
|
||||
(p "as is detailed in this build script, the cython output contains mangled function names. to unmangle them, we generate a series of define statements that get concatonated to the end of the c output file.")
|
||||
(p "as we have now established, a function `foo` in a .py file will become accessible under the name `foo` in a c file, so long as that c file includes the file in which the `foo` function is defined—though
|
||||
with the .py suffix substituted with a .c suffix. understanding this, we may examine the final macro which transposes our newly c-ified python functions in to a form that can be passed to
|
||||
`WRAP_FUNC_WITH_BUILTIN` and `DEFINE_BUILTIN`:")
|
||||
|
||||
(figure
|
||||
(figcaption "(bash.h) the `PY_FUNC` macro")
|
||||
(code (pre ~(run "syntax-hl data/thoughts/python-bash-builtins/bash.h | pup --pre 'pre' | tail -n34"))))
|
||||
|
||||
(p "this macro takes a `bash_name`, `modname`, and `function` and expands to a function definition for a function named the value of `bash_name`, which returns an int while taking an int argc and char** argv.
|
||||
this `bash_name` function is then passed to `WRAP_FUNC_WITH_BUILTIN` (expanding to a definition of a builtin wrapper function that calls `bash_name`), and then to `DEFINE_BUILTIN` (expanding to
|
||||
the actual `builtin` struct), thusly defining a builting named `bash_name` to call a series of wrapper functions (first the `WORD_LIST` to (int, char**) wrapper, and then the `bash_name` function) which
|
||||
arrive at a call to the relevant python code.")
|
||||
|
||||
(p "inside of this `bash_name` function, we firstly check to see if python has been initialized and should it not be, initialize it after appending the module defined by `modname` to the builtin module table.
|
||||
following this, we `dlopen()` the python shared object. this is necessary to load symbols defined within libpython.so, which are expected to be defined by c extensions loaded by python.")
|
||||
|
||||
(p "after checking for the initialization of—and possibly initializing—python, we import `modname`. within `modname`, we find the attribute associated with the name passed in the `function`
|
||||
argument of the `PY_FUNC` macro, and save this to the variable `func`. next, an argument tuple is built such that its elements are each string in the `argv` argument passed to `bash_name`.
|
||||
this is then used to call the `func` variable, after which we check for errors. in the presence of errors, we jump to the cleanup section and return the default value of `ret`, which is one. otherwise,
|
||||
we set the value of `ret` to the return value of `func`, and proceed with cleaning up. notably `Py_Finalize` is never called, thus leaving the interpreter running. it is necessary to do this because
|
||||
of some strangeness with how the python interpreter handles state.")
|
||||
|
||||
(p "this allows python functions of the signature ([str]) -> int to be called from bash such that the argument of the function is a list of all arguments passed to its associated builtin, and its return value
|
||||
is the exit code of the builtin. additionally, because `Py_Finalize` is never called, after the first call to a builtin the python interpreter remains initialized. this creates significant savings in performance when
|
||||
executing a large number of calls to a builtin, as compared to initializing a python interpreter for each call.")
|
||||
|
||||
(p "in practice, this looks like the following:")
|
||||
(figure
|
||||
(figcaption "foo.py")
|
||||
(code ~(run "syntax-hl data/thoughts/python-bash-builtins/foo.py")))
|
||||
|
||||
(figure
|
||||
(figcaption "bar.c")
|
||||
(code ~(run "syntax-hl data/thoughts/python-bash-builtins/bar.c")))
|
||||
|
||||
(br)
|
||||
(h2 "why on earth would anyone want this?")
|
||||
(hr)
|
||||
(p "say there exists some python script that does something, and we want to call it many times in a bash script. for this example, lets say the function simply prints \"foo\" to stdout.
|
||||
normally, to do this one would call `python3 foo.py` on each iteration of the loop:")
|
||||
|
||||
(figure
|
||||
(figcaption "the afforementioned method:")
|
||||
(code (pre
|
||||
"time { for i in {0..100}; do python3 foo.py >/dev/null; done }\n\n"
|
||||
"real 0m1.260s\n"
|
||||
"user 0m0.826s\n"
|
||||
"sys 0m0.435s")))
|
||||
|
||||
(p "for just printing \"foo\" 100 times, this takes an absurd amount of time. compiling this code to a bash builtin, we see:")
|
||||
(figure
|
||||
(figcaption "shell builtin method:")
|
||||
(code (pre
|
||||
"enable -f ./foo foo\n"
|
||||
"time { for i in {0..100}; do foo >/dev/null; done }\n\n"
|
||||
"real 0m0.001s\n"
|
||||
"user 0m0.001s\n"
|
||||
"sys 0m0.000s")))
|
||||
|
||||
(p "this difference becomes even more pronounced when the python scripts being called import large libraries like numpy. because, when compiled to a bash builtin, the python interpreter is
|
||||
only initialized once, any libraries only need to be imported once.")
|
||||
|
||||
(figure
|
||||
(figcaption "a useless python program that imports a large library")
|
||||
(code ~(run "syntax-hl data/thoughts/python-bash-builtins/math.py")))
|
||||
|
||||
(p "pretend this program actually does something useful, and that we want to process a large amount of data with it from the shell. to repeat the previous demonstration, doing this the traditional
|
||||
way goes as follows:")
|
||||
|
||||
(figure
|
||||
(figcaption "calling the python interpreter in a loop")
|
||||
(code (pre
|
||||
"time { for i in {0..100}; do python3 math.py 2 2 >/dev/null; done }\n\n"
|
||||
"real 0m11.261s\n"
|
||||
"user 1m49.283s\n"
|
||||
"sys 0m2.261s\n")))
|
||||
|
||||
(figure
|
||||
(figcaption "calling a shell builtin in a loop")
|
||||
(code (pre
|
||||
"time { for i in {0..100}; do math 2 2 >/dev/null; done }\n\n"
|
||||
"real 0m0.002s\n"
|
||||
"user 0m0.000s\n"
|
||||
"sys 0m0.002s\n")))
|
||||
|
||||
(p "the difference speaks for itself.")
|
||||
|
||||
(br)
|
||||
(h2 "using this to improve site performance")
|
||||
(hr)
|
||||
(p ~f"as was explained in the {(form->html (link "?thought=natalieee.net-v2" "the previous thought"))}, these posts are stored on the server as fragments of html documents and rendered at request time.
|
||||
this is done via a hy program that runs the same code that the server uses to run bash snippets for server side rendering. executing this program on posts is the lengthiest part of the post rendering
|
||||
process, which is why rendering a specific post takes ~330ms longer than rendering the list of posts.")
|
||||
|
||||
(p "using `hy2py`, hy code can be converted in to python code. accordingly, this allows us to compile said code in to a bash builtin. it follows that we can then replace the code to execute bash with a
|
||||
call to the resulting builtin. this results in a significant decrease in server rendering time:")
|
||||
|
||||
(figure
|
||||
(figcaption "benchmark comparing the old rendering code to the new code")
|
||||
(code ~(run "cat data/thoughts/python-bash-builtins/benchmark.html")))
|
||||
|
||||
(p "this is a significant improvement, as the benchmarks in the previous post established the time taken to render a post and subsequently respond to the associated request to be around 500ms.")
|
||||
|
||||
(br)
|
||||
(h2 "problems/limitations with this implementation")
|
||||
(p "it created this almost entirely without reading documentation, and thus there are several glaring flaws. foremost among these is the fact that one cannot include multiple .c cython output files
|
||||
in a .c file that is to be compiled to a builtin, due to redefinition errors. additionally, the build process is sort of clunky and probably requires manual modification on a per system basis.
|
||||
finally, there isn't a good way to compile a python program consisting of multiple .py files in to a builtin, without installing it as a library on python's import path. none of these, however,
|
||||
are particularly relevant to a toy proof-of-concept.")))
|
||||
|
@ -1,9 +1,9 @@
|
||||
(do ;; required for arcane scoping
|
||||
(defn check-param-defined [param-name]
|
||||
(defn check-param-exists [param-name]
|
||||
#[f[test "$(echo '{{' '{param-name}' '}}' | sed 's/ //g')" != '{{{param-name}}}']f])
|
||||
|
||||
(defn if-param-then-else [param-name param-defined otherwise [echo True]]
|
||||
#[f[ {(check-param-defined param-name)} && {(if echo "echo " "")}{param-defined} || {(if echo "echo " "")}{otherwise} ]f])
|
||||
#[f[ {(check-param-exists param-name)} && {(if echo "echo " "")}{param-defined} || {(if echo "echo " "")}{otherwise} ]f])
|
||||
|
||||
(defn param-if-param-else [param-name otherwise]
|
||||
(if-param-then-else param-name f"'{{{param-name}}}'" otherwise))
|
||||
@ -18,7 +18,7 @@
|
||||
`(a (:href "?filter-tag=${tag}") "${tag}")))
|
||||
|
||||
(setv thought-tag-list-format
|
||||
#[f[tags: $(while read -r tag{";"} do echo "{thought-tag-format}"{";"} done <<< "${{tags//;/$'\n'}}")]f])
|
||||
#[f[tags: $(while read -r tag{";"} do echo "{thought-tag-format}"{";"} done <<< "${{tags//{#[[;]]}/$'\n'}}")]f])
|
||||
|
||||
(setv thought-list-entry-format (form->html
|
||||
`(div (:id "${file_name}" :class thought-list-entry)
|
||||
@ -34,9 +34,8 @@
|
||||
|
||||
(defn thought [[title "empty title"] [description ""] [date "1970-01-01"] [tags []] [content '(p "no content provided")]]
|
||||
(inherit page-name)
|
||||
(when (not-in (get page-name -1) (with [thought-registry (open "../data/thought-registry" "r")] (.read thought-registry)))
|
||||
(with [thought-registry (open "../data/thought-registry" "a")]
|
||||
(.write thought-registry f"{(.join #[[;]] tags)}\t{(get page-name -1)}\t{title}\t{date}\t{description}\n")))
|
||||
(with [thought-registry (open "../data/thought-registry" "a")]
|
||||
(.write thought-registry f"{(.join #[[;]] tags)}\t{(get page-name -1)}\t{title}\t{date}\t{description}\n"))
|
||||
|
||||
(run "echo > /tmp/footnote_count")
|
||||
|
||||
@ -46,22 +45,21 @@
|
||||
~(run "put-footnotes")
|
||||
~(comments f"/html/thought/view-thought.html?thought={(get page-name -1)}")))
|
||||
|
||||
;; (run "echo -n > ../data/thought-registry")
|
||||
(run "echo -n > ../data/thought-registry")
|
||||
|
||||
(page
|
||||
:title f" $[{(param-if-param-else "thought" "natalie thoughts")}] "
|
||||
:title #[f[$[{(param-if-param-else "thought" "natalie thoughts")}]]f]
|
||||
:description #[f[
|
||||
$[{(if-param-then-else "thought"
|
||||
#[f["$({(get-thought-param-field "description")})"]f]
|
||||
#[f[$({(if-param-then-else "filter-tag"
|
||||
$[{(if-param-then-else "thought" #[f["$({(get-thought-param-field "description")})"]f] #[f[
|
||||
$({(if-param-then-else
|
||||
"filter-tag"
|
||||
"'thoughts filtered for "{filter-tag}"'"
|
||||
"an enumeration of natalie thoughts. it is functionally a blog")})]f])}]]f]
|
||||
|
||||
`(section
|
||||
(h1 ~#[f[
|
||||
$[{(if-param-then-else "thought"
|
||||
#[f[$({(get-thought-param-field "title")})]f]
|
||||
#[f[$({(if-param-then-else
|
||||
$[{(if-param-then-else "thought" #[f[$({(get-thought-param-field "title")})]f] #[f[
|
||||
$({(if-param-then-else
|
||||
"filter-tag"
|
||||
"'thoughts filtered for "{filter-tag}"'"
|
||||
"natalie thoughts. functionally a blog")})]f])}]]f])
|
||||
@ -77,6 +75,6 @@
|
||||
#[f[cat www/data/thought-registry | eval $({(if-param-then-else
|
||||
"filter-tag"
|
||||
#[-[grep '{filter-tag}']-]
|
||||
#[-["grep -E '.*'"]-])}) | sort -t $'\t' -k4{{,.6,.9}}rn | while IFS=$'\t' read -r tags file_name title date description{";"} do echo "{thought-list-entry-format}"{";"} done]f]
|
||||
#[-["grep -E '.*'"]-])}) | sort -t$'\t' -k4 -nr | while IFS=$'\t' read -r tags file_name title date description{";"} do echo "{thought-list-entry-format}"{";"} done]f]
|
||||
:echo False)}]]f]))))
|
||||
|
||||
|
@ -1,62 +0,0 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include <bash/builtins.h>
|
||||
#include <bash/builtins/bashgetopt.h>
|
||||
#include <bash/shell.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
extern char **make_builtin_argv(WORD_LIST*, int*);
|
||||
|
||||
#define DEFINE_BUILTIN(name) \
|
||||
struct builtin name##_struct = { \
|
||||
#name, \
|
||||
name##_builtin_func, \
|
||||
BUILTIN_ENABLED, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
0 \
|
||||
}
|
||||
|
||||
#define WRAP_FUNC_WITH_BUILTIN(name)\
|
||||
int name##_builtin_func(WORD_LIST *list) { \
|
||||
int argc, ret; \
|
||||
char **argv; \
|
||||
argv = make_builtin_argv(list, &argc); \
|
||||
ret = name(argc, argv); \
|
||||
free(argv); \
|
||||
return ret;\
|
||||
}
|
||||
|
||||
#define PY_FUNC(bash_name, modname, function) \
|
||||
int bash_name(int argc, char **argv) { \
|
||||
if (!Py_IsInitialized()) { \
|
||||
dlopen("libpython3.13.so", RTLD_NOW | RTLD_GLOBAL);\
|
||||
PyImport_AppendInittab(#modname, PyInit_##modname); \
|
||||
Py_Initialize(); \
|
||||
} \
|
||||
int ret = 1; \
|
||||
PyObject *mod = NULL, *func = NULL, *result; \
|
||||
\
|
||||
mod = PyImport_ImportModule(#modname); \
|
||||
func = PyObject_GetAttrString(mod, #function); \
|
||||
\
|
||||
PyObject *py_argv = PyTuple_New(argc - 1); \
|
||||
\
|
||||
for (int i = 1; i < argc; i++) { \
|
||||
PyObject *arg_as_str = PyUnicode_FromString(argv[i]); \
|
||||
PyTuple_SetItem(py_argv, i-1, arg_as_str); \
|
||||
} \
|
||||
\
|
||||
result = PyObject_CallObject(func, py_argv); \
|
||||
if (!result) { PyErr_Print(); goto finally; } \
|
||||
ret = (int) PyFloat_AsDouble(result); \
|
||||
\
|
||||
finally: \
|
||||
Py_XDECREF(result); \
|
||||
Py_XDECREF(py_argv); \
|
||||
Py_XDECREF(func); \
|
||||
Py_XDECREF(mod); \
|
||||
return ret; \
|
||||
} \
|
||||
WRAP_FUNC_WITH_BUILTIN(bash_name); \
|
||||
DEFINE_BUILTIN(bash_name);
|
@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
# maybe uses non-posix regex and thus may be non-portable. oops.
|
||||
# the bash regex implementation is actually platform dependent, lol.
|
||||
|
||||
shopt -s extglob
|
||||
|
||||
PY_VERSION=3.13
|
||||
PYFLAGS="$(/usr/bin/python${PY_VERSION}-config --cflags --ldflags)"
|
||||
BASH_INCLUDE_DIR="/usr/include/bash"
|
||||
|
||||
function translate_function_names() {
|
||||
# cython generates garbled names like __pyx_pf_7py_test_2foo for a python function named foo
|
||||
# this function generates a list of #defines that translate these names to what they are in python
|
||||
|
||||
file="${1}"
|
||||
|
||||
grep 'static PyObject \*__pyx_pf_' "${file}" | grep -v 'proto' | while read -r line; do
|
||||
desired_name=$"${line#*$"${file/.c/}"_}"
|
||||
desired_name="${desired_name/#+([0-9])/}"
|
||||
real_name="${line#*\*}"
|
||||
echo "#define ${desired_name%(*} ${real_name%(*}"
|
||||
done
|
||||
}
|
||||
|
||||
py2c() {
|
||||
rm "${1/.py/.c}"
|
||||
cython3 -3 "${1}" -o "${1/.py/.c}"
|
||||
translate_function_names $(basename ${1/.py/.c}) >> "${1/.py/.c}"
|
||||
}
|
||||
|
||||
compile() {
|
||||
gcc -I$BASH_INCLUDE_DIR{/include,/builtins,}\
|
||||
$PYFLAGS $1 -o ${1/.c/} -Wall\
|
||||
-lpython3.13 \
|
||||
-fPIC -shared -O3
|
||||
}
|
||||
|
||||
py2c execute-bash/execute_bash.py
|
||||
compile execute-bash/builtin.c
|
Binary file not shown.
@ -1,4 +0,0 @@
|
||||
#include "../bash.h"
|
||||
#include "execute_bash.c"
|
||||
|
||||
PY_FUNC(execute_bash, execute_bash, _execute_bash)
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +0,0 @@
|
||||
def _execute_bash(*args):
|
||||
from re import sub
|
||||
from os import environ as hy_env
|
||||
from subprocess import check_output
|
||||
env = hy_env
|
||||
env['PATH'] = hy_env['PATH'] + ':./scripts'
|
||||
path = args[0]
|
||||
with open(path, 'r') as fp:
|
||||
_hy_anon_var_1 = fp.read()
|
||||
print(sub('\\$\\[(.*?)\\]', lambda sequence: check_output(sequence.group(1), shell=True, executable='/bin/bash', env=env).decode().strip(), _hy_anon_var_1))
|
||||
return 0
|
11
www/src/scripts/execute-bash
Executable file
11
www/src/scripts/execute-bash
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env hy
|
||||
;; vim: filetype=hy
|
||||
(import sys [argv])
|
||||
(import re [sub])
|
||||
(import os [environ :as hy-env])
|
||||
(import subprocess [check-output])
|
||||
|
||||
(setv env hy-env)
|
||||
(setv (get env "PATH") (+ (get hy-env "PATH") ":./scripts"))
|
||||
|
||||
(print (sub r"\$\[(.*?)\]" (fn [sequence] (. (check-output (.group sequence 1) :shell True :executable "bash" :env env) (decode) (strip))) (with [fp (open (get argv 1) "r")] (.read fp))))
|
@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
eval "${1}" | aha -s | pup 'pre' --pre
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
path=$1
|
||||
caption=$2
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
enable -f ./www/site/scripts/builtins/execute-bash/builtin execute_bash
|
||||
output="$(execute_bash $1)"
|
||||
#!/bin/sh
|
||||
|
||||
output="$(execute-bash $1)"
|
||||
if [ -z "${2}" ]; then
|
||||
echo "${output}" | sed "${2}"
|
||||
else
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
tmpfile=$(mktemp)
|
||||
|
||||
nvim --headless $1 "+set noswapfile | set nonu | set nornu | set conceallevel=0" "+%s/^$/ /g" "+Lazy! load nvim-treesitter | TSEnable highlight | TOhtml | w! ${tmpfile}" '+qa!' 2>/dev/null
|
||||
timeout 0.6s nvim "$1" -c "set nonu | set nornu" -c "autocmd LspAttach * execute 'TOhtml | w! ${tmpfile} | qa!'" >/dev/null || nvim "$1" -c "set nonu | set nornu" -c "TOhtml | w! ${tmpfile} | qa!" >/dev/null
|
||||
|
||||
cat "${tmpfile}" | pup --pre 'style, pre' | sed '/^$/d'
|
||||
|
||||
|
@ -2,16 +2,11 @@
|
||||
|
||||
(defn comments [route]
|
||||
`(section (:class comments)
|
||||
(div
|
||||
(h2 comments)
|
||||
(p "as an anti-bot measure, in order for $VIEWER's comment to be stored on the server, $VIEWER" (span (:style "font-weight: bold") " MUST ")
|
||||
"enter the commit hash of the current deployment found in the bottom right of the page footer. failure to do so will result in the comment being disregarded.")
|
||||
|
||||
(form (:id comment :method post :action ~f"/comment?route={route}" :autocomplete off)
|
||||
(textarea (:id comment :name comment :placeholder comment :required True :maxlength 2048))
|
||||
(input (:id name :type text :name name :placeholder username :required True :maxlength 32))
|
||||
(input (:id website :type text :name site :placeholder "website (not required)" :maxlength 256))
|
||||
(input (:id commit :type text :name commit :placeholder "commit" :maxlength 8))
|
||||
(input (:id submit :type submit :value post))))
|
||||
(h2 comments)
|
||||
(form (:id comment :method post :action ~f"/comment?route={route}" :autocomplete off)
|
||||
(textarea (:id comment :name comment :placeholder comment :required True :maxlength 2048))
|
||||
(input (:id name :type text :name name :placeholder username :required True :maxlength 32))
|
||||
(input (:id website :type text :name site :placeholder "website (not required)" :maxlength 256))
|
||||
(input (:id submit :type submit :value post)))
|
||||
|
||||
(~f"$[ls -r ./www/data/comments/{(quote-plus route :safe "")} | xargs -I# cat ./www/data/comments/{(quote-plus route :safe "")}/#]")))
|
||||
|
@ -1,24 +1,18 @@
|
||||
(import utils [run])
|
||||
|
||||
(defn footer []
|
||||
`(footer
|
||||
`(footer
|
||||
(div
|
||||
(div
|
||||
"page rendered at $[date +%s]" (br)
|
||||
"page compiled at " ~(run "date +%s"))
|
||||
(div
|
||||
(footnote (:style "margin-top: 5px") "all content, with the exception of 88x31 buttons or unless otherwise noted is created by natalie and is licensed under a CC BY-NC-SA 4.0 license.")))
|
||||
(div
|
||||
(img (:src "/assets/88x31/natalieee.net.png" :alt "natalieee.net 88x31" :width "88" :height "31"))
|
||||
(a (:href "https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en")
|
||||
(img (:src "/assets/88x31/by-nc-sa.png" :alt "license 88x31" :width "88" :height "31"))))
|
||||
"page rendered at $[date +%s]"
|
||||
(br)
|
||||
"page compiled at " ~(run "date +%s"))
|
||||
(img (:src "/assets/88x31/natalieee.net.png" :alt "natalieee.net 88x31" :width "88" :height "31"))
|
||||
(div
|
||||
(footnote (:style "margin-top: 5px") "▖▖▖▖▘▖▖▖▖▘▌▖▖▖▘")
|
||||
(footnote (:style "margin-top: 5px")
|
||||
(div (:style "display: flex; flex-direction: row; width: 14ch; gap: 1ch; margin-right: 5px;")
|
||||
(a (:href "https://stellophiliac.github.io/roboring/0x6e6174/previous") "<-")
|
||||
(a (:href "https://stellophiliac.github.io/roboring") "roboring")
|
||||
(a (:href "https://stellophiliac.github.io/roboring/0x6e6174/next") "->"))
|
||||
|
||||
(iframe (:src "/arpa-n-gon/nav?current=natalieee.net.8.f.9.e.0.7.4.0.1.0.0.2.ip6.arpa&style=/assets/arpa-n-gon.css&from-iframe"))))
|
||||
(span ~(run "git log -1 --format=%h"))))
|
||||
(footnote (:style "margin-top: 5px") "all content, with the exception of 88x31 buttons or unless otherwise noted is created by natalie and is licensed under a CC BY-NC-SA 4.0 license."))
|
||||
(a (:href "https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en")
|
||||
(img (:src "/assets/88x31/by-nc-sa.png" :alt "license 88x31" :width "88" :height "31")))
|
||||
(footnote (:style "margin-top: 5px") "▖▖▖▖▘▖▖▖▖▘▌▖▖▖▘"
|
||||
(div (:style "flex-direction: row; width: 14ch; gap: 1ch; margin-right: 5px;")
|
||||
(a (:href "https://stellophiliac.github.io/roboring/0x6e6174/previous") "<-")
|
||||
(a (:href "https://stellophiliac.github.io/roboring") "roboring")
|
||||
(a (:href "https://stellophiliac.github.io/roboring/0x6e6174/next") "->")))))
|
||||
|
@ -1,8 +1,8 @@
|
||||
(import templates.header [header])
|
||||
(import templates.footer [footer])
|
||||
|
||||
|
||||
(import utils [run hy-env])
|
||||
|
||||
|
||||
(defn page [html
|
||||
[title "natalieee.net"]
|
||||
[author "natalie roentgen connolly"]
|
||||
@ -18,9 +18,7 @@
|
||||
body {display: none}]])
|
||||
(link (:rel preload :href "/assets/style.css" :as style))
|
||||
(link (:rel preload :href "/assets/bg.svg" :as image))
|
||||
(link (:rel preload :href "/assets/ansi-colors.css" :as style))
|
||||
(link (:rel "stylesheet" :href "/assets/style.css" :type "text/css"))
|
||||
(link (:rel "stylesheet" :href "/assets/ansi-colors.css" :type "text/css"))
|
||||
(meta (:http-equiv "content-type" :content "text/html; charset=utf-8"))
|
||||
(meta (:name "viewport" :content "width=device-width, initial-scale=1"))
|
||||
(meta (:name "author" :content ~author))
|
||||
|
Reference in New Issue
Block a user