diff --git a/package-lock.json b/package-lock.json
index 0926803..a249f5c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"": {
"name": "dropbox-api-v2-explorer",
"dependencies": {
+ "highlight.js": "^11.11.1",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
@@ -1607,6 +1608,15 @@
"node": ">=8"
}
},
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
diff --git a/package.json b/package.json
index 71dd7b6..79586d7 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"typescript": "^6.0.3"
},
"dependencies": {
+ "highlight.js": "^11.11.1",
"react": "^19.2.7",
"react-dom": "^19.2.7"
}
diff --git a/scripts/build.js b/scripts/build.js
index ac7a39f..fc63dd6 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -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'],
diff --git a/src/index.html b/src/index.html
index d1e762a..a1522e8 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,5 +1,6 @@
+
Dropbox API Explorer
diff --git a/src/team/index.html b/src/team/index.html
index d2c59ce..08021f7 100644
--- a/src/team/index.html
+++ b/src/team/index.html
@@ -1,5 +1,6 @@
+
Dropbox API Explorer
diff --git a/src/utils.ts b/src/utils.ts
index 95322ba..e77f19f 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -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;
const ce = react.createElement;
@@ -589,31 +598,39 @@ react.DetailedReactHTMLElement => {
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>
-}
-export class Highlight extends react.Component> {
- defaultProps = { className: '' };
-
- // TODO: fix this highlighting it breaks updates
- // componentDidMount = () => this.highlightCode();
- // componentDidUpdate = () => this.highlightCode();
-
- // highlightCode = () => [].forEach.call(
- // (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
diff --git a/test/unit/codeview.test.ts b/test/unit/codeview.test.ts
index efca696..b7cfda4 100644
--- a/test/unit/codeview.test.ts
+++ b/test/unit/codeview.test.ts
@@ -14,7 +14,8 @@ test('HTTP code view renders a hostname without a URL scheme', () => {
const request = renderToStaticMarkup(
formats.http.renderRPCLike(endpoint, '', {}, []),
);
+ 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:\/\//);
});
diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts
index d5c1fcd..8fdd8c4 100644
--- a/test/unit/utils.test.ts
+++ b/test/unit/utils.test.ts
@@ -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', () => {
@@ -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);
+});