(import-macros {: map : reduce} :lib.macro) (tset package :path (.. package.path ";./vendor/lpeglj/?.lua")) (local io (require :io)) (local math (require :math)) (local fennel (require :vendor.fennel)) (local html (require :vendor.html)) (local json (require :vendor.json)) (local array (require :lib.array)) (local str (require :lib.string)) (local ozchai (require :parser.ozchai)) (local ipuer (require :parser.ipuer)) (local artoftea (require :parser.artoftea)) (when _G.unpack (tset table :unpack _G.unpack)) (local db (luna.db.open "file:var/db.sqlite?_journal=WAL&_sync=NORMAL")) (local query-synonyms { "шэн" "шен" "шен" "шэн" "доска" "чабань" "чабань" "доска"}) (fn unescape [s] (assert (= (type s) :string) "s must be string") (pick-values 1 (-> s (string.gsub "<" "<") (string.gsub ">" ">") (string.gsub """ "\"") (string.gsub "&" "&")))) (fn site-name-template [name] (if (= name "ipuer") [:a {:class "site-icon" :href "https://ipuer.ru" :alt "Логотип Институт чай пуэр"} [:img {:src "/static/ipuer.jpg"}] "Институт чая пуэр"] (= name "artoftea") [:a {:class "site-icon" :href "https://artoftea.ru" :alt "Логотип Art of tea"} [:img {:src "/static/artoftea.png"}] "Art of tea"] (= name "ozchai") [:a {:class "site-icon" :href "https://ozchai.ru" :alt "Логотип #OZCHAI"} [:img {:src "/static/ozchai.ico"}] "Чайная #OZCHAI"] (= name "clubcha") [:a {:class "site-icon" :href "https://clubcha.ru" :alt "Логотип Железный Феникс"} [:img {:src "/static/clubcha.png"}] "Железный Феникс"] "")) (fn item-template [product] [:div {:class "tile"} [:a {:href product.url :style "display: block;"} [:img {:src product.image} ""]] (site-name-template product.site) [:a {:href product.url :style "text-decoration: none;"} [:NO-ESCAPE (.. "

" (unescape product.title) "

")]] [:div {:class "price"} (if product.price (.. product.price "₽") "") (if product.quantity (.. " за " product.quantity "г") "") (if (and product.price-per (< 0 product.price-per)) [:NO-ESCAPE (.. " (" product.price-per "₽ за 1г)")] "")] [:small {} (or product.description "")]]) (fn paginator-template [query page limit total] (local last-page (math.ceil (/ total limit))) (if (< limit total) [:div {:class "paginator"} [:div {:class "paginator-numbers"} (if (< 1 page) [:a {:href (.. "?page=" (- page 1) "&query=" query)} "<"] "") (faccumulate [res [:span {}] i 1 last-page] (do (table.insert res [:a {:href (.. "?page=" i "&query=" query) :class (if (= page i) "paginator-active" "")} (tostring i)]) res)) (if (< page last-page) [:a {:href (.. "?page=" (+ page 1) "&query=" query)} ">"] "")] [:div {} "Всего результатов: " [:strong {} (string.format "%d" total)]]] "")) (fn base-template [query sort page total ...] (local paginator (paginator-template query page 48 total)) [:html {:lang "en"} [:head {} [:meta {:charset "UTF-8"}] [:link {:rel :stylesheet :href "static/style.css"}] [:title {} "A new cool web server for lua"]] [:body {} [:div {:class "container"} [:div {:class "content"} [:aside {:class "aside"} [:div {:class "aside-content"} [:a {:href "/" :style "display: block;"} [:img {:class "logo" :src "static/logo.svg" :alt "Логотип meicha.ru"}]] [:form {:class "form"} [:input {:type :search :name :query :value query :autofocus true :placeholder "enter search query"}] [:button {:type :submit} "Искать"]] paginator]] [:section {} [:div {:class "list"} ...] [:footer {} paginator]]]]]]) (fn random-products [limit] (assert (< 0 limit) "limit must be > 0") (luna.db.query* db "SELECT id, site, title, description, image, url, price, weight, price_per AS \"price-per\", year FROM products ORDER BY RANDOM() LIMIT ?" [limit])) (fn query-products [page query sorters] (local query (table.concat (map (fn [_ q] (if (. query-synonyms q) (.. "(" q "* OR " (. query-synonyms q) "*)") (.. q "*"))) (str.split query)) " ")) (local total (luna.db.query db "SELECT count(*) FROM search WHERE search.`table` = 'products' AND search.name MATCH ?" [query])) {:results (luna.db.query* db "SELECT products.id, highlight(search, 0, '', '') AS \"title\", products.site, products.description, products.image, products.url, products.price, products.weight, products.price_per AS \"price-per\", products.year FROM search INNER JOIN products ON search.fid = products.id WHERE search.`table` = 'products' AND search.name MATCH ? ORDER BY rank LIMIT 48 OFFSET ?" [query (* (- page 1) 48)]) :total (if (< 0 (# total)) (. total 1 1) 0)}) (fn root-handler [{: path : query}] (if (= path "/") (let [headers {:content-type "text/html"} page (or (tonumber query.page) 1) search (or query.query "") sort "ASC" {: results : total} (if (~= "" search) (query-products page search sort) {:total 48 :results (random-products 48)})] (values 200 headers (html.render (base-template search sort page total (table.unpack (map #(item-template $2) results))) true))) (values 404 {} "not found"))) (luna.router.route "GET /" root-handler) (luna.router.static "GET /static/" "static/")