diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 89ad370a5..2cf5c498c 100644 Binary files a/pkg/interface/package-lock.json and b/pkg/interface/package-lock.json differ diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 786151f82..c1e679e46 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -24,7 +24,6 @@ "formik": "^2.1.5", "immer": "^8.0.1", "lodash": "^4.17.20", - "markdown-to-jsx": "^6.11.4", "moment": "^2.29.1", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", @@ -42,13 +41,15 @@ "react-use-gesture": "^9.1.3", "react-virtuoso": "^0.20.3", "react-visibility-sensor": "^5.1.1", + "remark": "^12.0.0", "remark-breaks": "^2.0.1", - "remark-disable-tokenizers": "^1.0.24", + "remark-disable-tokenizers": "1.1.0", "stacktrace-js": "^2.0.2", "style-loader": "^1.3.0", "styled-components": "^5.1.1", "styled-system": "^5.1.5", "suncalc": "^1.8.0", + "unist-util-visit": "^3.0.0", "urbit-ob": "^5.0.1", "workbox-core": "^6.0.2", "workbox-precaching": "^6.0.2", diff --git a/pkg/interface/src/logic/lib/publish.ts b/pkg/interface/src/logic/lib/publish.ts index 0615ecacd..c5be2bca5 100644 --- a/pkg/interface/src/logic/lib/publish.ts +++ b/pkg/interface/src/logic/lib/publish.ts @@ -3,6 +3,7 @@ import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; import bigInt, { BigInteger } from 'big-integer'; import { buntPost } from '~/logic/lib/post'; import { unixToDa } from '~/logic/lib/util'; +//import tokenizeMessage from './tokenizeMessage'; export function newPost( title: string, @@ -19,6 +20,9 @@ export function newPost( signatures: [] }; + // re-enable on mainnet deploy + //const tokenisedBody = tokenizeMessage(body); + const revContainer: Post = { ...root, index: root.index + '/1' }; const commentsContainer = { ...root, index: root.index + '/2' }; @@ -26,6 +30,7 @@ export function newPost( ...revContainer, index: revContainer.index + '/1', contents: [{ text: title }, { text: body }] + //contents: [{ text: title }, { text: body } ...tokenisedBody] }; const nodes = { @@ -54,10 +59,13 @@ export function newPost( export function editPost(rev: number, noteId: BigInteger, title: string, body: string) { const now = Date.now(); + // reenable + //const tokenisedBody = tokenizeMessage(body); const newRev: Post = { author: `~${window.ship}`, index: `/${noteId.toString()}/1/${rev}`, 'time-sent': now, + //contents: [{ text: title }, ...tokenisedBody], contents: [{ text: title }, { text: body }], hash: null, signatures: [] @@ -85,8 +93,9 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos if (!rev) { return empty; } - const [title, body] = rev.post.contents as TextContent[]; - return [revNum.toJSNumber(), title.text, body.text, rev.post]; + const title = rev.post.contents[0]; + const body = rev.post.contents.slice(1); + return [revNum.toJSNumber(), title.text, body, rev.post]; } export function getLatestCommentRevision(node: GraphNode): [number, Post] { @@ -113,10 +122,11 @@ export function getComments(node: GraphNode): GraphNode { return comments; } -export function getSnippet(body: string) { - const newlineIdx = body.indexOf('\n', 2); - const end = newlineIdx > -1 ? newlineIdx : body.length; - const start = body.substr(0, end); +export function getSnippet(body: any) { + const firstContent = Object.values(body[0])[0]; + const newlineIdx = firstContent.indexOf('\n', 2); + const end = newlineIdx > -1 ? newlineIdx : firstContent.length; + const start = firstContent.substr(0, end); - return (start === body || start.startsWith('![')) ? start : `${start}...`; + return (start === firstContent || firstContent.startsWith('![')) ? start : `${start}...`; } diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js index 2e5455c0d..f648b70ac 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -3,6 +3,8 @@ import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks'; const URL_REGEX = new RegExp(String(/^(([\w\-\+]+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source)); +const GROUP_REGEX = new RegExp(String(/^~[-a-z_]+\/[-a-z]+/.source)); + const isUrl = (string) => { try { return URL_REGEX.test(string); @@ -15,17 +17,27 @@ const isRef = (str) => { return isUrl(str) && str.startsWith('web+urbitgraph://'); }; +const isGroup = str => { + try { + return GROUP_REGEX.test(str); + } catch (e) { + return false; + } +} + +const convertToGroupRef = (group) => `web+urbitgraph://group/${group}`; + const tokenizeMessage = (text) => { - const messages = []; - let message = []; + let messages = []; + // by line + let currTextBlock = []; let isInCodeBlock = false; let endOfCodeBlock = false; text.split(/\r?\n/).forEach((line, index) => { - if (index !== 0) { - message.push('\n'); - } + // by space + let currTextLine = []; // A line of backticks enters and exits a codeblock - if (line.startsWith('```')) { + if (line.trim().startsWith('```')) { // But we need to check if we've ended a codeblock endOfCodeBlock = isInCodeBlock; isInCodeBlock = (!isInCodeBlock); @@ -34,9 +46,13 @@ const tokenizeMessage = (text) => { } if (isInCodeBlock || endOfCodeBlock) { - message.push(line); + currTextLine = [line]; } else { - line.split(/\s/).forEach((str) => { + const words = line.split(/\s/); + words.forEach((word, idx) => { + const str = isGroup(word) ? convertToGroupRef(word) : word; + + const last = words.length - 1 === idx; if ( (str.startsWith('`') && str !== '`') || (str === '`' && !isInCodeBlock) @@ -50,9 +66,12 @@ const tokenizeMessage = (text) => { } if(isRef(str) && !isInCodeBlock) { - if (message.length > 0) { + if (currTextLine.length > 0 || currTextBlock.length > 0) { // If we're in the middle of a message, add it to the stack and reset - messages.push({ text: message.join(' ') }); + currTextLine.push(''); + messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') }); + currTextBlock = last ? [''] : []; + currTextLine = []; } const link = parsePermalink(str); if(!link) { @@ -61,33 +80,39 @@ const tokenizeMessage = (text) => { const reference = permalinkToReference(link); messages.push(reference); } - message = []; + currTextLine = []; } else if (isUrl(str) && !isInCodeBlock) { - if (message.length > 0) { + if (currTextLine.length > 0 || currTextBlock.length > 0) { // If we're in the middle of a message, add it to the stack and reset - messages.push({ text: message.join(' ') }); - message = []; + currTextLine.push(''); + messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') }); + currTextBlock = last ? [''] : []; + currTextLine = []; } messages.push({ url: str }); - message = []; + currTextLine = []; } else if(urbitOb.isValidPatp(str) && !isInCodeBlock) { - if (message.length > 0) { + if (currTextLine.length > 0 || currTextBlock.length > 0) { // If we're in the middle of a message, add it to the stack and reset - messages.push({ text: message.join(' ') }); - message = []; + currTextLine.push(''); + messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') }); + currTextBlock = last ? [''] : []; + currTextLine = []; } messages.push({ mention: str }); - message = []; + currTextLine = []; + } else { - message.push(str); + currTextLine.push(str); } }); } + currTextBlock.push(currTextLine.join(' ')) }); - if (message.length) { + if (currTextBlock.length) { // Add any remaining message - messages.push({ text: message.join(' ') }); + messages.push({ text: currTextBlock.join('\n') }); } return messages; }; diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 9c5330637..32abc59cc 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -22,7 +22,8 @@ import useLocalState from '~/logic/state/local'; import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import { Dropdown } from '~/views/components/Dropdown'; import ProfileOverlay from '~/views/components/ProfileOverlay'; -import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide'; +import { GraphContent} from '~/views/landscape/components/Graph/GraphContent'; + export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; @@ -496,10 +497,10 @@ export const Message = React.memo(({ ) : ( <> )} - { } return ( - + ); }; diff --git a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx index ba66c1269..a2f219809 100644 --- a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx +++ b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx @@ -8,7 +8,7 @@ import { getSnippet } from '~/logic/lib/publish'; import { useGroupForAssoc } from '~/logic/state/group'; import Author from '~/views/components/Author'; import { MentionText } from '~/views/components/MentionText'; -import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide'; +import { GraphContent } from '~/views/landscape/components/Graph/GraphContent'; import ChatMessage from '../chat/components/ChatMessage'; import { NotePreviewContent } from '../publish/components/NotePreview'; import { PermalinkEmbed } from './embed'; @@ -72,10 +72,10 @@ function TranscludedComment(props: { group={group} /> - @@ -111,7 +111,7 @@ function TranscludedPublishNode(props: { diff --git a/pkg/interface/src/views/apps/publish/components/EditPost.tsx b/pkg/interface/src/views/apps/publish/components/EditPost.tsx index 3c0169d5c..61c7955af 100644 --- a/pkg/interface/src/views/apps/publish/components/EditPost.tsx +++ b/pkg/interface/src/views/apps/publish/components/EditPost.tsx @@ -6,6 +6,7 @@ import { RouteComponentProps, useLocation } from 'react-router-dom'; import GlobalApi from '~/logic/api/global'; import { editPost, getLatestRevision } from '~/logic/lib/publish'; import { useWaitForProps } from '~/logic/lib/useWaitForProps'; +import { referenceToPermalink } from '~/logic/lib/permalinks'; import { PostForm, PostFormSchema } from './NoteForm'; interface EditPostProps { @@ -21,10 +22,27 @@ export function EditPost(props: EditPostProps & RouteComponentProps): ReactEleme const [revNum, title, body] = getLatestRevision(note); const location = useLocation(); + let editContent = null; + editContent = body.reduce((val, curr) => { + if ('text' in curr) { + val = val + curr.text; + } else if ('mention' in curr) { + val = val + `~${curr.mention}`; + } else if ('url' in curr) { + val = val + curr.url; + } else if ('code' in curr) { + val = val + curr.code.expression; + } else if ('reference' in curr) { + val = `${val}${referenceToPermalink(curr).link}`; + } + + return val; + }, ''); + const waiter = useWaitForProps(props); const initial: PostFormSchema = { title, - body + body: editContent }; const onSubmit = async ( diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index 49cfbf629..9bf6792ba 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -1,12 +1,12 @@ -import { Action, Anchor, Box, Col, Row, Text } from '@tlon/indigo-react'; -import { Association, Graph, GraphNode, Group } from '@urbit/api'; +import React, { useState, useEffect } from 'react'; +import { Box, Text, Col, Anchor, Row, Action } from '@tlon/indigo-react'; import bigInt from 'big-integer'; -import React, { useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; import { Link, RouteComponentProps } from 'react-router-dom'; import GlobalApi from '~/logic/api/global'; import { roleForShip } from '~/logic/lib/group'; +import { Contacts, GraphNode, Graph, Association, Unreads, Group, Post } from '@urbit/api'; import { getPermalinkForGraph } from '~/logic/lib/permalinks'; +import { GraphContent } from '~/views/landscape/components/Graph/GraphContent'; import { getComments, getLatestRevision } from '~/logic/lib/publish'; import { useCopy } from '~/logic/lib/useCopy'; import { useQuery } from '~/logic/lib/useQuery'; @@ -35,11 +35,10 @@ const renderers = { } }; -export function NoteContent({ body }) { +export function NoteContent({ post, api }) { return ( - - + ); } @@ -124,7 +123,7 @@ export function Note(props: NoteProps & RouteComponentProps) { - + li, .md ol>li { - line-height: 1.5; -} -.md a { - border-bottom-style: solid; - border-bottom-width: 1px; -} - -.md img { - margin-bottom: 8px; -} - @media all and (prefers-color-scheme: dark) { .options.open { background-color: #4d4d4d; diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index b085c46ba..9aa685dcf 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -11,8 +11,8 @@ import { getPermalinkForGraph } from '~/logic/lib/permalinks'; import { getLatestCommentRevision } from '~/logic/lib/publish'; import { useCopy } from '~/logic/lib/useCopy'; import useMetadataState from '~/logic/state/metadata'; +import { GraphContent } from '../landscape/components/Graph/GraphContent'; import Author from '~/views/components/Author'; -import { GraphContentWide } from '../landscape/components/Graph/GraphContentWide'; const ClickBox = styled(Box)` cursor: pointer; @@ -31,14 +31,13 @@ interface CommentItemProps { highlighted: boolean; } -export function CommentItem(props: CommentItemProps): ReactElement { +export function CommentItem(props: CommentItemProps) { let { highlighted } = props; const { ship, name, api, comment, group } = props; const association = useMetadataState( useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name]) ); - const ref = useRef(null); - console.log(comment); + const ref = useRef(null); const [, post] = getLatestCommentRevision(comment); const disabled = props.pending; @@ -131,14 +130,14 @@ export function CommentItem(props: CommentItemProps): ReactElement { - diff --git a/pkg/interface/src/views/landscape/components/Graph/GraphContent.tsx b/pkg/interface/src/views/landscape/components/Graph/GraphContent.tsx new file mode 100644 index 000000000..614f85d2b --- /dev/null +++ b/pkg/interface/src/views/landscape/components/Graph/GraphContent.tsx @@ -0,0 +1,395 @@ +import React from 'react'; +import _ from 'lodash'; +import { Post, Content, ReferenceContent } from '@urbit/api'; +import { + Box, + Text, + Anchor, + H1, + H2, + H3, + H4, + Row, + Col, +} from '@tlon/indigo-react'; + +import GlobalApi from '~/logic/api/global'; +import TextContent from './content/text'; +import CodeContent from './content/code'; +import RemoteContent from '~/views/components/RemoteContent'; +import { Mention } from '~/views/components/MentionText'; +import { PermalinkEmbed } from '~/views/apps/permalinks/embed'; +import { referenceToPermalink } from '~/logic/lib/permalinks'; +import { GraphContentWide } from './GraphContentWide'; +import { PropFunc } from '~/types'; +import fromMarkdown from 'mdast-util-from-markdown'; +import Dot from '~/views/components/Dot'; +import { + Root, + Parent, + Content as AstContent, + BlockContent, +} from '@types/mdast'; +import { parseTall, parseWide } from './parse'; + +type StitchMode = 'merge' | 'block' | 'inline'; + +// XX make better +type GraphAstNode = any; + +interface GraphMentionNode { + type: 'graph-mention'; + ship: string; +} +interface GraphRefereceNode { + type: 'graph-reference'; + reference: ReferenceContent; +} + +interface GraphUrl { + type: 'graph-url'; + url: string; +} +const codeToMdAst = (content: CodeContent) => { + return { + type: 'root', + children: [ + { + type: 'code', + value: content.code.expression + }, + { + type: 'code', + value: (content.code.output || []).join('\n') + } + ] + }; + + +} + +const contentToMdAst = (tall: boolean) => ( + content: Content +): [StitchMode, any] => { + if ('text' in content) { + return ['merge', tall ? parseTall(content.text) : parseWide(content.text)] as [StitchMode, any]; + } else if ('code' in content) { + return [ + 'block', + codeToMdAst(content) + ]; + } else if ('reference' in content) { + return [ + 'block', + { + type: 'root', + children: [ + { + type: 'graph-reference', + reference: content.reference, + }, + ], + }, + ]; + } else if ('url' in content) { + return [ + 'block', + { + type: 'root', + children: [ + { + type: 'graph-url', + url: content.url, + }, + ], + }, + ]; + } else if ('mention' in content) { + return [ + 'inline', + { + type: 'root', + children: [ + { + type: 'graph-mention', + ship: content.mention, + }, + ], + }, + ]; + } + return [ + 'inline', + { + type: 'root', + children: [], + }, + ]; +}; + +function stitchInline(a: any, b: any) { + if (!a?.children) { + throw new Error('Bad stitchInline call: missing root'); + } + const lastParaIdx = a.children.length - 1; + const last = a.children[lastParaIdx]; + if (last?.children) { + const ros = { + ...a, + children: [ + ...a.children.slice(0, lastParaIdx), + stitchInline(last, b), + ...a.children.slice(lastParaIdx + 1), + ], + }; + return ros; + } + const res = { ...a, children: [...a.children, ...b] }; + return res; +} + +function last(arr: T[]) { + return arr[arr.length - 1]; +} + +function getChildren(node: T): AstContent[] { + if ('children' in node) { + // @ts-ignore + return node.children; + } + return []; +} + +export function asParent(node: T): Parent | undefined { + return ['paragraph', 'heading', 'list', 'listItem', 'table'].includes( + node.type + ) + ? (node as Parent) + : undefined; +} + +function stitchMerge(a: Root, b: Root) { + const aChildren = a.children; + const bChildren = b.children; + if (last(aChildren)?.type === bChildren[0]?.type) { + const aGrandchild = getChildren(last(aChildren)); + const bGrandchild = getChildren(bChildren[0]); + const mergedPara = { + ...last(aChildren), + children: [...aGrandchild, ...bGrandchild], + }; + return { + ...a, + children: [...aChildren.slice(0, -1), mergedPara, ...bChildren.slice(1)], + }; + } + return { ...a, children: [...aChildren, ...bChildren] }; +} + +function stitchBlock(a: Root, b: AstContent[]) { + return { ...a, children: [...a.children, ...b] }; +} + +function stitchInlineAfterBlock(a: Root, b: GraphMentionNode[]) { + return { + ...a, + children: [...a.children, { type: 'paragraph', children: b }], + }; +} + +function stitchAsts(asts: [StitchMode, GraphAstNode][]) { + return _.reduce( + asts.slice(1), + ([prevMode, ast], [mode, val]): [StitchMode, GraphAstNode] => { + if (prevMode === 'block') { + if (mode === 'inline') { + return [mode, stitchInlineAfterBlock(ast, val?.children ?? [])]; + } + if (mode === 'merge') { + return [mode, stitchBlock(ast, val?.children ?? [])]; + } + if (mode === 'block') { + return [mode, stitchBlock(ast, val?.children ?? [])]; + } + } + if (mode === 'inline') { + return [mode, stitchInline(ast, val?.children ?? [])]; + } + if (mode === 'merge') { + return [mode, stitchMerge(ast, val)]; + } + if (mode === 'block') { + return [mode, stitchBlock(ast, val?.children ?? [])]; + } + return [mode, ast]; + }, + asts[0] + ); +} +const header = ({ children, depth, ...rest }) => { + const level = depth; + const inner = + level === 1 ? ( +

{children}

+ ) : level === 2 ? ( +

{children}

+ ) : level === 3 ? ( +

{children}

+ ) : ( +

{children}

+ ); + return ( + + {inner} + + ); +}; + +const renderers = { + heading: header, + inlineCode: ({ language, value }) => { + return ( + + {value} + + ); + }, + + blockquote: ({ children, tall, ...rest }) => { + return ( + + {children} + + ); + }, + paragraph: ({ children }) => { + return ( + + {children} + + ); + }, + listItem: ({ children }) => { + return ( + + + {children} + + ); + }, + + code: ({ language, tall, value, ...rest }) => { + console.log(rest); + const inner = ( + + {value} + + ); + return tall ? {inner} : inner; + }, + link: (props) => { + return ( + + {props.children} + + ); + }, + list: ({ depth, children }) => { + return ( + + {children} + + ); + }, + 'graph-mention': ({ ship }) => , + 'graph-url': ({ url }) => ( + + + + ), + 'graph-reference': ({ api, reference }) => { + const { link } = referenceToPermalink({ reference }); + return ( + + ); + }, + root: ({ children }) => {children}, + text: ({ value }) => value, +}; + +export function Graphdown( + props: { + ast: GraphAstNode; + tall?: boolean; + depth?: number; + } & T +) { + const { ast, depth = 0, ...rest } = props; + const { type, children = [], ...nodeRest } = ast; + const Renderer = renderers[ast.type] ?? (() => `unknown element: ${type}`); + + return ( + + {children.map((c) => ( + + ))} + + ); +} + +export const GraphContent = React.memo(function GraphContent( + props: { + tall?: boolean; + transcluded?: number; + contents: Content[]; + api: GlobalApi; + showOurContact: boolean; + } & PropFunc +) { + const { + post, + contents, + tall = false, + transcluded = 0, + showOurContact, + api, + ...rest + } = props; + const [,ast] = stitchAsts(contents.map(contentToMdAst(tall))); + return ( + + + + ); +}); + diff --git a/pkg/interface/src/views/landscape/components/Graph/GraphContentWide.tsx b/pkg/interface/src/views/landscape/components/Graph/GraphContentWide.tsx deleted file mode 100644 index 81af94905..000000000 --- a/pkg/interface/src/views/landscape/components/Graph/GraphContentWide.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Box } from '@tlon/indigo-react'; -import { Content, Post, ReferenceContent } from '@urbit/api'; -import React from 'react'; -import GlobalApi from '~/logic/api/global'; -import { referenceToPermalink } from '~/logic/lib/permalinks'; -import { PropFunc } from '~/types'; -import { PermalinkEmbed } from '~/views/apps/permalinks/embed'; -import { Mention } from '~/views/components/MentionText'; -import RemoteContent from '~/views/components/RemoteContent'; -import CodeContent from './content/code'; -import TextContent from './content/text'; - -function GraphContentWideInner( - props: { - transcluded?: number; - post: Post; - api: GlobalApi; - showOurContact: boolean; - } & PropFunc -) { - const { post, transcluded = 0, showOurContact, api, ...rest } = props; - - return ( - - {post.contents.map((content: Content, i) => { - if ('text' in content) { - return ( - - ); - } else if ('code' in content) { - return ; - } else if ('reference' in content) { - const { link } = referenceToPermalink(content as ReferenceContent); - return ( - - ); - } else if ('url' in content) { - return ( - - - - ); - } else if ('mention' in content) { - const first = i => i === 0; - return (); - } - })} - - ); -} - -export const GraphContentWide = React.memo(GraphContentWideInner); diff --git a/pkg/interface/src/views/landscape/components/Graph/content/text.js b/pkg/interface/src/views/landscape/components/Graph/content/text.js index d492ecad8..23dc375a0 100644 --- a/pkg/interface/src/views/landscape/components/Graph/content/text.js +++ b/pkg/interface/src/views/landscape/components/Graph/content/text.js @@ -75,21 +75,39 @@ const renderers = { ); }, link: (props) => { - return {props.children}; + return {props.children} + }, + list: ({depth, children}) => { + return {children} } }; const MessageMarkdown = React.memo((props) => { - const { source, ...rest } = props; + const { source, allowHeaders, allowLists, ...rest } = props; const blockCode = source.split('```'); - const codeLines = blockCode.map(codes => codes.split('\n')); - const lines = codeLines.reduce((acc, val, i) => { - if (i % 2 === 1) { - return [...acc, `\`\`\`${val.join('\n')}\`\`\``]; - } else { - return [...acc, ...val]; + const codeLines = blockCode.map((codes) => codes.split('\n')); + let lines = []; + if (allowLists) { + lines.push(source); + } else { + lines = codeLines.reduce((acc, val, i) => { + if (i % 2 === 1) { + return [...acc, `\`\`\`${val.join('\n')}\`\`\``]; + } else { + return [...acc, ...val]; + } + }, []); + } + + const modifiedBlockTokens = DISABLED_BLOCK_TOKENS.filter(e => { + if (allowHeaders && allowLists) { + return (e in ["setextHeading", "atxHeading", "list"]) + } else if (allowHeaders) { + return (e in ["setextHeading", "atxHeading"]) + } else if (allowLists) { + return (e === "list") } - }, []); + }) return lines.map((line, i) => ( @@ -118,7 +136,7 @@ const MessageMarkdown = React.memo((props) => { [ RemarkDisableTokenizers, { - block: DISABLED_BLOCK_TOKENS, + block: modifiedBlockTokens, inline: DISABLED_INLINE_TOKENS } ] @@ -130,23 +148,26 @@ const MessageMarkdown = React.memo((props) => { export default function TextContent(props) { const content = props.content; + const allowHeaders = props.allowHeaders; + const allowLists = props.allowLists; - const group = content.text.match( + const group = content.text.trim().match( /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/ ); const isGroupLink = group !== null && // matched possible chatroom group[2].length > 2 && // possible ship? urbitOb.isValidPatp(group[2]) && // valid patp? - group[0] === content.text; // entire message is room name? + group[0] === content.text.trim(); // entire message is room name? if (isGroupLink) { - const resource = `/ship/${content.text}`; + const resource = `/ship/${content.text.trim()}`; return ( - + ); } diff --git a/pkg/interface/src/views/landscape/components/Graph/parse.ts b/pkg/interface/src/views/landscape/components/Graph/parse.ts new file mode 100644 index 000000000..9633090e2 --- /dev/null +++ b/pkg/interface/src/views/landscape/components/Graph/parse.ts @@ -0,0 +1,33 @@ +import remark from 'remark'; +import RemarkDisableTokenizers from 'remark-disable-tokenizers'; + +const DISABLED_BLOCK_TOKENS = [ + 'indentedCode', + 'atxHeading', + 'thematicBreak', + 'list', + 'setextHeading', + 'html', + 'definition', + 'table', +]; + +const DISABLED_INLINE_TOKENS = ['autoLink', 'url', 'email', 'reference']; + +const tallParser = remark().freeze(); + +export const parseTall = (text: string) => tallParser.parse(text); + +const wideParser = remark() + .use([ + [ + RemarkDisableTokenizers, + { + block: DISABLED_BLOCK_TOKENS, + inline: DISABLED_INLINE_TOKENS, + }, + ], + ]) + .freeze(); + +export const parseWide = (text: string) => wideParser.parse(text); diff --git a/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostContent.tsx b/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostContent.tsx index 6e8237769..a69501e96 100644 --- a/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostContent.tsx +++ b/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostContent.tsx @@ -3,7 +3,7 @@ import { Post } from '@urbit/api'; import React, { ReactElement } from 'react'; import styled from 'styled-components'; import GlobalApi from '~/logic/api/global'; -import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide'; +import { GraphContent } from '~/views/landscape/components/Graph/GraphContent'; const TruncatedBox = styled(Col)` display: -webkit-box; @@ -31,9 +31,9 @@ const PostContent = (props: PostContentProps): ReactElement => { textOverflow="ellipsis" overflow="hidden" > -