Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit d36c044

Browse files
committed
Add language to code blocks when editing messages
1 parent bc2bc87 commit d36c044

File tree

2 files changed

+304
-2
lines changed

2 files changed

+304
-2
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// @flow
2+
3+
import {
4+
getEntityRanges,
5+
BLOCK_TYPE,
6+
ENTITY_TYPE,
7+
INLINE_STYLE,
8+
} from 'draft-js-utils';
9+
10+
import type { ContentState, ContentBlock } from 'draft-js';
11+
12+
type Options = {
13+
gfm?: ?boolean,
14+
};
15+
16+
const { BOLD, CODE, ITALIC, STRIKETHROUGH, UNDERLINE } = INLINE_STYLE;
17+
18+
const CODE_INDENT = ' ';
19+
20+
const defaultOptions: Options = {
21+
gfm: false,
22+
};
23+
24+
class MarkupGenerator {
25+
blocks: Array<ContentBlock>;
26+
contentState: ContentState;
27+
currentBlock: number;
28+
output: Array<string>;
29+
totalBlocks: number;
30+
listItemCounts: Object;
31+
options: Options;
32+
33+
constructor(contentState: ContentState, options?: Options) {
34+
this.contentState = contentState;
35+
this.options = options || defaultOptions;
36+
}
37+
38+
generate(): string {
39+
this.output = [];
40+
this.blocks = this.contentState.getBlockMap().toArray();
41+
this.totalBlocks = this.blocks.length;
42+
this.currentBlock = 0;
43+
this.listItemCounts = {};
44+
while (this.currentBlock < this.totalBlocks) {
45+
this.processBlock();
46+
}
47+
return this.output.join('');
48+
}
49+
50+
processBlock() {
51+
let block = this.blocks[this.currentBlock];
52+
let blockType = block.getType();
53+
switch (blockType) {
54+
case BLOCK_TYPE.HEADER_ONE: {
55+
this.insertLineBreaks(1);
56+
this.output.push('# ' + this.renderBlockContent(block) + '\n');
57+
break;
58+
}
59+
case BLOCK_TYPE.HEADER_TWO: {
60+
this.insertLineBreaks(1);
61+
this.output.push('## ' + this.renderBlockContent(block) + '\n');
62+
break;
63+
}
64+
case BLOCK_TYPE.HEADER_THREE: {
65+
this.insertLineBreaks(1);
66+
this.output.push('### ' + this.renderBlockContent(block) + '\n');
67+
break;
68+
}
69+
case BLOCK_TYPE.HEADER_FOUR: {
70+
this.insertLineBreaks(1);
71+
this.output.push('#### ' + this.renderBlockContent(block) + '\n');
72+
break;
73+
}
74+
case BLOCK_TYPE.HEADER_FIVE: {
75+
this.insertLineBreaks(1);
76+
this.output.push('##### ' + this.renderBlockContent(block) + '\n');
77+
break;
78+
}
79+
case BLOCK_TYPE.HEADER_SIX: {
80+
this.insertLineBreaks(1);
81+
this.output.push('###### ' + this.renderBlockContent(block) + '\n');
82+
break;
83+
}
84+
case BLOCK_TYPE.UNORDERED_LIST_ITEM: {
85+
let blockDepth = block.getDepth();
86+
let lastBlock = this.getLastBlock();
87+
let lastBlockType = lastBlock ? lastBlock.getType() : null;
88+
let lastBlockDepth =
89+
lastBlock && canHaveDepth(lastBlockType)
90+
? lastBlock.getDepth()
91+
: null;
92+
if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
93+
this.insertLineBreaks(1);
94+
// Insert an additional line break if following opposite list type.
95+
if (lastBlockType === BLOCK_TYPE.ORDERED_LIST_ITEM) {
96+
this.insertLineBreaks(1);
97+
}
98+
}
99+
let indent = ' '.repeat(block.depth * 4);
100+
this.output.push(indent + '- ' + this.renderBlockContent(block) + '\n');
101+
break;
102+
}
103+
case BLOCK_TYPE.ORDERED_LIST_ITEM: {
104+
let blockDepth = block.getDepth();
105+
let lastBlock = this.getLastBlock();
106+
let lastBlockType = lastBlock ? lastBlock.getType() : null;
107+
let lastBlockDepth =
108+
lastBlock && canHaveDepth(lastBlockType)
109+
? lastBlock.getDepth()
110+
: null;
111+
if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
112+
this.insertLineBreaks(1);
113+
// Insert an additional line break if following opposite list type.
114+
if (lastBlockType === BLOCK_TYPE.UNORDERED_LIST_ITEM) {
115+
this.insertLineBreaks(1);
116+
}
117+
}
118+
let indent = ' '.repeat(blockDepth * 4);
119+
// TODO: figure out what to do with two-digit numbers
120+
let count = this.getListItemCount(block) % 10;
121+
this.output.push(
122+
indent + `${count}. ` + this.renderBlockContent(block) + '\n'
123+
);
124+
break;
125+
}
126+
case BLOCK_TYPE.BLOCKQUOTE: {
127+
this.insertLineBreaks(1);
128+
this.output.push(' > ' + this.renderBlockContent(block) + '\n');
129+
break;
130+
}
131+
case BLOCK_TYPE.CODE: {
132+
this.insertLineBreaks(1);
133+
if (this.options.gfm) {
134+
const language =
135+
block.getData() && block.getData().get('language')
136+
? block.getData().get('language')
137+
: '';
138+
this.output.push(`\`\`\`${language}\n`);
139+
this.output.push(this.renderBlockContent(block) + '\n');
140+
this.output.push('```\n');
141+
} else {
142+
this.output.push(CODE_INDENT + this.renderBlockContent(block) + '\n');
143+
}
144+
break;
145+
}
146+
default: {
147+
this.insertLineBreaks(1);
148+
this.output.push(this.renderBlockContent(block) + '\n');
149+
break;
150+
}
151+
}
152+
this.currentBlock += 1;
153+
}
154+
155+
getLastBlock(): ContentBlock {
156+
return this.blocks[this.currentBlock - 1];
157+
}
158+
159+
getNextBlock(): ContentBlock {
160+
return this.blocks[this.currentBlock + 1];
161+
}
162+
163+
getListItemCount(block: ContentBlock): number {
164+
let blockType = block.getType();
165+
let blockDepth = block.getDepth();
166+
// To decide if we need to start over we need to backtrack (skipping list
167+
// items that are of greater depth)
168+
let index = this.currentBlock - 1;
169+
let prevBlock = this.blocks[index];
170+
while (
171+
prevBlock &&
172+
canHaveDepth(prevBlock.getType()) &&
173+
prevBlock.getDepth() > blockDepth
174+
) {
175+
index -= 1;
176+
prevBlock = this.blocks[index];
177+
}
178+
if (
179+
!prevBlock ||
180+
prevBlock.getType() !== blockType ||
181+
prevBlock.getDepth() !== blockDepth
182+
) {
183+
this.listItemCounts[blockDepth] = 0;
184+
}
185+
return (this.listItemCounts[blockDepth] =
186+
this.listItemCounts[blockDepth] + 1);
187+
}
188+
189+
insertLineBreaks(n: number) {
190+
if (this.currentBlock > 0) {
191+
for (let i = 0; i < n; i++) {
192+
this.output.push('\n');
193+
}
194+
}
195+
}
196+
197+
renderBlockContent(block: ContentBlock): string {
198+
let { contentState } = this;
199+
let blockType = block.getType();
200+
let text = block.getText();
201+
if (text === '') {
202+
// Prevent element collapse if completely empty.
203+
// TODO: Replace with constant.
204+
return '\u200B';
205+
}
206+
let charMetaList = block.getCharacterList();
207+
let entityPieces = getEntityRanges(text, charMetaList);
208+
return entityPieces
209+
.map(([entityKey, stylePieces]) => {
210+
let content = stylePieces
211+
.map(([text, style]) => {
212+
// Don't allow empty inline elements.
213+
if (!text) {
214+
return '';
215+
}
216+
let content = text;
217+
// Don't encode any text inside a code block.
218+
if (blockType === BLOCK_TYPE.CODE) {
219+
return content;
220+
}
221+
// NOTE: We attempt some basic character escaping here, although
222+
// I don't know if escape sequences are really valid in markdown,
223+
// there's not a canonical spec to lean on.
224+
if (style.has(CODE)) {
225+
return '`' + encodeCode(content) + '`';
226+
}
227+
content = encodeContent(text);
228+
if (style.has(BOLD)) {
229+
content = `**${content}**`;
230+
}
231+
if (style.has(UNDERLINE)) {
232+
// TODO: encode `+`?
233+
content = `++${content}++`;
234+
}
235+
if (style.has(ITALIC)) {
236+
content = `_${content}_`;
237+
}
238+
if (style.has(STRIKETHROUGH)) {
239+
// TODO: encode `~`?
240+
content = `~~${content}~~`;
241+
}
242+
return content;
243+
})
244+
.join('');
245+
let entity = entityKey ? contentState.getEntity(entityKey) : null;
246+
if (entity != null && entity.getType() === ENTITY_TYPE.LINK) {
247+
let data = entity.getData();
248+
let url = data.href || data.url || '';
249+
let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
250+
return `[${content}](${encodeURL(url)}${title})`;
251+
} else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) {
252+
let data = entity.getData();
253+
let src = data.src || '';
254+
let alt = data.alt ? `${escapeTitle(data.alt)}` : '';
255+
return `![${alt}](${encodeURL(src)})`;
256+
} else {
257+
return content;
258+
}
259+
})
260+
.join('');
261+
}
262+
}
263+
264+
function canHaveDepth(blockType: any): boolean {
265+
switch (blockType) {
266+
case BLOCK_TYPE.UNORDERED_LIST_ITEM:
267+
case BLOCK_TYPE.ORDERED_LIST_ITEM:
268+
return true;
269+
default:
270+
return false;
271+
}
272+
}
273+
274+
function encodeContent(text) {
275+
return text.replace(/[*_`]/g, '\\$&');
276+
}
277+
278+
function encodeCode(text) {
279+
return text.replace(/`/g, '\\`');
280+
}
281+
282+
// Encode chars that would normally be allowed in a URL but would conflict with
283+
// our markdown syntax: `[foo](http://foo/)`
284+
function encodeURL(url) {
285+
return url.replace(/\)/g, '%29');
286+
}
287+
288+
// Escape quotes using backslash.
289+
function escapeTitle(text) {
290+
return text.replace(/"/g, '\\"');
291+
}
292+
293+
export default function stateToMarkdown(
294+
content: ContentState,
295+
options?: Options
296+
): string {
297+
return new MarkupGenerator(content, options).generate();
298+
}

src/components/message/editingBody.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
import * as React from 'react';
33
import { convertFromRaw } from 'draft-js';
4-
import { stateToMarkdown } from 'draft-js-export-markdown';
4+
import stateToMarkdown from 'shared/draft-utils/state-to-markdown.js';
55
import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo.js';
66
import { Input } from '../chatInput/style';
77
import { EditorInput, EditActions } from './style';
@@ -11,6 +11,10 @@ import { addToastWithTimeout } from 'src/actions/toasts';
1111
import compose from 'recompose/compose';
1212
import { connect } from 'react-redux';
1313
import editMessageMutation from 'shared/graphql/mutations/message/editMessage';
14+
import processMessageContent, {
15+
messageTypeObj,
16+
attachLanguageToCodeBlocks,
17+
} from 'shared/draft-utils/process-message-content';
1418

1519
type Props = {
1620
message: MessageInfoType,
@@ -31,7 +35,7 @@ const EditingChatInput = (props: Props) => {
3135
props.message.messageType === 'text'
3236
? props.message.content.body
3337
: stateToMarkdown(
34-
convertFromRaw(JSON.parse(props.message.content.body)),
38+
convertFromRaw(JSON.parse(body)),
3539
{
3640
gfm: true,
3741
}

0 commit comments

Comments
 (0)