···66```sh
77gleam add chilp@1
88```
99+## Usage
9101010-## Examples
1111+You can use chilp as any Lustre component, the main module has an easy builder which allows you to input typed `Anchors` (links to posts from which we fetch comments) instead of using attributes. If you prefer, you could also use `widget.element()` or create a Chilp widget yourself using element.element. As long as `widget.register()` is ran Chilp should be able to pick it up.
1212+1313+### Examples
11141215- [lustre_chilp_app](https://forge.strawmelonjuice.com/strawmelonjuice/chilp/src/branch/main/examples/lustre_chilp_app)
1616+1717+### Commands
1818+1919+Chilp's power is that it can rely on other backends to provide moderation, user verification, etc. However, sometimes you may want to create some custom behaviour for your site specifically. In that case, you should be able to add a comment to any offending comments to this end which Chilp recognises and processes. Chilp commands are prefixed like `-chilp <command>`.
2020+2121+- `hide`: Commenting `-chilp hide` on a comment hides that particular comment and all it's children.
2222+- `silence`: Commenting `-chilp silence` on a comment shows that particular comment but hides all it's children.
13231424Further documentation can be found at <https://hexdocs.pm/chilp>.
1525
+1-1
gleam.toml
···11name = "chilp"
22description = "Allows you to use Mastodon and Bluesky comments on your Lustre blog."
33-version = "2.0.0-rc"
33+version = "2.0.0-rc2"
44gleam = ">= 1.15.0"
55licences = ["Apache-2.0"]
66repository = { type = "tangled", user = "did:plc:jgtfsmv25thfs4zmydtbccnn", repo = "chilp" }
-123
src/chilp/internal.gleam
···11-import gleam/list
22-import gleam/string
33-import html_parser
44-import lustre/attribute
55-import lustre/element
66-import lustre/element/html
77-88-/// Attempts to do what DOMpurify does...
99-pub fn sanitise(html: String) -> String {
1010- // To a tree is the easy part.
1111- html_parser.as_tree(html)
1212- // Then reconstructing it sanely...
1313- |> sanitise_reconstruct
1414-}
1515-1616-/// Attempts to do what DOMpurify does... ...while lustre-ifying it!
1717-pub fn sanitise_ls(html: String) -> element.Element(a) {
1818- // To a tree is the easy part.
1919- html_parser.as_tree(html)
2020- // Then reconstructing it sanely...
2121- |> sanitise_reconstruct_ls
2222-}
2323-2424-fn sanitise_reconstruct(el: html_parser.Element) {
2525- case el {
2626- html_parser.EmptyElement -> ""
2727- html_parser.StartElement(name:, attributes:, children:) -> {
2828- // Opening tag
2929- let opener = case name {
3030- "b" -> "<b"
3131- "i" -> "<i"
3232- "em" -> "<em"
3333- "strong" -> "<strong"
3434- "a" -> "<a"
3535- "p" -> "<p"
3636- "br" -> "<br"
3737- "span" -> "<span"
3838- _ -> ""
3939- }
4040- opener
4141- <> list.map(attributes, fn(attrib) {
4242- case attrib {
4343- _ if opener == "" -> {
4444- ""
4545- }
4646- html_parser.Attribute(key: "href", value: link) ->
4747- " href=\"" <> link <> "\" "
4848- html_parser.Attribute(key: "class", value: classes) ->
4949- " class=\"" <> classes <> "\" "
5050- html_parser.Attribute(key: "target", value: target) ->
5151- " target=\"" <> target <> "\" "
5252- html_parser.Attribute(_, _) -> ""
5353- }
5454- })
5555- |> string.concat()
5656- <> case opener == "" {
5757- False -> ">"
5858- True -> ""
5959- }
6060- <> list.map(children, sanitise_reconstruct) |> string.concat
6161- // Closing tag
6262- <> case name {
6363- "b" -> "</b>"
6464- "i" -> "</i>"
6565- "em" -> "</em>"
6666- "strong" -> "</strong>"
6767- "a" -> "</a>"
6868- "p" -> "</p>"
6969- "br" -> "</br>"
7070- "span" -> "</span>"
7171- _ -> ""
7272- }
7373- }
7474- // AFAIK we don't have this due to parsing as tree.
7575- html_parser.EndElement(_) -> "ERROR: Did not expect an element end here!"
7676- html_parser.Content(cnt) -> cnt
7777- }
7878-}
7979-8080-fn sanitise_reconstruct_ls(el: html_parser.Element) -> element.Element(a) {
8181- case el {
8282- html_parser.EmptyElement -> element.none()
8383- html_parser.StartElement(name:, attributes:, children:) -> {
8484- // attributes
8585- let attribs =
8686- list.map(attributes, fn(attrib) {
8787- case attrib {
8888- html_parser.Attribute(key: "href", value: link) ->
8989- attribute.href(link)
9090- html_parser.Attribute(key: "class", value: classes) ->
9191- attribute.class(classes)
9292- html_parser.Attribute(key: "target", value: target) ->
9393- attribute.target(target)
9494- html_parser.Attribute(_, _) -> attribute.none()
9595- }
9696- })
9797- [
9898- list.map(children, sanitise_reconstruct_ls)
9999- |> case name {
100100- "b" -> html.b(attribs, _)
101101- "i" -> html.i(attribs, _)
102102- "em" -> html.em(attribs, _)
103103- "strong" -> html.strong(attribs, _)
104104- "a" -> html.a(
105105- [attribute.class("link link-secondary-content"), ..attribs],
106106- _,
107107- )
108108-109109- "p" -> html.p(attribs, _)
110110- "br" -> fn(_) { html.br(attribs) }
111111- "span" -> html.span(attribs, _)
112112- _ -> element.fragment
113113- },
114114- element.text(" "),
115115- ]
116116- |> element.fragment()
117117- }
118118- // AFAIK we don't have this due to parsing as tree.
119119- html_parser.EndElement(_) ->
120120- element.text("ERROR: Did not expect an element end here!")
121121- html_parser.Content(cnt) -> element.text(cnt)
122122- }
123123-}
+115-8
src/chilp/widget.gleam
···11// IMPORTS ---------------------------------------------------------------------
22-33-import chilp/internal
42import chilp/widget/anchors
53import gleam/bool
64import gleam/dynamic/decode
···1513import gleam/time/duration
1614import gleam/time/timestamp
1715import gleam/uri
1616+import html_parser
1817import lustre
1918import lustre/attribute.{attribute}
2019import lustre/component
···393392 html.section(
394393 [attribute.class("pt-5 space-y-10")],
395394 list.sort(model.cached_coalesced_view, sort_comments)
396396- |> list.map(view_rendered_comment),
395395+ |> list.map(view_rendered_comment(
396396+ _,
397397+ option.map(model.bluesky_anchor, fn(bsky) {
398398+ "https://bsky.app/profile/" <> bsky.did
399399+ }),
400400+ option.map(model.mastodon_anchor, fn(masto) {
401401+ "https://"
402402+ <> masto.instance
403403+ <> "/"
404404+ <> model.mastodon_op_username
405405+ }),
406406+ )),
397407 ),
398408 ],
399409 ),
400410 ])
401411}
402412403403-fn view_rendered_comment(comment: CoalescedView) -> Element(Msg) {
413413+fn view_rendered_comment(
414414+ comment: CoalescedView,
415415+ bsky_op_profile: Option(String),
416416+ mastodon_op_profile: Option(String),
417417+) -> Element(Msg) {
418418+ use <- bool.guard(
419419+ when: {
420420+ list.find(comment.children, fn(child) {
421421+ child.author_profile_link
422422+ == case comment.source, bsky_op_profile, mastodon_op_profile {
423423+ "Mastodon", _, Some(op) -> op
424424+ "Bluesky", Some(op), _ -> op
425425+ _, _, _ -> ""
426426+ }
427427+ && child.content |> element.to_readable_string() == "-chilp hide"
428428+ })
429429+ // If this is ok, that means a hide command by the op was found. We return this thread and don't need to process anything else in it.
430430+ |> result.is_ok()
431431+ },
432432+ return: element.none(),
433433+ )
404434 html.article([attribute.class("comment mt-2")], [
405435 html.header([attribute.class("flex")], [
406436 html.img([
···513543 "pl-5 border-s-4 border-default bg-neutral-secondary-soft",
514544 ),
515545 ],
516516- list.sort(comment.children, sort_comments)
517517- |> list.map(view_rendered_comment),
546546+ case
547547+ {
548548+ list.find(comment.children, fn(child) {
549549+ child.author_profile_link
550550+ == case comment.source, bsky_op_profile, mastodon_op_profile {
551551+ "Mastodon", _, Some(op) -> op
552552+ "Bluesky", Some(op), _ -> op
553553+ _, _, _ -> ""
554554+ }
555555+ && child.content |> element.to_readable_string()
556556+ == "-chilp silence"
557557+ })
558558+ // If this is ok, that means a hide command by the op was found. We return this thread and don't need to process anything else in it.
559559+ |> result.is_ok()
560560+ }
561561+ {
562562+ True ->
563563+ list.sort(comment.children, sort_comments)
564564+ |> list.map(view_rendered_comment(
565565+ _,
566566+ bsky_op_profile,
567567+ mastodon_op_profile,
568568+ ))
569569+ False -> [element.text("Comments on this comment are hidden.")]
570570+ },
518571 )
519572 },
520573 ]),
···824877}
825878826879// HELPERS ---------------------------------------------------------------------
880880+881881+/// Attempts to do what DOMpurify does... ...while lustre-ifying it!
882882+fn sanitise_ls(html: String) -> element.Element(a) {
883883+ // To a tree is the easy part.
884884+ html_parser.as_tree(html)
885885+ // Then reconstructing it sanely...
886886+ |> sanitise_reconstruct_ls
887887+}
888888+889889+fn sanitise_reconstruct_ls(el: html_parser.Element) -> element.Element(a) {
890890+ case el {
891891+ html_parser.EmptyElement -> element.none()
892892+ html_parser.StartElement(name:, attributes:, children:) -> {
893893+ // attributes
894894+ let attribs =
895895+ list.map(attributes, fn(attrib) {
896896+ case attrib {
897897+ html_parser.Attribute(key: "href", value: link) ->
898898+ attribute.href(link)
899899+ html_parser.Attribute(key: "class", value: classes) ->
900900+ attribute.class(classes)
901901+ html_parser.Attribute(key: "target", value: target) ->
902902+ attribute.target(target)
903903+ html_parser.Attribute(_, _) -> attribute.none()
904904+ }
905905+ })
906906+ [
907907+ list.map(children, sanitise_reconstruct_ls)
908908+ |> case name {
909909+ "b" -> html.b(attribs, _)
910910+ "i" -> html.i(attribs, _)
911911+ "em" -> html.em(attribs, _)
912912+ "strong" -> html.strong(attribs, _)
913913+ "a" -> html.a(
914914+ [attribute.class("link link-secondary-content"), ..attribs],
915915+ _,
916916+ )
917917+918918+ "p" -> html.p(attribs, _)
919919+ "br" -> fn(_) { html.br(attribs) }
920920+ "span" -> html.span(attribs, _)
921921+ _ -> element.fragment
922922+ },
923923+ element.text(" "),
924924+ ]
925925+ |> element.fragment()
926926+ }
927927+ // AFAIK we don't have this due to parsing as tree.
928928+ html_parser.EndElement(_) ->
929929+ element.text("ERROR: Did not expect an element end here!")
930930+ html_parser.Content(cnt) -> element.text(cnt)
931931+ }
932932+}
933933+827934fn sort_comments(c1: CoalescedView, c2: CoalescedView) -> order.Order {
828935 case int.compare(c1.agreeability, c2.agreeability) {
829936 order.Eq -> timestamp.compare(c1.created_at, c2.created_at)
···9821089 )
9831090 use favourite_count <- decode.field("favourites_count", decode.int)
9841091 use unescaped_html_content <- decode.field("content", decode.string)
985985- let content = internal.sanitise_ls(unescaped_html_content)
10921092+ let content = sanitise_ls(unescaped_html_content)
98610939871094 let children = []
9881095···10501157 )
10511158 Error(MastodonDescendant(
10521159 created_at:,
10531053- id:,
11601160+ id: _,
10541161 uri:,
10551162 content:,
10561163 favourite_count:,