diff --git a/package.json b/package.json index 66b7c436..2db293b6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "main": "./dist/main.js", "config": {}, "dependencies": { + "@observablehq/inspector": "3.1.0", "automerge": "github:automerge/automerge#opaque-strings", "base64-js": "^1.3.1", "bs58": "^4.0.1", diff --git a/src/renderer/Hooks.ts b/src/renderer/Hooks.ts index 9b7d662d..99e54c59 100644 --- a/src/renderer/Hooks.ts +++ b/src/renderer/Hooks.ts @@ -50,13 +50,17 @@ export function useHandle( return () => {} } - const handle = repo.open(url) + try { + const handle = repo.open(url) - const cleanup = cb(handle) + const cleanup = cb(handle) - return () => { - handle.close() - cleanup && cleanup() + return () => { + handle.close() + cleanup && cleanup() + } + } catch (error) { + return () => {} } }, [url]) diff --git a/src/renderer/ShareLink.ts b/src/renderer/ShareLink.ts index be3ebcb3..8a862139 100644 --- a/src/renderer/ShareLink.ts +++ b/src/renderer/ShareLink.ts @@ -67,9 +67,10 @@ export function parseDocumentLink(link: string): Parts { export function parts(str: string) { const { protocol, pathname, query } = url.parse(str) + const { pushpinContentType } = query ? querystring.parse(query) : { pushpinContentType: null } return { scheme: protocol ? protocol.substr(0, protocol.length - 1) : '', - type: querystring.parse(query || '').pushpinContentType.toString(), + type: pushpinContentType ? pushpinContentType.toString() : '', docId: (pathname || '').substr(1), } } diff --git a/src/renderer/components/Root.tsx b/src/renderer/components/Root.tsx index 476ce5c3..cde6eba7 100644 --- a/src/renderer/components/Root.tsx +++ b/src/renderer/components/Root.tsx @@ -19,6 +19,7 @@ import './content-types/files' import './content-types/storage-peer' // other single-context components +import './content-types/Inspector' import './content-types/TextContent' import './content-types/ThreadContent' import './content-types/UrlContent' diff --git a/src/renderer/components/content-types/Inspector.css b/src/renderer/components/content-types/Inspector.css new file mode 100644 index 00000000..3bf05ca1 --- /dev/null +++ b/src/renderer/components/content-types/Inspector.css @@ -0,0 +1,107 @@ +:root { + --syntax_normal: #1b1e23; + --syntax_comment: #a9b0bc; + --syntax_number: #20a5ba; + --syntax_keyword: #c30771; + --syntax_atom: #10a778; + --syntax_string: #008ec4; + --syntax_error: #ffbedc; + --syntax_unknown_variable: #838383; + --syntax_known_variable: #005f87; + --syntax_matchbracket: #20bbfc; + --syntax_key: #6636b4; + --mono_fonts: 82%/1.5 Menlo, Consolas, monospace; +} + +.Inspector { + display: flex; + background-color: white; + width: 100%; + overflow: auto; + height: 100%; + padding: 1px 1px 0px 1px; +} + +.observablehq--expanded, +.observablehq--collapsed, +.observablehq--function, +.observablehq--import, +.observablehq--string:before, +.observablehq--string:after, +.observablehq--gray { + color: var(--syntax_normal); +} + +.observablehq--collapsed, +.observablehq--inspect a { + cursor: pointer; +} + +.observablehq--field { + text-indent: -1em; + margin-left: 1em; +} + +.observablehq--empty { + color: var(--syntax_comment); +} + +.observablehq--keyword, +.observablehq--blue { + color: #3182bd; +} + +.observablehq--forbidden, +.observablehq--pink { + color: #e377c2; +} + +.observablehq--orange { + color: #e6550d; +} + +.observablehq--null, +.observablehq--undefined, +.observablehq--boolean { + color: var(--syntax_atom); +} + +.observablehq--number, +.observablehq--bigint, +.observablehq--date, +.observablehq--regexp, +.observablehq--symbol, +.observablehq--green { + color: var(--syntax_number); +} + +.observablehq--index, +.observablehq--key { + color: var(--syntax_key); +} + +.observablehq--empty { + font-style: oblique; +} + +.observablehq--string, +.observablehq--purple { + color: var(--syntax_string); +} + +.observablehq--error, +.observablehq--red { + color: #e7040f; +} + +.observablehq--inspect { + font: var(--mono_fonts); + overflow-x: auto; + display: block; + white-space: pre; +} + +.observablehq--error .observablehq--inspect { + word-break: break-all; + white-space: pre-wrap; +} diff --git a/src/renderer/components/content-types/Inspector.tsx b/src/renderer/components/content-types/Inspector.tsx new file mode 100644 index 00000000..9fcf3119 --- /dev/null +++ b/src/renderer/components/content-types/Inspector.tsx @@ -0,0 +1,161 @@ +import React, { useEffect, useRef } from 'react' +import { Handle } from 'hypermerge' + +import * as Observable from '@observablehq/inspector' +import * as ContentTypes from '../../ContentTypes' +import { ContentProps } from '../Content' +import { HypermergeUrl, parts } from '../../ShareLink' +import { useDocument } from '../../Hooks' +import Badge from '../Badge' +import TitleEditor from '../TitleEditor' +import * as ContentData from '../../ContentData' +// import '@observablehq/inspector/src/style.css' +import './Inspector.css' + +interface InspectorDoc { + targetURL: HypermergeUrl +} + +interface Props extends ContentProps { + uniquelySelected?: boolean +} + +interface InspectorOpts { + targetURL: HypermergeUrl | null + change: (cb: (doc: InspectorDoc) => void) => void +} + +Inspector.minWidth = 6 +Inspector.minHeight = 2 +Inspector.defaultWidth = 12 +Inspector.defaultHeight = 18 + +const decodeTargetURL = (url: string) => { + try { + const { scheme, docId } = parts(url) + if (scheme !== 'hypermerge') { + throw new Error(`Invalid url scheme: ${scheme} (expected hypermerge)`) + } + + if (!docId) { + throw new Error(`Missing docId in ${url}`) + } + + return `hypermerge:/${docId}` as HypermergeUrl + } catch (_) { + return null + } +} + +export default function Inspector(props: Props) { + const [doc, changeDoc] = useDocument(props.hypermergeUrl) + const targetURL = doc && decodeTargetURL(doc.targetURL) + + const [ref] = useInspector({ + targetURL, + change(fn: (doc: InspectorDoc) => void) { + changeDoc((doc) => fn(doc)) + }, + }) + const placeholder = `URL of the document to inspect e.g ${props.hypermergeUrl}` + + return ( +
+ +
+
+ ) +} + +function useInspector({ + targetURL, + change, +}: InspectorOpts): [React.Ref, Observable.Inspector | null] { + const ref = useRef(null) + const inspector = useRef(null) + const [targetDoc] = useDocument(targetURL) + // const makeChange = useStaticCallback(change) + + useEffect(() => { + if (!ref.current) return () => {} + + const container = ref.current + inspector.current = new Observable.Inspector(container) + inspector.current.pending() + + return () => { + // inspector.current = null + container.textContent = '' + + inspector.current = new Observable.Inspector(container) + inspector.current.pending() + } + }, [ref.current]) // eslint-disable-line + + useEffect(() => { + if (!inspector.current) return () => {} + + if (targetDoc) { + inspector.current.fulfilled(targetDoc) + } + + return () => { + if (ref.current) { + ref.current.textContent = '' + } + } + }, [targetDoc, targetURL]) + + return [ref, inspector.current] +} + +function stopPropagation(e: React.SyntheticEvent) { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() +} + +async function createFrom(contentData: ContentData.ContentData, handle: Handle) { + handle.change((doc) => {}) +} + +function create({ text }, handle: Handle) { + handle.change((doc) => {}) +} + +function InspectorInList(props: ContentProps) { + const [doc] = useDocument(props.hypermergeUrl) + function onDragStart(e: React.DragEvent) { + e.dataTransfer.setData('application/pushpin-url', props.url) + } + + if (!doc) return null + + return ( +
+ + + + +
+ ) +} + +const supportsMimeType = (mimeType) => true + +ContentTypes.register({ + type: 'inspect', + name: 'Inspector', + icon: 'cogs', + contexts: { + board: Inspector, + workspace: Inspector, + list: InspectorInList, + }, + create, + createFrom, + supportsMimeType, +}) diff --git a/yarn.lock b/yarn.lock index ef41db12..3cbc4f41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,6 +351,13 @@ dependencies: core-js "^2.5.7" +"@observablehq/inspector@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@observablehq/inspector/-/inspector-3.1.0.tgz#4726f9aabc58aa410a4ba27adf89b180fa9a302b" + integrity sha512-DOx40q05QZdQnYpUsKch+8raWGmKSBh5DIAinQRoK+ij7Eq/PKpHJvNrrhThF4+2b/qViUhZGOwMLPhlC2qMzg== + dependencies: + esm "^3.0.84" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3265,6 +3272,11 @@ eslint@^5.16.0: table "^5.2.3" text-table "^0.2.0" +esm@^3.0.84: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"