1 (uiop:define-package
#:lw2.data-viewers.post
2 (:use
#:cl
#:lw2.utils
#:lw2.sites
#:lw2.backend
#:lw2.context
#:lw2.clean-html
#:lw2.schema-type
#:lw2.html-reader
#:lw2.interface-utils
#:lw2.user-context
#:lw2.links
#:lw2.backlinks
)
3 (:import-from
#:alexandria
#:when-let
)
4 (:export
#:post-headline-to-html
#:post-body-to-html
))
6 (in-package #:lw2.data-viewers.post
)
8 (named-readtables:in-readtable html-reader
)
10 (define-schema-type :post
()
11 ((post-id string
:alias
:--id
)
15 (coauthors (or null list
) :backend-type backend-lw2
:subfields
(:--id
))
16 (url (or null string
))
17 (feed-link (or null string
) :backend-type backend-feed-crossposts
)
18 (canonical-source (or null string
) :backend-type backend-feed-crossposts
)
20 (base-score (or null fixnum
))
21 (af-base-score (or null fixnum
))
22 (comment-count (or null fixnum
))
23 (page-url (or null string
))
24 (word-count (or null fixnum
))
25 (frontpage-date (or null string
))
26 (curated-date (or null string
))
27 (legacy-id t
:backend-type backend-lw2
)
29 (af boolean
:backend-type backend-alignment-forum
)
31 (question boolean
:backend-type backend-q-and-a
)
32 ;; todo: allow recursive schema types and clean this up
33 (target-post-relations list
35 :backend-type backend-related-questions
36 :subfields
((:target-post
:--id
:slug
:title
:user-id
:url
:feed-link
37 :posted-at
:base-score
:comment-count
:page-url
38 :word-count
:frontpage-date
:curated-date
:meta
39 :af
:question
:vote-count
)))
40 (source-post-relations list
42 :backend-type backend-related-questions
43 :subfields
((:source-post
:--id
:slug
:title
:user-id
:url
:feed-link
44 :posted-at
:base-score
:comment-count
:page-url
45 :word-count
:frontpage-date
:curated-date
:meta
46 :af
:question
:vote-count
)))
47 (vote-count (or null fixnum
))
48 (nomination-count-2019 (or null fixnum
) :backend-type backend-lw2
)
49 (review-count-2019 (or null fixnum
) :backend-type backend-lw2
)
50 (is-event boolean
:backend-type backend-events
)
51 (local-start-time (or null string
) :backend-type backend-events
)
52 (local-end-time (or null string
) :backend-type backend-events
)
53 (location (or null string
) :backend-type backend-events
)
54 (google-location (or null list
) :backend-type backend-events
)
55 (contact-info (or null string
) :backend-type backend-events
)
56 (comment-sort-order (or null string
) :backend-type backend-lw2
)
57 (tags list
:graphql-ignore t
)
58 (submit-to-frontpage boolean
:backend-type backend-lw2-misc-features
)
59 (html-body (or null string
) :context
:body
)))
61 (defgeneric rectify-post
* (backend post
) ; TODO this should go in a more generic postprocessing method
62 (:method
((backend t
) post
) post
)
63 (:method
((backend backend-lw2-misc-workarounds
) post
)
64 (let* ((post (if (next-method-p) (call-next-method) post
))
65 (url (cdr (assoc :url post
)))
66 (url (and url
(string-trim " " url
))))
67 (cond ((null url
) post
)
68 ((or (uiop:string-prefix-p
"http" url
) (uiop:string-prefix-p
"/" url
)) post
)
69 (t (acons :url
(concatenate 'string
"http://" url
) post
)))))
70 (:method
((backend backend-feed-crossposts
) post
)
71 (if (cdr (assoc :url post
))
73 (acons :url
(cdr (assoc :feed-link post
)) post
))))
75 (defun rectify-post (post) (rectify-post* *current-backend
* post
))
77 (defgeneric tag-list-to-html
(backend tags
)
78 (:method
((backend backend-accordius
) tags
)
79 (dolist (tag tags
) (alist-bind ((text string
)) tag
<a href
=("/tags/~A" text
)>(progn text
)</a
>)))
80 (:method
((backend backend-lw2-tags
) tags
)
81 (dolist (tag tags
) (alist-bind ((name string
) (slug string
)) (cdr (assoc :tag tag
)) <a href
=("/tag/~A" slug
)>(progn name
)</a
>))))
83 (defun qualified-linking (url meta-location
)
84 <nav class
="qualified-linking">
85 <input type
="checkbox" tabindex
="-1" id
=("qualified-linking-toolbar-toggle-checkbox-~(~A~)" meta-location
)>
86 <label for
=("qualified-linking-toolbar-toggle-checkbox-~(~A~)" meta-location
)><span
>\
;</span></label>
87 <div class
="qualified-linking-toolbar">
88 <a href
=url
>Post permalink
</a
>
89 <a href
=("~A?comments=false" url
)>Link without comments
</a
>
90 <a href
=("~A?hide-nav-bars=true" url
)>Link without top nav bars
</a
>
91 <a href
=("~A?comments=false&hide-nav-bars=true" url
)>Link without comments or top nav bars
</a
>
95 (defun post-section-to-html (post &key skip-section
)
96 (schema-bind (:post
(rectify-post post
) (user-id frontpage-date curated-date meta is-event af draft
))
97 (multiple-value-bind (class title href
)
98 (cond (af (if (eq skip-section
:alignment-forum
) nil
(values "alignment-forum" "View Alignment Forum posts" "/index?view=alignment-forum")))
99 ; show alignment forum even if skip-section is t
100 ((eq skip-section t
) nil
)
102 (curated-date (if (eq skip-section
:featured
) nil
(values "featured" "View Featured posts" "/index?view=featured")))
103 (frontpage-date (if (eq skip-section
:frontpage
) nil
(values "frontpage" "View Frontpage posts" "/")))
104 (meta (if (eq skip-section
:meta
) nil
(values "meta" "View Meta posts" "/index?view=meta")))
105 (is-event (values "events" "View Events" "/index?view=events"))
106 (t (if (eq skip-section
:personal
) nil
(values "personal" (format nil
"View posts by ~A" (get-username user-id
)) (format nil
"/users/~A?show=posts" (get-user-slug user-id
))))))
107 <a class
=("post-section ~A" class
) title
=title href
=href
></a
>)))
109 (defun post-meta-to-html (post context skip-section meta-location
)
110 (schema-bind (:post
(rectify-post post
) :auto
)
111 <div class
=("post-meta~@[ ~(~A~)-post-meta~]" meta-location
)>
112 (labels ((emit-author (user-id)
113 (if (user-deleted user-id
)
114 <span class
="author">[deleted]</span>
115 <a class=("author~{ ~A~}" (list-cond
116 ((logged-in-userid user-id) "own-user-author")))
117 href=("/users/~A" (get-user-slug user-id))
119 data-full-name=(get-user-full-name user-id)>
120 (get-username user-id)
123 <div class="coauthors">
124 (emit-author user-id)
125 (do ((remaining-coauthors coauthors (rest remaining-coauthors))) ((not remaining-coauthors))
126 (with-html-stream-output (:stream stream)
128 (cond ((second remaining-coauthors) ", ")
131 (emit-author (cdr (assoc :--id (first remaining-coauthors)))))
133 (emit-author user-id)))
134 (pretty-time-html posted-at)
135 (vote-buttons base-score :with-buttons (eq context :body) :vote-count vote-count :post-id post-id :af-score (and (eq context :body) af af-base-score))
136 <a class="comment-count" href=("~A#comments" (if (eq context :body) "" (generate-item-link :post post)))>
137 (safe (pretty-number (or comment-count 0) "comment"))
139 (when (and (eq context :listing) word-count)
140 <span class="read-time" title=(safe (pretty-number word-count "word" :text))>(max 1 (round word-count 300))<span> min read</span></span>)
141 (if page-url <a class="lw2-link" href=(clean-lw-link page-url)>(main-site-abbreviation *current-site*)<span> link</span></a>)
142 (when (and legacy-id (eq context :body))
143 <a class="archive-link" href=("https://web.archive.org/web/*/~A" (lw2.legacy-archive:lw-legacy-url legacy-id title))>Archive</a>)
144 (when (nonzero-number-p nomination-count-2019)
145 <a href=("~A#nominations" (if (eq context :body) "" (generate-item-link :post post))) class="nomination-count">(safe (pretty-number nomination-count-2019 "nomination"))</a>)
146 (when (nonzero-number-p review-count-2019)
147 <a href=("~A#reviews" (if (eq context :body) "" (generate-item-link :post post))) class="review-count">(safe (pretty-number review-count-2019 "review"))</a>)
148 (with-html-stream-output (post-section-to-html post :skip-section skip-section))
149 (when (and (eq context :body) tags)
151 (tag-list-to-html *current-backend* tags)
153 (when-let ((url-host (and (eq context :listing) url (ignore-errors (quri:uri-host (quri:uri (string-trim " " url)))))))
154 <div class="link-post-domain">("(~A)" url-host)</div>)
155 (when (eq context :body)
156 (qualified-linking (generate-item-link :post post) meta-location))
159 (defun post-headline-to-html (post &key skip-section need-auth)
160 (schema-bind (:post (rectify-post post) (post-id user-id url question title))
161 <h1 class=("listing~{ ~A~}" (list-cond
162 (url "link-post-listing")
163 (question "question-post-listing")
164 ((logged-in-userid user-id) "own-post-listing")))>
165 (if url <a class="link-post-link" href=(presentable-link url)></a>)
166 <a class="post-title-link" href=(generate-post-auth-link post nil nil need-auth)>
167 (if question <span class="post-type-prefix">[Question] </span>)
168 (safe (clean-text-to-html title))
170 (if (logged-in-userid user-id) <a class="edit-post-link button" href=("/edit-post?post-id=~A" post-id)></a>)
172 (post-meta-to-html post :listing skip-section nil)))
174 (defun post-body-to-html (post)
175 (schema-bind (:post (rectify-post post) (post-id url question title html-body is-event local-start-time local-end-time location google-location contact-info) :context :body)
176 <main class=("post~{ ~A~}" (list-cond
178 (question "question-post")))>
179 <h1 class="post-title">
180 (if question <span class="post-type-prefix">[Question] </span>)
181 (safe (clean-text-to-html title :hyphenation nil))
183 (with-html-stream-output (post-meta-to-html post :body nil :top))
185 (labels ((brief-date (timestamp)
186 (local-time:format-timestring nil timestamp :timezone local-time:+utc-zone+ :format '(:day #\Space :long-month #\Space :year)))
187 (brief-time (timestamp)
188 (local-time:format-timestring nil timestamp :timezone local-time:+utc-zone+ :format '(:hour12 #\: (:min 2) #\Space :ampm))))
189 <div class="event-info">
190 (alist-bind ((geometry list)
191 (google-maps-url (or null string) :url))
193 (alist-bind ((lat (or null real)) (lng (or null real))) (cdr (assoc :location geometry))
195 (let* ((north (+ lat 0.125)) (south (- lat 0.125)) (east (+ lng 0.25)) (west (- lng 0.25)))
197 <iframe src=("https://www.openstreetmap.org/export/embed.html?bbox=~F,~F,~F,~F&layer=mapnik&marker=~F,~F" west south east north lat lng)></iframe>
199 (let* ((start-timestamp (and local-start-time (local-time:parse-timestring local-start-time)))
200 (end-timestamp (and local-end-time (local-time:parse-timestring local-end-time)))
201 (same-day (and start-timestamp end-timestamp (= (local-time:day-of start-timestamp) (local-time:day-of end-timestamp)))))
204 <li><a href=("https://www.google.com/maps/place/~F,~F" lat lng)>[Open in Google Maps]</a> <a href=("geo:~F,~F" lat lng)>[Open in local app]</a></li>)
205 (when start-timestamp
206 <li>(brief-date start-timestamp), (brief-time start-timestamp)
208 <span>—(unless same-day (format nil "~A, " (brief-date end-timestamp)))(brief-time end-timestamp)</span>)
211 <li>(safe (clean-text-to-html location))</li>)
213 <li>Contact: (safe (clean-text-to-html contact-info))</li>)
216 (when (or url (nonempty-string html-body))
217 <div class="body-text post-body">
218 (if url <p><a class="link-post-link" href=(presentable-link url)>Link post</a></p>)
219 (with-html-stream-output (:stream stream)
220 (let ((*before-clean-hook* (lambda () (clear-backlinks post-id)))
221 (*link-hook* (lambda (link) (add-backlink link post-id)))
222 (lw2.lmdb:*memoized-output-stream* stream))
223 (clean-html* (or html-body "") :with-toc t :post-id post-id)))
225 (backlinks-to-html (get-backlinks post-id) post-id)
226 (when (nonempty-string html-body)
227 (with-html-stream-output (post-meta-to-html post :body nil :bottom)))