diff --git a/.hintrc b/.hintrc index 8f3ae3d..b222dc4 100644 --- a/.hintrc +++ b/.hintrc @@ -1,3 +1,3 @@ { "extends": ["development"] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 74d4f0d..f2e46b8 100644 --- a/README.md +++ b/README.md @@ -46,33 +46,36 @@ file shows the following: There are also some additional security features we have implemented: -- Content scripts are injected in an isolated world and never modify the DOM. +- The content script are injected in an isolated world and never modify the DOM. This prevents detection while still allowing access to the DOM. -- Instead of EC6 classes, we use nested functions to create truly - private methods in our content script. I.e: +- Instead of ES6 classes, we use nested functions to create truly + private methods in our content script. I.e, something like the following: + + ```ts - (async () => { - function _privateMethod() { - // Super secret internals - } - - function publicMethod() { - // Public API - } - - return { - publicMethod - }; - })(); + const clazzFactory = (async () => { + async function _privateMethod(): Promise { + // Super secret internals + } + + function publicMethod(): void { + // Public API + } + + return { + publicMethod + }; + }); + const clazz = clazzFactory(); + + clazz.publicMethod(); ``` - +--> Finally, for transparency's sake, yes, the initial commit added _a lot_. That's because this project had been in the works for a while. This was a rich @@ -171,8 +174,25 @@ Basically, it's run alongside the page and has access to the page's source. If our extension wasn't trying to be undetectable, we could use this to modify the page, such as with ad blocking. -## Caveats + +## Issues and Contribution + +For a list of planned features and fixes, see the [TODOs](TODO.md) + +Below are some caveats this extension has that don't have immediate fixes: + +- [XML declarations](https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-XMLDecl) + are not included in inspected output due to the lack of Firefox DOM APIs + necessary to easily regenerate one. For more information, see the + [xmlVersion](https://developer.mozilla.org/en-US/docs/Web/API/Document/xmlVersion), + [xmlEncoding](https://developer.mozilla.org/en-US/docs/Web/API/Document/xmlEncoding) + and [xmlstandalone](https://developer.mozilla.org/en-US/docs/Web/API/Document#document.xmlstandalone) + document properties. +- We can't currently catch every attribute event. Because `MutationObserver`s + run at the microtask level, and because attribute `MutationRecord`s don't + include the new attribute value, we don't yet have a way to get the values + of attributes every time they're updated, only most times. - This extension is subject to the same restrictions as any extension. That means that protected URLs, such as `chrome://`, `edge://`, `chrome-extension://` and `about://`, cannot be inspected. Ironically, this @@ -190,16 +210,15 @@ the page, such as with ad blocking. would then allow websites to detect the extension. There is nothing any browser extension can do to cicrumvent this. Some examples: - In Chrome, a website could send a GET request to - `chrome-extension:///manifest.json`. If it's successful, you - have our extension installed. + `chrome-extension:///manifest.json`. If it's successful, you + have our extension installed. + + ## Credits diff --git a/TODO.md b/TODO.md index 07915f3..58b352d 100644 --- a/TODO.md +++ b/TODO.md @@ -2,65 +2,56 @@ ## Functional Priorities +- StateManager needs to process multiple messages per render, defer rendering, + or decouple the renderer (`setState`) to avoid + `Uncaught Error: Too many re-renders. React limits the number of renders` + `to prevent an infinite loop` on complex websites +- Add Firefox extension support, + see: [signing](https://extensionworkshop.com/documentation/publish/) - Write `README.md` - How it Works section - Add dropdown functionality to all nodes with children -- For accessibility purposes, add the ability - to select and navigate between elements in the inspector -- When elements are added, children should be processed -- Fix adding nodes with previous node IDs, as they seem to be rendered - out of order when, e.g, removing a node in DevTools and undoing the action -- Render XML attributes in an XHTML document, e.g: - - ```xhtml - - ``` - +- When elements are added, children and text should be processed +- Fix duplicate attribute names when adding a second or more attributes + to an element using DevTool's "Edit as HTML" function + (possible race condition) +- Fix adding nodes with previous node IDs + (e.g, removing a node in DevTools and then undoing the action) - For security reasons, validate all messages to ensure no compromisation has occurred within any untrustworthy part of the extension (see: the [issue tracker](https://issuetracker.google.com/issues/311491887) and the referenced [docs](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/security/compromised-renderers.md#Messaging)) -- Tell the user when a protected page cannot be inspected -- Add documentation where it's missing or necessary (e.g, @param or @returns) -- Support attribute mutations - - Have attributes and text referenced through an ID in virtual nodes instead - of storing key-value pairs and regenerating keys by a fixed pattern +- Add documentation where it's missing or necessary (e.g, @param or @template) - Support character data mutations - Finish supporting all applicable node types - Test nested document nodes (see: [SO](https://stackoverflow.com/questions/26010355/is-there-a-way-to-uniquely-identify-an-iframe-that-the-content-script-runs-in-fo)) - Shadow roots can be accessed using [`chrome.dom.openOrClosedShadowRoot`](https://developer.chrome.com/docs/extensions/reference/api/dom?hl=en#method-openOrClosedShadowRoot) +- [Validate](https://validator.w3.org) sample inspector pages + conform to a11y standards ## Technical Priorities -- StateManager needs to process multiple messages per render - or defer rendering to avoid - `Uncaught Error: Too many re-renders. React limits the number of renders` - `to prevent an infinite loop` on complex websites +- Rethink debug view (possibly a test-id field?) +- For a11y purposes, add the ability + to select and navigate between elements in the inspector +- Add an [error boundary](https://react.dev/link/error-boundaries) + for improved debugging UX - Move top-level configuration files to their own folder - Migrate ChildManager to a [reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) for increased maintainability -- ChildManager prop drills `nodes`: - use a shared context and provider (in ChildManager) to manage deep node state -- Format VS Code editor on save with Prettier - Configure ESLint: - Ensure ESLint config file path is correctly recognized and interpreted -- Add chromium-extension-boilerplate as an upstream repo dependency -- Add a light mode: + - Replace Prettier formatting with ESLint to resolve rule conflicts +- Add a light mode toggle - ```css - :root { - color-scheme: light dark; - color: light-dark(); - } - ``` +## Backlog Priorities -## Future Priorities - -- Replace shared assert function with node:assert.assert -- Remove redundant information from messages and props +- Tell the user when a protected page cannot be inspected +- Configure React source map generation + (`React.createElement` statements are ugly) - Create and add donation medium - Modify ESLint config: @@ -87,11 +78,9 @@ (i.e, script injection or manifest registering) - Support Webpack chunking - Add unit tests and end-to-end tests -- Add Firefox extension support (see: [signing](https://extensionworkshop.com/documentation/publish/)) - [Internationalize](https://developer.chrome.com/docs/extensions/reference/api/i18n) - If project scope allows it, add an options page to allow the content script to be automatically reinjected on page or tab (re)load - If project scope allows it, add a network request viewer -- If the project scope allows it, install the dependencies necessary for Webhint - and its .hintrc config file and resolve any vulnerabilities -- If the project scope allows it, add more accessibility customization +- If the project scope allows it, install the dependencies necessary + for Webhint and its .hintrc config file and resolve any vulnerabilities diff --git a/package-lock.json b/package-lock.json index 0abc475..baa7ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "async-mutex": "^0.5.0", - "form-data": "^4.0.3", + "form-data": "^4.0.4", "https": "^1.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -25,10 +25,10 @@ "css-loader": "^7.1.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^5.2.0", "generate-json-webpack-plugin": "^2.0.0", "html-loader": "^4.2.0", - "html-webpack-plugin": "^5.5.0", + "html-webpack-plugin": "^5.6.4", "nodemon": "^3.1.9", "prettier": "^2.8.3", "sass": "^1.57.1", @@ -38,7 +38,7 @@ "terser-webpack-plugin": "^5.3.14", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", - "type-fest": "^3.5.2", + "type-fest": "^5.0.1", "typescript": "^5.8.3", "webpack": "^5.98.0", "webpack-cli": "^4.10.0", @@ -1515,16 +1515,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1659,10 +1649,11 @@ } }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2123,17 +2114,18 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -2155,11 +2147,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -3293,15 +3289,16 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -3890,9 +3887,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4560,10 +4557,11 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", "dev": true, + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -6960,10 +6958,11 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7674,16 +7673,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9299,6 +9288,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -9742,12 +9744,16 @@ } }, "node_modules/type-fest": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.2.tgz", - "integrity": "sha512-Ph7S4EhXzWy0sbljEuZo0tTNoLl+K2tPauGrQpcwUWrOVneLePTuhVzcuzVJJ6RU5DsNwQZka+8YtkXXU4z9cA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.0.1.tgz", + "integrity": "sha512-9MpwAI52m8H6ssA542UxSLnSiSD2dsC3/L85g6hVubLSXd82wdI80eZwTWhdOfN67NlA+D+oipAs1MlcTcu3KA==", "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index e65cc6f..d233403 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "async-mutex": "^0.5.0", - "form-data": "^4.0.3", + "form-data": "^4.0.4", "https": "^1.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -32,10 +32,10 @@ "css-loader": "^7.1.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^5.2.0", "generate-json-webpack-plugin": "^2.0.0", "html-loader": "^4.2.0", - "html-webpack-plugin": "^5.5.0", + "html-webpack-plugin": "^5.6.4", "nodemon": "^3.1.9", "prettier": "^2.8.3", "sass": "^1.57.1", @@ -45,7 +45,7 @@ "terser-webpack-plugin": "^5.3.14", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", - "type-fest": "^3.5.2", + "type-fest": "^5.0.1", "typescript": "^5.8.3", "webpack": "^5.98.0", "webpack-cli": "^4.10.0", diff --git a/src/globals.d.ts b/src/globals.d.ts index 8d28773..fdc1e36 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -8,6 +8,8 @@ declare global { // Extends existing type interface Attr { nodeType: Node['ATTRIBUTE_NODE']; + nodeValue: string; + previousSibling: null; } // Extends existing type @@ -34,7 +36,10 @@ declare global { // Extends existing type interface ProcessingInstruction { + // For XML declaration, see: + // https://www.w3.org/TR/2006/REC-xml11-20060816/#sec-prolog-dtd nodeType: Node['PROCESSING_INSTRUCTION_NODE']; + nodeValue: string; } // Extends existing type diff --git a/src/pages/popup/base.tsx b/src/pages/popup/base.tsx index 6991a48..6059c6f 100644 --- a/src/pages/popup/base.tsx +++ b/src/pages/popup/base.tsx @@ -1,52 +1,21 @@ -import React, { ReactNode } from 'react'; -import { VirtualInlineText, VirtualAttribute } from './components'; +import { ReactNode } from 'react'; export interface StoredVirtualNodeProps { nodeType: number; nodeName: string; nodeValue: string | null; - attributes: Record; childNodeIds: string[]; parentId?: string; } -export type NonStoredProps

= VirtualNodeProps & Omit; - -export interface VirtualNodeProps - extends Omit { - id: string; - children?: ReactNode[]; +export interface NoChildren { + childNodeIds: never[]; } -export function VirtualNode(props: Readonly) { - const attrs = - props.attributes == null - ? '' - : Object.entries(props.attributes).map(([name, value]) => { - const id = `${props.id}-attr-${name}`; - - return ( - - ); - }); - - return ( -

-            {`<${props.nodeName}`}
-            {attrs}>
-            
-            {props.children}
-            {``}
-        
- ); -} +export type NonStoredProps

= Omit< + StoredVirtualNodeProps & P, + 'childNodeIds' | 'attributeIds' +> & { + id: string; + children?: P extends NoChildren ? never[] : ReactNode[]; +}; diff --git a/src/pages/popup/childManager.tsx b/src/pages/popup/childManager.tsx index 9dfc6d1..e616508 100644 --- a/src/pages/popup/childManager.tsx +++ b/src/pages/popup/childManager.tsx @@ -1,26 +1,25 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React, { ReactNode, useContext } from 'react'; import { StoredVirtualNodeProps } from './base'; import { + StoredVirtualAttributeProps, StoredVirtualCdataSectionProps, StoredVirtualCommentProps, StoredVirtualDoctypeProps, StoredVirtualDocumentProps, StoredVirtualElementProps, + StoredVirtualProcessingInstructionProps, StoredVirtualTextProps, + VirtualAttribute, VirtualCdataSection, VirtualComment, VirtualDoctype, VirtualDocument, VirtualElement, + VirtualInlineText, + VirtualProcessingInstruction, VirtualText } from './components'; - -export type NodeState = { [id: string]: StoredVirtualNodeProps }; - -interface ChildManagerProps { - id: string; - nodes: NodeState; -} +import { NodeContext, NodeState } from './popup'; function renderDebug(id: Readonly): ReactNode { return ( @@ -30,48 +29,92 @@ function renderDebug(id: Readonly): ReactNode { ); } -function renderChildren( - node: Readonly, - nodes: Readonly -): ReactElement[] { - return node.childNodeIds.map((id) => ( - - )); +function renderChildren(node: Readonly): ReactNode[] { + return node.childNodeIds.map((id) => ); } function renderElement( - props: Readonly, - node: Readonly -): ReactElement { + id: Readonly, + node: Readonly, + nodes: Readonly +): ReactNode { + const attrs = node.attributeIds.keys().map((attrId) => { + const attrNode = nodes[attrId] as StoredVirtualAttributeProps; + + return ( + + ); + }); + + let renderingChildren: ReactNode[]; + + if (node.childNodeIds.length < 1 && node.nodeValue == null) { + renderingChildren = [...attrs, ' />']; + } else { + renderingChildren = [ + ...attrs, + '>', + , + ...renderChildren(node), + `` + ]; + } + return ( <> - {renderDebug(props.id)} + {renderDebug(id)} - {renderChildren(node, props.nodes)} + {renderingChildren} ); } +function renderAttribute( + id: Readonly, + node: Readonly +): ReactNode { + return ( + <> + {/* {renderDebug(id)} */} + + + ); +} + function renderText( - props: Readonly, + id: Readonly, node: Readonly -): ReactElement { +): ReactNode { return ( <> - {renderDebug(props.id)} + {renderDebug(id)} @@ -80,18 +123,36 @@ function renderText( } function renderCdataSection( - props: Readonly, + id: Readonly, node: Readonly -): ReactElement { +): ReactNode { return ( <> - {renderDebug(props.id)} + {renderDebug(id)} + + ); +} + +function renderProcessingInstruction( + id: Readonly, + node: Readonly +): ReactNode { + return ( + <> + {renderDebug(id)} + @@ -100,18 +161,17 @@ function renderCdataSection( } function renderComment( - props: Readonly, + id: Readonly, node: Readonly -): ReactElement { +): ReactNode { return ( <> - {renderDebug(props.id)} + {renderDebug(id)} @@ -120,39 +180,37 @@ function renderComment( } function renderDocument( - props: Readonly, + id: Readonly, node: Readonly -): ReactElement { +): ReactNode { return ( <> - {renderDebug(props.id)} + {renderDebug(id)} - {renderChildren(node, props.nodes)} + {renderChildren(node)} ); } function renderDoctype( - props: Readonly, + id: Readonly, node: Readonly -): ReactElement { +): ReactNode { return ( <> - {renderDebug(props.id)} + {renderDebug(id)} ) { - const node = props.nodes[props.id]; +export function ChildManager({ id }: { readonly id: string }): ReactNode { + const nodes = useContext(NodeContext); + const node = nodes[id]; switch (node.nodeType) { case Node.ELEMENT_NODE: { - return renderElement(props, node as StoredVirtualElementProps); + return renderElement(id, node as StoredVirtualElementProps, nodes); + } + case Node.ATTRIBUTE_NODE: { + return renderAttribute(id, node as StoredVirtualAttributeProps); } case Node.TEXT_NODE: { - return renderText(props, node as StoredVirtualTextProps); + return renderText(id, node as StoredVirtualTextProps); } case Node.CDATA_SECTION_NODE: { return renderCdataSection( - props, + id, node as StoredVirtualCdataSectionProps ); } + case Node.PROCESSING_INSTRUCTION_NODE: { + return renderProcessingInstruction( + id, + node as StoredVirtualProcessingInstructionProps + ); + } case Node.COMMENT_NODE: { - return renderComment(props, node as StoredVirtualCommentProps); + return renderComment(id, node as StoredVirtualCommentProps); } case Node.DOCUMENT_NODE: { - return renderDocument(props, node as StoredVirtualDocumentProps); + return renderDocument(id, node as StoredVirtualDocumentProps); } case Node.DOCUMENT_TYPE_NODE: { - return renderDoctype(props, node as StoredVirtualDoctypeProps); + return renderDoctype(id, node as StoredVirtualDoctypeProps); } - case Node.ATTRIBUTE_NODE: case Node.ENTITY_REFERENCE_NODE: case Node.ENTITY_NODE: - case Node.PROCESSING_INSTRUCTION_NODE: case Node.DOCUMENT_FRAGMENT_NODE: case Node.NOTATION_NODE: default: { return ( <> - {renderDebug(props.id)} -

+                    {renderDebug(id)}
+                    
{`Unsupported node type: ${node.nodeType}`} -
+ ); } diff --git a/src/pages/popup/components/attribute.tsx b/src/pages/popup/components/attribute.tsx index 0caf4d1..9665724 100644 --- a/src/pages/popup/components/attribute.tsx +++ b/src/pages/popup/components/attribute.tsx @@ -1,31 +1,39 @@ -import React, { Fragment, ReactElement } from 'react'; +import React, { ReactNode } from 'react'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; +import { BaseUpdateMsg } from '../msgs'; -export interface VirtualAttributeProps { +type SharedValues = { id: string; + parentId: string; nodeType: Node['ATTRIBUTE_NODE']; nodeName: string; nodeValue: string | null; - attributes: Record; - childNodeIds?: never[]; - parentId?: string; -} + prevSiblingId?: undefined; +}; + +export type UpdateAttributeMsg = BaseUpdateMsg & SharedValues; + +export type StoredVirtualAttributeProps = StoredVirtualNodeProps & + SharedValues & + NoChildren; + +export type VirtualAttributeProps = NonStoredProps; /** * The document source virtual element attribute component */ export function VirtualAttribute( props: Readonly -): ReactElement { +): ReactNode { return ( - + <>
{' ' + props.nodeName}
{props.nodeValue == null ? undefined : ( <> -
="
-
{props.nodeValue}
-
"
+ =" +
{props.nodeValue}
" )} -
+ ); } diff --git a/src/pages/popup/components/cdataSection.tsx b/src/pages/popup/components/cdataSection.tsx index d79d2f1..815adee 100644 --- a/src/pages/popup/components/cdataSection.tsx +++ b/src/pages/popup/components/cdataSection.tsx @@ -1,29 +1,31 @@ -import React from 'react'; -import { NonStoredProps, StoredVirtualNodeProps } from '../base'; +import React, { ReactNode } from 'react'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; +import { VirtualInlineText } from './text'; interface SharedValues { parentId: string; nodeType: Node['CDATA_SECTION_NODE']; nodeName: '#cdata-section'; nodeValue: string; - attributes: Record; prevSiblingId?: string; - children?: never[]; } export type UpdateCdataSectionMsg = BaseUpdateMsg & SharedValues; export type StoredVirtualCdataSectionProps = StoredVirtualNodeProps & - SharedValues; + SharedValues & + NoChildren; export type VirtualCdataSectionProps = NonStoredProps; -export function VirtualCdataSection(props: Readonly) { +export function VirtualCdataSection( + props: Readonly +): ReactNode { return ( -
-            

{``}

-
+
+ `} /> +
); } diff --git a/src/pages/popup/components/comment.tsx b/src/pages/popup/components/comment.tsx index f2bd86c..b42f8c2 100644 --- a/src/pages/popup/components/comment.tsx +++ b/src/pages/popup/components/comment.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { NonStoredProps, StoredVirtualNodeProps } from '../base'; +import React, { ReactNode } from 'react'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; interface SharedValues { @@ -7,21 +7,19 @@ interface SharedValues { nodeType: Node['COMMENT_NODE']; nodeName: '#comment'; nodeValue: string; - attributes: Record; prevSiblingId?: string; - children?: never[]; } export type UpdateCommentMsg = BaseUpdateMsg & SharedValues; -export type StoredVirtualCommentProps = StoredVirtualNodeProps & SharedValues; +export type StoredVirtualCommentProps = StoredVirtualNodeProps & + SharedValues & + NoChildren; export type VirtualCommentProps = NonStoredProps; -export function VirtualComment(props: Readonly) { - return ( -
-            {``}
-        
- ); +export function VirtualComment( + props: Readonly +): ReactNode { + return
{``}
; } diff --git a/src/pages/popup/components/doctype.tsx b/src/pages/popup/components/doctype.tsx index d3574a6..71c3244 100644 --- a/src/pages/popup/components/doctype.tsx +++ b/src/pages/popup/components/doctype.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { NonStoredProps, StoredVirtualNodeProps } from '../base'; +import React, { ReactNode } from 'react'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; interface SharedValues { @@ -7,35 +7,36 @@ interface SharedValues { nodeType: Node['DOCUMENT_TYPE_NODE']; nodeName: string; nodeValue: null; - attributes: Record; publicId: string; systemId: string; - children?: never[]; } export type UpdateDoctypeMsg = BaseUpdateMsg & SharedValues; export type StoredVirtualDoctypeProps = StoredVirtualNodeProps & - SharedValues & { childNodeIds: never[] }; + SharedValues & + NoChildren; export type VirtualDoctypeProps = NonStoredProps; -export function VirtualDoctype(props: Readonly) { +export function VirtualDoctype( + props: Readonly +): ReactNode { let xmlId = ''; if (props.publicId !== '') { xmlId += ` PUBLIC "${props.publicId}"`; - if (props.systemId === '') { - xmlId += ` ${props.systemId}`; + if (props.systemId !== '') { + xmlId += ` "${props.systemId}"`; } } else if (props.systemId !== '') { xmlId += ` SYSTEM "${props.systemId}"`; } return ( -
+        
{``} -
+ ); } diff --git a/src/pages/popup/components/document.tsx b/src/pages/popup/components/document.tsx index 46339f4..0972aef 100644 --- a/src/pages/popup/components/document.tsx +++ b/src/pages/popup/components/document.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactNode } from 'react'; import { NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; @@ -6,7 +6,6 @@ interface SharedValues { nodeType: Node['DOCUMENT_NODE']; nodeName: '#document'; nodeValue: null; - attributes: Record; prevSiblingId?: undefined; documentURI: string; } @@ -17,11 +16,11 @@ export type StoredVirtualDocumentProps = StoredVirtualNodeProps & SharedValues; export type VirtualDocumentProps = NonStoredProps; -export function VirtualDocument(props: VirtualDocumentProps): ReactElement { +export function VirtualDocument(props: VirtualDocumentProps): ReactNode { // For security, don't change the rel attribute // See: https://stackoverflow.com/a/17711167/8387760 return ( -
+        
{`${props.nodeName} (`} {`)`}
    {props.children}
-
+ ); } diff --git a/src/pages/popup/components/element.tsx b/src/pages/popup/components/element.tsx index 71da6e3..9640a3f 100644 --- a/src/pages/popup/components/element.tsx +++ b/src/pages/popup/components/element.tsx @@ -1,15 +1,26 @@ -import { NonStoredProps, StoredVirtualNodeProps, VirtualNode } from '../base'; +import React, { ReactNode } from 'react'; +import { NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; interface SharedValues { nodeType: Node['ELEMENT_NODE']; - attributes: Record; } export type UpdateElementMsg = BaseUpdateMsg & SharedValues; -export type StoredVirtualElementProps = StoredVirtualNodeProps & SharedValues; +export type StoredVirtualElementProps = StoredVirtualNodeProps & + SharedValues & { attributeIds: Set }; export type VirtualElementProps = NonStoredProps; -export { VirtualNode as VirtualElement }; +export function VirtualElement( + props: Readonly +): ReactNode { + // Parts of this component's rendering are handled by the child manager + return ( +
+ {`<${props.nodeName}`} + {props.children} +
+ ); +} diff --git a/src/pages/popup/components/index.tsx b/src/pages/popup/components/index.tsx index ea3aed8..579ddf2 100644 --- a/src/pages/popup/components/index.tsx +++ b/src/pages/popup/components/index.tsx @@ -5,3 +5,4 @@ export * from './doctype'; export * from './document'; export * from './element'; export * from './text'; +export * from './processingInstruction'; diff --git a/src/pages/popup/components/processingInstruction.tsx b/src/pages/popup/components/processingInstruction.tsx new file mode 100644 index 0000000..187277d --- /dev/null +++ b/src/pages/popup/components/processingInstruction.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from 'react'; +import { BaseUpdateMsg } from '../msgs'; +import { VirtualInlineText } from './text'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; + +export interface SharedValues { + parentId: string; + nodeType: Node['PROCESSING_INSTRUCTION_NODE']; + nodeValue: string; + prevSiblingId?: string; +} + +export type UpdateProcessingInstructionMsg = BaseUpdateMsg & SharedValues; + +export type StoredVirtualProcessingInstructionProps = StoredVirtualNodeProps & + SharedValues & + NoChildren; + +export type VirtualProcessingInstructionProps = + NonStoredProps; + +export function VirtualProcessingInstruction( + props: VirtualProcessingInstructionProps +): ReactNode { + const data = props.nodeValue === '' ? '' : ' ' + props.nodeValue; + + return ( +
+ `} /> +
+ ); +} diff --git a/src/pages/popup/components/text.tsx b/src/pages/popup/components/text.tsx index 4de5210..5f1ac8c 100644 --- a/src/pages/popup/components/text.tsx +++ b/src/pages/popup/components/text.tsx @@ -1,5 +1,5 @@ -import React, { ReactElement } from 'react'; -import { NonStoredProps, StoredVirtualNodeProps } from '../base'; +import React, { ReactNode } from 'react'; +import { NoChildren, NonStoredProps, StoredVirtualNodeProps } from '../base'; import { BaseUpdateMsg } from '../msgs'; interface SharedValues { @@ -7,40 +7,33 @@ interface SharedValues { nodeType: Node['TEXT_NODE']; nodeName: '#text'; nodeValue: string; - attributes: Record; prevSiblingId?: string; - children?: never[]; } export type UpdateTextMsg = BaseUpdateMsg & SharedValues; export type StoredVirtualTextProps = StoredVirtualNodeProps & - SharedValues & { childNodeIds: never[] }; + SharedValues & + NoChildren; export type VirtualTextProps = NonStoredProps; /** * The document source block virtual text node component */ -export function VirtualText(props: Readonly): ReactElement { +export function VirtualText(props: Readonly): ReactNode { const hidden = props.nodeValue === ''; return ( -