From eb464f84c4791d77ec521ad6f6622b576cbe6ecf Mon Sep 17 00:00:00 2001 From: trisua Date: Thu, 17 Jul 2025 00:36:19 -0400 Subject: [PATCH] chore: change name --- Cargo.lock | 2 +- Cargo.toml | 4 +- README.md | 6 +- examples/boilerplate.lisp | 1319 ++++++++++++++++++++++++++++++++++++- src/lib.rs | 4 +- 5 files changed, 1315 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 628dd95..314aa8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,5 @@ version = 4 [[package]] -name = "bberry" +name = "nanoneo" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 13f2005..51fe963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "bberry" +name = "nanoneo" description = "lisp-like dsl which \"compiles\" into html" version = "0.2.0" edition = "2024" authors = ["trisuaso"] -repository = "https://trisua.com/t/bberry.git" +repository = "https://trisua.com/t/nanoneo.git" license = "AGPL-3.0-or-later" diff --git a/README.md b/README.md index cffb5b6..de86024 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🫐 bberry +# 🐈‍⬛ nanoneo Super simple parser and "compiler" for turning a lisp-like DSL into HTML. @@ -43,7 +43,7 @@ This yields: # Syntax -bberry has some super simple syntax helpers: +nanoneo has some super simple syntax helpers: - You can create raw HTML elements using `(text "...")` or `(tag' "...")` - You can add attributes using `(attr "key" "value")` or `("key" "value")` @@ -53,4 +53,4 @@ bberry has some super simple syntax helpers: # License -bberry is licensed under the [AGPL-3.0](./LICENSE). +nanoneo is licensed under the [AGPL-3.0](./LICENSE). diff --git a/examples/boilerplate.lisp b/examples/boilerplate.lisp index 6c17a22..66b4553 100644 --- a/examples/boilerplate.lisp +++ b/examples/boilerplate.lisp @@ -1,15 +1,1310 @@ -(text "") ; using a raw string for the doctype is fine -(html - (head - ; everything that belongs in the head element - (title (text "Document")) +(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}") +(img + ("title" "{{ username }}'s avatar") + ("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}") + ("alt" "@{{ username }}") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) - (meta ("charset" "UTF-8")) - (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) - (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")) +(text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}") +(img + ("src" "/api/v1/communities/{{ id }}/avatar") + ("alt" "{{ community.title }}'s avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) - (link ("rel" "stylesheet") ("href" "#"))) +(text "{% else %}") +(img + ("src" "/api/v1/communities/{{ id }}/avatar") + ("alt" "{{ id }}'s avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) - (body - ; the actual body only starts here - (span ("style" "color: red") (text "Hello, world!")))) +(text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}") +(img + ("title" "{{ username }}'s banner") + ("src" "/api/v1/auth/user/{{ username }}/banner") + ("alt" "@{{ username }}'s banner") + ("class" "banner shadow w-full") + ("loading" "lazy") + ("style" "border-radius: {{ border_radius }};")) + +(text "{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if community %}") +(img + ("src" "/api/v1/communities/{{ id }}/banner") + ("alt" "{{ community.title }}'s banner") + ("class" "banner shadow") + ("loading" "lazy")) + +(text "{% else %}") +(img + ("src" "/api/v1/communities/{{ id }}/banner") + ("alt" "{{ id }}'s banner") + ("class" "banner shadow") + ("loading" "lazy")) + +(text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}") +(a + ("class" "card secondary w-full flex items-center gap-4") + ("href" "/community/{{ community.title }}") + (text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}") + (div + ("class" "flex flex-col") + (h3 + ("class" "name lg:long") + (text "{{ community.context.display_name }}")) + (span + ("class" "fade") + (b + (text "{{ community.member_count }}")) + (text "members")))) + +(text "{%- endmacro %} {% macro username(user) -%}") +(div + ("style" "display: contents") + (text "{% if user.settings.display_name -%} {{ user.settings.display_name }} {% else %} {{ user.username }} {%- endif %}")) + +(text "{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0, secondary=false) -%}") +(button + ("title" "Like") + ("class" "{% if secondary -%}quaternary{% else %}camo{%- endif %} small") + ("hook_element" "reaction.like") + ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])") + (text "{{ icon \"heart\" }} {% if likes > 0 -%}") + (span + (text "{{ likes }}")) + (text "{%- endif %}")) + +(text "{% if not user or not user.settings.hide_dislikes -%}") +(button + ("title" "Dislike") + ("class" "{% if secondary -%}quaternary{% else %}camo{%- endif %} small") + ("hook_element" "reaction.dislike") + ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])") + (text "{{ icon \"heart-crack\" }} {% if dislikes > 0 -%}") + (span + (text "{{ dislikes }}")) + (text "{%- endif %}")) + +(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%}") +(div + ("class" "flex items-center") + (a + ("href" "/@{{ user.username }}") + ("class" "flush") + ("style" "font-weight: 600") + ("target" "_top") + (text "{{ self::username(user=user) }}")) + (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") + (span + ("title" "Verified") + ("style" "color: var(--color-primary)") + ("class" "flex items-center") + (text "{{ icon \"badge-check\" }}")) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") +(div + ("style" "display: contents") + (text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}")) + +(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false) -%} {% if community and show_community and community.id != config.town_square or question %}") +(div + ("class" "card-nest") + (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}") + (div + ("class" "card small") + (a + ("href" "/api/v1/communities/find/{{ post.community }}") + ("class" "flush flex gap-1 items-center") + (text "{{ self::community_avatar(id=post.community, community=community) }}") + (b + (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) + (text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}"))) + (text "{%- endif %} {%- endif %}") + (div + ("class" "card flex flex-col gap-2 {% if secondary -%}secondary{%- endif %}") + ("id" "post:{{ post.id }}") + ("data-community" "{{ post.community }}") + ("data-ownsup" "{{ owner.permissions|has_supporter }}") + ("hook" "verify_emojis") + (div + ("class" "w-full flex gap-2") + (text "{% if not expect_repost -%}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}")) + (text "{%- endif %}") + (div + ("class" "flex flex-col w-full gap-1 post_right {% if expect_repost -%}repost{%- endif %}") + (div + ("class" "flex flex-wrap gap-2 items-center") + (text "{% if expect_repost -%}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}")) + (text "{%- endif %}") + (span + ("class" "name") + (text "{{ self::full_username(user=owner) }}")) + (text "{% if post.context.edited != 0 -%}") + (div + ("class" "flex") + (span + ("class" "fade date") + (text "{{ post.context.edited }}")) + (sup + ("title" "Edited") + (text "*"))) + (text "{% else %}") + (span + ("class" "fade date") + (text "{{ post.created }}")) + (text "{%- endif %} {% if post.context.is_nsfw -%}") + (span + ("title" "NSFW post") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"square-asterisk\" }}")) + (text "{%- endif %} {% if post.context.repost and post.context.repost.reposting %}") + (span + ("title" "Repost") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"repeat-2\" }}")) + (text "{%- endif %} {% if post.community == config.town_square -%}") + (span + ("title" "Posted to profile") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"user-round\" }}")) + (text "{%- endif %} {% if post.is_deleted -%}") + (span + ("title" "Deleted") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"trash-2\" }}")) + (text "{%- endif %}")) + (text "{% if not post.context.content_warning -%}") + (span + ("id" "post-content:{{ post.id }}") + ("class" "no_p_margin") + ("hook" "long") + (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (div + ("class" "card tertiary red flex items-center gap-2") + (text "{{ icon \"frown\" }}") + (span + (text "Could not find original post..."))) + (text "{%- endif %} {%- endif %}")) + (text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}") + (details + (summary + ("class" "card flex gap-2 flex-wrap items-center tertiary red w-full") + (text "{{ icon \"triangle-alert\" }}") + (b + (text "{{ post.context.content_warning }}"))) + (div + ("class" "flex flex-col gap-2") + (span + ("id" "post-content:{{ post.id }}") + ("class" "no_p_margin") + ("hook" "long") + (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (div + ("class" "card tertiary red flex items-center gap-2") + (text "{{ icon \"frown\" }}") + (span + (text "Could not find original post..."))) + (text "{%- endif %} {%- endif %}")) + (text "{{ self::post_media(upload_ids=post.uploads) }}"))) + (text "{%- endif %}") + (div + ("class" "flex flex-wrap gap-2 fade") + (text "{% for tag in post.context.tags %}") + (a + ("href" "/@{{ owner.username }}?tag={{ tag }}") + ("class" "flush fade") + (text "#{{ tag }}")) + (text "{% endfor %}")))) + (div + ("class" "flex justify-between items-center gap-2 w-full") + (text "{% if user -%}") + (div + ("class" "flex gap-1 reactions_box") + ("hook" "check_reactions") + ("hook-arg:id" "{{ post.id }}") + (text "{% if post.context.reactions_enabled -%} {% if post.content|length > 0 -%} {{ self::likes(id=post.id, asset_type=\"Post\", likes=post.likes, dislikes=post.dislikes) }} {%- endif %} {%- endif %} {% if post.context.repost and post.context.repost.reposting -%}") + (a + ("href" "/post/{{ post.context.repost.reposting }}") + ("class" "button small camo") + (text "{{ icon \"expand\" }}")) + (text "{%- endif %}")) + (text "{% else %}") + (div) + (text "{%- endif %}") + (div + ("class" "flex gap-1 buttons_box") + (a + ("href" "/post/{{ post.id }}") + ("class" "button camo small") + (text "{{ icon \"message-circle\" }}") + (span + (text "{{ post.comment_count }}"))) + (a + ("href" "/post/{{ post.id }}") + ("class" "button camo small") + ("target" "_blank") + (text "{{ icon \"external-link\" }}")) + (text "{% if user -%}") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (text "{% if config.town_square and post.context.reposts_enabled %}") + (b + ("class" "title") + (text "{{ text \"general:label.share\" }}")) + (button + ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}'])") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.repost\" }}"))) + (a + ("class" "button") + ("href" "/communities/intents/post?quote={{ post.id }}") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quote_post\" }}"))) + (text "{%- endif %} {% if user.id != post.owner -%}") + (b + ("class" "title") + (text "{{ text \"general:label.safety\" }}")) + (button + ("class" "red") + ("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:action.report\" }}"))) + (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") + (b + ("class" "title") + (text "{{ text \"general:action.manage\" }}")) + (text "{% if user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}#/configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}"))) + (text "{% if not post.is_deleted -%}") + (button + ("class" "red") + ("onclick" "trigger('me::remove_post', ['{{ post.id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))) + (text "{%- endif %} {% if is_helper and post.is_deleted -%}") + (button + ("class" "red") + ("onclick" "trigger('me::purge_post', ['{{ post.id }}'])") + (text "{{ icon \"trash-2\" }}") + (span + (text "{{ text \"general:action.purge\" }}"))) + (button + ("class" "green") + ("onclick" "trigger('me::restore_post', ['{{ post.id }}'])") + (text "{{ icon \"undo\" }}") + (span + (text "{{ text \"general:action.restore\" }}"))) + (text "{%- endif %} {%- endif %}"))) + (text "{%- endif %}")))) + (text "{% if community and show_community and community.id != config.town_square or question %}")) + +(text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0%}") +(div + ("class" "media_gallery gap-2") + (text "{% for upload in upload_ids %}") + (img + ("src" "/api/v1/uploads/{{ upload }}") + ("alt" "Image upload") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) + (text "{% endfor %}")) + +(text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") +(div + ("class" "w-full card-nest") + (div + ("class" "card small notif_title flex items-center") + (text "{% if not notification.read -%}") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-link)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6"))) + (text "{%- endif %}") + (b + ("class" "no_p_margin") + (text "{{ notification.title|markdown|safe }}"))) + (div + ("class" "card notif_content flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ notification.content|markdown|safe }}")) + (div + ("class" "card secondary w-full flex flex-wrap gap-2") + (text "{% if notification.read -%}") + (button + ("class" "tertiary") + ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', false])") + (text "{{ icon \"undo\" }}") + (span + (text "{{ text \"notifs:action.mark_as_unread\" }}"))) + (text "{% else %}") + (button + ("class" "green tertiary") + ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"notifs:action.mark_as_read\" }}"))) + (text "{%- endif %}") + (button + ("class" "red tertiary") + ("onclick" "trigger('me::remove_notification', ['{{ notification.id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + +(text "{%- endmacro %} {% macro user_card(user) -%}") +(a + ("class" "card-nest w-full") + ("href" "/@{{ user.username }}") + (div + ("class" "card small") + ("style" "padding: 0") + (text "{{ self::banner(username=user.username, border_radius=\"0px\") }}")) + (div + ("class" "card secondary flex items-center gap-4") + (text "{{ self::avatar(username=user.username, size=\"48px\") }}") + (div + ("class" "flex items-center") + (b + (text "{{ self::username(user=user) }}")) + (text "{{ self::online_indicator(user=user) }}")))) + +(text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}") +(div + ("class" "flex justify-between gap-2 w-full") + (text "{% if page > 0 -%}") + (a + ("class" "button quaternary") + ("href" "?page={{ page - 1 }}{{ key }}{{ value }}") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:link.previous\" }}"))) + (text "{% else %}") + (div) + (text "{%- endif %} {% if items != 0 -%}") + (a + ("class" "button quaternary") + ("href" "?page={{ page + 1 }}{{ key }}{{ value }}") + (span + (text "{{ text \"general:link.next\" }}")) + (text "{{ icon \"arrow-right\"}}")) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro online_indicator(user) -%} {% if not user.settings.private_last_seen or is_helper %}") +(div + ("class" "online_indicator") + ("style" "display: contents") + ("hook" "online_indicator") + ("hook-arg:last_seen" "{{ user.last_seen }}") + (div + ("style" "display: none") + ("hook_ui_ident" "online") + ("title" "Online") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-green)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + (div + ("style" "display: none") + ("hook_ui_ident" "idle") + ("title" "Idle") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-yellow)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + (div + ("style" "display: none") + ("hook_ui_ident" "offline") + ("title" "Offline") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: hsl(0, 0%, 50%)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6"))))) + +(text "{% else %}") +(div + ("title" "Offline") + ("style" "display: contents") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: hsl(0, 0%, 50%)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + +(text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}") +(style + (text ":root, * { + --hue: {{ user.settings.theme_hue }} !important; + }")) + +(text "{%- endif %} {% if user.settings.theme_sat -%}") +(style + (text ":root, * { + --sat: {{ user.settings.theme_sat }} !important; + }")) + +(text "{%- endif %} {% if user.settings.theme_lit -%}") +(style + (text ":root, * { + --lit: {{ user.settings.theme_lit }} !important; + }")) + +(text "{%- endif %} {% if theme_preference -%}") +(script + (text "function match_user_theme() { + const pref = \"{{ theme_preference }}\".toLowerCase(); + + if (pref === \"auto\") { + return; + } + + document.documentElement.className = pref; + } + + setTimeout(() => { + match_user_theme(); + }, 150);")) + +(text "{%- endif %}") +(div + ("style" "display: none;") + (text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}") + (style + (text "{{ user.settings.theme_custom_css }}")) + (text "{%- endif %}")) + +(text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}") +(style + (text ":root, + * { + --{{ css }}: {{ color|color }} !important; + }")) + +(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}") +(div + ("class" "card{% if secondary -%} secondary{%- endif %} flex gap-2") + (text "{% if owner.id == 0 -%}") + (span + (text "{% if profile and profile.settings.anonymous_avatar_url -%}") + (img + ("src" "/api/v1/util/proxy?url={{ profile.settings.anonymous_avatar_url }}") + ("alt" "anonymous' avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: 52px")) + (text "{% else %} {{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }} {%- endif %}")) + (text "{% else %}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}")) + (text "{%- endif %}") + (div + ("class" "flex flex-col gap-1") + (div + ("class" "flex items-center gap-2 flex-wrap") + (span + ("class" "name") + (text "{% if owner.id == 0 -%} {% if profile and profile.settings.anonymous_username -%}") + (span + ("class" "flex items-center gap-2") + (b + (text "{{ profile.settings.anonymous_username }}")) + (span + ("title" "Anonymous user") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"drama\" }}"))) + (text "{% else %}") + (b + (text "anonymous")) + (text "{%- endif %} {% else %} {{ self::full_username(user=owner) }} {%- endif %}")) + (span + ("class" "date") + (text "{{ question.created }}")) + (span + ("title" "Question") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"message-circle-heart\" }}")) + (text "{% if question.context.is_nsfw -%}") + (span + ("title" "NSFW community") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"square-asterisk\" }}")) + (text "{%- endif %} {% if question.community > 0 and show_community -%}") + (a + ("href" "/api/v1/communities/find/{{ question.community }}") + ("class" "flex items-center") + (text "{{ self::community_avatar(id=question.community, size=\"24px\") }}")) + (text "{%- endif %} {% if question.is_global -%}") + (a + ("class" "notification chip") + ("href" "/question/{{ question.id }}") + (text "{{ question.answer_count }} answers")) + (text "{%- endif %}")) + (span + ("class" "no_p_margin") + ("style" "font-weight: 500") + (text "{{ question.content|markdown|safe }}")) + (div + ("class" "flex gap-2 items-center justify-between")))) + +(text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false) -%}") +(div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"message-circle-heart\" }}") + (span + ("class" "no_p_margin") + (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_question_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))) + +(script + (text "async function create_question_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"questions::create\"]); + fetch(\"/api/v1/questions\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + receiver: \"{{ receiver }}\", + community: \"{{ community }}\", + is_global: \"{{ is_global }}\" == \"true\", + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + e.target.reset(); + } + }); + }")) + +(text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") +(div + ("class" "card-nest") + (text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}") + (div + ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") + (div + ("class" "flex gap-1 reactions_box") + ("hook" "check_reactions") + ("hook-arg:id" "{{ question[0].id }}") + (text "{{ self::likes(id=question[0].id, asset_type=\"Question\", likes=question[0].likes, dislikes=question[0].dislikes, secondary=false) }}")) + (div + ("class" "flex gap-1 buttons_box") + (a + ("href" "/question/{{ question[0].id }}") + ("class" "button small") + (text "{{ icon \"external-link\" }} {% if user -%}") + (span + (text "{{ text \"requests:label.answer\" }}")) + (text "{% else %}") + (span + (text "{{ text \"general:action.open\" }}")) + (text "{%- endif %}")) + (text "{% if user -%} {% if can_manage_questions or is_helper or question[1].id == user.id %}") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("class" "camo small red") + ("onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (text "{%- endif %} {%- endif %}")))) + +(text "{%- endmacro %} {% macro spotify_playing(state, size=\"60px\") -%} {% if state and state.data %}") +(div + ("class" "card-nest") + (div + ("class" "card flex items-center justify-between gap-2 small") + (div + ("class" "flex items-center gap-2") + (b + (text "Listening on")) + (text "{{ icon \"spotify\" }}")) + (span + ("class" "fade date short") + (text "{{ state.data.timestamp }}"))) + (div + ("class" "card secondary flex gap-2") + (a + ("href" "{{ state.external_urls.album }}") + (img + ("src" "{{ state.external_urls.album_img }}") + ("alt" "Album cover") + ("loading" "lazy") + ("class" "avatar") + ("style" "--size: {{ size }}"))) + (div + ("class" "flex flex-col") + (h5 + ("class" "w-full") + (a + ("href" "{{ state.external_urls.track }}") + ("class" "flush") + (text "{{ state.data.track }}"))) + (span + ("class" "fade") + (a + ("href" "{{ state.external_urls.artist }}") + ("class" "flush") + (text "{{ state.data.artist }}"))) + (span + ("hook" "spotify_time_text") + ("hook-arg:updated" "{{ state.data.timestamp }}") + ("hook-arg:progress" "{{ state.data.progress_ms }}") + ("hook-arg:duration" "{{ state.data.duration_ms }}") + ("hook-arg:display" "full"))))) + +(text "{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size=\"60px\") -%} {% if state and state.data %}") +(div + ("class" "card-nest") + (div + ("class" "card flex items-center justify-between gap-2 small") + (div + ("class" "flex items-center gap-2") + (b + (text "Listening on")) + (text "{{ icon \"last_fm\" }}")) + (span + ("class" "fade date short") + (text "{{ state.data.timestamp }}"))) + (div + ("class" "card secondary flex gap-2") + (a + ("href" "{{ state.external_urls.track }}") + (img + ("src" "{{ state.external_urls.track_img }}") + ("alt" "Track cover") + ("loading" "lazy") + ("class" "avatar") + ("style" "--size: {{ size }}"))) + (div + ("class" "flex flex-col") + (h5 + ("class" "w-full") + (a + ("href" "{{ state.external_urls.track }}") + ("class" "flush") + (text "{{ state.data.track }}"))) + (span + ("class" "fade") + (a + ("href" "{{ state.external_urls.artist }}") + ("class" "flush") + (text "{{ state.data.artist }}"))) + (text "{% if state.data.duration_ms and state.data.duration_ms != \"0\" -%}") + (span + ("hook" "spotify_time_text") + ("hook-arg:updated" "{{ state.data.timestamp }}") + ("hook-arg:progress" "25000") + ("hook-arg:duration" "{{ state.data.duration_ms }}") + ("hook-arg:display" "full")) + (text "{%- endif %}")))) + +(text "{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}") +(div + ("style" "display: contents;") + (text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}")) + +(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}") +(div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (text "{% if can_manage_message or (user and user.id == message.owner) -%}") + (button + ("class" "red") + ("onclick" "delete_message('{{ message.id }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))) + (text "{%- endif %}") + (button + ("onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"general:action.open\" }}"))) + (button + ("onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])") + (text "{{ icon \"copy\" }}") + (span + (text "{{ text \"general:action.copy_link\" }}"))) + (button + ("onclick" "mention_user('{{ owner.username }}')") + (text "{{ icon \"at-sign\" }}") + (span + (text "{{ text \"chats:action.mention_user\" }}"))))) + +(text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}") +(div + ("class" "card secondary message flex gap-2 {% if grouped -%}grouped{%- endif %}") + ("id" "message-{{ message.id }}") + (text "{% if not grouped -%}") + (a + ("href" "/@{{ user.username }}") + ("target" "_top") + (text "{{ self::avatar(username=user.username, size=\"42px\") }}")) + (text "{%- endif %}") + (div + ("class" "flex flex-col gap-1 w-full") + (text "{% if not grouped -%}") + (div + ("class" "flex gap-2 w-full justify-between flex-wrap") + (div + ("class" "flex gap-2") + (text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}") + (span + ("class" "date") + (text "{{ message.edited }}") + (sup + ("title" "Edited") + (text "*"))) + (text "{% else %}") + (span + ("class" "date") + (text "{{ message.created }}")) + (text "{%- endif %}")) + (div + ("class" "flex gap-2 hidden") + (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))) + (text "{%- endif %}") + (div + ("class" "flex w-full gap-2 justify-between") + (span + ("class" "no_p_margin") + (text "{{ message.content|markdown|safe }}")) + (text "{% if grouped -%}") + (div + ("class" "hidden") + (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")) + (text "{%- endif %}")))) + +(text "{%- endmacro %} {% macro user_menu() -%}") +(div + ("class" "inner") + (b + ("class" "title") + (text "{{ user.username }}")) + (a + ("href" "/@{{ user.username }}") + (text "{{ icon \"circle-user-round\" }}") + (span + (text "{{ text \"auth:link.my_profile\" }}"))) + (a + ("href" "/settings") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"auth:link.settings\" }}"))) + (text "{% if is_helper -%}") + (b + ("class" "title") + (text "{{ text \"general:label.mod\" }}")) + (a + ("href" "/mod_panel/audit_log") + (text "{{ icon \"scroll-text\" }}") + (span + (text "{{ text \"general:link.audit_log\" }}"))) + (a + ("href" "/mod_panel/reports") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:link.reports\" }}"))) + (a + ("href" "/mod_panel/ip_bans") + (text "{{ icon \"ban\" }}") + (span + (text "{{ text \"general:link.ip_bans\" }}"))) + (a + ("href" "/mod_panel/stats") + (text "{{ icon \"chart-line\" }}") + (span + (text "{{ text \"general:link.stats\" }}"))) + (text "{%- endif %}") + (b + ("class" "title") + (text "{{ config.name }}")) + (a + ("href" "https://trisua.com/t/tetratto") + (text "{{ icon \"code\" }}") + (span + (text "{{ text \"general:link.source_code\" }}"))) + ; + ; {{ icon "book" }} + ; {{ text "general:link.reference" }} + ; + (div + ("class" "title")) + (button + ("onclick" "trigger('me::switch_account')") + (text "{{ icon \"ellipsis\" }}") + (span + (text "{{ text \"general:action.switch_account\" }}"))) + (button + ("class" "red") + ("onclick" "trigger('me::logout')") + (text "{{ icon \"log-out\" }}") + (span + (text "{{ text \"auth:action.logout\" }}")))) + +(text "{%- endmacro %} {% macro user_status(other_user) -%} {% if other_user.settings.status %}") +(div + ("class" "flex items-center gap-2") + (span + (text "{{ other_user.settings.status }}")) + ; connection icon + (text "{% if (other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track) or (other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track) %} {{ icon \"music\" }} {% endif %}")) + +(text "{% elif other_user.connections.LastFm[0].data.name and other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track %}") +(div + ("class" "flex items-center gap-2") + (text "{{ icon \"music\" }}") + (span + (b + (text "Listening to")) + (text "{{ other_user.connections.LastFm[1].data.artist }}"))) + +(text "{% elif other_user.connections.Spotify[0].data.name and other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track %}") +(div + ("class" "flex items-center gap-2") + (text "{{ icon \"music\" }}") + (span + (b + (text "Listening to")) + (text "{{ other_user.connections.Spotify[1].data.artist }}"))) + +(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}") +(div + ("class" "flex gap-2 items-center card tiny user_plate {% if secondary -%}secondary{%- endif %}") + (a + ("href" "/@{{ user.username }}") + (text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}")) + (div + ("class" "flex justify-center flex-col") + ("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: 150px{%- endif %}") + (text "{{ self::full_username(user=user) }}") + (div + ("class" "user_status") + (text "{{ self::user_status(other_user=user) }}"))) + (text "{% if show_menu -%}") + (div + ("class" "dropdown") + (button + ("class" "camo small square") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"settings\" c(dropdown-arrow) }}")) + (text "{{ self::user_menu() }}")) + (text "{% elif show_kick %}") + (div + ("class" "dropdown") + ("style" "margin-left: auto") + (button + ("class" "camo small square") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("class" "red") + ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"chats:action.kick_member\" }}"))))) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}") +(button + ("class" "button small square quaternary") + ("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()") + ("title" "Emojis") + ("type" "button") + (text "{{ icon \"smile-plus\" }}")) + +(text "{% if render_dialog -%}") +(dialog + ("id" "emoji_dialog") + (div + ("class" "inner flex flex-col gap-2") + (script + ("type" "module") + ("src" "https://unpkg.com/emoji-picker-element@1.22.8/index.js")) + (emoji-picker + ("style" " + --border-radius: var(--radius); + --background: var(--color-super-raised); + --input-border-radiFus: var(--radius); + --input-border-color: var(--color-primary); + --indicator-color: var(--color-primary); + --emoji-padding: 0.25rem; + box-shadow: 0 0 4px var(--color-shadow); + ") + ("class" "w-full")) + (script + (text "setTimeout(async () => { + document.querySelector(\"emoji-picker\").customEmoji = + await trigger(\"me::emojis\"); + + const style = document.createElement(\"style\"); + style.textContent = `.custom-emoji { border-radius: 4px !important; } .category { font-weight: 600; }`; + document + .querySelector(\"emoji-picker\") + .shadowRoot.appendChild(style); + }, 150); + + document + .querySelector(\"emoji-picker\") + .addEventListener(\"emoji-click\", async (event) => { + if (event.detail.skinTone > 0) { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += event.detail.unicode; + + document.getElementById(\"emoji_dialog\").close(); + return; + } + + if (event.detail.unicode) { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += ` :${await ( + await fetch(\"/api/v1/lookup_emoji\", { + method: \"POST\", + body: event.detail.unicode, + }) + ).text()}:`; + } else { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += ` :${event.detail.emoji.shortcodes[0]}:`; + } + + document.getElementById(\"emoji_dialog\").close(); + });")) + (div + ("class" "flex justify-between") + (div) + (div + ("class" "flex gap-2") + (button + ("class" "bold red quaternary") + ("onclick" "document.getElementById('emoji_dialog').close()") + ("type" "button") + (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))))) + +(text "{%- endif %} {%- endmacro %} {% macro file_picker(files_list_id) -%}") +(button + ("class" "button small square quaternary") + ("onclick" "pick_file()") + ("title" "Images") + ("type" "button") + (text "{{ icon \"image-up\" }}")) + +(input + ("type" "file") + ("multiple" "") + ("accept" "image/png,image/jpeg,image/avif,image/webp") + ("style" "display: none") + ("name" "file_picker")) + +(div + ("style" "display: none") + ("id" "file_template") + (text "{{ icon \"image\" }}") + (b + ("class" "name shorter") + ("style" "overflow-wrap: normal") + (text ".file_name"))) + +(script + (text "(() => { + const input = document.querySelector(\"input[name=file_picker]\"); + const element = document.getElementById(\"{{ files_list_id }}\"); + const template = document.getElementById(\"file_template\"); + + globalThis.pick_file = () => { + input.click(); + }; + + globalThis.render_file_picker_files = () => { + element.innerHTML = \"\"; + + let idx = 0; + for (const file of input.files) { + element.innerHTML += `
${template.innerHTML.replace( + \".file_name\", + file.name, + )}
`; + + idx += 1; + } + }; + + globalThis.remove_file = (idx) => { + const files = Array.from(input.files); + files.splice(idx - 1, 1); + + // update files + const list = new DataTransfer(); + + for (item of files) { + list.items.add(item); + } + + input.files = list.files; + + // render + render_file_picker_files(); + }; + + input.addEventListener(\"change\", () => { + render_file_picker_files(); + }); + })();")) + +(text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}") +(div + ("class" "card w-full supporter_ad") + ("ui_ident" "supporter_ad") + ("onclick" "window.location.href = '/settings#/account/billing'") + (div + ("class" "card w-full flex flex-wrap items-center gap-2 justify-between") + (text "{% if body -%}") + (b + (text "{{ body }}")) + (text "{% else %}") + (b + (text "{{ text \"general:label.supporter_motivation\" }}")) + (text "{%- endif %}") + (a + ("href" "/settings#/account/billing") + ("class" "button small") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"general:action.become_supporter\" }}"))))) + +(text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}") +(div + ("class" "flex gap-2") + (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}") + (button + ("class" "small square quaternary") + ("title" "More options") + ("onclick" "document.getElementById('post_options_dialog').showModal()") + ("type" "button") + (text "{{ icon \"ellipsis\" }}"))) + +(dialog + ("id" "post_options_dialog") + (div + ("class" "inner flex flex-col gap-2") + (div + ("id" "post_options") + ("class" "flex flex-col gap-2")) + (hr) + (div + ("class" "flex justify-between") + (div) + (div + ("class" "flex gap-2") + (button + ("class" "bold red quaternary") + ("onclick" "document.getElementById('post_options_dialog').close()") + ("type" "button") + (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))) + (script + (text "setTimeout(() => { + window.POST_INITIAL_SETTINGS = { + comments_enabled: true, + reposts_enabled: true, + reactions_enabled: true, + is_nsfw: false, + content_warning: \"\", + tags: [], + }; + + window.BLANK_INITIAL_SETTINGS = JSON.stringify( + window.POST_INITIAL_SETTINGS, + ); + + const settings_fields = [ + [ + [ + \"comments_enabled\", + \"Allow people to comment on your post\", + ], + window.POST_INITIAL_SETTINGS.comments_enabled.toString(), + \"checkbox\", + ], + [ + [ + \"reposts_enabled\", + \"Allow people to repost/quote your post\", + ], + window.POST_INITIAL_SETTINGS.reposts_enabled.toString(), + \"checkbox\", + ], + [ + [ + \"reactions_enabled\", + \"Allow people to like/dislike your post\", + ], + window.POST_INITIAL_SETTINGS.reactions_enabled.toString(), + \"checkbox\", + ], + [ + [\"is_nsfw\", \"Hide from public timelines\"], + window.POST_INITIAL_SETTINGS.is_nsfw.toString(), + \"checkbox\", + ], + [ + [\"content_warning\", \"Content warning\"], + window.POST_INITIAL_SETTINGS.content_warning, + \"textarea\", + ], + [ + [\"tags\", \"Tags\"], + window.POST_INITIAL_SETTINGS.tags, + \"input\", + { + embed_html: + 'Tags should be separated by a comma.', + }, + ], + ]; + + document.getElementById(\"post_options\").innerHTML = \"\"; + trigger(\"ui::generate_settings_ui\", [ + document.getElementById(\"post_options\"), + settings_fields, + window.POST_INITIAL_SETTINGS, + { + tags: (new_tags) => { + window.POST_INITIAL_SETTINGS.tags = new_tags + .split(\",\") + .map((t) => t.trim()); + }, + }, + ]); + }, 250); + + globalThis.update_settings_maybe = async (id) => { + if ( + JSON.stringify(window.POST_INITIAL_SETTINGS) !== + window.BLANK_INITIAL_SETTINGS + ) { + await fetch(`/api/v1/posts/${id}/context`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + context: window.POST_INITIAL_SETTINGS, + }), + }); + } + };")))) + +(text "{%- endmacro %}") diff --git a/src/lib.rs b/src/lib.rs index d1b74ce..a857fb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,7 +92,7 @@ mod test { .unwrap(); let duration = start.elapsed().unwrap(); - println!("took: {}μs", duration.as_micros()); - assert!(duration < Duration::from_micros(500)) + println!("took: {}μs", duration.as_millis()); + assert!(duration < Duration::from_millis(100)) } }