From 861ae55aec41fb7c87f825c9f5c198c1af603f73 Mon Sep 17 00:00:00 2001
From: mgabdev <>
Date: Tue, 16 Jun 2020 19:44:30 -0400
Subject: [PATCH] Rich Text Editor (WIP) x2
---
Gemfile | 2 +
Gemfile.lock | 2 +
.../gabsocial/actions/importer/normalizer.js | 37 +-
.../gabsocial/components/composer.js | 17 +-
.../components/rich_text_editor_bar.js | 11 +-
.../gabsocial/components/status_list.js | 4 +-
.../features/ui/util/draft-to-markdown.js | 456 ++++++++++++++++++
app/javascript/styles/global.css | 10 +
app/lib/formatter.rb | 171 ++++++-
app/serializers/rest/status_serializer.rb | 3 -
package.json | 3 -
yarn.lock | 39 +-
12 files changed, 643 insertions(+), 112 deletions(-)
create mode 100644 app/javascript/gabsocial/features/ui/util/draft-to-markdown.js
diff --git a/Gemfile b/Gemfile
index ec339981..387d5508 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
diff --git a/Gemfile.lock b/Gemfile.lock
index e64ff643..5d095708 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/javascript/gabsocial/actions/importer/normalizer.js b/app/javascript/gabsocial/actions/importer/normalizer.js
index ac17d6b6..1de5700f 100644
--- a/app/javascript/gabsocial/actions/importer/normalizer.js
+++ b/app/javascript/gabsocial/actions/importer/normalizer.js
@@ -1,7 +1,4 @@
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(/
/g, '\n').replace(/<\/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 theContent = !!normalStatus.rich_content ? normalStatus.rich_content : normalStatus.content; - 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; - - normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(theContent, emojiMap, false, true); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); diff --git a/app/javascript/gabsocial/components/composer.js b/app/javascript/gabsocial/components/composer.js index 1a0d9700..7be61d27 100644 --- a/app/javascript/gabsocial/components/composer.js +++ b/app/javascript/gabsocial/components/composer.js @@ -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() diff --git a/app/javascript/gabsocial/components/rich_text_editor_bar.js b/app/javascript/gabsocial/components/rich_text_editor_bar.js index f8b8d9dd..f6b220e9 100644 --- a/app/javascript/gabsocial/components/rich_text_editor_bar.js +++ b/app/javascript/gabsocial/components/rich_text_editor_bar.js @@ -1,5 +1,4 @@ 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 { /> )) } - + />*/} ) } diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index 93c65a0a..55801f64 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -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, diff --git a/app/javascript/gabsocial/features/ui/util/draft-to-markdown.js b/app/javascript/gabsocial/features/ui/util/draft-to-markdown.js new file mode 100644 index 00000000..ca95bcc2 --- /dev/null +++ b/app/javascript/gabsocial/features/ui/util/draft-to-markdown.js @@ -0,0 +1,456 @@ +// https://raw.githubusercontent.com/Rosey/markdown-draft-js/main/src/draft-to-markdown.js +const TRAILING_WHITESPACE = /[ \u0020\t\n]*$/; + +// This escapes some markdown but there's a few cases that are TODO - +// - List items +// - Back tics (see https://github.com/Rosey/markdown-draft-js/issues/52#issuecomment-388458017) +// - Complex markdown, like links or images. Not sure it's even worth it, because if you're typing +// that into draft chances are you know its markdown and maybe expect it convert? :/ +const MARKDOWN_STYLE_CHARACTERS = ['*', '_', '~', '`']; +const MARKDOWN_STYLE_CHARACTER_REGXP = /(\*|_|~|\\|`)/g; + +// I hate this a bit, being outside of the function’s scope +// but can’t think of a better way to keep track of how many ordered list +// items were are on, as draft doesn’t explicitly tell us in the raw object 😢. +// This is a hash that will be assigned values based on depth, so like +// orderedListNumber[0] = 1 would mean that ordered list at depth 0 is on number 1. +// orderedListNumber[0] = 2 would mean that ordered list at depth 0 is on number 2. +// This is so we have the right number of numbers when doing a list, eg +// 1. Item One +// 2. Item two +// 3. Item three +// And so on. +var orderedListNumber = {}, + previousOrderedListDepth = 0; + +// A map of draftjs block types -> markdown open and close characters +// Both the open and close methods must exist, even if they simply return an empty string. +// They should always return a string. +const StyleItems = { + // BLOCK LEVEL + 'unordered-list-item': { + open: function () { + return '- '; + }, + + close: function () { + return ''; + } + }, + + 'ordered-list-item': { + open: function (block, number = 1) { + return `${number}. `; + }, + + close: function () { + return ''; + } + }, + + 'blockquote': { + open: function () { + return '> '; + }, + + close: function () { + return ''; + } + }, + + 'header-one': { + open: function () { + return '# '; + }, + + close: function () { + return ''; + } + }, + + 'code-block': { + open: function (block) { + return '```' + (block.data.language || '') + '\n'; + }, + + close: function () { + return '\n```'; + } + }, + + // INLINE LEVEL + 'BOLD': { + open: function () { + return '**'; + }, + + close: function () { + return '**'; + } + }, + + 'ITALIC': { + open: function () { + return '*'; + }, + + close: function () { + return '*'; + } + }, + + 'UNDERLINE': { + open: function () { + return '_'; + }, + + close: function () { + return '_'; + } + }, + + 'STRIKETHROUGH': { + open: function () { + return '~~'; + }, + + close: function () { + return '~~'; + } + }, + + 'CODE': { + open: function () { + return '`'; + }, + + close: function () { + return '`'; + } + } +}; + +// A map of draftjs entity types -> markdown open and close characters +// entities are different from block types because they have additional data attached to them. +// an entity object is passed in to both open and close, in case it's needed for string generation. +// +// Both the open and close methods must exist, even if they simply return an empty string. +// They should always return a string. +const EntityItems = { + // +} + +// Bit of a hack - we normally want a double newline after a block, +// but for list items we just want one (unless it's the _last_ list item in a group.) +const SingleNewlineAfterBlock = [ + 'unordered-list-item', + 'ordered-list-item' +]; + +function isEmptyBlock(block) { + return block.text.length === 0 && block.entityRanges.length === 0 && Object.keys(block.data || {}).length === 0; +} + +/** + * Generate markdown for a single block javascript object + * DraftJS raw object contains an array of blocks, which is the main "structure" + * of the text. Each block = a new line. + * + * @param {Object} block - block to generate markdown for + * @param {Number} index - index of the block in the blocks array + * @param {Object} rawDraftObject - entire raw draft object (needed for accessing the entityMap) + * @param {Object} options - additional options passed in by the user calling this method. + * + * @return {String} markdown string +**/ +function renderBlock(block, index, rawDraftObject, options) { + var openInlineStyles = [], + markdownToAdd = []; + var markdownString = '', + customStyleItems = options.styleItems || {}, + customEntityItems = options.entityItems || {}, + escapeMarkdownCharacters = options.hasOwnProperty('escapeMarkdownCharacters') ? options.escapeMarkdownCharacters : true; + + var type = block.type; + + var markdownStyleCharactersToEscape = []; + + // draft-js emits empty blocks that have type set… don’t style them unless the user wants to preserve new lines + // (if newlines are preserved each empty line should be "styled" eg in case of blockquote we want to see a blockquote.) + // but if newlines aren’t preserved then we'd end up having double or triple or etc markdown characters, which is a bug. + if (isEmptyBlock(block) && !options.preserveNewlines) { + type = 'unstyled'; + } + + // Render main block wrapping element + if (customStyleItems[type] || StyleItems[type]) { + if (type === 'unordered-list-item' || type === 'ordered-list-item') { + markdownString += ' '.repeat(block.depth * 4); + } + + if (type === 'ordered-list-item') { + orderedListNumber[block.depth] = orderedListNumber[block.depth] || 1; + markdownString += (customStyleItems[type] || StyleItems[type]).open(block, orderedListNumber[block.depth]); + orderedListNumber[block.depth]++; + + // Have to reset the number for orderedListNumber if we are breaking out of a list so that if + // there's another nested list at the same level further down, it starts at 1 again. + // COMPLICATED 😭 + if (previousOrderedListDepth > block.depth) { + orderedListNumber[previousOrderedListDepth] = 1; + } + + previousOrderedListDepth = block.depth; + } else { + orderedListNumber = {}; + markdownString += (customStyleItems[type] || StyleItems[type]).open(block); + } + } + + // Render text within content, along with any inline styles/entities + Array.from(block.text).some(function (character, characterIndex) { + // Close any entity tags that need closing + block.entityRanges.forEach(function (range, rangeIndex) { + if (range.offset + range.length === characterIndex) { + var entity = rawDraftObject.entityMap[range.key]; + if (customEntityItems[entity.type] || EntityItems[entity.type]) { + markdownString += (customEntityItems[entity.type] || EntityItems[entity.type]).close(entity); + } + } + }); + + // Close any inline tags that need closing + openInlineStyles.forEach(function (style, styleIndex) { + if (style.offset + style.length === characterIndex) { + if ((customStyleItems[style.style] || StyleItems[style.style])) { + var styleIndex = openInlineStyles.indexOf(style); + // Handle nested case - close any open inline styles before closing the parent + if (styleIndex > -1 && styleIndex !== openInlineStyles.length - 1) { + for (var i = openInlineStyles.length - 1; i !== styleIndex; i--) { + var styleItem = (customStyleItems[openInlineStyles[i].style] || StyleItems[openInlineStyles[i].style]); + if (styleItem) { + var trailingWhitespace = TRAILING_WHITESPACE.exec(markdownString); + markdownString = markdownString.slice(0, markdownString.length - trailingWhitespace[0].length); + markdownString += styleItem.close(); + markdownString += trailingWhitespace[0]; + } + } + } + + // Close the actual inline style being closed + // Have to trim whitespace first and then re-add after because markdown can't handle leading/trailing whitespace + var trailingWhitespace = TRAILING_WHITESPACE.exec(markdownString); + markdownString = markdownString.slice(0, markdownString.length - trailingWhitespace[0].length); + + markdownString += (customStyleItems[style.style] || StyleItems[style.style]).close(); + markdownString += trailingWhitespace[0]; + + // Handle nested case - reopen any inline styles after closing the parent + if (styleIndex > -1 && styleIndex !== openInlineStyles.length - 1) { + for (var i = openInlineStyles.length - 1; i !== styleIndex; i--) { + var styleItem = (customStyleItems[openInlineStyles[i].style] || StyleItems[openInlineStyles[i].style]); + if (styleItem && openInlineStyles[i].offset + openInlineStyles[i].length > characterIndex) { + markdownString += styleItem.open(); + } else { + openInlineStyles.splice(i, 1); + } + } + } + + openInlineStyles.splice(styleIndex, 1); + } + } + }); + + // Open any inline tags that need opening + block.inlineStyleRanges.forEach(function (style, styleIndex) { + if (style.offset === characterIndex) { + if ((customStyleItems[style.style] || StyleItems[style.style])) { + var styleToAdd = (customStyleItems[style.style] || StyleItems[style.style]).open(); + markdownToAdd.push({ + type: 'style', + style: style, + value: styleToAdd + }); + } + } + }); + + // Open any entity tags that need opening + block.entityRanges.forEach(function (range, rangeIndex) { + if (range.offset === characterIndex) { + var entity = rawDraftObject.entityMap[range.key]; + if (customEntityItems[entity.type] || EntityItems[entity.type]) { + var entityToAdd = (customEntityItems[entity.type] || EntityItems[entity.type]).open(entity); + markdownToAdd.push({ + type: 'entity', + value: entityToAdd + }); + } + } + }); + + // These are all the opening entity and style types being added to the markdown string for this loop + // we store in an array and add here because if the character is WS character, we want to hang onto it and not apply it until the next non-whitespace + // character before adding the markdown, since markdown doesn’t play nice with leading whitespace (eg '** bold**' is no good, whereas ' **bold**' is good.) + if (character !== ' ' && markdownToAdd.length) { + markdownString += markdownToAdd.map(function (item) { + return item.value; + }).join(''); + + markdownToAdd.forEach(function (item) { + if (item.type === 'style') { + // We hang on to this because we may need to close it early and then re-open if there are nested styles being opened and closed. + openInlineStyles.push(item.style); + } + }); + + markdownToAdd = []; + } + + if (block.type !== 'code-block' && escapeMarkdownCharacters) { + let insideInlineCodeStyle = openInlineStyles.find((style) => style.style === 'CODE'); + + if (insideInlineCodeStyle) { + // Todo - The syntax to escape backtics when inside backtic code already is to use MORE backtics wrapping. + // So we need to see how many backtics in a row we have and then when converting to markdown, use that # + 1 + + // EG ``Test ` Hllo `` + // OR ```Test `` Hello``` + // OR ````Test ``` Hello ```` + // Similar work has to be done for codeblocks. + } else { + // Special escape logic for blockquotes and heading characters + if (characterIndex === 0 && character === '#' && block.text[1] && block.text[1] === ' ') { + character = character.replace('#', '\\#'); + } else if (characterIndex === 0 && character === '>') { + character = character.replace('>', '\\>'); + } + + // Escaping inline markdown characters + // 🧹 If someone can think of a more elegant solution, I would love that. + // orginally this was just a little char replace using a simple regular expression, but there’s lots of cases where + // a markdown character does not actually get converted to markdown, like this case: http://google.com/i_am_a_link + // so this code now tries to be smart and keeps track of potential “opening” characters as well as potential “closing” + // characters, and only escapes if both opening and closing exist, and they have the correct whitepace-before-open, whitespace-or-end-of-string-after-close pattern + if (MARKDOWN_STYLE_CHARACTERS.includes(character)) { + let openingStyle = markdownStyleCharactersToEscape.find(function (item) { + return item.character === character; + }); + + if (!openingStyle && block.text[characterIndex - 1] === ' ' && block.text[characterIndex + 1] !== ' ') { + markdownStyleCharactersToEscape.push({ + character: character, + index: characterIndex, + markdownStringIndexStart: markdownString.length + character.length - 1, + markdownStringIndexEnd: markdownString.length + character.length + }); + } else if (openingStyle && block.text[characterIndex - 1] === character && characterIndex === openingStyle.index + 1) { + openingStyle.markdownStringIndexEnd += 1; + } else if (openingStyle) { + let openingStyleLength = openingStyle.markdownStringIndexEnd - openingStyle.markdownStringIndexStart; + let escapeCharacter = false; + let popOpeningStyle = false; + if (openingStyleLength === 1 && (block.text[characterIndex + 1] === ' ' || !block.text[characterIndex + 1])) { + popOpeningStyle = true; + escapeCharacter = true; + } + + if (openingStyleLength === 2 && block.text[characterIndex + 1] === character) { + escapeCharacter = true; + } + + if (openingStyleLength === 2 && block.text[characterIndex - 1] === character && (block.text[characterIndex + 1] === ' ' || !block.text[characterIndex + 1])) { + popOpeningStyle = true; + escapeCharacter = true; + } + + if (popOpeningStyle) { + markdownStyleCharactersToEscape.splice(markdownStyleCharactersToEscape.indexOf(openingStyle), 1); + let replacementString = markdownString.slice(openingStyle.markdownStringIndexStart, openingStyle.markdownStringIndexEnd); + replacementString = replacementString.replace(MARKDOWN_STYLE_CHARACTER_REGXP, '\\$1'); + markdownString = (markdownString.slice(0, openingStyle.markdownStringIndexStart) + replacementString + markdownString.slice(openingStyle.markdownStringIndexEnd)); + } + + if (escapeCharacter) { + character = `\\${character}`; + } + } + } + } + } + + if (character === '\n' && type === 'blockquote') { + markdownString += '\n> '; + } else { + markdownString += character; + } + }); + + // Close any remaining entity tags + block.entityRanges.forEach(function (range, rangeIndex) { + if (range.offset + range.length === Array.from(block.text).length) { + var entity = rawDraftObject.entityMap[range.key]; + if (customEntityItems[entity.type] || EntityItems[entity.type]) { + markdownString += (customEntityItems[entity.type] || EntityItems[entity.type]).close(entity); + } + } + }); + + // Close any remaining inline tags (if an inline tag ends at the very last char, we won't catch it inside the loop) + openInlineStyles.reverse().forEach(function (style) { + var trailingWhitespace = TRAILING_WHITESPACE.exec(markdownString); + markdownString = markdownString.slice(0, markdownString.length - trailingWhitespace[0].length); + markdownString += (customStyleItems[style.style] || StyleItems[style.style]).close(); + markdownString += trailingWhitespace[0]; + }); + + // Close block level item + if (customStyleItems[type] || StyleItems[type]) { + markdownString += (customStyleItems[type] || StyleItems[type]).close(block); + } + + // Determine how many newlines to add - generally we want 2, but for list items we just want one when they are succeeded by another list item. + if (SingleNewlineAfterBlock.indexOf(type) !== -1 && rawDraftObject.blocks[index + 1] && SingleNewlineAfterBlock.indexOf(rawDraftObject.blocks[index + 1].type) !== -1) { + markdownString += '\n'; + } else if (rawDraftObject.blocks[index + 1]) { + if (rawDraftObject.blocks[index].text) { + if (SingleNewlineAfterBlock.indexOf(type) !== -1 + && SingleNewlineAfterBlock.indexOf(rawDraftObject.blocks[index + 1].type) === -1) { + markdownString += '\n\n'; + } else if (!options.preserveNewlines) { + // 2 newlines if not preserving + markdownString += '\n\n'; + } else { + markdownString += '\n'; + } + } else if (options.preserveNewlines) { + markdownString += '\n'; + } + } + + return markdownString; +} + +/** + * Generate markdown for a raw draftjs object + * DraftJS raw object contains an array of blocks, which is the main "structure" + * of the text. Each block = a new line. + * + * @param {Object} rawDraftObject - draftjs object to generate markdown for + * @param {Object} options - optional additional data, see readme for what options can be passed in. + * + * @return {String} markdown string +**/ +function draftToMarkdown(rawDraftObject, options) { + options = options || {}; + var markdownString = ''; + rawDraftObject.blocks.forEach(function (block, index) { + markdownString += renderBlock(block, index, rawDraftObject, options); + }); + + orderedListNumber = {}; // See variable definitions at the top of the page to see why we have to do this sad hack. + return markdownString; +} + +export default draftToMarkdown; \ No newline at end of file diff --git a/app/javascript/styles/global.css b/app/javascript/styles/global.css index a7e7d6d4..49879372 100644 --- a/app/javascript/styles/global.css +++ b/app/javascript/styles/global.css @@ -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; diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 049924e0..1e46befc 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -3,6 +3,73 @@ require 'singleton' require_relative './sanitize_config' +class HTMLRenderer < Redcarpet::Render::HTML + def block_code(code, language) + "
#{encode(code).gsub("\n", "
")}
"
+ end
+
+ def block_quote(quote)
+ "#{quote}" + end + + def codespan(code) + "
#{code}
"
+ end
+
+ def double_emphasis(text)
+ "#{text}"
+ end
+
+ def emphasis(text)
+ "#{text}"
+ end
+
+ def header(text, header_level)
+ "