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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import invariant from "invariant";
import type { JsonValue } from "type-fest";

import { jsonParseSafe } from "@code-chronicles/util/jsonParseSafe";

type ProxiedPropertyKey = "innerHTML" | "innerText" | "textContent";

type MiddlewareFn = (
data: JsonValue,
script: HTMLScriptElement,
property: ProxiedPropertyKey,
) => JsonValue;

function inject<TProto extends { constructor: Function }>(
proto: TProto,
property: ProxiedPropertyKey,
middlewareFn: MiddlewareFn,
): void {
const prevDescriptor = Object.getOwnPropertyDescriptor(proto, property);

invariant(
prevDescriptor && prevDescriptor.get,
`\`${proto.constructor.name}.prototype.${property}\` property descriptor didn't have the expected form!`,
);
const prevDescriptorGet = prevDescriptor.get;

Object.defineProperty(HTMLScriptElement.prototype, property, {
...prevDescriptor,
get(this: HTMLScriptElement) {
const data = prevDescriptorGet.call(this);

// If the data doesn't parse as JSON, we pass it through unchanged.
// If it does parse as JSON, we'll run the middleware.
const parsedData = jsonParseSafe(data);
if (parsedData) {
return JSON.stringify(middlewareFn(parsedData.data, this, property));
}

return data;
},
});
}

/**
* Injects a function to process and possibly replace JSON data stored within
* <script> tags. The middleware will run when properties such as `innerHTML`
* get accessed, and it must return the possibly updated JSON data.
*/
export function injectJsonScriptMiddleware(middlewareFn: MiddlewareFn): void {
inject(Element.prototype, "innerHTML", middlewareFn);
inject(HTMLElement.prototype, "innerText", middlewareFn);
inject(Node.prototype, "textContent", middlewareFn);
}
6 changes: 6 additions & 0 deletions workspaces/leetcode-zen-mode/src/extension/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mapJsonBlobData } from "@code-chronicles/util/mapJsonBlobData";

import { injectJsonScriptMiddleware } from "./injectJsonScriptMiddleware.ts";
import { injectXhrBlobResponseMiddleware } from "./injectXhrBlobResponseMiddleware.ts";
import { patchWebpackChunkLoading } from "./patchWebpackChunkLoading.ts";
import { rewriteLeetCodeGraphQLData } from "./rewriteLeetCodeGraphQLData.ts";
Expand All @@ -23,6 +24,11 @@ function main() {
return blob;
});

// LeetCode also reads data stored as JSON within some <script> tags within
// the page, for example the __NEXT_DATA__ used by Next.js. We will inject
// the middleware there as well.
injectJsonScriptMiddleware(rewriteLeetCodeGraphQLData);

// Additionally, we will patch some of the actual page code! We will do so
// by trying to intercept `webpack` chunk loading, so that we can patch the
// modules used by the page.
Expand Down
11 changes: 11 additions & 0 deletions workspaces/util/src/jsonParseSafe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { JsonValue } from "type-fest";

export function jsonParseSafe(
...args: Parameters<typeof JSON.parse>
): { data: JsonValue } | undefined {
try {
return { data: JSON.parse(...args) };
} catch {
return undefined;
}
}
Loading