From 09b4c05ce19d5ec824b4b382b9b8eb58baba2da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=A7=E5=B3=B6=E3=81=B2=E3=81=AA=E3=81=9F?= Date: Mon, 20 Aug 2018 23:41:39 +0900 Subject: [PATCH 1/4] SecurityUpdate --- Gemfile | 2 + Gemfile.lock | 2 + app/lib/formatter.rb | 17 +- app/lib/formatter_markdown.rb | 340 ++++++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 app/lib/formatter_markdown.rb diff --git a/Gemfile b/Gemfile index 263be0ac3..cbf00459c 100644 --- a/Gemfile +++ b/Gemfile @@ -96,6 +96,8 @@ gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: '345b7a57333 gem 'json-ld-preloaded', '~> 3.0' gem 'rdf-normalize', '~> 0.3' +gem 'redcarpet', "~> 3.4.0" + group :development, :test do gem 'fabrication', '~> 2.20' gem 'fuubar', '~> 2.4' diff --git a/Gemfile.lock b/Gemfile.lock index f185f3fa5..eb5e5445a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -496,6 +496,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) rdf (>= 2.2, < 4.0) + redcarpet (3.4.0) redis (4.1.2) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) @@ -755,6 +756,7 @@ DEPENDENCIES rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) rdf-normalize (~> 0.3) + redcarpet (~> 3.4.0) redis (~> 4.1) redis-namespace (~> 1.5) redis-rails (~> 5.0) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index b5f42305f..c1ad9c701 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'singleton' +require_relative './formatter_markdown' require_relative './sanitize_config' class Formatter @@ -35,12 +36,21 @@ class Formatter linkable_accounts << status.account html = raw_content + + mdFormatter = Formatter_Markdown.new(html) + html = mdFormatter.formatted + html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = encode_and_link_urls(html, linkable_accounts) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] html = simple_format(html, {}, sanitize: false) html = html.delete("\n") + mdLinkDecoder = MDLinkDecoder.new(html) + html = mdLinkDecoder.decode + + html.gsub!(/(&)/){"&"} + html.html_safe # rubocop:disable Rails/OutputSafety end @@ -111,13 +121,18 @@ class Formatter def encode_and_link_urls(html, accounts = nil, options = {}) entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) + mdExtractor = MDExtractor.new(html) + entities.concat(mdExtractor.extractEntities) + if accounts.is_a?(Hash) options = accounts accounts = nil end rewrite(html.dup, entities) do |entity| - if entity[:url] + if entity[:markdown] + html[entity[:indices][0]...entity[:indices][1]] + elsif entity[:url] link_to_url(entity, options) elsif entity[:hashtag] link_to_hashtag(entity) diff --git a/app/lib/formatter_markdown.rb b/app/lib/formatter_markdown.rb new file mode 100644 index 000000000..30a08d558 --- /dev/null +++ b/app/lib/formatter_markdown.rb @@ -0,0 +1,340 @@ +require 'uri' +require 'redcarpet' +require 'redcarpet/render_strip' + +class Formatter_Markdown + def initialize(html) + @html = html.dup + end + + def formatted + mdRenderer = CustomMDRenderer.new( + strikethrough: true, + hard_wrap: true, + autolink: false, + superscript:false, + fenced_link: true, + fenced_image: true, + no_intra_emphasis: true, + no_links: true, + no_styles: true, + no_images: true, + filter_html: true, + escape_html: true, + safe_links_only: true, + with_toc_data: true, + xhtml: false, + prettify: true, + link_attributes: true + ) + + md = Redcarpet::Markdown.new( + mdRenderer, + strikethrough: true, + hard_wrap: true, + superscript:false, + autolink: false, + space_after_headers: true, + no_intra_emphasis: true, + no_links: true, + no_styles: true, + no_images: true, + filter_html: true, + escape_html: true, + safe_links_only: true, + with_toc_data: true, + xhtml: false, + prettify: true, + link_attributes: true + ) + s = @html + s.gsub!(/\n[\n]+/) {"\n \n"}# 改行周りの問題を修正 + s.gsub!(/`[ ]+`/) {"` `"}# code内が半角スペースのみだとHTMLが壊れるのでそれの回避 + + renderedMD = md.render(s) + + result = renderedMD + result.gsub!(/(<\w+)([^>]*>)/) { "#{$1} data-md='true' #{$2}" }# ToDo data-md="true" を認識して他鯖の人にmarkdownの使用を伝える機能の実装 + result.gsub!(/(https?:\/\/[^<>"\[\]  ]+)/){"#{$1} "}#URLの後ろにスペースをねじ込む奴 mastodonのURL認識がゆるいのをmarkdownで対処 + + result + + end + + class CustomMDRenderer < Redcarpet::Render::HTML + + #基本的な実装の流れ + #URLの削除(mastodonの機能上URLとして認識されると十中八九HTMLが壊れるので) + #markdownコンテンツ内でのmarkdownコンテンツの禁止(意図しないHTMLタグの生成によってHTMLの不正出力を防ぐ目的) + #最後にHTMLに出力される際にHTML的にヤバイ子たちのエスケープ + + def paragraph(text) + %(#{text.strip}) + end + + def linebreak() + %(
) + end + + def block_quote(quote) + urlRemoved = "#{remove_url(quote)}" + escapedContents = "#{blockquote_markdown_escape(urlRemoved)}" + %(
#{escapedContents.strip}
) + end + + def header(text, header_level) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}\n) + end + + def codespan(code) + urlRemoved = "#{remove_url(code)}" + escapedCode = "#{escape_bbcode(urlRemoved)}" + %(#{encode(escapedCode)}) + end + + def list(contents, list_type) + if list_type == :unordered + %() + elsif list_type == :ordered + %(
    #{contents.strip}
) + else + %(<#{list_type}>#{contents.strip}) + end + end + + def list_item(text, list_type) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(
  • #{encode(mdContentsRemoved)}
  • ) + end + + def emphasis(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def double_emphasis(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def triple_emphasis(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def strikethrough(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def superscript(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def underline(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + def highlight(text) + urlRemoved = "#{remove_url(text)}" + mdContentsRemoved = "#{markdown_escape(urlRemoved)}" + %(#{encode(mdContentsRemoved)}) + end + + #オートリンクはmastodonとの相性が悪いので基本的には使わない + + def autolink(link, link_type) + %(リンク) + end + + #https以外の物がURLとして記入された時にTextをHTML的に考えて安全に表示するように変更 + + def image(link, title, alt_text) + + if alt_text =~ /[<>"\[\]  ]+/ + alt_text = "設定なし" + end + + imgcheck = "#{link}" + if imgcheck !~ /\Ahttps:\/\/[^<>"\[\]  ]+\z/ + %(#{encode(alt_text)}) + else + %() + end + end + + def link(link, title, content) + + if content =~ /([<>"\[\]  ]+|https?:\/\/|#|@)/ + content = "リンク" + elsif content !~ /.+/ + content = "リンク" + end + + linkcheck = "#{link}" + if linkcheck !~ /\Ahttps:\/\/[^<>"\[\]  ]+\z/ + %(#{encode(content)}) + else + %(#{encode(content)}) + end + end + + #ここから下はいろいろエスケープするための奴 + + #HTML的に考えてよろしくない子たちをエスケープする奴 + def encode(html) + HTMLEntities.new.encode(html) + end + + #markdownコンテンツないでURLが生成されるのを防ぐためのエスケープする奴 + def remove_url(string) + url = string.gsub(/https?:\/\//){ "URL:" } + reply = url.gsub(/@/){ "@" } + hashTag = reply.gsub(/#/){ "#" } + end + + #前々から要望があったcode内でBBCodeを無効化するための奴 + def escape_bbcode(string) + string.gsub(/\[/){ "[" } + end + + #markdownの中でmarkdownを展開させないためのエスケープする奴 + + #blockquote以外は下のが使える + def markdown_escape(string) + string.gsub(/<[^>]+>/) { "" } + end + + #blockquoteコンテンツ内でblockquoteタグだけを許可するためのエスケープ + def blockquote_markdown_escape(string) + string.gsub(/<([\/]?a[^>]*|[\/]?img[^>]*|[\/]?code[^>]*|[\/]?h[1-6][^>]*|[\/]?sup[^>]*|[\/]?sub[^>]*|[\/]?small[^>]*|[\/]?ul[^>]*|[\/]?ol[^>]*|[\/]?li[^>]*|[\/]?hr[^>]*|[\/]?s[^>]*|[\/]?u[^>]*|[\/]?mark[^>]*)>/) { "" } + end + + #テストで書きなぐった奴 + def html_escape(string) + string.gsub(/['&\"<>\/]/, { + '&' => '&', + '<' => '<', + '>' => '>', + '"' => '"', + "'" => ''', + "/" => '/', + }) + end + + end + +end + +#URLとかいう人類には早すぎたやばい子たちを大人しくするために必要な機構 + +class MDLinkDecoder + def initialize(html) + @html = html.dup + end + + def decode + imageDecoded = @html.gsub(/]*)>/) { "" } + + imageDecoded.gsub(/]*)>/) { "" } + end +end + +#エスケープを回避するHTMLタグの設定とかその他 + +class MDExtractor + def initialize(html) + @html = html.dup + end + + def extractEntities + [ + extractByHTMLTagName("h1"), + extractByHTMLTagName("h2"), + extractByHTMLTagName("h3"), + extractByHTMLTagName("h4"), + extractByHTMLTagName("h5"), + extractByHTMLTagName("h6"), + extractByHTMLTagName("em"), + extractByHTMLTagName("sup"), + extractByHTMLTagName("sub"), + extractByHTMLTagName("small"), + extractByHTMLTagName("u"), + extractByHTMLTagName("strong"), + extractByHTMLTagName("ul", false, false, "li"), + extractByHTMLTagName("ol", false, false, "li"), + extractByHTMLTagName("code"), + extractByHTMLTagName("blockquote", false), + extractByHTMLTagName("hr", false, true), + extractByHTMLTagName("br", false, true), + extractByHTMLTagName("a"), + extractByHTMLTagName("img", false, true), + extractByHTMLTagName("s") + ].flatten.compact + end + + def extractByHTMLTagName(tagName, isNoNest = true, isSingle = false, itemTagName = nil) + entities = [] + + @html.to_s.scan(htmlTagPatternByCond(tagName, isNoNest, isSingle, itemTagName)) do + match = $~ + + beginPos = match.char_begin(0) + endPos = match.char_end(0) + #puts "MDExtractor extracted with:\n" + @html + "\nbeginPos: " + beginPos.to_s + ", endPos: " + endPos.to_s + ", length: " + @html.length.to_s + + entity = { + :markdown => true, + :indices => [beginPos, endPos] + } + + entities.push(entity) + end + + entities + end + + def htmlTagPatternByCond(tagName, isNoNest, isSingle, itemTagName) + if isSingle + htmlTagPatternSingle(tagName) + elsif isNoNest + htmlTagPatternNoNest(tagName) + elsif itemTagName && itemTagName.length > 0 + htmlTagPatternOuterMostWithItem(tagName, itemTagName) + else + htmlTagPatternOuterMost(tagName) + end + end + + def htmlTagPattern(tagName) + Regexp.compile("<#{tagName} data-md=[^>]*>(?:[^<]|<#{tagName} data-md=[^>]*>|<\\/#{tagName}>)*(?:<\\/#{tagName}>)*") + end + + def htmlTagPatternNoNest(tagName) + Regexp.compile("<#{tagName} data-md=[^>]*>(?:.|\n)*?<\\/#{tagName}>") + end + + def htmlTagPatternSingle(tagName) + Regexp.compile("<#{tagName} data-md=[^>]*>") + end + + # https://stackoverflow.com/questions/546433/regular-expression-to-match-outer-brackets + def htmlTagPatternOuterMost(tagName) + Regexp.compile("<#{tagName} data-md=[^>]*>(?:[^<>]|(\\g<0>))*<\/#{tagName}>") + end + + def htmlTagPatternOuterMostWithItem(tagName, itemTagName) + Regexp.compile("<#{tagName} data-md=[^>]*>(?:[^<>]|<#{itemTagName} data-md=[^>]*>|<\\/#{itemTagName}>|(\\g<0>))*<\/#{tagName}>") + end +end \ No newline at end of file From 0542d92c40d5e651b47f5552ab93ec8eb04292a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=A7=E5=B3=B6=E3=81=B2=E3=81=AA=E3=81=9F?= Date: Mon, 20 Aug 2018 23:42:23 +0900 Subject: [PATCH 2/4] add Markdown front-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdownのフロントエンドの追加です --- app/javascript/styles/application.scss | 2 + app/javascript/styles/contrast.scss | 2 + app/javascript/styles/markdown.scss | 196 ++++++++++++++++++++++ app/javascript/styles/mastodon-light.scss | 2 + 4 files changed, 202 insertions(+) create mode 100644 app/javascript/styles/markdown.scss diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 8ebc45b62..cf32a70b0 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 'markdown'; diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss index 5b43aecbe..31805363c 100644 --- a/app/javascript/styles/contrast.scss +++ b/app/javascript/styles/contrast.scss @@ -1,3 +1,5 @@ @import 'contrast/variables'; @import 'application'; @import 'contrast/diff'; + +@import 'markdown'; diff --git a/app/javascript/styles/markdown.scss b/app/javascript/styles/markdown.scss new file mode 100644 index 000000000..d1c1adb9c --- /dev/null +++ b/app/javascript/styles/markdown.scss @@ -0,0 +1,196 @@ +.status__content { + + font-family: inherit; + max-height: 210px; + overflow-x: hidden; + overflow-y: auto; + + h1{ + font-size: 195%; + border-bottom: 2px solid #ff3333; + } + + h2{ + font-size: 180%; + border-bottom: 2px solid #ff3333; + } + + h3{ + font-size: 165%; + border-bottom: 2px solid #ff3333; + } + + h4{ + font-size: 150%; + border-bottom: 2px solid #ff3333; + } + + h5{ + font-size: 135%; + border-bottom: 2px solid #ff3333; + } + + h6{ + font-size: 120%; + border-bottom: 2px solid #ff3333; + } + + em{ + font-style: italic; + } + + strong{ + font-weight: bold; + } + + code{ + display: block; + border-left: 2px solid; + border-color: #079903; + color: $white; + padding-left: 10px; + margin-top: 5px; + margin-bottom: 5px; + margin-left: 5px; + background-color: #000000; + } + + pre{ + display: inline-block; + font-family: 'Noto Sans Japanese', sans-serif; + font-weight: 400; + } + + p ~ blockquote { + margin-top: -8px; + } + + blockquote{ + padding-left: 8px; + margin-top: 0.5em; + margin-bottom: 0.5em; + color: $white; + background-color: $ui-base-color; + display: block; + border-left: 4px solid $classic-highlight-color; + } + + ul{ + list-style: inside; + br{ + display: none; + } + } + + ul>ul{ + br{ + display: none; + } + } + + ul>li>ul{ + padding-left: 1em; + list-style: inside circle; + } + + ul>li{ + padding-left: 1em; + list-style: inside circle; + } + + ul>br>li{ + padding-left: 1em; + list-style: inside circle; + } + + ul>ul>li{ + padding-left: 2em; + list-style: inside circle; + } + + ul>br>ul>br>li{ + padding-left: 2em; + list-style: inside circle; + } + + ol{ + list-style: inside decimal; + } + + ol>li>ol{ + padding-left: 1em; + } + + p>a>img{ + width: 100%; + height: 100%; + object-fit: contain; + } + + a>img{ + width: 100%; + height: 100%; + object-fit: contain; + } + + p>a{ + color: #1FBFF9; + } + + a{ + color: #1FBFF9; + } + + sup{ + font-size: 75.5%; + vertical-align: top; + position: relative; + top: -0.5em; + } + + sub{ + font-size: 75.5%; + vertical-align: bottom; + position: relative; + top: 0.5em; + } + + small{ + font-size: 50.0%; + vertical-align: bottom; + position: relative; + } + + table { + margin-top: 0.5em; + margin-bottom: 0.5em; + color: $classic-highlight-color; + display: block; + width: 100%; + overflow: auto; + border-spacing: 0; + border-collapse: collapse; + } + + table tr{ + background-color: #000000; + } + + table th, table td{ + padding: 6px 13px; + border: 1px solid $classic-highlight-color; + } + + table th{ + font-weight: 600; + } + + table thead tr{ + background-color: $black; + } + + td, th{ + padding: 0; + } + + } \ No newline at end of file diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss index 756a12d86..c99938bfa 100644 --- a/app/javascript/styles/mastodon-light.scss +++ b/app/javascript/styles/mastodon-light.scss @@ -1,3 +1,5 @@ @import 'mastodon-light/variables'; @import 'application'; @import 'mastodon-light/diff'; + +@import 'markdown'; From 4d4f8b5f5217dc47b144767905f5b55c1865df30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=A7=E5=B3=B6=E3=81=B2=E3=81=AA=E3=81=9F?= Date: Wed, 28 Nov 2018 20:02:48 +0900 Subject: [PATCH 3/4] UPDATE Markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit アスタルテで動いてる最新のMarkdownに追従する更新です。 一応テストしてから実装してください。 今回のアップデートで + 見出しのレイアウト変更 + リストのレイアウトの変更 + 罫線のレイアウトの変更 + より詳細なclass分け + codeのハイライト + 外部サーバー向け画像添付メッセージの表示 + 1件のセキュリティー的な問題の修正 などが変更になっています --- app/javascript/styles/markdown.scss | 462 ++++++++++++++++------------ app/lib/formatter_markdown.rb | 43 ++- 2 files changed, 301 insertions(+), 204 deletions(-) diff --git a/app/javascript/styles/markdown.scss b/app/javascript/styles/markdown.scss index d1c1adb9c..be08de0cd 100644 --- a/app/javascript/styles/markdown.scss +++ b/app/javascript/styles/markdown.scss @@ -1,196 +1,266 @@ -.status__content { - - font-family: inherit; - max-height: 210px; - overflow-x: hidden; - overflow-y: auto; - - h1{ - font-size: 195%; - border-bottom: 2px solid #ff3333; - } - - h2{ - font-size: 180%; - border-bottom: 2px solid #ff3333; - } - - h3{ - font-size: 165%; - border-bottom: 2px solid #ff3333; - } - - h4{ - font-size: 150%; - border-bottom: 2px solid #ff3333; - } - - h5{ - font-size: 135%; - border-bottom: 2px solid #ff3333; - } - - h6{ - font-size: 120%; - border-bottom: 2px solid #ff3333; - } - - em{ - font-style: italic; - } - - strong{ - font-weight: bold; - } - - code{ - display: block; - border-left: 2px solid; - border-color: #079903; - color: $white; - padding-left: 10px; - margin-top: 5px; - margin-bottom: 5px; - margin-left: 5px; - background-color: #000000; - } - - pre{ - display: inline-block; - font-family: 'Noto Sans Japanese', sans-serif; - font-weight: 400; - } - - p ~ blockquote { - margin-top: -8px; - } - - blockquote{ - padding-left: 8px; - margin-top: 0.5em; - margin-bottom: 0.5em; - color: $white; - background-color: $ui-base-color; - display: block; - border-left: 4px solid $classic-highlight-color; - } - - ul{ - list-style: inside; - br{ - display: none; - } - } - - ul>ul{ - br{ - display: none; - } - } - - ul>li>ul{ - padding-left: 1em; - list-style: inside circle; - } - - ul>li{ - padding-left: 1em; - list-style: inside circle; - } - - ul>br>li{ - padding-left: 1em; - list-style: inside circle; - } - - ul>ul>li{ - padding-left: 2em; - list-style: inside circle; - } - - ul>br>ul>br>li{ - padding-left: 2em; - list-style: inside circle; - } - - ol{ - list-style: inside decimal; - } - - ol>li>ol{ - padding-left: 1em; - } - - p>a>img{ - width: 100%; - height: 100%; - object-fit: contain; - } - - a>img{ - width: 100%; - height: 100%; - object-fit: contain; - } - - p>a{ - color: #1FBFF9; - } - - a{ - color: #1FBFF9; - } - - sup{ - font-size: 75.5%; - vertical-align: top; - position: relative; - top: -0.5em; - } - - sub{ - font-size: 75.5%; - vertical-align: bottom; - position: relative; - top: 0.5em; - } - - small{ - font-size: 50.0%; - vertical-align: bottom; - position: relative; - } - - table { - margin-top: 0.5em; - margin-bottom: 0.5em; - color: $classic-highlight-color; - display: block; - width: 100%; - overflow: auto; - border-spacing: 0; - border-collapse: collapse; - } - - table tr{ - background-color: #000000; - } - - table th, table td{ - padding: 6px 13px; - border: 1px solid $classic-highlight-color; - } - - table th{ - font-weight: 600; - } - - table thead tr{ - background-color: $black; - } - - td, th{ - padding: 0; - } - - } \ No newline at end of file +.status__content { + + font-family: inherit; + + h1{ + color: #ec840d; + font-weight: bold; + font-size: 1.6em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #ff0000; + } + + h2{ + color: #ec840d; + font-weight: bold; + font-size: 1.5em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #fffb00; + } + + h3{ + color: #ec840d; + font-weight: bold; + font-size: 1.4em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #2bff00; + } + + h4{ + color: #ec840d; + font-weight: bold; + font-size: 1.3em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #00ffea; + } + + h5{ + color: #ec840d; + font-weight: bold; + font-size: 1.2em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #0004ff; + } + + h6{ + color: #ec840d; + font-weight: bold; + font-size: 1.1em; + padding: 0.5em; + display: inline-block; + line-height: 1.3; + background: #dbebf8; + vertical-align: middle; + border-radius: 25px 25px 25px 25px; + text-align: center; + border-bottom: solid 3px #7700ff; + } + + em{ + font-style: italic; + } + + strong{ + font-weight: bold; + } + + code{ + display: block; + border-left: 2px solid; + border-color: #079903; + color: $white; + padding-left: 10px; + margin-top: 5px; + margin-bottom: 5px; + margin-left: 5px; + background-color: #000000; + + .positive{ + color: #5bda57; + } + + .negative{ + color: #ff4949; + } + + .rust-fanc{ + color: #ba7eff; + } + + .ruby-func{ + color: #24a8e6; + } + + .rust-macro{ + color: #d2ff6a; + } + + .contents{ + color: #ff9925; + } + } + + pre{ + display: inline-block; + font-family: 'Noto Sans Japanese', sans-serif; + font-weight: 400; + } + + p ~ blockquote { + margin-top: -8px; + } + + blockquote{ + padding-left: 8px; + margin-top: 0.5em; + margin-bottom: 0.5em; + color: $primary-text-color; + background-color: $ui-base-color; + display: block; + border-left: 4px solid $classic-highlight-color; + } + + ul.md-contents { + border: double 4px #21b384; + padding: 0.5em 1em 0.5em 2.3em; + position: relative; + } + ul li.md-contents { + line-height: 1.5; + padding: 0.2em 0; + list-style-type: none!important; + } + ul li.md-contents:before { + font-family: FontAwesome; + content: "\f0a4"; + position: absolute; + left : 1em; + color: #21b384; + } + + ol.md-contents { + border: double 4px #ff954f; + padding: 0.5em 1em 0.5em 1em; + position: relative; + list-style: inside decimal; + } + + ol li.md-contents { + line-height: 1.5; + padding: 0.2em 0; + } + + hr { + border-width: 2px 0px 0px 0px; + border-style: dashed; + border-color: #ff7676; + height: 1px; + } + + p>a>img{ + width: 100%; + height: 100%; + object-fit: contain; + } + + a>img{ + width: 100%; + height: 100%; + object-fit: contain; + } + + p>a{ + color: #1FBFF9; + } + + a{ + color: #1FBFF9; + } + + sup{ + font-size: 75.5%; + vertical-align: top; + position: relative; + top: -0.5em; + } + + sub{ + font-size: 75.5%; + vertical-align: bottom; + position: relative; + top: 0.5em; + } + + small{ + font-size: 50.0%; + vertical-align: bottom; + position: relative; + } + + table { + margin-top: 0.5em; + margin-bottom: 0.5em; + color: $classic-highlight-color; + display: block; + width: 100%; + overflow: auto; + border-spacing: 0; + border-collapse: collapse; + } + + table tr{ + background-color: #000000; + } + + table th, table td{ + padding: 6px 13px; + border: 1px solid $classic-highlight-color; + } + + table th{ + font-weight: 600; + } + + table thead tr{ + background-color: $black; + } + + td, th{ + padding: 0; + } + + span.img_FTL { + display: none; + } + +} \ No newline at end of file diff --git a/app/lib/formatter_markdown.rb b/app/lib/formatter_markdown.rb index 30a08d558..0ee01b972 100644 --- a/app/lib/formatter_markdown.rb +++ b/app/lib/formatter_markdown.rb @@ -88,26 +88,31 @@ class Formatter_Markdown %(#{encode(mdContentsRemoved)}\n) end + def block_code(code, language) + %(
    #{code.strip}) + end + def codespan(code) urlRemoved = "#{remove_url(code)}" escapedCode = "#{escape_bbcode(urlRemoved)}" - %(#{encode(escapedCode)}) + encoded = "#{encode(escapedCode)}" + %(#{code_contents(encoded)}) end def list(contents, list_type) if list_type == :unordered - %(
      #{contents.strip}
    ) + %(
      #{contents.strip}
    ) elsif list_type == :ordered - %(
      #{contents.strip}
    ) + %(
      #{contents.strip}
    ) else - %(<#{list_type}>#{contents.strip}) + %(<#{list_type} class='md-contents'>#{contents.strip}) end end def list_item(text, list_type) urlRemoved = "#{remove_url(text)}" mdContentsRemoved = "#{markdown_escape(urlRemoved)}" - %(
  • #{encode(mdContentsRemoved)}
  • ) + %(
  • #{encode(mdContentsRemoved)}
  • ) end def emphasis(text) @@ -170,7 +175,7 @@ class Formatter_Markdown if imgcheck !~ /\Ahttps:\/\/[^<>"\[\]  ]+\z/ %(#{encode(alt_text)}) else - %() + %(画像が添付されています。) end end @@ -206,7 +211,7 @@ class Formatter_Markdown #前々から要望があったcode内でBBCodeを無効化するための奴 def escape_bbcode(string) - string.gsub(/\[/){ "[" } + string.gsub(/\[/){ "[" } end #markdownの中でmarkdownを展開させないためのエスケープする奴 @@ -221,6 +226,27 @@ class Formatter_Markdown string.gsub(/<([\/]?a[^>]*|[\/]?img[^>]*|[\/]?code[^>]*|[\/]?h[1-6][^>]*|[\/]?sup[^>]*|[\/]?sub[^>]*|[\/]?small[^>]*|[\/]?ul[^>]*|[\/]?ol[^>]*|[\/]?li[^>]*|[\/]?hr[^>]*|[\/]?s[^>]*|[\/]?u[^>]*|[\/]?mark[^>]*)>/) { "" } end + #code内の一部を色分けするための変更 + def code_contents(string) + simple = string.gsub(/(true|error|false|failed|def|puts|end|fn|let|mut|use|String|println!)/ , + "true" => "#{:true}", + "error" => "#{:error}", + "false" => "#{:false}", + "failed" => "#{:failed}", + "def" => "#{:def}", + "puts" => "#{:puts}", + "end" => "#{:end}", + "fn" => "#{:fn}", + "let" => "#{:let}", + "mut" => "#{:mut}", + "use" => "#{:use}", + "String" => "#{:String}", + "println!" => "#{:println!}", + ) + simple.gsub(/("[a-zA-Z0-9_ ,]+")/){ "#{$1}" } +# "" => "#{:}", + end + #テストで書きなぐった奴 def html_escape(string) string.gsub(/['&\"<>\/]/, { @@ -280,7 +306,8 @@ class MDExtractor extractByHTMLTagName("br", false, true), extractByHTMLTagName("a"), extractByHTMLTagName("img", false, true), - extractByHTMLTagName("s") + extractByHTMLTagName("s"), + extractByHTMLTagName("span") ].flatten.compact end From 8c9d3e4c2d5b02359f18de76c10685044995d741 Mon Sep 17 00:00:00 2001 From: YoheiZuho Date: Mon, 12 Aug 2019 17:07:21 +0000 Subject: [PATCH 4/4] =?UTF-8?q?Markdown=E3=81=AE=E5=86=8D=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/announcement_icon_button.js | 90 +++++++++++++ .../compose/components/announcements.js | 127 ++++++++++++++++++ .../containers/announcements_container.js | 11 ++ .../mastodon/features/compose/index.js | 2 + app/javascript/styles/application.scss | 1 + app/javascript/styles/astarte.scss | 59 ++++++++ app/lib/formatter_markdown.rb | 2 +- 7 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 app/javascript/mastodon/components/announcement_icon_button.js create mode 100644 app/javascript/mastodon/features/compose/components/announcements.js create mode 100644 app/javascript/mastodon/features/compose/containers/announcements_container.js create mode 100644 app/javascript/styles/astarte.scss diff --git a/app/javascript/mastodon/components/announcement_icon_button.js b/app/javascript/mastodon/components/announcement_icon_button.js new file mode 100644 index 000000000..0de58c6f7 --- /dev/null +++ b/app/javascript/mastodon/components/announcement_icon_button.js @@ -0,0 +1,90 @@ +import React from 'react'; +import Motion from 'react-motion/lib/Motion'; +import spring from 'react-motion/lib/spring'; +import PropTypes from 'prop-types'; + +class IconButton extends React.PureComponent { + + static propTypes = { + className: PropTypes.string, + title: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + onClick: PropTypes.func, + size: PropTypes.number, + active: PropTypes.bool, + style: PropTypes.object, + activeStyle: PropTypes.object, + disabled: PropTypes.bool, + inverted: PropTypes.bool, + animate: PropTypes.bool, + overlay: PropTypes.bool, + }; + + static defaultProps = { + size: 18, + active: false, + disabled: false, + animate: false, + overlay: false, + }; + + handleClick = (e) => { + e.preventDefault(); + + if (!this.props.disabled) { + this.props.onClick(e); + } + } + + render () { + const style = { + fontSize: `${this.props.size}px`, + width: `${this.props.size * 1.28571429}px`, + height: `${this.props.size * 1.28571429}px`, + lineHeight: `${this.props.size}px`, + ...this.props.style, + ...(this.props.active ? this.props.activeStyle : {}), + }; + + const classes = ['icon-button']; + + if (this.props.active) { + classes.push('active'); + } + + if (this.props.disabled) { + classes.push('disabled'); + } + + if (this.props.inverted) { + classes.push('inverted'); + } + + if (this.props.overlay) { + classes.push('overlayed'); + } + + if (this.props.className) { + classes.push(this.props.className); + } + + return ( + + {({ rotate }) => +