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: ,
},
@@ -242,10 +250,20 @@ class ComposerOptions extends ImmutablePureComponent {
text: ,
},
markdown: {
- icon: 'arrow-circle-down',
+ icon: 'hashtag',
name: 'text/markdown',
text: ,
},
+ xbbcode: {
+ icon: 'thumb-tack',
+ name: 'text/x-bbcode',
+ text: ,
+ },
+ xbbcodemarkdown: {
+ icon: 'arrow-circle-down',
+ name: 'text/x-bbcode+markdown',
+ text: ,
+ },
};
// The result.
@@ -315,11 +333,13 @@ class ComposerOptions extends ImmutablePureComponent {
{showContentTypeChoice && (
&__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 => '', :html_close => '',
+ :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 => '',
+ :description => '', :example => '',
+ },
+ :ol => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :li => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :sub => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :sup => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :h1 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :h2 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :h3 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :h4 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :h5 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :h6 => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :abbr => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :hr => {
+ :html_open => '
', :html_close => '',
+ :description => '', :example => '',
+ },
+ :b => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :i => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :flip => {
+ :html_open => '', :html_close => '',
+ :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 => '', :html_close => '',
+ :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 => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :kbd => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :code => {
+ :html_open => '', :html_close => '
',
+ :description => '', :example => '',
+ },
+ :u => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :s => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :del => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :left => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :center => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :right => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :lfloat => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :rfloat => {
+ :html_open => '', :html_close => '',
+ :description => '', :example => '',
+ },
+ :spoiler => {
+ :html_open => '', :html_close => '',
+ :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>/im, '
')
+ 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}— #{encode(footer)}
"
+ 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