Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"typescript": "^6.0.3"
},
"dependencies": {
"highlight.js": "^11.11.1",
"react": "^19.2.7",
"react-dom": "^19.2.7"
}
Expand Down
7 changes: 5 additions & 2 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ const copyStaticFiles = async () => {
};

const buildOptions = {
entryPoints: [path.join(sourceDirectory, 'main.ts')],
entryPoints: {
all: path.join(sourceDirectory, 'main.ts'),
highlight: path.join(rootDirectory, 'node_modules', 'highlight.js', 'styles', 'github.css'),
},
bundle: true,
outfile: path.join(buildDirectory, 'all.js'),
outdir: buildDirectory,
platform: 'browser',
sourcemap: true,
target: ['es2015'],
Expand Down
1 change: 1 addition & 0 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<head>
<link href="sidebar.css" type="text/css" rel="stylesheet" />
<link href="highlight.css" type="text/css" rel="stylesheet" />
<link href="https://cfl.dropboxstatic.com/static/images/favicon-vflUeLeeY.ico" rel="shortcut icon">
<title>Dropbox API Explorer</title>

Expand Down
1 change: 1 addition & 0 deletions src/team/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<head>
<link href="../sidebar.css" type="text/css" rel="stylesheet" />
<link href="../highlight.css" type="text/css" rel="stylesheet" />
<link href="https://cf.dropboxstatic.com/static/images/favicon-vflk5FiAC.ico" rel="shortcut icon">
<title>Dropbox API Explorer</title>

Expand Down
63 changes: 40 additions & 23 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@
*/

import * as react from 'react';
import { ReactNode } from 'react';
import hljs from 'highlight.js/lib/core';
import bash from 'highlight.js/lib/languages/bash';
import http from 'highlight.js/lib/languages/http';
import json from 'highlight.js/lib/languages/json';
import python from 'highlight.js/lib/languages/python';
import * as cookie from './cookie';

hljs.registerLanguage('bash', bash);
hljs.registerLanguage('http', http);
hljs.registerLanguage('json', json);
hljs.registerLanguage('python', python);

type MappingFn = (key: string, value: any, i: number) => react.DetailedReactHTMLElement<any, any>;

const ce = react.createElement;
Expand Down Expand Up @@ -589,31 +598,39 @@ react.DetailedReactHTMLElement<any, any> => {
export const escapeUnicode = (s: string): string => s.replace(/[\u007f-\uffff]/g,
(c: string) => `\\u${(`0000${c.charCodeAt(0).toString(16)}`).slice(-4)}`);

// Used to get highlight.js to syntax-highlight the codeview and response areas.
// Source: https://github.com/akiran/react-highlight/blob/main/src/index.jsx
// Converts the nested React elements used by the code viewers back to source text.
const reactNodeToText = (node: react.ReactNode): string => {
if (typeof node === 'string' || typeof node === 'number') {
return String(node);
}
if (Array.isArray(node)) {
return node.map(reactNodeToText).join('');
}
if (react.isValidElement<{ children?: react.ReactNode }>(node)) {
return reactNodeToText(node.props.children);
}
return '';
};

interface HltProps {
className: string;
children: react.ClassicElement<Record<string, unknown>>
}
export class Highlight extends react.Component<HltProps, Record<string, unknown>> {
defaultProps = { className: '' };

// TODO: fix this highlighting it breaks updates
// componentDidMount = () => this.highlightCode();
// componentDidUpdate = () => this.highlightCode();

// highlightCode = () => [].forEach.call(
// (<Element>reactDom.findDOMNode(this)).querySelectorAll('pre code'),
// (node: Node) => hljs.highlightBlock(node)
// );

public render(): ReactNode {
return react.createElement('pre', { className: this.props.className },
react.createElement('code', { className: this.props.className },
this.props.children));
}
className?: string;
children: react.ReactNode
}

// Highlight source before rendering so highlight.js never mutates React-managed DOM.
export const Highlight = ({ className = '', children }: HltProps): react.ReactElement => {
const source = reactNodeToText(children);
const highlighted = className === ''
? hljs.highlightAuto(source)
: hljs.highlight(source, { language: className, ignoreIllegals: true });

return react.createElement('pre', { className: 'hljs' },
react.createElement('code', {
className: className === '' ? undefined : `language-${className}`,
dangerouslySetInnerHTML: { __html: highlighted.value },
}));
};

// Utility functions for getting the headers for an API call

// The headers for an RPC-like endpoint HTTP request
Expand Down
5 changes: 3 additions & 2 deletions test/unit/codeview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ test('HTTP code view renders a hostname without a URL scheme', () => {
const request = renderToStaticMarkup(
formats.http.renderRPCLike(endpoint, '<ACCESS_TOKEN>', {}, []),
);
const requestText = request.replace(/<[^>]+>/g, '');

assert.match(request, /Host: api\.dropboxapi\.com\n/);
assert.doesNotMatch(request, /Host: https:\/\//);
assert.match(requestText, /Host: api\.dropboxapi\.com\n/);
assert.doesNotMatch(requestText, /Host: https:\/\//);
});
17 changes: 17 additions & 0 deletions test/unit/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import * as Utils from '../../src/utils';

test('Endpoint can be constructed', () => {
Expand All @@ -17,3 +19,18 @@ test('Endpoint can be constructed', () => {
assert.equal(endpoint.ns, 'users');
assert.equal(endpoint.name, 'get_current_account');
});

test('Highlight renders updated source without DOM mutation', () => {
const renderJson = (value: boolean): string => renderToStaticMarkup(
createElement(Utils.Highlight, { className: 'json' },
createElement('span', null, JSON.stringify({ enabled: value }))),
);

const enabled = renderJson(true);
const disabled = renderJson(false);

assert.match(enabled, /hljs-attr/);
assert.match(enabled, /hljs-keyword[^>]*>true/);
assert.match(disabled, /hljs-keyword[^>]*>false/);
assert.doesNotMatch(disabled, />true</);
});