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
#:lw2.comment-threads
#:output-comments
)
4 (:import-from
#:alexandria
#:when-let
)
5 (:export
#:post-headline-to-html
#:post-body-to-html
))
7 (in-package #:lw2.data-viewers.post
)
9 (named-readtables:in-readtable html-reader
)
11 (define-schema-type :post
()
12 ((post-id string
:alias
:--id
)
15 (user-id (or null string
))
16 (coauthors (or null list
) :backend-type backend-lw2
:subfields
(:--id
))
17 (url (or null string
))
18 (feed-link (or null string
) :backend-type backend-feed-crossposts
)
19 (canonical-source (or null string
) :backend-type backend-feed-crossposts
)
21 (base-score (or null fixnum
))
22 (af-base-score (or null fixnum
))
23 (comment-count (or null fixnum
))
24 (page-url (or null string
))
25 (word-count (or null fixnum
))
26 (frontpage-date (or null string
))
27 (curated-date (or null string
))
28 (legacy-id t
:backend-type backend-lw2
)
30 (af boolean
:backend-type backend-alignment-forum
)
32 (question boolean
:backend-type backend-q-and-a
)
33 (debate boolean
:backend-type backend-debates
)
34 (debate-responses list
:graphql-ignore t
)
35 ;; todo: allow recursive schema types and clean this up
36 (target-post-relations list
38 :backend-type backend-related-questions
39 :subfields
((:target-post
:--id
:slug
:title
:user-id
:url
:feed-link
40 :posted-at
:base-score
:comment-count
:page-url
41 :word-count
:frontpage-date
:curated-date
:meta
42 :af
:question
:vote-count
)))
43 (source-post-relations list
45 :backend-type backend-related-questions
46 :subfields
((:source-post
:--id
:slug
:title
:user-id
:url
:feed-link
47 :posted-at
:base-score
:comment-count
:page-url
48 :word-count
:frontpage-date
:curated-date
:meta
49 :af
:question
:vote-count
)))
50 (vote-count (or null fixnum
))
51 (review-count (or null fixnum
) :backend-type backend-lw2
)
52 (is-event boolean
:backend-type backend-events
)
53 (local-start-time (or null string
) :backend-type backend-events
)
54 (local-end-time (or null string
) :backend-type backend-events
)
55 (location (or null string
) :backend-type backend-events
)
56 (google-location (or null list
) :backend-type backend-events
)
57 (contact-info (or null string
) :backend-type backend-events
)
58 (comment-sort-order (or null string
) :backend-type backend-lw2
)
59 (tags list
:graphql-ignore t
)
60 (submit-to-frontpage boolean
:backend-type backend-lw2-misc-features
)
61 (fm-crosspost list
:backend-type backend-magnum-crossposts
)
62 (html-body (or null string
) :context
:body
)))
64 (defgeneric rectify-post
* (backend post
) ; TODO this should go in a more generic postprocessing method
65 (:method
((backend t
) post
) post
)
66 (:method
((backend backend-lw2-misc-workarounds
) post
)
67 (let* ((post (if (next-method-p) (call-next-method) post
))
68 (url (cdr (assoc :url post
)))
69 (url (and url
(string-trim " " url
))))
70 (cond ((null url
) post
)
71 ((or (uiop:string-prefix-p
"http" url
) (uiop:string-prefix-p
"/" url
)) post
)
72 (t (acons :url
(concatenate 'string
"http://" url
) post
)))))
73 (:method
((backend backend-feed-crossposts
) post
)
74 (if (cdr (assoc :url post
))
76 (acons :url
(cdr (assoc :feed-link post
)) post
))))
78 (defun rectify-post (post) (rectify-post* *current-backend
* post
))
80 (defgeneric tag-list-to-html
(backend tags
)
81 (:method
((backend backend-accordius
) tags
)
82 (dolist (tag tags
) (alist-bind ((text string
)) tag
<a href
=("/tags/~A" text
)>(progn text
)</a
>)))
83 (:method
((backend backend-lw2-tags
) tags
)
84 (dolist (tag tags
) (alist-bind ((name string
) (slug string
)) (cdr (assoc :tag tag
)) <a href
=("/tag/~A" slug
)>(progn name
)</a
>))))
86 (defun qualified-linking (url meta-location
)
87 <nav class
="qualified-linking">
88 <input type
="checkbox" tabindex
="-1" id
=("qualified-linking-toolbar-toggle-checkbox-~(~A~)" meta-location
)>
89 <label for
=("qualified-linking-toolbar-toggle-checkbox-~(~A~)" meta-location
)><span
>\
;</span></label>
90 <div class
="qualified-linking-toolbar">
91 <a href
=url
>Post permalink
</a
>
92 <a href
=("~A?comments=false" url
)>Link without comments
</a
>
93 <a href
=("~A?hide-nav-bars=true" url
)>Link without top nav bars
</a
>
94 <a href
=("~A?comments=false&hide-nav-bars=true" url
)>Link without comments or top nav bars
</a
>
98 (defun post-section-to-html (post &key skip-section
)
99 (schema-bind (:post
(rectify-post post
) (user-id frontpage-date curated-date meta is-event af draft
))
100 (multiple-value-bind (class title href
)
101 (cond (af (if (eq skip-section
:alignment-forum
) nil
(values "alignment-forum" "View Alignment Forum posts" "/index?view=alignment-forum")))
102 ; show alignment forum even if skip-section is t
103 ((eq skip-section t
) nil
)
105 (curated-date (if (eq skip-section
:featured
) nil
(values "featured" "View Featured posts" "/index?view=featured")))
106 (frontpage-date (if (eq skip-section
:frontpage
) nil
(values "frontpage" "View Frontpage posts" "/")))
107 (meta (if (eq skip-section
:meta
) nil
(values "meta" "View Meta posts" "/index?view=meta")))
108 (is-event (values "events" "View Events" "/index?view=events"))
109 (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
))))))
110 <a class
=("post-section ~A" class
) title
=title href
=href
></a
>)))
112 (defun post-meta-to-html (post context skip-section meta-location
)
113 (schema-bind (:post
(rectify-post post
) :auto
)
114 <div class
=("post-meta~@[ ~(~A~)-post-meta~]" meta-location
)>
115 (labels ((emit-author (user-id)
116 (if (user-deleted user-id
)
117 <span class
="author">[deleted]</span>
118 <a class=("author~{ ~A~}" (list-cond
119 ((logged-in-userid user-id) "own-user-author")))
120 href=("/users/~A" (get-user-slug user-id))
122 data-full-name=(get-user-full-name user-id)>
123 (get-username user-id)
126 <div class="coauthors">
127 (emit-author user-id)
128 (do ((remaining-coauthors coauthors (rest remaining-coauthors))) ((not remaining-coauthors))
129 (with-html-stream-output (:stream stream)
131 (cond ((second remaining-coauthors) ", ")
134 (emit-author (cdr (assoc :--id (first remaining-coauthors)))))
136 (emit-author user-id)))
137 (pretty-time-html posted-at)
138 (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))
139 <a class="comment-count" href=("~A#comments" (if (eq context :body) "" (generate-item-link :post post)))>
140 (safe (pretty-number (or comment-count 0) "comment"))
143 <span class="read-time" title=(safe (pretty-number word-count "word" :text))>(max 1 (round word-count 300))<span> min read</span></span>)
144 (if page-url <a class="lw2-link" href=(clean-lw-link page-url)>(main-site-abbreviation *current-site*)<span> link</span></a>)
145 (when (and legacy-id (eq context :body))
146 <a class="archive-link" href=("https://web.archive.org/web/*/~A" (lw2.legacy-archive:lw-legacy-url legacy-id title))>Archive</a>)
147 (when (nonzero-number-p review-count)
148 <a href=("~A#reviews" (if (eq context :body) "" (generate-item-link :post post))) class="review-count">(safe (pretty-number review-count "review"))</a>)
149 (with-html-stream-output (post-section-to-html post :skip-section skip-section))
150 (when (and (eq context :body) tags)
152 (tag-list-to-html *current-backend* tags)
154 (when-let ((url-host (and (eq context :listing) url (ignore-errors (quri:uri-host (quri:uri (string-trim " " url)))))))
155 <div class="link-post-domain">("(~A)" url-host)</div>)
156 (when (eq context :body)
157 (qualified-linking (generate-item-link :post post) meta-location))
160 (defun post-headline-to-html (post &key skip-section need-auth)
161 (schema-bind (:post (rectify-post post) (post-id user-id url question title is-event))
162 <h1 class=("listing~{ ~A~}" (list-cond
163 (url "link-post-listing")
164 (question "question-post-listing")
165 ((logged-in-userid user-id) "own-post-listing")))>
166 (if url <a class="link-post-link" href=(presentable-link url)></a>)
167 <a class="post-title-link" href=(generate-post-auth-link post :need-auth need-auth :item-subtype (if is-event "event" "post"))>
168 (if question <span class="post-type-prefix">[Question] </span>)
169 (safe (clean-text-to-html (or (nonempty-string title) "[untitled post]")))
171 (if (logged-in-userid user-id) <a class="edit-post-link button" href=("/edit-post?post-id=~A" post-id)></a>)
173 (post-meta-to-html post :listing skip-section nil)))
175 (defun post-body-to-html (post)
176 (schema-bind (:post (rectify-post post) (post-id url question title html-body debate debate-responses is-event local-start-time local-end-time location google-location contact-info) :context :body)
177 <main class=("post~{ ~A~}" (list-cond
179 (question "question-post")))>
180 <h1 class="post-title">
181 (if question <span class="post-type-prefix">[Question] </span>)
182 (safe (clean-text-to-html title :hyphenation nil))
184 (with-html-stream-output (post-meta-to-html post :body nil :top))
186 (labels ((brief-date (timestamp)
187 (local-time:format-timestring nil timestamp :timezone local-time:+utc-zone+ :format '(:day #\Space :long-month #\Space :year)))
188 (brief-time (timestamp)
189 (local-time:format-timestring nil timestamp :timezone local-time:+utc-zone+ :format '(:hour12 #\: (:min 2) #\Space :ampm))))
190 <div class="event-info">
191 (alist-bind ((geometry list)
192 (google-maps-url (or null string) :url))
194 (alist-bind ((lat (or null real)) (lng (or null real))) (cdr (assoc :location geometry))
196 (let* ((north (+ lat 0.125)) (south (- lat 0.125)) (east (+ lng 0.25)) (west (- lng 0.25)))
198 <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>
200 (let* ((start-timestamp (and local-start-time (local-time:parse-timestring local-start-time)))
201 (end-timestamp (and local-end-time (local-time:parse-timestring local-end-time)))
202 (same-day (and start-timestamp end-timestamp (= (local-time:day-of start-timestamp) (local-time:day-of end-timestamp)))))
205 <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>)
206 (when start-timestamp
207 <li>(brief-date start-timestamp), (brief-time start-timestamp)
209 <span>—(unless same-day (format nil "~A, " (brief-date end-timestamp)))(brief-time end-timestamp)</span>)
212 <li>(safe (clean-text-to-html location))</li>)
214 <li>Contact: (safe (clean-text-to-html contact-info))</li>)
217 (when (and (or url (nonempty-string html-body))
218 (not (and debate debate-responses)))
219 <div class="body-text post-body">
220 (if url <p><a class="link-post-link" href=(presentable-link url)>Link post</a></p>)
221 (with-html-stream-output (:stream stream)
222 (let ((*before-clean-hook* (lambda () (clear-backlinks post-id)))
223 (*link-hook* (lambda (link) (add-backlink link post-id)))
224 (lw2.lmdb:*memoized-output-stream* stream))
225 (clean-html* (or html-body "") :with-toc t :post-id post-id)))
227 (when debate-responses
228 (with-html-stream-output (:stream stream)
229 (output-comments stream "debate-response" debate-responses nil :replies-open nil)))
230 (backlinks-to-html (get-backlinks post-id) post-id)
231 (when (nonempty-string html-body)
232 (with-html-stream-output (post-meta-to-html post :body nil :bottom)))