the browser-facing portion of osu!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 143 lines 4.3 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2// See the LICENCE file in the repository root for full licence text. 3 4import { EmbedElement, ParagraphElement } from 'editor'; 5import { 6 BeatmapDiscussionReview, 7 DocumentIssueEmbed, 8} from 'interfaces/beatmap-discussion-review'; 9import { Editor, Element as SlateElement, Node as SlateNode, Range as SlateRange, Text, Transforms } from 'slate'; 10import { parseTimestamp } from 'utils/beatmapset-discussion-helper'; 11import { present } from 'utils/string'; 12 13export const blockCount = (input: SlateElement[]) => input.length; 14 15export const slateDocumentIsEmpty = (doc: SlateElement[]) => doc.length === 0 || ( 16 doc.length === 1 && 17 doc[0].type === 'paragraph' && 18 doc[0].children.length === 1 && 19 doc[0].children[0].text === '' 20); 21 22export const insideEmbed = (editor: Editor) => { 23 const node = getCurrentNode(editor); 24 if (node == null) return false; 25 26 return 'type' in node && node.type === 'embed'; 27}; 28 29export const insideEmptyNode = (editor: Editor) => { 30 const parent = getCurrentNode(editor); 31 if (parent == null) return false; 32 33 if ('type' in parent) { 34 return Editor.isEmpty(editor, parent); 35 } 36 37 return false; 38}; 39 40export const isFormatActive = (editor: Editor, format: 'bold' | 'italic') => { 41 const [match] = Editor.nodes(editor, { 42 match: (node) => Text.isText(node) && node[format] === true, 43 mode: 'all', 44 }); 45 return match != null; 46}; 47 48const getCurrentNode = (editor: Editor) => { 49 if (editor.selection) { 50 return SlateNode.parent(editor, SlateRange.start(editor.selection).path); 51 } 52}; 53 54export const toggleFormat = (editor: Editor, format: 'bold' | 'italic') => { 55 Transforms.setNodes( 56 editor, 57 { [format]: isFormatActive(editor, format) ? null : true }, 58 { match: (node) => Text.isText(node), split: true }, 59 ); 60}; 61 62// TODO: check typing 63function serializeEmbed(node: EmbedElement): DocumentIssueEmbed { 64 if (node.discussionId != null) { 65 return { 66 discussion_id: node.discussionId, 67 type: 'embed', 68 }; 69 } else { 70 return { 71 beatmap_id: node.beatmapId ?? null, 72 discussion_type: node.discussionType, 73 text: node.children[0].text, 74 timestamp: parseTimestamp(node.timestamp), 75 type: 'embed', 76 }; 77 } 78} 79 80// Prevent invalid markdown from being generated by whitespace after/before the opening/closing marks 81function serializeMarkedText(text: string, format: string) { 82 const trimmedText = text.trim(); 83 if (trimmedText.length === 0) { 84 return; 85 } 86 87 const formattedText = `${format}${trimmedText}${format}`; 88 89 if (trimmedText === text) { 90 return formattedText; 91 } 92 93 return text.replace(trimmedText, formattedText); 94} 95 96function serializeParagraph(node: ParagraphElement) { 97 return node.children.map((child) => { 98 if (child.text !== '') { 99 const text = child.text.replace(/([*_\\])/g, '\\$1'); 100 // simplified logic that forces nested marks to be split; 101 // removing whitespace while preserving the nested marks gets messy. 102 if (child.bold && child.italic) { 103 return serializeMarkedText(text, '***'); 104 } else if (child.bold) { 105 return serializeMarkedText(text, '**'); 106 } else if (child.italic) { 107 return serializeMarkedText(text, '*'); 108 } else { 109 return text; 110 } 111 } 112 }).join(''); 113} 114 115export const slateDocumentContainsNewProblem = (input: SlateElement[]) => 116 input.some((node) => node.type === 'embed' && node.discussionType === 'problem' && node.discussionId == null); 117 118export const serializeSlateDocument = (input: SlateElement[]) => { 119 const review: BeatmapDiscussionReview = []; 120 121 input.forEach((node) => { 122 switch (node.type) { 123 case 'paragraph': 124 review.push({ 125 text: serializeParagraph(node), 126 type: 'paragraph', 127 }); 128 break; 129 130 case 'embed': 131 review.push(serializeEmbed(node)); 132 break; 133 } 134 }); 135 136 // strip last block if it's empty (i.e. the placeholder that allows easier insertion at the end of a document) 137 const lastBlock = review[review.length - 1]; 138 if (lastBlock.type === 'paragraph' && !present(lastBlock.text)) { 139 review.pop(); 140 } 141 142 return JSON.stringify(review); 143};