Compare commits

..
Author SHA1 Message Date
Jeremy Daer d1376a97ee Serve Trix from the action_text-trix gem instead of a vendored copy
The importmap pinned a hand-vendored copy of Trix 2.1.19
(vendor/javascript/trix.esm.min.js) that dependency tooling can't see.
The action_text-trix gem already in the bundle ships the same 2.1.19
release as app/assets/javascripts/trix.js and adds it to the asset
precompile list, so pin against that instead and drop the vendored blob.
Trix updates now arrive through ordinary gem bumps, visible to
Dependabot and security advisories.

The gem's build is UMD rather than ESM, which makes no difference here:
application.js imports "trix" only for its side effect of registering
the trix-editor and trix-toolbar custom elements, and the UMD build does
that via its globalThis branch. No bindings are imported from the
module, and Trix styling remains app-owned in
app/assets/stylesheets/actiontext.css.
2026-08-10 20:58:41 -07:00
Jeremy Daer d3f22d78e1 Harden message content filtering + bump thruster to 0.1.23 (#237)
* Bump thruster 0.1.15 → 0.1.23

* Scrub disallowed attributes on allowed tags in message rendering

SanitizeTags removes disallowed tags from message presentation, but
attributes on the tags it allows passed through untouched. Extend the
content-filter chain with a SanitizeAttributes filter that runs Rails'
safe-list sanitizer over the remaining markup, stripping event-handler
attributes and unsafe URI schemes as defense-in-depth alongside the
existing Content-Security-Policy.

Running it after SanitizeTags with the same allowed-tags list makes the
sanitizer's tag pass a no-op, preserving SanitizeTags' remove-not-unwrap
semantics while scrubbing attributes. The attribute allowlist is the
standard ActionText set (which keeps attachments intact) plus class,
which presentation styling relies on.
2026-08-10 15:55:02 -07:00
14 changed files with 160 additions and 102 deletions

No files matched your search

+4 -4
View File
@@ -375,10 +375,10 @@ GEM
railties (>= 6.0.0)
stringio (3.1.8)
thor (1.4.0)
thruster (0.1.15-aarch64-linux)
thruster (0.1.15-arm64-darwin)
thruster (0.1.15-x86_64-darwin)
thruster (0.1.15-x86_64-linux)
thruster (0.1.23-aarch64-linux)
thruster (0.1.23-arm64-darwin)
thruster (0.1.23-x86_64-darwin)
thruster (0.1.23-x86_64-linux)
tilt (2.6.1)
timeout (0.6.1)
tsort (0.2.0)
@@ -1,14 +1,5 @@
class Accounts::CustomStylesController < ApplicationController
allow_unauthenticated_access only: :show
before_action :ensure_can_administer, :set_account, except: :show
def show
if stale?(etag: Current.account)
expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
render plain: Current.account&.custom_styles.to_s, content_type: "text/css"
end
end
before_action :ensure_can_administer, :set_account
def edit
end
+6
View File
@@ -12,6 +12,12 @@ module ApplicationHelper
end
end
def custom_styles_tag
if custom_styles = Current.account&.custom_styles
tag.style(custom_styles.to_s.html_safe, data: { turbo_track: "reload" })
end
end
def body_classes
[ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
end
+1 -1
View File
@@ -1,3 +1,3 @@
module ContentFilters
TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags)
TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags, SanitizeAttributes)
end
@@ -0,0 +1,47 @@
class ContentFilters::SanitizeAttributes < ActionText::Content::Filter
def applicable?
true
end
# Scrub attributes on the tags that SanitizeTags allows, using Rails' safe-list
# sanitizer so unsafe URI schemes and event-handler attributes are stripped.
# Runs after SanitizeTags, so passing the same allowed tags makes the tag pass
# a no-op and only attributes are scrubbed.
def apply
sanitizer.sanitize fragment.to_html, tags: allowed_tags, attributes: allowed_attributes
end
private
# Presentation styling relies on class attributes (e.g. unfurled link embeds),
# which the standard ActionText set doesn't include.
EXTRA_ALLOWED_ATTRIBUTES = %w[ class ]
# ActionText::ContentHelper.sanitizer is a single process-wide instance, and
# rails-html-sanitizer reuses one mutable permit scrubber that it reconfigures
# from the tags/attributes on every #sanitize call. Sharing it here would race
# our stricter tag set against ActionText's default rendering on concurrent
# requests, so use a dedicated instance of the same sanitizer class to keep the
# scrubber isolated.
def sanitizer
sanitizer_class.new
end
def sanitizer_class
ActionText::ContentHelper.sanitizer.class
end
def allowed_tags
ContentFilters::SanitizeTags::ALLOWED_TAGS
end
def allowed_attributes
standard_allowed_attributes + EXTRA_ALLOWED_ATTRIBUTES
end
# Mirrors ActionText::ContentHelper#sanitizer_allowed_attributes, which isn't
# exposed at the module level.
def standard_allowed_attributes
ActionText::ContentHelper.allowed_attributes ||
(sanitizer_class.allowed_attributes + ActionText::Attachment::ATTRIBUTES).to_a
end
end
+1 -1
View File
@@ -22,7 +22,7 @@
<%= tag.link rel: "apple-touch-icon", href: fresh_account_logo_path %>
<%= stylesheet_link_tag :all, "data-turbo-track": "reload" %>
<%= tag.link rel: "stylesheet", href: fresh_custom_styles_path, data: { turbo_track: "reload" } %>
<%= custom_styles_tag %>
<%= javascript_importmap_tags %>
+2 -1
View File
@@ -1,6 +1,7 @@
<%# Be sure to check/update messages/_template.html.erb when changing this file %>
<% cache message do %>
<%# Bump this version when the message presentation filters change what they emit. Editing this line changes the template digest, which busts BOTH this fragment cache and the collection cache that keys on this partial's digest (helper Ruby changes alone don't). %>
<% cache [ message, "presentation-v2" ] do %>
<%= message_tag message do %>
<h2 class="message__day-separator"><%= local_datetime_tag message.created_at, style: :date %></h2>
+1 -1
View File
@@ -5,7 +5,7 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin "@hotwired/turbo-rails", to: "turbo.js"
pin "@rails/actioncable", to: "actioncable.esm.js"
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
pin "trix", to: "trix.esm.min.js" # @2.1.19
pin "trix", to: "trix.js"
pin "@rails/actiontext", to: "actiontext.js"
pin "highlight.js", to: "highlight.js/core.js"
+1 -5
View File
@@ -21,7 +21,7 @@ Rails.application.routes.draw do
resource :join_code, only: :create
resource :logo, only: %i[ show destroy ]
resource :custom_styles, only: %i[ show edit update ]
resource :custom_styles, only: %i[ edit update ]
end
end
@@ -29,10 +29,6 @@ Rails.application.routes.draw do
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
end
direct :fresh_custom_styles do |options|
route_for :account_custom_styles, v: Current.account&.updated_at&.to_fs(:epoch)
end
get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create"
@@ -5,34 +5,6 @@ class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
sign_in :david
end
test "show serves custom styles as plain CSS" do
accounts(:signal).update! custom_styles: ":root { --color-text: red; }"
get account_custom_styles_url
assert_response :ok
assert_equal "text/css", @response.media_type
assert_equal ":root { --color-text: red; }", @response.body
end
test "show is accessible without authentication" do
accounts(:signal).update! custom_styles: ":root { --color-text: red; }"
reset!
get account_custom_styles_url
assert_response :ok
assert_equal "text/css", @response.media_type
end
test "show serves markup verbatim as inert CSS text, never HTML" do
payload = "</style><script>alert(1)</script>"
accounts(:signal).update! custom_styles: payload
get account_custom_styles_url
assert_response :ok
assert_equal "text/css", @response.media_type
assert_equal payload, @response.body
end
test "edit" do
get edit_account_custom_styles_url
assert_response :ok
+69
View File
@@ -61,6 +61,75 @@ class ContentFiltersTest < ActionView::TestCase
assert_equal "Hello World", filtered.to_html
end
test "message with a link using an unsafe URI scheme" do
message = Message.create! room: rooms(:pets), body: '<div><a href="javascript:alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_no_match /javascript:/, filtered.to_html
assert_match /<a>x<\/a>/, filtered.to_html
end
test "message with an event handler attribute on an allowed tag" do
message = Message.create! room: rooms(:pets), body: '<div><a href="/x" onmouseover="alert(1)">x</a> <span onclick="alert(2)">y</span></div>', client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_no_match /onmouseover/, filtered.to_html
assert_no_match /onclick/, filtered.to_html
assert_match /<a href="\/x">x<\/a>/, filtered.to_html
assert_match /<span>y<\/span>/, filtered.to_html
end
test "message with a data URI link" do
message = Message.create! room: rooms(:pets), body: '<div><a href="data:text/html,pwned">x</a></div>', client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_no_match /data:/, filtered.to_html
assert_match /<a>x<\/a>/, filtered.to_html
end
test "message with a safe link and formatting is preserved" do
body = '<div><a href="https://example.com">example</a> <strong>bold</strong> <code>code</code><ul><li>one</li><li>two</li></ul></div>'
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_match /<a href="https:\/\/example\.com">example<\/a>/, filtered.to_html
assert_match /<strong>bold<\/strong>/, filtered.to_html
assert_match /<code>code<\/code>/, filtered.to_html
assert_match /<ul>\s*<li>one<\/li>\s*<li>two<\/li>\s*<\/ul>/, filtered.to_html
end
test "SanitizeAttributes uses an isolated sanitizer rather than the shared ActionText instance" do
filter = ContentFilters::SanitizeAttributes.new(ActionText::Content.new("<div>x</div>"))
isolated = filter.send(:sanitizer)
assert_not_same ActionText::ContentHelper.sanitizer, isolated,
"must not reuse the process-wide ActionText sanitizer, whose permit scrubber is mutated per call"
assert_not_same isolated, filter.send(:sanitizer),
"each call must build a fresh sanitizer so concurrent requests never share scrubber state"
assert_kind_of ActionText::ContentHelper.sanitizer.class, isolated
end
test "SanitizeAttributes neutralizes unsafe input and preserves benign content on its isolated sanitizer" do
body = <<~HTML.squish
<div><a href="javascript:alert(1)" onclick="x()">link</a>
<a href="data:text/html,pwned">data</a>
<span class="cf-twitter-avatar" onmouseover="y()">avatar</span>
<img src="https://evil.example/x.svg"> Hey #{mention_attachment_for(:david)}</div>
HTML
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::SanitizeAttributes.apply(message.body.body).to_html
assert_no_match /javascript:/, filtered
assert_no_match /data:text\/html/, filtered
assert_no_match /onclick/, filtered
assert_no_match /onmouseover/, filtered
assert_no_match /evil\.example/, filtered
assert_match /<span class="cf-twitter-avatar">avatar<\/span>/, filtered
assert_match />link</, filtered
assert_match /<action-text-attachment sgid="#{users(:david).attachable_sgid}"/, filtered
end
test "message with a mention attachment" do
message = Message.create! room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason)
+27
View File
@@ -0,0 +1,27 @@
require "test_helper"
class MessagesHelperTest < ActionView::TestCase
test "message_presentation neutralizes unsafe URI schemes in links" do
message = Message.create! room: rooms(:pets), body: '<div><a href="javascript:alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
presentation = view.message_presentation(message)
assert_no_match /javascript:/, presentation
assert_match /<a>x<\/a>/, presentation
end
test "message_presentation strips event handler attributes from allowed tags" do
message = Message.create! room: rooms(:pets), body: '<div><a href="/x" onmouseover="alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
presentation = view.message_presentation(message)
assert_no_match /onmouseover/, presentation
assert_match /<a href="\/x">x<\/a>/, presentation
end
test "message_presentation preserves safe links and formatting" do
message = Message.create! room: rooms(:pets), body: '<div><a href="https://example.com">example</a> <strong>bold</strong></div>', client_message_id: "0015", creator: users(:jason)
presentation = view.message_presentation(message)
assert_match /<a href="https:\/\/example\.com"[^>]*>example<\/a>/, presentation
assert_match /<strong>bold<\/strong>/, presentation
end
end
-44
View File
@@ -1,44 +0,0 @@
require "application_system_test_case"
class CustomStylesXssTest < ApplicationSystemTestCase
# A stored payload that breaks out of an inline <style> context and executes
# as HTML. Rendered inline (the old behavior) this <script> runs and sets the
# flag; delivered as an external text/css stylesheet it is inert text.
PAYLOAD = %(</style><script>window.__xss_fired = true</script>)
setup do
accounts(:signal).update!(custom_styles: PAYLOAD)
sign_in "jz@37signals.com"
end
test "custom styles payload loads as a stylesheet and never executes" do
# Navigate to the page under test explicitly (matching the writebook
# sibling) rather than leaning on the post-login landing page — otherwise
# this coverage could silently evaporate if that landing changes.
visit root_url
# (a) custom styles arrive via an external stylesheet link, not inline markup
assert_selector "link[rel='stylesheet'][href*='custom_styles']", visible: false
# (b) the payload's <script> never executed — no breakout from CSS context
assert_nil evaluate_script("window.__xss_fired")
# (b') and it injected no live <script> element into the document
assert_no_selector "script", text: "__xss_fired", visible: false
# (c) the browser fetched the payload verbatim as text/css, so it is styled,
# never parsed as HTML
fetched = page.evaluate_async_script(<<~JS)
var done = arguments[arguments.length - 1];
var href = document.querySelector("link[rel='stylesheet'][href*='custom_styles']").href;
fetch(href).then(function(r) {
return r.text().then(function(body) {
done({ type: r.headers.get("content-type"), body: body });
});
});
JS
assert_includes fetched["type"], "text/css"
assert_includes fetched["body"], PAYLOAD
end
end
-7
View File
@@ -1,7 +0,0 @@
/*
Trix 2.1.19
Copyright © 2026 37signals, LLC
*/
var t="2.1.19";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,htmlAttributes:["language"],text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={ADD_ATTR:["language"],SAFE_FOR_XML:!1,RETURN_DOM:!0},c={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const u=[c.bytes,c.KB,c.MB,c.GB,c.TB,c.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(c.bytes);case 1:return"1 ".concat(c.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(u[i])}}};const d="\ufeff",g=" ",m=function(t){for(const e in t){const i=t[e];this[e]=i}return this},p=document.documentElement,f=p.matches,b=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||p,l=i,c="capturing"===r,u=function(t){null!=s&&0==--s&&u.destroy();const e=x(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return u.destroy=()=>a.removeEventListener(t,u,c),a.addEventListener(t,u,c),u},v=function(t){let{bubbles:e,cancelable:i,attributes:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=!1!==e,i=!1!==i;const r=document.createEvent("Events");return r.initEvent(t,e,i),null!=n&&m.call(r,n),r},A=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:p,s=v(t,{bubbles:i,cancelable:n,attributes:r});return o.dispatchEvent(s)},y=function(t,e){if(1===(null==t?void 0:t.nodeType))return f.call(t,e)},x=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(y(t,e))return t;t=t.parentNode}}},C=t=>document.activeElement!==t&&E(t,document.activeElement),E=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},S=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},T=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},k=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},w=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let D;const L=funLine truncated
/*! @license DOMPurify 3.4.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.2/LICENSE */const{entries:oe,setPrototypeOf:se,isFrozen:ae,getPrototypeOf:le,getOwnPropertyDescriptor:ce}=Object;let{freeze:ue,seal:he,create:de}=Object,{apply:ge,construct:me}="undefined"!=typeof Reflect&&Reflect;ue||(ue=function(t){return t}),he||(he=function(t){return t}),ge||(ge=function(t,e){for(var i=arguments.length,n=new Array(i>2?i-2:0),r=2;r<i;r++)n[r-2]=arguments[r];return t.apply(e,n)}),me||(me=function(t){for(var e=arguments.length,i=new Array(e>1?e-1:0),n=1;n<e;n++)i[n-1]=arguments[n];return new t(...i)});const pe=Me(Array.prototype.forEach),fe=Me(Array.prototype.lastIndexOf),be=Me(Array.prototype.pop),ve=Me(Array.prototype.push),Ae=Me(Array.prototype.splice),ye=Array.isArray,xe=Me(String.prototype.toLowerCase),Ce=Me(String.prototype.toString),Ee=Me(String.prototype.match),Se=Me(String.prototype.replace),Re=Me(String.prototype.indexOf),Te=Me(String.prototype.trim),ke=Me(Number.prototype.toString),we=Me(Boolean.prototype.toString),De="undefined"==typeof BigInt?null:Me(BigInt.prototype.toString),Le="undefined"==typeof Symbol?null:Me(Symbol.prototype.toString),Ne=Me(Object.prototype.hasOwnProperty),Ie=Me(Object.prototype.toString),Oe=Me(RegExp.prototype.test),Fe=(Pe=TypeError,function(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return me(Pe,e)});var Pe;function Me(t){return function(e){e instanceof RegExp&&(e.lastIndex=0);for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r<i;r++)n[r-1]=arguments[r];return ge(t,e,n)}}function Be(t,e){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:xe;if(se&&se(t,null),!ye(e))return t;let n=e.length;for(;n--;){let r=e[n];if("string"==typeof r){const t=i(r);t!==r&&(ae(e)||(e[n]=t),r=t)}t[r]=!0}return t}function _e(t){for(let e=0;e<t.length;e++){Ne(t,e)||(t[e]=null)}return t}function je(t){const e=de(null);for(const[i,n]of oe(t)){Ne(t,i)&&(ye(n)?e[i]=_e(n):n&&"object"==typeof n&&n.constructor===Object?e[i]=je(n):e[i]=n)}return e}function We(t,e){for(;null!==t;){const i=ce(t,e);if(i){if(i.get)return Me(i.get);if("function"==typeof i.value)return Me(i.value)}t=le(t)}return function(){return null}}const Ue=ue(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ve=ue(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),ze=ue(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),He=ue(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),qe=ue(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Je=ue(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Ke=ue(["#text"]),Ge=ue(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","contLine truncated
//# sourceMappingURL=trix.esm.min.js.map