Commit 861ae55a authored by mgabdev's avatar mgabdev

Rich Text Editor (WIP) x2

parent 20a3221c
......@@ -94,6 +94,8 @@ gem 'json-ld', '~> 3.0'
gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3'
gem 'redcarpet', '~> 3.4'
group :development, :test do
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3'
......
......@@ -479,6 +479,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)
......@@ -740,6 +741,7 @@ DEPENDENCIES
rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3)
redcarpet (~> 3.4)
redis (~> 4.1)
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
......
import escapeTextContentForBrowser from 'escape-html'
import { markdownToDraft } from 'markdown-draft-js'
import { Remarkable } from 'remarkable'
import * as entities from 'entities'
import emojify from '../../components/emoji/emoji'
import { unescapeHTML } from '../../utils/html'
import { expandSpoilers } from '../../initial_state'
......@@ -66,40 +63,8 @@ export function normalizeStatus(status, normalOldStatus) {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = [spoilerText, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
let theContent
if (!!normalStatus.rich_content) {
theContent = normalStatus.rich_content
// let rawObject = markdownToDraft(theContent, {
// preserveNewlines: true,
// remarkablePreset: 'commonmark',
// remarkableOptions: {
// enable: {
// inline: ['del', 'ins'],
// }
// }
// });
const md = new Remarkable({
html: false,
breaks: true,
})
let html = md.render(theContent)
html = entities.decodeHTML(html)
theContent = html
console.log("html:", html)
console.log("theContent:", theContent)
console.log("status:", status)
console.log("normalStatus:", normalStatus)
// console.log("rawObject:", rawObject)
} else {
theContent = normalStatus.content
}
// let theContent = !!normalStatus.rich_content ? normalStatus.rich_content : normalStatus.content;
const theContent = !!normalStatus.rich_content ? normalStatus.rich_content : normalStatus.content;
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(theContent, emojiMap, false, true);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
......
......@@ -4,10 +4,8 @@ import {
CompositeDecorator,
RichUtils,
convertToRaw,
convertFromRaw,
ContentState,
} from 'draft-js'
import { draftToMarkdown } from 'markdown-draft-js'
import draftToMarkdown from '../features/ui/util/draft-to-markdown'
import { urlRegex } from '../features/ui/util/url_regex'
import classNames from 'classnames/bind'
import RichTextEditorBar from './rich_text_editor_bar'
......@@ -134,7 +132,8 @@ class Composer extends PureComponent {
const rawObject = convertToRaw(content);
const markdownString = draftToMarkdown(rawObject, {
preserveNewlines: true,
escapeMarkdownCharacters: false,
preserveNewlines: false,
remarkablePreset: 'commonmark',
remarkableOptions: {
disable: {
......@@ -147,6 +146,7 @@ class Composer extends PureComponent {
});
console.log("text:", markdownString)
// console.log("html:", html)
this.props.onChange(null, text, markdownString, selectionStart)
}
......@@ -154,11 +154,12 @@ class Composer extends PureComponent {
// **bold**
// *italic*
// __underline__
// ~strikethrough~
// # title
// ~~strike~~
// # header
// > quote
// `code`
// ```code```
// ```
// code
// ```
focus = () => {
this.textbox.editor.focus()
......
import { RichUtils } from 'draft-js'
import { defineMessages, injectIntl } from 'react-intl'
import classNames from 'classnames/bind'
import { me } from '../initial_state'
import { makeGetAccount } from '../selectors'
......@@ -70,10 +69,6 @@ const RTE_ITEMS = [
},
]
const messages = defineMessages({
follow: { id: 'follow', defaultMessage: 'Follow' },
})
const mapStateToProps = (state) => {
const getAccount = makeGetAccount()
const account = getAccount(state, me)
......@@ -86,13 +81,11 @@ const mapStateToProps = (state) => {
}
export default
@injectIntl
@connect(mapStateToProps)
class RichTextEditorBar extends PureComponent {
static propTypes = {
editorState: PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
isPro: PropTypes.bool.isRequired,
rteControlsVisible: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
......@@ -127,7 +120,7 @@ class RichTextEditorBar extends PureComponent {
/>
))
}
<Button
{/*<Button
backgroundColor='none'
color='secondary'
onClick={this.handleOnTogglePopoutEditor}
......@@ -137,7 +130,7 @@ class RichTextEditorBar extends PureComponent {
iconClassName={_s.inheritFill}
iconSize='12px'
radiusSmall
/>
/>*/}
</div>
)
}
......
......@@ -3,7 +3,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { createSelector } from 'reselect';
import sample from 'lodash.sample';
// import sample from 'lodash.sample';
import debounce from 'lodash.debounce'
import { me, promotions } from '../initial_state';
import { dequeueTimeline } from '../actions/timelines';
......@@ -44,7 +44,7 @@ const mapStateToProps = (state, { timelineId }) => {
if (!timelineId) return {}
const getStatusIds = makeGetStatusIds();
const promotion = promotions.length > 0 && sample(promotions.filter(p => p.timeline_id === timelineId));
const promotion = false//promotions.length > 0 && sample(promotions.filter(p => p.timeline_id === timelineId));
const statusIds = getStatusIds(state, {
type: timelineId.substring(0, 5) === 'group' ? 'group' : timelineId,
......
This diff is collapsed.
......@@ -169,6 +169,10 @@ pre {
margin-bottom: 0.5rem;
}
.statusContent h1 * {
font-size: var(--fs_xl) !important;
}
.statusContent ul,
.statusContent ol {
padding-left: 40px;
......@@ -183,6 +187,12 @@ pre {
/* list-style-type: disc; */
}
.statusContent code {
background-color: rgba(0,0,0,.05);
padding-left: 0.5em;
padding-right: 0.5em;
}
.dangerousContent,
.dangerousContent * {
margin-top: 0;
......
......@@ -3,6 +3,73 @@
require 'singleton'
require_relative './sanitize_config'
class HTMLRenderer < Redcarpet::Render::HTML
def block_code(code, language)
"<pre><code>#{encode(code).gsub("\n", "<br/>")}</code></pre>"
end
def block_quote(quote)
"<blockquote>#{quote}</blockquote>"
end
def codespan(code)
"<code>#{code}</code>"
end
def double_emphasis(text)
"<strong>#{text}</strong>"
end
def emphasis(text)
"<em>#{text}</em>"
end
def header(text, header_level)
"<h1>#{text}</h1>"
end
def triple_emphasis(text)
"<b><em>#{text}</em></b>"
end
def strikethrough(text)
"<del>#{text}</del>"
end
def underline(text)
"<u>#{text}</u>"
end
def list(contents, list_type)
if list_type == :ordered
"<ol>#{contents}</ol>"
elsif list_type == :unordered
"<ul>#{contents}</ul>"
else
content
end
end
def list_item(text, list_type)
"<li>#{text}</li>"
end
def autolink(link, link_type)
return link if link_type == :email
Formatter.instance.link_url(link)
end
private
def html_entities
@html_entities ||= HTMLEntities.new
end
def encode(html)
html_entities.encode(html)
end
end
class Formatter
include Singleton
include RoutingHelper
......@@ -39,20 +106,39 @@ class Formatter
linkable_accounts << status.account
html = raw_content
html = encode_and_link_urls(html, linkable_accounts)
puts "TELLY FORMAT-1: " + html.to_s
html = format_markdown(html) if options[:use_markdown]
puts "TELLY FORMAT-2: " + html.to_s
html = encode_and_link_urls(html, linkable_accounts, keep_html: options[:use_markdown])
puts "TELLY FORMAT-3: " + html.to_s
html = reformat(html, true) unless options[:use_markdown]
puts "TELLY FORMAT-4: " + html.to_s
html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
html = simple_format(html, {}, sanitize: false)
puts "TELLY FORMAT-5: " + html.to_s
html = html.delete("\n")
unless options[:use_markdown]
puts "TELLY FORMAT-4: " + html
html = html.gsub(/(?:\n\r?|\r\n?)/, '<br />')
html = html.delete("\n")
end
puts "TELLY FORMAT-6: " + html.to_s
html.html_safe # rubocop:disable Rails/OutputSafety
puts "telly-html: " + html.to_s
html
end
def format_markdown(html)
html = markdown_formatter.render(html)
html.delete("\r").delete("\n")
end
def reformat(html)
sanitize(html, Sanitize::Config::GABSOCIAL_STRICT)
def reformat(html, outgoing = false)
sanitize(html, Sanitize::Config::GABSOCIAL_STRICT.merge(outgoing: outgoing))
rescue ArgumentError
''
end
......@@ -93,8 +179,7 @@ class Formatter
end
def format_field(account, str, **options)
return reformat(str).html_safe unless account.local? # rubocop:disable Rails/OutputSafety
html = encode_and_link_urls(str, me: true)
html = account.local? ? encode_and_link_urls(str, me: true) : reformat(str)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
end
......@@ -107,8 +192,43 @@ class Formatter
html.html_safe # rubocop:disable Rails/OutputSafety
end
def link_url(url)
"<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener noreferrer\">#{link_html(url)}</a>"
end
private
def markdown_formatter
extensions = {
autolink: false,
no_intra_emphasis: true,
fenced_code_blocks: true,
disable_indented_code_blocks: true,
strikethrough: true,
lax_spacing: true,
space_after_headers: true,
superscript: false,
underline: true,
highlight: false,
footnotes: false,
}
renderer = HTMLRenderer.new({
filter_html: false,
escape_html: false,
no_images: true,
no_styles: true,
safe_links_only: false,
hard_wrap: false,
no_links: true,
with_toc_data: false,
prettify: false,
link_attributes: nil
})
Redcarpet::Markdown.new(renderer, extensions)
end
def html_entities
@html_entities ||= HTMLEntities.new
end
......@@ -125,7 +245,7 @@ class Formatter
accounts = nil
end
rewrite(html.dup, entities) do |entity|
rewrite(html.dup, entities, options[:keep_html]) do |entity|
if entity[:url]
link_to_url(entity, options)
elsif entity[:hashtag]
......@@ -195,7 +315,7 @@ class Formatter
html
end
def rewrite(text, entities)
def rewrite(text, entities, keep_html = false)
text = text.to_s
# Sort by start index
......@@ -208,12 +328,12 @@ class Formatter
last_index = entities.reduce(0) do |index, entity|
indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
result << encode(text[index...indices.first])
result << (keep_html ? text[index...indices.first] : encode(text[index...indices.first]))
result << yield(entity)
indices.last
end
result << encode(text[last_index..-1])
result << (keep_html ? text[last_index..-1] : encode(text[last_index..-1]))
result.flatten.join
end
......@@ -256,6 +376,29 @@ class Formatter
Extractor.remove_overlapping_entities(special + standard)
end
def html_friendly_extractor(html, options = {})
gaps = []
total_offset = 0
escaped = html.gsub(/<[^>]*>|&#[0-9]+;/) do |match|
total_offset += match.length - 1
end_offset = Regexp.last_match.end(0)
gaps << [end_offset - total_offset, total_offset]
"\u200b"
end
entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) +
Extractor.extract_mentions_or_lists_with_indices(escaped)
Extractor.remove_overlapping_entities(entities).map do |extract|
pos = extract[:indices].first
offset_idx = gaps.rindex { |gap| gap.first <= pos }
offset = offset_idx.nil? ? 0 : gaps[offset_idx].last
next extract.merge(
:indices => [extract[:indices].first + offset, extract[:indices].last + offset]
)
end
end
def link_to_url(entity, options = {})
url = Addressable::URI.parse(entity[:url])
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
......@@ -308,4 +451,4 @@ class Formatter
"<a data-focusable=\"true\" role=\"link\" href=\"#{encode(TagManager.instance.url_for(account))}\" class=\"u-url mention\">@#{encode(account.acct)}</a>"
end
end
end
\ No newline at end of file
......@@ -74,9 +74,6 @@ class REST::StatusSerializer < ActiveModel::Serializer
def rich_content
Formatter.instance.format(object, use_markdown: true).strip
# raw_content = object.markdown
# return '' if raw_content.blank?
# raw_content.strip
end
def url
......
......@@ -1472,7 +1472,7 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
argparse@^1.0.10, argparse@^1.0.7:
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
......@@ -1640,13 +1640,6 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autolinker@^3.11.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.14.1.tgz#6ae4b812b6eaf42d4d68138b9e67757cbf2bc1e4"
integrity sha512-yvsRHIaY51EYDml6MGlbqyJGfl4n7zezGYf+R7gvM8c5LNpRGc4SISkvgAswSS8SWxk/OrGCylKV9mJyVstz7w==
dependencies:
tslib "^1.9.3"
autoprefixer@^9.5.1:
version "9.7.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4"
......@@ -3394,11 +3387,6 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
entities@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
enzyme-adapter-react-16@^1.7.1:
version "1.15.2"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501"
......@@ -6099,13 +6087,6 @@ mark-loader@^0.1.6:
resolved "https://registry.yarnpkg.com/mark-loader/-/mark-loader-0.1.6.tgz#0abb477dca7421d70e20128ff6489f5cae8676d5"
integrity sha1-CrtHfcp0IdcOIBKP9kifXK6GdtU=
markdown-draft-js@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/markdown-draft-js/-/markdown-draft-js-2.2.0.tgz#1a4796c4d7907761ae417e2176ab858dfaa1166a"
integrity sha512-OW53nd5m7KrnuXjH8jNKB3SQ5KidKD/+pHS+3daVKTsyjsiozghjUxzNH+5JLxzaBECHmJ7itE39RzMgYm5EFQ==
dependencies:
remarkable "2.0.0"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
......@@ -8230,22 +8211,6 @@ regjsparser@^0.6.4:
dependencies:
jsesc "~0.5.0"
remarkable@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-2.0.0.tgz#795f965bede8300362ce51a716edc322d9e7a4ca"
integrity sha512-3gvKFAgL4xmmVRKAMNm6UzDo/rO2gPVkZrWagp6AXEA4JvCcMcRx9aapYbb7AJAmLLvi/u06+EhzqoS7ha9qOg==
dependencies:
argparse "^1.0.10"
autolinker "^3.11.0"
remarkable@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-2.0.1.tgz#280ae6627384dfb13d98ee3995627ca550a12f31"
integrity sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==
dependencies:
argparse "^1.0.10"
autolinker "^3.11.0"
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
......@@ -9455,7 +9420,7 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
tslib@^1.9.0, tslib@^1.9.3:
tslib@^1.9.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment