Merge pull request #247 from YoheiZuho/features/security
Features/security
This commit is contained in:
commit
2ff9998c0b
2
Gemfile
2
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'
|
||||
|
@ -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)
|
||||
|
@ -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 (
|
||||
<Motion defaultStyle={{ rotate: this.props.active ? 180 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? 0 : 180) : 0 }}>
|
||||
{({ rotate }) =>
|
||||
<button
|
||||
aria-label={this.props.title}
|
||||
title={this.props.title}
|
||||
className={classes.join(' ')}
|
||||
onClick={this.handleClick}
|
||||
style={style}
|
||||
>
|
||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||
</button>
|
||||
}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default IconButton;
|
@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import Immutable from 'immutable';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'react-router-dom/Link';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from '../../../components/announcement_icon_button';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
const Collapsable = ({ fullHeight, minHeight, isVisible, children }) => (
|
||||
<Motion defaultStyle={{ height: isVisible ? fullHeight : minHeight }} style={{ height: spring(!isVisible ? minHeight : fullHeight) }}>
|
||||
{({ height }) =>
|
||||
<div style={{ height: `${height}px`, overflow: 'hidden' }}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</Motion>
|
||||
);
|
||||
|
||||
Collapsable.propTypes = {
|
||||
fullHeight: PropTypes.number.isRequired,
|
||||
minHeight: PropTypes.number.isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
welcome: { id: 'welcome.message', defaultMessage: '{domain}へようこそ!' },
|
||||
markdown: { id: 'markdown.list', defaultMessage: 'markdown一覧' },
|
||||
});
|
||||
|
||||
const hashtags = Immutable.fromJS([
|
||||
'神崎ドン自己紹介',
|
||||
]);
|
||||
|
||||
class Announcements extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
homeSize: PropTypes.number,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
showId: null,
|
||||
isLoaded: false,
|
||||
};
|
||||
|
||||
onClick = (announcementId, currentState) => {
|
||||
this.setState({ showId: currentState.showId === announcementId ? null : announcementId });
|
||||
}
|
||||
nl2br (text) {
|
||||
return text.split(/(\n)/g).map((line, i) => {
|
||||
if (line.match(/(\n)/g)) {
|
||||
return React.createElement('br', { key: i });
|
||||
}
|
||||
return line;
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<ul className='announcements'>
|
||||
<li>
|
||||
<Collapsable isVisible={this.state.showId === 'markdown'} fullHeight={1240} minHeight={20} >
|
||||
<div className='announcements__body'>
|
||||
<p>{ this.nl2br(intl.formatMessage(messages.markdown, { domain: document.title }))}<br />
|
||||
<br />
|
||||
(半角)は半角スペースを入力する必要がある場所です。(半角)だけの列は半角スペースのみが入力された列が必要であるを指します。<br /><br />
|
||||
〜〜〜〜〜〜見出し〜〜〜〜〜〜<br /><br />
|
||||
#(半角)見出しテキスト<br /><br />
|
||||
#は1〜6個重ねることができます。<br /><br />
|
||||
〜〜〜〜コードブロック〜〜〜〜<br /><br />
|
||||
`コード`<br /><br />
|
||||
〜〜〜〜〜〜引用〜〜〜〜〜〜<br /><br />
|
||||
>引用文<br />
|
||||
(半角)<br />
|
||||
ここから先は引用が切れます<br />
|
||||
引用は複数回重ねることが可能です。<br /><br />
|
||||
〜〜〜〜〜〜リスト〜〜〜〜〜〜<br /><br />
|
||||
(半角)<br />
|
||||
+(半角)内容1<br />
|
||||
+(半角)内容2<br />
|
||||
(半角)<br /><br />
|
||||
内容の数に制限はありません。<br />
|
||||
投稿トップにリストを持ってくる場合に限り1行目の(半角)は必要ありません。<br />
|
||||
+(半角)を1.(半角)に置き換えることで数字付きリストになります。<br /><br />
|
||||
〜〜〜〜〜上付き文字〜〜〜〜〜<br /><br />
|
||||
_上付き文字_<br /><br />
|
||||
〜〜〜〜〜下付き文字〜〜〜〜〜<br /><br />
|
||||
__下付き文字__<br /><br />
|
||||
〜〜〜〜〜小さい文字〜〜〜〜〜<br /><br />
|
||||
___小さい文字___<br /><br />
|
||||
〜〜〜〜〜取り消し線〜〜〜〜〜<br /><br />
|
||||
~~取り消したい文字列~~<br /><br />
|
||||
〜〜〜〜〜〜横罫線〜〜〜〜〜〜<br /><br />
|
||||
___<br /><br />
|
||||
〜〜〜〜〜〜リンク〜〜〜〜〜〜<br /><br />
|
||||
[リンク文章](https://・・・)<br /><br />
|
||||
〜〜〜〜〜〜画像〜〜〜〜〜〜<br /><br />
|
||||
<br /><br />
|
||||
リンク、画像ともにURLにはhttps://から始まる物のみご利用可能です。
|
||||
</p>
|
||||
</div>
|
||||
</Collapsable>
|
||||
<div className='announcements__icon'>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon='caret-up' onClick={() => this.onClick('markdown', this.state)} size={20} animate active={this.state.showId === 'markdown'} />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.state.isLoaded) {
|
||||
if (!nextProps.isLoading && (nextProps.homeSize === 0 || this.props.homeSize !== nextProps.homeSize)) {
|
||||
this.setState({ isLoaded: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(Announcements);
|
@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Announcements from '../components/announcements';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
homeSize: state.getIn(['timelines', 'home', 'items']).size,
|
||||
isLoading: state.getIn(['timelines', 'home', 'isLoading']),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Announcements);
|
@ -15,6 +15,7 @@ import { changeComposing } from '../../actions/compose';
|
||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
|
||||
import { mascot } from '../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AnnouncementsContainer from './containers/announcements_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
@ -108,6 +109,7 @@ class Compose extends React.PureComponent {
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
<AnnouncementsContainer />
|
||||
|
||||
<div className='drawer__inner__mastodon'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
|
@ -26,3 +26,6 @@
|
||||
@import 'mastodon/dashboard';
|
||||
@import 'mastodon/rtl';
|
||||
@import 'mastodon/accessibility';
|
||||
|
||||
@import 'markdown';
|
||||
@import 'astarte';
|
||||
|
59
app/javascript/styles/astarte.scss
Normal file
59
app/javascript/styles/astarte.scss
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
.announcements {
|
||||
padding: 0 10px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
color: #282c37;
|
||||
background: darken($white, 10%);
|
||||
border-radius: 4px;
|
||||
|
||||
& + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.announcements__admin {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
padding: 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.announcements__icon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
margin: -5px 5px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.announcements__body {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
padding: 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
clear: both;
|
||||
color: #282c37;
|
||||
background: darken($white, 5%);
|
||||
text-decoration: none;
|
||||
padding: 1px 10px 0;
|
||||
border: solid 1px #282c37;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import 'contrast/variables';
|
||||
@import 'application';
|
||||
@import 'contrast/diff';
|
||||
|
||||
@import 'markdown';
|
||||
|
266
app/javascript/styles/markdown.scss
Normal file
266
app/javascript/styles/markdown.scss
Normal file
@ -0,0 +1,266 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
@import 'mastodon-light/variables';
|
||||
@import 'application';
|
||||
@import 'mastodon-light/diff';
|
||||
|
||||
@import 'markdown';
|
||||
|
@ -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)
|
||||
|
367
app/lib/formatter_markdown.rb
Normal file
367
app/lib/formatter_markdown.rb
Normal file
@ -0,0 +1,367 @@
|
||||
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()
|
||||
%(<br>)
|
||||
end
|
||||
|
||||
def block_quote(quote)
|
||||
urlRemoved = "#{remove_url(quote)}"
|
||||
escapedContents = "#{blockquote_markdown_escape(urlRemoved)}"
|
||||
%(<blockquote>#{escapedContents.strip}</blockquote>)
|
||||
end
|
||||
|
||||
def header(text, header_level)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<h#{header_level}>#{encode(mdContentsRemoved)}</h#{header_level}>\n)
|
||||
end
|
||||
|
||||
def block_code(code, language)
|
||||
%(<br>#{code.strip})
|
||||
end
|
||||
|
||||
def codespan(code)
|
||||
urlRemoved = "#{remove_url(code)}"
|
||||
escapedCode = "#{escape_bbcode(urlRemoved)}"
|
||||
encoded = "#{encode(escapedCode)}"
|
||||
%(<code>#{code_contents(encoded)}</code>)
|
||||
end
|
||||
|
||||
def list(contents, list_type)
|
||||
if list_type == :unordered
|
||||
%(<ul class='md-contents'>#{contents.strip}</ul>)
|
||||
elsif list_type == :ordered
|
||||
%(<ol class='md-contents'>#{contents.strip}</ol>)
|
||||
else
|
||||
%(<#{list_type} class='md-contents'>#{contents.strip}</#{list_type}>)
|
||||
end
|
||||
end
|
||||
|
||||
def list_item(text, list_type)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<li class='md-contents'>#{encode(mdContentsRemoved)}</li>)
|
||||
end
|
||||
|
||||
def emphasis(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<sup>#{encode(mdContentsRemoved)}</sup>)
|
||||
end
|
||||
|
||||
def double_emphasis(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<sub>#{encode(mdContentsRemoved)}</sub>)
|
||||
end
|
||||
|
||||
def triple_emphasis(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<small>#{encode(mdContentsRemoved)}</small>)
|
||||
end
|
||||
|
||||
def strikethrough(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<s>#{encode(mdContentsRemoved)}</s>)
|
||||
end
|
||||
|
||||
def superscript(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<sup>#{encode(mdContentsRemoved)}</sup>)
|
||||
end
|
||||
|
||||
def underline(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<u>#{encode(mdContentsRemoved)}</u>)
|
||||
end
|
||||
|
||||
def highlight(text)
|
||||
urlRemoved = "#{remove_url(text)}"
|
||||
mdContentsRemoved = "#{markdown_escape(urlRemoved)}"
|
||||
%(<mark>#{encode(mdContentsRemoved)}</mark>)
|
||||
end
|
||||
|
||||
#オートリンクはmastodonとの相性が悪いので基本的には使わない
|
||||
|
||||
def autolink(link, link_type)
|
||||
%(<a herf="#{link}">リンク</a>)
|
||||
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
|
||||
%(<span class="img_FTL">画像が添付されています。</span><img src="#{URI.encode_www_form_component(link)}">)
|
||||
end
|
||||
end
|
||||
|
||||
def link(link, title, content)
|
||||
|
||||
if content =~ /([<>"\[\] ]+|https?:\/\/|#|@)/
|
||||
content = "リンク"
|
||||
elsif content !~ /.+/
|
||||
content = "リンク"
|
||||
end
|
||||
|
||||
linkcheck = "#{link}"
|
||||
if linkcheck !~ /\Ahttps:\/\/[^<>"\[\] ]+\z/
|
||||
%(#{encode(content)})
|
||||
else
|
||||
%(<a href="#{URI.encode_www_form_component(link)}">#{encode(content)}</a>)
|
||||
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
|
||||
|
||||
#code内の一部を色分けするための変更
|
||||
def code_contents(string)
|
||||
simple = string.gsub(/(true|error|false|failed|def|puts|end|fn|let|mut|use|String|println!)/ ,
|
||||
"true" => "<span class='positive'>#{:true}</span>",
|
||||
"error" => "<span class='negative'>#{:error}</span>",
|
||||
"false" => "<span class='negative'>#{:false}</span>",
|
||||
"failed" => "<span class='negative'>#{:failed}</span>",
|
||||
"def" => "<span class='ruby-func'>#{:def}</span>",
|
||||
"puts" => "<span class='ruby-func'>#{:puts}</span>",
|
||||
"end" => "<span class='ruby-func'>#{:end}</span>",
|
||||
"fn" => "<span class='rust-fanc'>#{:fn}</span>",
|
||||
"let" => "<span class='rust-fanc'>#{:let}</span>",
|
||||
"mut" => "<span class='rust-fanc'>#{:mut}</span>",
|
||||
"use" => "<span class='rust-fanc'>#{:use}</span>",
|
||||
"String" => "<span class='rust-macro'>#{:String}</span>",
|
||||
"println!" => "<span class='rust-macro'>#{:println!}</span>",
|
||||
)
|
||||
simple.gsub(/("[a-zA-Z0-9_ ,]+")/){ "<span class='contents'>#{$1}</span>" }
|
||||
# "" => "<span class=''>#{:}</span>",
|
||||
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(/<img data-md='true'\s+src="([^"]+)"([^>]*)>/) { "<a href=\"" + URI.decode_www_form_component($1) + "\"" + $2 + "><img data-md='true' src=\"" + URI.decode_www_form_component($1) + "\"" + $2 + "></a>" }
|
||||
|
||||
imageDecoded.gsub(/<a data-md='true'\s+href="([^"]+)"([^>]*)>/) { "<a data-md='true' href=\"" + URI.decode_www_form_component($1) + "\"" + $2 + ">" }
|
||||
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"),
|
||||
extractByHTMLTagName("span")
|
||||
].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
|
Loading…
Reference in New Issue
Block a user