diff options
-rw-r--r-- | Gemfile | 2 | ||||
-rw-r--r-- | Gemfile.lock | 4 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/compose/components/options.js | 30 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/styles/bbcode.scss | 56 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/styles/index.scss | 2 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/styles/monsterpit.scss | 80 | ||||
-rw-r--r-- | app/javascript/styles/application.scss | 2 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/bbcode.scss | 56 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/monsterpit.scss | 74 | ||||
-rw-r--r-- | app/lib/bangtags.rb | 51 | ||||
-rw-r--r-- | app/lib/formatter.rb | 178 | ||||
-rw-r--r-- | app/lib/sanitize_config.rb | 5 | ||||
-rw-r--r-- | app/models/status.rb | 5 | ||||
-rw-r--r-- | config/settings.yml | 2 |
14 files changed, 517 insertions, 30 deletions
diff --git a/Gemfile b/Gemfile index fbf228ff2..aa818123e 100644 --- a/Gemfile +++ b/Gemfile @@ -149,3 +149,5 @@ group :production do end gem 'concurrent-ruby', require: false + +gem "ruby-bbcode", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index ad0f64e3d..359c748fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -533,6 +533,8 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) + ruby-bbcode (2.0.3) + activesupport (>= 4.2.2) ruby-progressbar (1.10.0) ruby-saml (1.9.0) nokogiri (>= 1.5.10) @@ -667,7 +669,6 @@ DEPENDENCIES brakeman (~> 4.5) browser bullet (~> 6.0) - bundler (~> 1.17) bundler-audit (~> 0.6) capistrano (~> 3.11) capistrano-rails (~> 1.4) @@ -751,6 +752,7 @@ DEPENDENCIES rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) rubocop (~> 0.69) + ruby-bbcode (~> 2.0) sanitize (~> 5.0) scss_lint (~> 0.58) sidekiq (~> 5.2) diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 0c94f5514..46b32b4a3 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -25,6 +25,14 @@ const messages = defineMessages({ defaultMessage: 'Attach...', id: 'compose.attach', }, + bbcode: { + defaultMessage: 'BBCode', + id: 'compose.content-type.bbcode', + }, + bbdown: { + defaultMessage: 'BBdown', + id: 'compose.content-type.bbdown', + }, change_privacy: { defaultMessage: 'Adjust status privacy', id: 'privacy.change', @@ -232,7 +240,7 @@ class ComposerOptions extends ImmutablePureComponent { const contentTypeItems = { plain: { - icon: 'align-left', + icon: 'file-text', name: 'text/plain', text: <FormattedMessage {...messages.plain} />, }, @@ -242,10 +250,20 @@ class ComposerOptions extends ImmutablePureComponent { text: <FormattedMessage {...messages.html} />, }, markdown: { - icon: 'arrow-circle-down', + icon: 'hashtag', name: 'text/markdown', text: <FormattedMessage {...messages.markdown} />, }, + xbbcode: { + icon: 'thumb-tack', + name: 'text/x-bbcode', + text: <FormattedMessage {...messages.bbcode} />, + }, + xbbcodemarkdown: { + icon: 'arrow-circle-down', + name: 'text/x-bbcode+markdown', + text: <FormattedMessage {...messages.bbdown} />, + }, }; // The result. @@ -315,11 +333,13 @@ class ComposerOptions extends ImmutablePureComponent { {showContentTypeChoice && ( <Dropdown disabled={disabled} - icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon} + icon={(contentTypeItems[contentType.split('/')[1].replace(/[+-]/g, '')] || {}).icon} items={[ - contentTypeItems.plain, - contentTypeItems.html, + contentTypeItems.xbbcodemarkdown, contentTypeItems.markdown, + contentTypeItems.xbbcode, + contentTypeItems.html, + contentTypeItems.plain, ]} onChange={onChangeContentType} onModalClose={onModalClose} diff --git a/app/javascript/flavours/glitch/styles/bbcode.scss b/app/javascript/flavours/glitch/styles/bbcode.scss new file mode 100644 index 000000000..80b2aa57b --- /dev/null +++ b/app/javascript/flavours/glitch/styles/bbcode.scss @@ -0,0 +1,56 @@ +/* +// Original: +// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss +*/ + +.bbcode { + &__flip-horizontal { + display: inline-block; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); + } + + &__flip-vertical { + display: inline-block; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); + } + + @for $i from 1 through 6 { + &__size-#{$i} { + font-size: #{6 * $i}px; + + & .emojione { + width: #{6 * $i}px !important; + height: #{6 * $i}px !important; + } + + & .hoverplay { + padding-left: #{6 * $i}px !important; + } + } + } + + @for $i from 1 through 2 { + &__size-#{$i}:hover { + font-size: 12px; + } + } + + &__left { display: block; text-align: left; } + &__center { display: block; text-align: center; } + &__right { display: block; text-align: right; } + &__lfloat { float: left; } + &__rfloat { float: right; } + &__spoiler-wrapper { + background: black; + color: black; + padding: 1px 2em 1px 2em; + } + &__spoiler { color: black; visibility: hidden; } + &__spoiler-wrapper:hover > &__spoiler, + &__spoiler-wrapper:active > &__spoiler + { color: white; visibility: visible; } +} diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index e1c25ac0d..90152c65c 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -24,3 +24,5 @@ @import 'accessibility'; @import 'rtl'; @import 'dashboard'; +@import 'bbcode'; +@import 'monsterpit'; diff --git a/app/javascript/flavours/glitch/styles/monsterpit.scss b/app/javascript/flavours/glitch/styles/monsterpit.scss new file mode 100644 index 000000000..7dccd81a4 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterpit.scss @@ -0,0 +1,80 @@ +.status__content__text, +.reply-indicator__content, +.composer--reply > .content, +.account__header__content, +{ + s { text-decoration: line-through; } + del { text-decoration: line-through; } + h6 { font-size: 8px; font-weight: bold; } + hr { border-color: lighten($dark-text-color, 10%); } + sub { + vertical-align: sub; + font-size: smaller; + } + sup { + vertical-align: super; + font-size: smaller; + } + pre, code { + color: lighten($dark-text-color, 33%); + } + mark { + background-color: #ccff15; + color: black; + } + blockquote { + font-style: italic; + } + .caption { + display: block; + margin: auto; + font-size: 12px !important; + padding-top: 0; + text-align: center; + max-width: 80%; + } + .caption-hidden { + display: none; + } + p.signature { + color: lighten($dark-text-color, 20%); + font-style: italic; + font-size: 12px; + text-align: right; + } +} + +div.media-caption { + p { + font-size: 12px !important; + margin-bottom: 0; + text-align: center; + } + a { + color: $secondary-text-color; + text-decoration: none; + font-weight: bold; + + &:hover { + text-decoration: underline; + + .fa { + color: lighten($dark-text-color, 7%); + } + } + + &.mention { + &:hover { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + + .fa { + color: $dark-text-color; + } + } +} diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 6db3bc3dc..fe3edca47 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -26,3 +26,5 @@ @import 'mastodon/dashboard'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; +@import 'mastodon/bbcode'; +@import 'mastodon/monsterpit'; diff --git a/app/javascript/styles/mastodon/bbcode.scss b/app/javascript/styles/mastodon/bbcode.scss new file mode 100644 index 000000000..80b2aa57b --- /dev/null +++ b/app/javascript/styles/mastodon/bbcode.scss @@ -0,0 +1,56 @@ +/* +// Original: +// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss +*/ + +.bbcode { + &__flip-horizontal { + display: inline-block; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); + } + + &__flip-vertical { + display: inline-block; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); + } + + @for $i from 1 through 6 { + &__size-#{$i} { + font-size: #{6 * $i}px; + + & .emojione { + width: #{6 * $i}px !important; + height: #{6 * $i}px !important; + } + + & .hoverplay { + padding-left: #{6 * $i}px !important; + } + } + } + + @for $i from 1 through 2 { + &__size-#{$i}:hover { + font-size: 12px; + } + } + + &__left { display: block; text-align: left; } + &__center { display: block; text-align: center; } + &__right { display: block; text-align: right; } + &__lfloat { float: left; } + &__rfloat { float: right; } + &__spoiler-wrapper { + background: black; + color: black; + padding: 1px 2em 1px 2em; + } + &__spoiler { color: black; visibility: hidden; } + &__spoiler-wrapper:hover > &__spoiler, + &__spoiler-wrapper:active > &__spoiler + { color: white; visibility: visible; } +} diff --git a/app/javascript/styles/mastodon/monsterpit.scss b/app/javascript/styles/mastodon/monsterpit.scss new file mode 100644 index 000000000..98d7450ec --- /dev/null +++ b/app/javascript/styles/mastodon/monsterpit.scss @@ -0,0 +1,74 @@ +.status__content__text, +.reply-indicator__content, +.composer--reply > .content, +.account__header__content, +{ + s { text-decoration: line-through; } + del { text-decoration: line-through; } + h6 { font-size: 8px; font-weight: bold; } + hr { border-color: lighten($dark-text-color, 10%); } + sub { + vertical-align: sub; + font-size: smaller; + } + sup { + vertical-align: super; + font-size: smaller; + } + pre, code { + color: lighten($dark-text-color, 33%); + } + mark { + background-color: #ccff15; + color: black; + } + blockquote { + font-style: italic; + } + .caption { + display: block; + margin: auto; + font-size: 12px !important; + padding-top: 0; + text-align: center; + max-width: 80%; + } + .caption-hidden { + display: none; + } +} + +div.media-caption { + p { + font-size: 12px !important; + margin-bottom: 0; + text-align: center; + } + a { + color: $secondary-text-color; + text-decoration: none; + font-weight: bold; + + &:hover { + text-decoration: underline; + + .fa { + color: lighten($dark-text-color, 7%); + } + } + + &.mention { + &:hover { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + + .fa { + color: $dark-text-color; + } + } +} diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb index 404d20a0f..4ba6b5e92 100644 --- a/app/lib/bangtags.rb +++ b/app/lib/bangtags.rb @@ -26,7 +26,7 @@ class Bangtags # list of transformation commands @tf_cmds = [] # list of post-processing commands - @post_cmds = [['signature']] + @post_cmds = [] # hash of bangtag variables @vars = account.vars # keep track of what variables we're appending the value of between chunks @@ -36,7 +36,7 @@ class Bangtags end def process - return unless status.text&.present? + return unless status.text&.present? && status.text.include?('#!') status.text.gsub!('#!!', "#\u200c!") @@ -367,16 +367,19 @@ class Bangtags who = cmd[2] if who.blank? @vars.delete('_they:are') + status.footer = nil next elsif who == 'not' who = cmd[3] next if who.blank? name = who.downcase.gsub(/\s+/, '') @vars.delete("_they:are:#{name}") - @vars.delete('_they:are') if @vars['_they:are'] == name + next unless @vars['_they:are'] == name + @vars.delete('_they:are') + status.footer = nil next end - name = who.downcase.gsub(/\s+/, '') + name = who.downcase.gsub(/\s+/, '').strip description = cmd[3..-1].join(':').strip if description.blank? if @vars["_they:are:#{name}"].nil? @@ -385,7 +388,8 @@ class Bangtags else @vars["_they:are:#{name}"] = description end - @vars['_they:are'] = name.strip + @vars['_they:are'] = name + status.footer = @vars["_they:are:#{name}"] end when 'sharekey' next if cmd[1].nil? @@ -401,6 +405,30 @@ class Bangtags @vore_stack.push('_draft') @component_stack.push(:var) add_tags(status, 'self:draft') + when 'format', 'type' + chunk = nil + next if cmd[1].nil? + content_types = { + 't' => 'text/plain', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'plain' => 'text/plain', + 'plaintext' => 'text/plain', + + 'm' => 'text/markdown', + 'md' => 'text/markdown', + 'markdown' => 'text/markdown', + + 'b' => 'text/x-bbcode', + 'bbc' => 'text/x-bbcode', + 'bbcode' => 'text/x-bbcode', + + 'bm' => 'text/x-bbcode+markdown', + 'bbm' => 'text/x-bbcode+markdown', + 'bbdown' => 'text/x-bbcode+markdown', + } + v = cmd[1].downcase + status.content_type = content_types[c] unless content_types[c].nil? when 'visibility' chunk = nil next if cmd[1].nil? @@ -421,7 +449,7 @@ class Bangtags 'world' => :public, } v = cmd[1].downcase - status.visibility = visibilities[v] if visibilities[v].nil? + status.visibility = visibilities[v] unless visibilities[v].nil? end end @@ -472,17 +500,6 @@ class Bangtags def postprocess_before_save @post_cmds.each do |post_cmd| case post_cmd[0] - when 'signature' - name = @vars['_they:are'] - next if name.blank? - description = @vars["_they:are:#{name}"] - next if description.blank? || @chunks.last(5).join.include?('—') - status.local_only = true if Status::LOCAL_ONLY_TOKENS.match?(@chunks.last) - if @chunks.first(5).any? { |c| c.strip.match?(/[\r\n]/) || c.lstrip.match?(/^(?:[>#]|```|---|\* |\d+\)|\[\wi+)/) } - @chunks << "\n\n[right]— #{description}\u200c[/right]" - else - @chunks << " [rfloat]— #{description}\u200c[/rfloat]" - end when 'media' media_idx = post_cmd[1] media_cmd = post_cmd[2] diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index cb9ca8336..42911b52a 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -30,6 +30,141 @@ class Formatter include ActionView::Helpers::TextHelper + BBCODE_TAGS = { + :url => { + :html_open => '<a href="%url%" rel="noopener nofollow" target="_blank">', :html_close => '</a>', + :description => '', :example => '', + :allow_quick_param => true, :allow_between_as_param => false, + :quick_param_format => /(\S+)/, + :quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected', + :param_tokens => [{:token => :url}] + }, + :ul => { + :html_open => '<ul>', :html_close => '</ul>', + :description => '', :example => '', + }, + :ol => { + :html_open => '<ol>', :html_close => '</ol>', + :description => '', :example => '', + }, + :li => { + :html_open => '<li>', :html_close => '</li>', + :description => '', :example => '', + }, + :sub => { + :html_open => '<sub>', :html_close => '</sub>', + :description => '', :example => '', + }, + :sup => { + :html_open => '<sup>', :html_close => '</sup>', + :description => '', :example => '', + }, + :h1 => { + :html_open => '<h1>', :html_close => '</h1>', + :description => '', :example => '', + }, + :h2 => { + :html_open => '<h2>', :html_close => '</h2>', + :description => '', :example => '', + }, + :h3 => { + :html_open => '<h3>', :html_close => '</h3>', + :description => '', :example => '', + }, + :h4 => { + :html_open => '<h4>', :html_close => '</h4>', + :description => '', :example => '', + }, + :h5 => { + :html_open => '<h5>', :html_close => '</h5>', + :description => '', :example => '', + }, + :h6 => { + :html_open => '<h6>', :html_close => '</h6>', + :description => '', :example => '', + }, + :abbr => { + :html_open => '<abbr>', :html_close => '</abbr>', + :description => '', :example => '', + }, + :hr => { + :html_open => '<hr>', :html_close => '</hr>', + :description => '', :example => '', + }, + :b => { + :html_open => '<strong>', :html_close => '</strong>', + :description => '', :example => '', + }, + :i => { + :html_open => '<em>', :html_close => '</em>', + :description => '', :example => '', + }, + :flip => { + :html_open => '<span class="bbcode__flip-%direction%">', :html_close => '</span>', + :description => '', :example => '', + :allow_quick_param => true, :allow_between_as_param => false, + :quick_param_format => /(h|v)/, + :quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected', + :param_tokens => [{:token => :direction}] + }, + :size => { + :html_open => '<span class="bbcode__size-%size%">', :html_close => '</span>', + :description => '', :example => '', + :allow_quick_param => true, :allow_between_as_param => false, + :quick_param_format => /([1-6])/, + :quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected', + :param_tokens => [{:token => :size}] + }, + :quote => { + :html_open => '<blockquote>', :html_close => '</blockquote>', + :description => '', :example => '', + }, + :kbd => { + :html_open => '<pre><code>', :html_close => '</code></pre>', + :description => '', :example => '', + }, + :code => { + :html_open => '<pre>', :html_close => '</pre>', + :description => '', :example => '', + }, + :u => { + :html_open => '<u>', :html_close => '</u>', + :description => '', :example => '', + }, + :s => { + :html_open => '<s>', :html_close => '</s>', + :description => '', :example => '', + }, + :del => { + :html_open => '<del>', :html_close => '</del>', + :description => '', :example => '', + }, + :left => { + :html_open => '<span class="bbcode__left">', :html_close => '</span>', + :description => '', :example => '', + }, + :center => { + :html_open => '<span class="bbcode__center">', :html_close => '</span>', + :description => '', :example => '', + }, + :right => { + :html_open => '<span class="bbcode__right">', :html_close => '</span>', + :description => '', :example => '', + }, + :lfloat => { + :html_open => '<span class="bbcode__lfloat">', :html_close => '</span>', + :description => '', :example => '', + }, + :rfloat => { + :html_open => '<span class="bbcode__rfloat">', :html_close => '</span>', + :description => '', :example => '', + }, + :spoiler => { + :html_open => '<span class="bbcode__spoiler-wrapper"><span class="bbcode__spoiler">', :html_close => '</span></span>', + :description => '', :example => '', + }, + } + def format(status, **options) if status.reblog? prepend_reblog = status.reblog.account.acct @@ -57,15 +192,26 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog - html = format_markdown(html) if status.content_type == 'text/markdown' - html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) + + case status.content_type + when 'text/markdown' + html = format_markdown(html) + when 'text/x-bbcode' + html = format_bbcode(html) + when 'text/x-bbcode+markdown' + html = format_bbdown(html) + end + + html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/x-bbcode text/x-bbcode+markdown text/html).include?(status.content_type)) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - unless %w(text/markdown text/html).include?(status.content_type) + unless %w(text/markdown text/x-bbcode text/x-bbcode+markdown text/html).include?(status.content_type) html = simple_format(html, {}, sanitize: false) html = html.delete("\n") end + html = append_footer(html, status.footer) + html.html_safe # rubocop:disable Rails/OutputSafety end @@ -74,6 +220,19 @@ class Formatter html.delete("\r").delete("\n") end + def format_bbcode(html, sanitize = true) + html = bbcode_formatter(html) + html = html.gsub(/<hr>.*<\/hr>/im, '<hr />') + return html unless sanitize + html = reformat(html) + html.delete("\n") + end + + def format_bbdown(html) + html = format_bbcode(html, false) + format_markdown(html) + end + def reformat(html) sanitize(html, Sanitize::Config::MASTODON_STRICT) end @@ -134,6 +293,19 @@ class Formatter private + def append_footer(html, footer) + return html if footer.blank? + "#{html.strip}<p class=\"signature\">— #{encode(footer)}</p>" + end + + def bbcode_formatter(html) + begin + html = html.bbcode_to_html(false, BBCODE_TAGS, :enable, *BBCODE_TAGS.keys) + rescue Exception => e + end + html + end + def markdown_formatter return @markdown_formatter if defined?(@markdown_formatter) diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index db6f50ed1..9756f2ef6 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -14,6 +14,8 @@ class Sanitize next true if e =~ /^(h|p|u|dt|e)-/ # microformats classes next true if e =~ /^(mention|hashtag)$/ # semantic classes next true if e =~ /^(ellipsis|invisible)$/ # link formatting classes + next true if e =~ /^bbcode__([a-z1-6\-]+)$/ # bbcode + next true if e == 'signature' end node['class'] = class_list.join(' ') @@ -23,10 +25,11 @@ class Sanitize elements: %w(p br span a abbr del pre sub sup blockquote code b strong u i em h1 h2 h3 h4 h5 h6 ul ol li hr), attributes: { - 'a' => %w(href rel class title), + 'a' => %w(href rel class title alt), 'span' => %w(class), 'abbr' => %w(title), 'blockquote' => %w(cite), + 'p' => %w(class), }, add_attributes: { diff --git a/app/models/status.rb b/app/models/status.rb index 0de92e0c6..895ac8fd6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -23,11 +23,12 @@ # in_reply_to_account_id :bigint(8) # local_only :boolean # poll_id :bigint(8) -# content_type :string # tsv :tsvector # curated :boolean default(FALSE), not null # sharekey :string # network :boolean default(FALSE), not null +# content_type :string +# footer :text # class Status < ApplicationRecord @@ -81,7 +82,7 @@ class Status < ApplicationRecord validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? - validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true + validates :content_type, inclusion: { in: %w(text/plain text/markdown text/x-bbcode text/x-bbcode+markdown text/html) }, allow_nil: true accepts_nested_attributes_for :poll diff --git a/config/settings.yml b/config/settings.yml index 0e6e97647..fc2233c9f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -64,7 +64,7 @@ defaults: &defaults show_known_fediverse_at_about_page: true show_reblogs_in_public_timelines: false show_replies_in_public_timelines: false - default_content_type: 'text/plain' + default_content_type: 'text/x-bbcode+markdown' development: <<: *defaults |