├── pnpm-workspace.yaml ├── packages ├── prosemirror-dev-toolkit │ ├── cypress │ │ ├── fixtures │ │ │ ├── snapshot-broken │ │ │ ├── snapshot-0.json │ │ │ ├── snapshot-1.json │ │ │ └── snapshot-incompatible.json │ │ ├── e2e │ │ │ ├── __image_snapshots__ │ │ │ │ ├── # History tab Should track transactions and show diffs #0.png │ │ │ │ ├── # DevTools Should render and allow to be closed reopened #0.png │ │ │ │ ├── # Schema tab Should show the current schema nodes and marks #0.png │ │ │ │ ├── # State tab Should allow expanding and collapsing and tree-view nodes #0.png │ │ │ │ ├── # Plugins tab Should show the default plugins and allow inspecting them #0.png │ │ │ │ ├── # History tab Should group selection transactions and allow inspecting them #0.png │ │ │ │ ├── # History tab Should group selection transactions and allow inspecting them #1.png │ │ │ │ └── # Structure tab Should show the DocView of the current and doc and Node info #0.png │ │ │ ├── plugins-tab.spec.cy.ts │ │ │ ├── schema-tab.spec.cy.ts │ │ │ ├── state-tab.spec.cy.ts │ │ │ └── devtools.spec.cy.ts │ │ ├── tsconfig.json │ │ └── support │ │ │ ├── index.d.ts │ │ │ ├── e2e.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── src │ │ ├── test-utils │ │ │ ├── setupTests.js │ │ │ ├── filterPlugin.ts │ │ │ ├── setupEditor.ts │ │ │ └── schema.ts │ │ ├── empty.ts │ │ ├── typings │ │ │ ├── snapshots.ts │ │ │ ├── pm.ts │ │ │ └── history.ts │ │ ├── types.ts │ │ ├── history-and-diff │ │ │ ├── diff.ts │ │ │ ├── transaction.ts │ │ │ ├── createHistoryEntry.ts │ │ │ └── subscribeToTransactions.ts │ │ ├── createOrFindPlace.ts │ │ ├── components │ │ │ ├── clickOutside.ts │ │ │ ├── Button.svelte │ │ │ ├── __tests__ │ │ │ │ ├── Button.spec.ts │ │ │ │ └── DevTools.spec.ts │ │ │ ├── PasteModal.svelte │ │ │ ├── DevTools.svelte │ │ │ └── FloatingBtn.svelte │ │ ├── global.scss │ │ ├── index.ts │ │ ├── tabs │ │ │ ├── structure │ │ │ │ ├── DocView.svelte │ │ │ │ ├── colors.ts │ │ │ │ └── DocNode.svelte │ │ │ ├── SplitView.svelte │ │ │ ├── state │ │ │ │ ├── getActiveMarks.ts │ │ │ │ ├── selection.ts │ │ │ │ └── StateTab.svelte │ │ │ ├── List.svelte │ │ │ ├── SchemaTab.svelte │ │ │ ├── TabsMenu.svelte │ │ │ ├── snapshots │ │ │ │ ├── SnapshotsTab.svelte │ │ │ │ └── SnapshotsList.svelte │ │ │ ├── history │ │ │ │ ├── DiffValue.svelte │ │ │ │ ├── HistoryList.svelte │ │ │ │ └── mapDeltas.ts │ │ │ └── PluginsTab.svelte │ │ ├── global.d.ts │ │ ├── context.ts │ │ ├── ProseMirrorDevToolkit.ts │ │ ├── stores │ │ │ ├── stateHistory.ts │ │ │ └── snapshots.ts │ │ ├── __tests__ │ │ │ ├── applyDevTools.spec.ts │ │ │ └── edge-cases.spec.ts │ │ └── applyDevTools.ts │ ├── svelte.config.js │ ├── cypress.config.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── NOTES.md │ ├── package.json │ └── rollup.config.js ├── site │ ├── src │ │ ├── vite-env.d.ts │ │ ├── global-types.d.ts │ │ ├── pm │ │ │ ├── editor.css │ │ │ ├── schema-types.ts │ │ │ ├── example-plugin │ │ │ │ ├── types.ts │ │ │ │ ├── findChangedNodes.ts │ │ │ │ └── index.ts │ │ │ ├── useEditor.ts │ │ │ ├── prosemirror-example-setup.css │ │ │ ├── PMEditor.tsx │ │ │ └── menu.css │ │ ├── index.tsx │ │ ├── index.css │ │ ├── pages │ │ │ ├── NoEditorPage.tsx │ │ │ ├── DevToolsPage.tsx │ │ │ ├── FrontPage.tsx │ │ │ ├── PlainPMPage.tsx │ │ │ ├── IFramePage.tsx │ │ │ └── YjsPage.tsx │ │ ├── components │ │ │ ├── Layout.tsx │ │ │ ├── NavBar.tsx │ │ │ └── Editor.tsx │ │ └── Routes.tsx │ ├── README.md │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── CHANGELOG.md └── extension │ ├── public │ ├── pm.png │ ├── devtools-128.png │ ├── devtools-16.png │ ├── devtools-32.png │ ├── devtools-48.png │ ├── devtools-disabled-16.png │ ├── devtools-disabled-32.png │ ├── devtools-disabled-48.png │ ├── devtools-disabled-128.png │ └── manifest.json │ ├── src │ ├── types │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── pop-up.ts │ │ ├── inject.ts │ │ ├── sw.ts │ │ └── consts.ts │ ├── pop-up │ │ ├── index.ts │ │ └── store.ts │ ├── sw │ │ ├── getCurrentTab.ts │ │ ├── index.ts │ │ ├── openWindow.ts │ │ └── ports.ts │ ├── inject │ │ ├── utils.ts │ │ ├── findEditorViews.ts │ │ ├── store.ts │ │ ├── index.ts │ │ └── pmViewDescHack.ts │ └── proxy │ │ └── index.ts │ ├── svelte.config.js │ ├── pop-up.html │ ├── tsconfig.json │ ├── README.md │ ├── vite.config.input.ts │ ├── vite.config.ts │ ├── package.json │ └── CHANGELOG.md ├── .prettierignore ├── .husky └── pre-commit ├── .changeset ├── config.json └── README.md ├── .prettierrc ├── .github ├── actions │ ├── pnpm │ │ └── action.yml │ └── build-test │ │ └── action.yml └── workflows │ ├── pull-request.yml │ ├── gh-pages.yml │ └── version-publish.yml ├── .gitignore ├── LICENSE ├── .eslintrc.json ├── ROADMAP.md ├── package.json └── inject.js /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/fixtures/snapshot-broken: -------------------------------------------------------------------------------- 1 | " -------------------------------------------------------------------------------- /packages/site/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/test-utils/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | pnpm-lock.yaml 3 | node_modules 4 | build 5 | dist 6 | tmp 7 | .changeset -------------------------------------------------------------------------------- /packages/extension/public/pm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/pm.png -------------------------------------------------------------------------------- /packages/extension/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts' 2 | export * from './inject' 3 | export * from './pop-up' 4 | export * from './sw' 5 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/empty.ts: -------------------------------------------------------------------------------- 1 | // empty export to remove `chalk` from `jsondiffpatch` during bundling. 2 | 3 | export default null 4 | -------------------------------------------------------------------------------- /packages/extension/public/devtools-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-128.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-16.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-32.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-48.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-disabled-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-disabled-16.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-disabled-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-disabled-32.png -------------------------------------------------------------------------------- /packages/extension/public/devtools-disabled-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-disabled-48.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/typings/snapshots.ts: -------------------------------------------------------------------------------- 1 | export interface Snapshot { 2 | name: string 3 | timestamp: number 4 | doc: { [key: string]: any } 5 | } 6 | -------------------------------------------------------------------------------- /packages/extension/public/devtools-disabled-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/extension/public/devtools-disabled-128.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/fixtures/snapshot-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "doc", 3 | "content": [ 4 | { 5 | "type": "paragraph" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | if (exec < /dev/tty) ; then 5 | # /dev/tty is available 6 | exec < /dev/tty && yarn cs 7 | fi 8 | -------------------------------------------------------------------------------- /packages/extension/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type Ok = { 2 | data: T 3 | } 4 | export type Err = { 5 | err: string 6 | code: number 7 | } 8 | export type Result = Ok | Err 9 | -------------------------------------------------------------------------------- /packages/site/src/global-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'prosemirror-dev-tools' { 2 | import type { EditorView } from 'prosemirror-view' 3 | export const applyDevTools: (view: EditorView) => () => void 4 | } 5 | -------------------------------------------------------------------------------- /packages/site/README.md: -------------------------------------------------------------------------------- 1 | # Site 2 | 3 | Cypress tests are executed against this app to test various features of the prosemirror-dev-toolkit. 4 | 5 | Use `pnpm start` to launch the site at http://localhost:3300 6 | -------------------------------------------------------------------------------- /packages/extension/svelte.config.js: -------------------------------------------------------------------------------- 1 | import autoPreprocess from 'svelte-preprocess' 2 | 3 | const preprocessOptions = {} 4 | 5 | export default { 6 | preprocess: autoPreprocess(preprocessOptions), 7 | preprocessOptions 8 | } 9 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ButtonPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' 2 | export interface DevToolsOpts { 3 | devToolsExpanded?: boolean 4 | buttonPosition?: ButtonPosition 5 | disableWebComponent?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /packages/extension/src/pop-up/index.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | import { init } from './store' 3 | 4 | init() 5 | 6 | const el = document.querySelector('.__prosemirror-dev-toolkit-extension__') 7 | if (el) { 8 | new App({ target: el }) 9 | } 10 | 11 | export {} 12 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/typings/pm.ts: -------------------------------------------------------------------------------- 1 | import type { Fragment as Frag, Node as PMNode } from 'prosemirror-model' 2 | import type { Plugin as PMPlugin } from 'prosemirror-state' 3 | 4 | export type Plugin = PMPlugin & { key: string } 5 | 6 | export type Fragment = Frag & { content: PMNode[] } 7 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should track transactions and show diffs #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should track transactions and show diffs #0.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# DevTools Should render and allow to be closed reopened #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# DevTools Should render and allow to be closed reopened #0.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Schema tab Should show the current schema nodes and marks #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Schema tab Should show the current schema nodes and marks #0.png -------------------------------------------------------------------------------- /packages/site/src/pm/editor.css: -------------------------------------------------------------------------------- 1 | .pm-editor { 2 | border: 1px solid black; 3 | } 4 | 5 | .pm-editor.main { 6 | overflow: scroll; 7 | max-height: 500px; 8 | } 9 | 10 | .ProseMirror { 11 | min-height: 140px; 12 | overflow-wrap: break-word; 13 | outline: none; 14 | padding: 10px; 15 | white-space: pre-wrap; 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# State tab Should allow expanding and collapsing and tree-view nodes #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# State tab Should allow expanding and collapsing and tree-view nodes #0.png -------------------------------------------------------------------------------- /packages/extension/pop-up.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ProseMirror DevTools 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Plugins tab Should show the default plugins and allow inspecting them #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Plugins tab Should show the default plugins and allow inspecting them #0.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should group selection transactions and allow inspecting them #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should group selection transactions and allow inspecting them #0.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should group selection transactions and allow inspecting them #1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# History tab Should group selection transactions and allow inspecting them #1.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Structure tab Should show the DocView of the current and doc and Node info #0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-dev-toolkit/HEAD/packages/prosemirror-dev-toolkit/cypress/e2e/__image_snapshots__/# Structure tab Should show the DocView of the current and doc and Node info #0.png -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/history-and-diff/diff.ts: -------------------------------------------------------------------------------- 1 | import { DiffPatcher } from 'jsondiffpatch' 2 | 3 | const diffPatcher = new DiffPatcher({ 4 | arrays: { detectMove: false, includeValueOnMove: false }, 5 | textDiff: { minLength: 1 } 6 | }) 7 | 8 | export function diff(inputA: any, inputB: any) { 9 | return diffPatcher.diff(inputA, inputB) 10 | } 11 | -------------------------------------------------------------------------------- /packages/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | prosemirror-dev-toolkit 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/extension/src/sw/getCurrentTab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns current Chrome tab 3 | * 4 | * https://developer.chrome.com/docs/extensions/reference/tabs/#get-the-current-tab 5 | * @returns 6 | */ 7 | export async function getCurrentTab() { 8 | const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }) 9 | return tab as chrome.tabs.Tab | undefined 10 | } 11 | -------------------------------------------------------------------------------- /packages/site/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | import { Routes } from './Routes' 5 | 6 | import './index.css' 7 | import './pm/editor.css' 8 | import './pm/prosemirror-example-setup.css' 9 | import './pm/menu.css' 10 | 11 | const root = createRoot(document.getElementById('root') as HTMLElement) 12 | root.render() 13 | -------------------------------------------------------------------------------- /packages/site/src/pm/schema-types.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'prosemirror-model' 2 | 3 | export type ExampleSchema = Schema 4 | export type Marks = 'bold' | 'code' | 'italic' | 'link' 5 | export type Nodes = 6 | | 'blockquote' 7 | | 'code_block' 8 | | 'doc' 9 | | 'hard_break' 10 | | 'heading' 11 | | 'horizontal_rule' 12 | | 'image' 13 | | 'paragraph' 14 | | 'text' 15 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import autoPreprocess from 'svelte-preprocess' 2 | 3 | /** @type {import('svelte-preprocess/dist/types').AutoPreprocessOptions} */ 4 | const preprocessOptions = { 5 | scss: { 6 | prependData: `@import 'src/global.scss';` 7 | } 8 | } 9 | 10 | export default { 11 | preprocess: autoPreprocess(preprocessOptions), 12 | preprocessOptions 13 | } 14 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["esnext", "dom", "dom.iterable"], 5 | "types": ["node", "cypress", "@testing-library/cypress"], 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | }, 9 | "allowJs": true, 10 | "include": ["**/*.d.ts", "**/*.js", "**/*.ts", "**/*.cjs", "**/*.mjs"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference 2 | /// 3 | 4 | declare namespace Cypress { 5 | interface Chainable { 6 | devTools: () => Cypress.Chainable> 7 | resetDoc: () => Cypress.Chainable> 8 | includesStringCount: (str: string) => Promise 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/createOrFindPlace.ts: -------------------------------------------------------------------------------- 1 | const DEVTOOLS_CSS_CLASS = '__prosemirror-dev-toolkit__' 2 | 3 | export function createOrFindPlace() { 4 | let place: HTMLElement | null = document.querySelector(`.${DEVTOOLS_CSS_CLASS}`) 5 | 6 | if (!place) { 7 | place = document.createElement('div') 8 | place.className = DEVTOOLS_CSS_CLASS 9 | document.body.appendChild(place) 10 | } 11 | 12 | return place 13 | } 14 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/test-utils/filterPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, PluginKey } from 'prosemirror-state' 2 | 3 | export interface PluginState { 4 | active: boolean 5 | } 6 | 7 | export const filterPluginKey = new PluginKey('filter-plugin') 8 | 9 | export const filterPlugin = () => 10 | new Plugin({ 11 | key: filterPluginKey, 12 | filterTransaction: (tr, oldState) => { 13 | return false 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/fixtures/snapshot-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "doc", 3 | "content": [ 4 | { 5 | "type": "paragraph" 6 | }, 7 | { 8 | "type": "paragraph", 9 | "content": [ 10 | { 11 | "type": "text", 12 | "marks": [ 13 | { 14 | "type": "bold" 15 | } 16 | ], 17 | "text": "asdf qwer" 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/components/clickOutside.ts: -------------------------------------------------------------------------------- 1 | export function clickOutside(el: HTMLElement, onClickOutside: () => void) { 2 | const onClick = (event: MouseEvent) => { 3 | el && !event.composedPath().includes(el) && !event.defaultPrevented && onClickOutside() 4 | } 5 | 6 | document.addEventListener('click', onClick, true) 7 | 8 | return { 9 | destroy() { 10 | document.removeEventListener('click', onClick, true) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/fixtures/snapshot-incompatible.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "doc", 3 | "content": [ 4 | { 5 | "type": "paragraph" 6 | }, 7 | { 8 | "type": "paragraph", 9 | "content": [ 10 | { 11 | "type": "text", 12 | "marks": [ 13 | { 14 | "type": "highlight" 15 | } 16 | ], 17 | "text": "asdf qwer" 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.css", 5 | "options": { 6 | "singleQuote": false 7 | } 8 | } 9 | ], 10 | "arrowParens": "avoid", 11 | "plugins": ["prettier-plugin-svelte"], 12 | "printWidth": 100, 13 | "semi": false, 14 | "singleQuote": true, 15 | "svelteSortOrder": "options-scripts-markup-styles", 16 | "svelteStrictMode": false, 17 | "svelteBracketNewLine": true, 18 | "svelteIndentScriptAndStyle": true, 19 | "tabWidth": 2, 20 | "trailingComma": "none" 21 | } 22 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/history-and-diff/transaction.ts: -------------------------------------------------------------------------------- 1 | import type { Transaction } from 'prosemirror-state' 2 | 3 | const addedProperties = [ 4 | 'docChanged', 5 | 'isGeneric', 6 | 'scrolledIntoView', 7 | 'selectionSet', 8 | 'storedMarksSet' 9 | ] 10 | 11 | export function addPropertiesToTransaction(tr: Transaction) { 12 | return Object.keys(tr) 13 | .concat(addedProperties) 14 | .reduce((acc, key) => { 15 | // @ts-ignore 16 | acc[key] = tr[key] 17 | return acc 18 | }, {} as Transaction) 19 | } 20 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/typings/history.ts: -------------------------------------------------------------------------------- 1 | import type { EditorState, Transaction } from 'prosemirror-state' 2 | 3 | export interface HistoryEntry { 4 | id: string 5 | state: EditorState 6 | trs: Transaction[] 7 | timestamp: number 8 | timeStr: string 9 | contentDiff?: { [key: string]: any } 10 | selectionDiff?: { [key: string]: any } 11 | selectionHtml: string 12 | } 13 | 14 | export interface HistoryGroup { 15 | id: number 16 | isGroup: boolean 17 | topEntryId: string 18 | entryIds: string[] 19 | expanded: boolean 20 | } 21 | -------------------------------------------------------------------------------- /packages/site/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-base: #fffafa; 3 | --color-gray-light: #e2e2e2; 4 | --color-gray: #bbb; 5 | --color-primary-lighter: #b9ceff; 6 | --color-primary-light: #9a69c7; 7 | --color-primary: #551a8b; 8 | --color-text-dark: #222; 9 | 10 | --width-tablet: 768px; 11 | } 12 | html { 13 | font-size: 16px; 14 | font-weight: 400; 15 | color: var(--color-text-dark); 16 | } 17 | body { 18 | /* background: var(--color-bg-base); */ 19 | margin: 0.5rem; 20 | padding: 0; 21 | } 22 | * { 23 | box-sizing: border-box; 24 | } 25 | -------------------------------------------------------------------------------- /packages/site/src/pages/NoEditorPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import styled from 'styled-components' 3 | 4 | export function NoEditorPage() { 5 | return ( 6 | 7 |
8 |

9 | 10 | prosemirror-dev-toolkit 11 | 12 |

13 |

This page has no editor to test whether devTools unmounts properly.

14 |
15 |
16 | ) 17 | } 18 | 19 | const Container = styled.div`` 20 | -------------------------------------------------------------------------------- /packages/site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactRefresh from '@vitejs/plugin-react-refresh' 2 | import { defineConfig } from 'vite' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | const { GH_PAGES } = process.env 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | base: GH_PAGES ? '/prosemirror-dev-toolkit/' : undefined, 10 | plugins: [reactRefresh(), tsconfigPaths()], 11 | server: { 12 | port: parseInt(process.env.PORT || '3300'), 13 | strictPort: true 14 | }, 15 | define: { 16 | 'process.env': process.env 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "types": ["svelte", "@types/chrome"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "baseUrl": "src", 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import './commands' 17 | -------------------------------------------------------------------------------- /packages/site/src/pages/DevToolsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '../components/Editor' 5 | 6 | export function DevToolsPage() { 7 | return ( 8 | 9 |
10 |

11 | 12 | Original prosemirror-dev-tools 13 | 14 |

15 |

16 | Github repo 17 |

18 |
19 | 20 |
21 | ) 22 | } 23 | 24 | const Container = styled.div`` 25 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/global.scss: -------------------------------------------------------------------------------- 1 | $color-black: #222; 2 | $color-blue-bg: #363755; 3 | $color-blue-light: #85d9ef; 4 | /* $color-green-text: #b8e248; */ 5 | $color-dim-text: #727288; // used for empty plugin titles 6 | $color-gray-light: #d3d3d9; 7 | $color-green: green; // diff inserted 8 | $color-green-light: #87cc86; // diff deleted color 9 | $color-purple-dark: rgb(80, 68, 93); // list darker bg 10 | $color-purple: rgb(96, 76, 104); // borders in eg lists 11 | $color-purple-light: rgb(99, 99, 123); // list text color 12 | $color-red: #d66363; // diff deleted 13 | $color-red-gray: rgb(187, 145, 163); // h2 color 14 | $color-red-light: rgb(255, 162, 177); 15 | $color-yellow: #eaea37; // diff updated 16 | $color-white: #fff; 17 | -------------------------------------------------------------------------------- /packages/extension/README.md: -------------------------------------------------------------------------------- 1 | # prosemirror-dev-toolkit Chrome extension 2 | 3 | https://chrome.google.com/webstore/detail/prosemirror-developer-too/gkgbmhfgcpfnogoeclbaiencdjkefonj 4 | 5 | Chrome API https://developer.chrome.com/docs/extensions/reference/ 6 | 7 | ## How to run 8 | 9 | 1. `pnpm i` 10 | 2. Build the toolkit: `pnpm --filter prosemirror-dev-toolkit build` 11 | 3. Build/watch the extension: `pnpm --filter extension build` or `extension dev` 12 | 4. Go to `chrome://extensions` 13 | 5. Click "Load unpacked" https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/ 14 | 6. Go to this repository's `packages/extension/dist` folder and select it 15 | 7. Extension should be loaded! Click the refresh icon to reload the extension after changes 16 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Node as PMNode } from 'prosemirror-model' 2 | import type { Command, Transaction } from 'prosemirror-state' 3 | import type { EditorView } from 'prosemirror-view' 4 | 5 | import type { Plugin } from './typings/pm' 6 | import type { applyDevTools } from './applyDevTools' 7 | 8 | declare global { 9 | interface Window { 10 | applyDevTools: typeof applyDevTools 11 | editorView?: EditorView 12 | pmCmd?: (cmd: Command) => void 13 | _node?: { node: PMNode; pos: number } 14 | _doc?: { [key: string]: any } 15 | _trs?: Transaction[] 16 | _plugin?: [Plugin | undefined, unknown] 17 | } 18 | } 19 | 20 | export { applyDevTools, removeDevTools } from './applyDevTools' 21 | export * from './types' 22 | -------------------------------------------------------------------------------- /packages/site/src/pages/FrontPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '../components/Editor' 5 | 6 | export function FrontPage() { 7 | return ( 8 | 9 |
10 |

11 | 12 | prosemirror-dev-toolkit 13 | 14 |

15 |

16 | Github repo 17 |

18 |

An example React app with ProseMirror editor that uses prosemirror-dev-toolkit.

19 |
20 | 21 |
22 | ) 23 | } 24 | 25 | const Container = styled.div`` 26 | -------------------------------------------------------------------------------- /packages/site/src/pm/example-plugin/types.ts: -------------------------------------------------------------------------------- 1 | import { DecorationSet } from 'prosemirror-view' 2 | import { Transaction } from 'prosemirror-state' 3 | import { Node as PMNode, NodeType } from 'prosemirror-model' 4 | 5 | export type Operation = 'insert' | 'replace' | 'delete' 6 | 7 | export class DummyClass { 8 | values: any[] = [{ 1: [1, 2, 3] }, { a: 'hello' }, 1] 9 | } 10 | export interface TrackedNodes { 11 | tr: Transaction 12 | changedNodesMap: Map 13 | changedNodesTypesSet: Set 14 | } 15 | export interface PluginState { 16 | decorationSet: DecorationSet 17 | exampleMap: Map 18 | exampleSet: Set 19 | exampleClasses: DummyClass[] 20 | trackedTrs: TrackedNodes[] 21 | joined: TrackedNodes 22 | } 23 | -------------------------------------------------------------------------------- /.github/actions/pnpm/action.yml: -------------------------------------------------------------------------------- 1 | name: pnpm 2 | description: Setup pnpm and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Install pnpm 8 | shell: bash 9 | run: npm i pnpm -g 10 | 11 | - name: Setup pnpm config 12 | shell: bash 13 | run: pnpm config set store-dir ~/.pnpm-store 14 | 15 | - name: Setup caching 16 | uses: actions/cache@v3 17 | with: 18 | path: | 19 | ~/.pnpm-store 20 | ~/.cache/Cypress 21 | key: ${{ runner.os }}-node-${{ matrix.node-version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 22 | restore-keys: ${{ runner.os }}-node-${{ matrix.node-version }}-pnpm- 23 | 24 | - name: Install dependencies 25 | shell: bash 26 | run: pnpm i --frozen-lockfile 27 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/test-utils/setupEditor.ts: -------------------------------------------------------------------------------- 1 | import { DirectEditorProps, EditorView } from 'prosemirror-view' 2 | import { EditorState } from 'prosemirror-state' 3 | import { exampleSetup } from 'prosemirror-example-setup' 4 | 5 | import { schema } from './schema' 6 | 7 | interface Opts { 8 | exampleSetup?: boolean 9 | props?: Omit 10 | } 11 | 12 | const DEFAULT_OPTIONS = { 13 | exampleSetup: false 14 | } 15 | 16 | export function setupEditor(element: HTMLElement, opts: Opts = DEFAULT_OPTIONS) { 17 | return new EditorView( 18 | { mount: element }, 19 | { 20 | state: EditorState.create({ 21 | schema, 22 | plugins: opts.exampleSetup ? exampleSetup({ schema }) : [] 23 | }), 24 | ...opts.props 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Build and test pull requests 2 | 3 | on: [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | timeout-minutes: 15 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [16.x, 18.x] 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v3 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node_version }} 23 | 24 | - name: Install dependencies with pnpm 25 | uses: ./.github/actions/pnpm 26 | 27 | - name: Build & test 28 | uses: ./.github/actions/build-test 29 | -------------------------------------------------------------------------------- /packages/extension/src/types/pop-up.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState, InjectData } from './sw' 2 | 3 | export type PopUpState = GlobalState & { 4 | inject: InjectData 5 | } 6 | 7 | export type DeepPartial = { 8 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] 9 | } 10 | 11 | export interface PopUpPayload { 12 | source: 'pm-dev-tools' 13 | origin: 'pop-up' 14 | type: T 15 | data: D 16 | } 17 | export interface PopUpMessageMap { 18 | 'mount-pop-up': PopUpPayload<'mount-pop-up'> 19 | 'update-global-data': PopUpPayload<'update-global-data', DeepPartial> 20 | 'update-page-data': PopUpPayload<'update-page-data', Partial> 21 | 'toggle-disable': PopUpPayload<'toggle-disable'> 22 | 'reapply-devtools': PopUpPayload<'reapply-devtools'> 23 | 'open-in-window': PopUpPayload<'open-in-window'> 24 | } 25 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/structure/DocView.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
    21 | 22 |
23 | 24 | 31 | -------------------------------------------------------------------------------- /packages/extension/src/sw/index.ts: -------------------------------------------------------------------------------- 1 | import { listenToConnections } from './ports' 2 | 3 | function register() { 4 | return chrome.scripting.registerContentScripts([ 5 | { 6 | id: 'prosemirror-dev-toolkit-inject', 7 | allFrames: true, 8 | matches: [''], 9 | js: ['inject.js'], 10 | runAt: 'document_start', 11 | world: 'MAIN' 12 | } 13 | ]) 14 | } 15 | 16 | try { 17 | register() 18 | } catch (err: any) { 19 | // When developing the extension, the old inject script might conflict with the new one 20 | if (err.toString().includes('Duplicate script ID')) { 21 | chrome.scripting 22 | .unregisterContentScripts({ 23 | ids: ['prosemirror-dev-toolkit-inject'] 24 | }) 25 | .then(() => register()) 26 | } 27 | } 28 | chrome.runtime.onConnect.addListener(listenToConnections) 29 | 30 | export {} 31 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | .pnpm-debug.log 11 | .pnpm-store 12 | .npmrc 13 | 14 | # misc 15 | *.DS_Store 16 | .directory 17 | .project 18 | .settings 19 | .idea 20 | .tscache 21 | .logs 22 | .built 23 | .tmpOutput 24 | .vagrant 25 | coverage 26 | .vscode 27 | certs 28 | .eslintcache 29 | 30 | # envs 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | .env 36 | 37 | # build 38 | build 39 | dist 40 | 41 | tmp/ 42 | 43 | # Github actions 44 | .cache 45 | .netrc 46 | 47 | # Cypress 48 | **/cypress/screenshots 49 | **/cypress/**/*.actual.png 50 | **/cypress/**/*.diff.png 51 | **/cypress/videos 52 | **/cypress/downloads 53 | 54 | # Svelte 55 | package 56 | .svelte-kit -------------------------------------------------------------------------------- /packages/extension/src/types/inject.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState, InjectData } from './sw' 2 | 3 | export interface FoundInstance { 4 | size: number 5 | element: string 6 | } 7 | export type InjectState = Omit & { 8 | inject: InjectData 9 | } 10 | export type InjectStatus = 'finding' | 'finished' | 'error' 11 | 12 | export interface InjectPayload { 13 | source: 'pm-dev-tools' 14 | origin: 'inject' 15 | type: T 16 | data: D 17 | } 18 | 19 | export interface InjectMessageMap { 20 | 'inject-status': InjectPayload<'inject-status', InjectStatus> 21 | 'inject-found-instances': InjectPayload<'inject-found-instances', { instances: FoundInstance[] }> 22 | 'update-global-data': InjectPayload<'update-global-data', Partial> 23 | 'toggle-disable': InjectPayload<'toggle-disable'> 24 | reload: InjectPayload<'reload'> 25 | } 26 | -------------------------------------------------------------------------------- /packages/site/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { NavBar } from './NavBar' 5 | 6 | interface Props { 7 | children: React.ReactNode 8 | } 9 | 10 | export const NoContainerLayout: React.FC = ({ children }) => ( 11 | 12 | 13 | {children} 14 | 15 | ) 16 | 17 | export const DefaultLayout: React.FC = ({ children }) => ( 18 | 19 | 20 | {children} 21 | 22 | ) 23 | 24 | const MainWrapper = styled.div` 25 | min-height: 100vh; 26 | ` 27 | const MainContainer = styled.main` 28 | margin: 40px auto 0 auto; 29 | max-width: 680px; 30 | padding-bottom: 20px; 31 | @media only screen and (max-width: 720px) { 32 | margin: 40px 20px 0 20px; 33 | padding-bottom: 20px; 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/components/__tests__/Button.spec.ts: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/svelte' 2 | import Button from '../Button.svelte' 3 | import { vi } from 'vitest' 4 | 5 | // https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component/ 6 | 7 | describe('Button component', () => { 8 | it('should render', async () => { 9 | const results = render(Button) 10 | const onClick = vi.fn() 11 | results.component.$on('click', onClick) 12 | 13 | const button = results.container.querySelector('button') 14 | expect(button).not.toBeNull() 15 | 16 | // Using await when firing events is unique to the svelte testing library because 17 | // we have to wait for the next `tick` so that Svelte flushes all pending state changes. 18 | await fireEvent.click(button as HTMLElement) 19 | 20 | expect(results.container).toBeInTheDocument() 21 | expect(onClick.mock.calls.length).toEqual(1) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/extension/src/types/sw.ts: -------------------------------------------------------------------------------- 1 | import type { DevToolsOpts } from 'prosemirror-dev-toolkit' 2 | import { FoundInstance, InjectState, InjectStatus } from './inject' 3 | import { PopUpState } from './pop-up' 4 | 5 | export interface GlobalState { 6 | disabled: boolean 7 | showOptions: boolean 8 | showDebug: boolean 9 | devToolsOpts: DevToolsOpts 10 | } 11 | export interface PageData { 12 | inject: InjectData 13 | pagePort?: chrome.runtime.Port 14 | popUpPort?: chrome.runtime.Port 15 | } 16 | export interface InjectData { 17 | instance: number 18 | selector: string 19 | status: InjectStatus 20 | instances: FoundInstance[] 21 | } 22 | 23 | export interface SWPayload { 24 | source: 'pm-dev-tools' 25 | origin: 'sw' 26 | type: T 27 | data: D 28 | } 29 | 30 | export interface SWMessageMap { 31 | 'pop-up-state': SWPayload<'pop-up-state', PopUpState> 32 | 'inject-state': SWPayload<'inject-state', InjectState> 33 | 'rerun-inject': SWPayload<'rerun-inject'> 34 | } 35 | -------------------------------------------------------------------------------- /packages/extension/src/types/consts.ts: -------------------------------------------------------------------------------- 1 | import { InjectState } from './inject' 2 | import { PopUpState } from './pop-up' 3 | import { GlobalState, InjectData } from './sw' 4 | 5 | export const PAGE_PORT = 'pm-devtools-page' 6 | export const POP_UP_PORT = 'pm-devtools-pop-up' 7 | 8 | export const DEFAULT_INJECT_DATA: InjectData = { 9 | instance: 0, 10 | selector: '.ProseMirror', 11 | status: 'finding' as const, 12 | instances: [] 13 | } 14 | 15 | const DEFAULT_STATE = { 16 | disabled: false, 17 | devToolsOpts: { 18 | devToolsExpanded: false, 19 | buttonPosition: 'bottom-right' as const 20 | } 21 | } 22 | 23 | export const DEFAULT_INJECT_STATE: InjectState = { 24 | ...DEFAULT_STATE, 25 | inject: DEFAULT_INJECT_DATA 26 | } 27 | 28 | export const DEFAULT_GLOBAL_STATE: GlobalState = { 29 | ...DEFAULT_STATE, 30 | showOptions: false, 31 | showDebug: false 32 | } 33 | 34 | export const DEFAULT_POP_UP_STATE: PopUpState = { 35 | ...DEFAULT_GLOBAL_STATE, 36 | inject: DEFAULT_INJECT_DATA 37 | } 38 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html' { 2 | // indent_size (default 4) — indentation size, 3 | // indent_char (default space) — character to indent with, 4 | // max_char (default 70) - maximum amount of characters per line, 5 | // brace_style (default "collapse") - "collapse" | "expand" | "end-expand" 6 | // put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line. 7 | // unformatted (defaults to inline tags) - list of tags, that shouldn't be reformatted 8 | // indent_scripts (default normal) - "keep"|"separate"|"normal" 9 | export interface Opts { 10 | indent_size?: number 11 | indent_char?: string 12 | max_char?: number 13 | brace_style?: 'collapse' | 'expand' | 'end-expand' 14 | unformatted?: string[] 15 | indent_scripts?: 'keep' | 'separate' | 'normal' 16 | } 17 | export function prettyPrint(html: string, opts?: Opts): string 18 | } 19 | -------------------------------------------------------------------------------- /packages/site/src/pm/useEditor.ts: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef } from 'react' 2 | import { EditorView } from 'prosemirror-view' 3 | import { EditorState } from 'prosemirror-state' 4 | import { exampleSetup } from 'prosemirror-example-setup' 5 | 6 | import { schema } from 'pm/schema' 7 | 8 | export function useEditor( 9 | editorDOMRef: React.MutableRefObject, 10 | cb?: (view: EditorView) => void 11 | ) { 12 | const editorViewRef = useRef(null) 13 | 14 | useLayoutEffect(() => { 15 | const state = EditorState.create({ 16 | schema, 17 | plugins: exampleSetup({ schema }) 18 | }) 19 | const editorViewDOM = editorDOMRef.current 20 | if (editorViewDOM) { 21 | editorViewRef.current = new EditorView( 22 | { mount: editorViewDOM }, 23 | { 24 | state 25 | } 26 | ) 27 | cb && cb(editorViewRef.current) 28 | } 29 | return () => { 30 | editorViewRef.current?.destroy() 31 | } 32 | }, []) 33 | } 34 | -------------------------------------------------------------------------------- /packages/site/src/pages/PlainPMPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef } from 'react' 2 | import styled from 'styled-components' 3 | import { applyDevTools as applyDevToolkit } from 'prosemirror-dev-toolkit' 4 | 5 | import { useEditor } from 'pm/useEditor' 6 | 7 | export function PlainPMPage() { 8 | const editorDOMRef = useRef(null) 9 | useEditor(editorDOMRef, view => { 10 | applyDevToolkit(view, { 11 | devToolsExpanded: true 12 | }) 13 | }) 14 | 15 | return ( 16 | 17 |
18 |

19 | 20 | prosemirror-dev-toolkit 21 | 22 |

23 |

24 | This page mounts a ProseMirror editor without any extra props and it's used in Cypress 25 | tests 26 |

27 |
28 |
29 | 30 | ) 31 | } 32 | 33 | const Container = styled.div`` 34 | -------------------------------------------------------------------------------- /packages/extension/vite.config.input.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tsconfigPaths from 'vite-tsconfig-paths' 3 | 4 | import path from 'path' 5 | 6 | const { INPUT = '' } = process.env 7 | 8 | export default defineConfig({ 9 | plugins: [tsconfigPaths()], 10 | build: { 11 | minify: false, 12 | emptyOutDir: false, 13 | rollupOptions: { 14 | output: { 15 | chunkFileNames: '[name].js', 16 | entryFileNames: '[name].js' 17 | }, 18 | input: { 19 | [INPUT]: path.resolve(`./src/${INPUT}/index.ts`) 20 | }, 21 | plugins: [ 22 | { 23 | name: 'wrap-in-iife', 24 | generateBundle(outputOptions, bundle) { 25 | Object.keys(bundle).forEach(fileName => { 26 | const file = bundle[fileName] 27 | if (fileName.slice(-3) === '.js' && 'code' in file) { 28 | file.code = `(() => {\n${file.code}})()` 29 | } 30 | }) 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Teemu Koivisto 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins' 3 | 4 | let shouldSkip = false 5 | 6 | export default defineConfig({ 7 | e2e: { 8 | baseUrl: 'http://localhost:3300', 9 | setupNodeEvents(on, config) { 10 | initPlugin(on, config) 11 | on('task', { 12 | resetShouldSkipFlag() { 13 | shouldSkip = false 14 | return null 15 | }, 16 | shouldSkip(value) { 17 | if (value != null) shouldSkip = value 18 | return shouldSkip 19 | } 20 | }) 21 | on('before:browser:launch', (browser, launchOptions) => { 22 | if (browser.name === 'chrome' && browser.isHeadless) { 23 | launchOptions.args.push('--hide-scrollbars') 24 | launchOptions.args.push('--high-dpi-support') 25 | launchOptions.args.push('--window-size=1280,800') 26 | } 27 | return launchOptions 28 | }) 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /packages/extension/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | import path from 'path' 6 | 7 | /** @type {import('vite').UserConfig} */ 8 | export default defineConfig({ 9 | plugins: [svelte({}), tsconfigPaths()], 10 | build: { 11 | minify: false, 12 | rollupOptions: { 13 | output: { 14 | chunkFileNames: '[name].js', 15 | entryFileNames: '[name].js' 16 | }, 17 | input: { 18 | 'pop-up': path.resolve('./pop-up.html') 19 | }, 20 | plugins: [ 21 | { 22 | name: 'wrap-in-iife', 23 | generateBundle(outputOptions, bundle) { 24 | Object.keys(bundle).forEach(fileName => { 25 | const file = bundle[fileName] 26 | if (fileName.slice(-3) === '.js' && 'code' in file) { 27 | file.code = `(() => {\n${file.code}})()` 28 | } 29 | }) 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "types": ["svelte", "@testing-library/jest-dom", "vitest/globals"], 6 | "allowJs": true, 7 | "declaration": true, 8 | "declarationDir": "./dist", 9 | "outDir": "./dist", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "$components/*": ["src/components/*"], 23 | "$context": ["src/context"], 24 | "$stores/*": ["src/stores/*"], 25 | "$tabs/*": ["src/tabs/*"], 26 | "$test-utils": ["./src/test-utils"], 27 | "$test-utils/*": ["./src/test-utils/*"], 28 | "$typings/*": ["src/typings/*"] 29 | } 30 | }, 31 | "include": ["src"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/context.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from 'prosemirror-view' 2 | import type { EditorState, Transaction } from 'prosemirror-state' 3 | import type { Node as PMNode } from 'prosemirror-model' 4 | import { getContext as getCtx, setContext as setCtx } from 'svelte' 5 | 6 | import { buildColors } from './tabs/structure/colors' 7 | 8 | export type Contexts = { 9 | 'editor-view': { 10 | view: EditorView 11 | replaceEditorContent: (state: EditorState) => void 12 | execCmd: (cmd: (state: EditorState, dispatch?: (tr: Transaction) => void) => void) => void 13 | } 14 | 'doc-view': { 15 | selected: { 16 | type: string 17 | start: number 18 | end: number 19 | } 20 | colors: ReturnType 21 | handleNodeClick: (n: PMNode, startPos: number, scrollInto?: boolean) => void 22 | } 23 | } 24 | 25 | export const setContext = (ctx: K, val: Contexts[K]) => 26 | setCtx(ctx, val) 27 | 28 | export const getContext = (ctx: K) => getCtx(ctx) 29 | -------------------------------------------------------------------------------- /packages/site/src/pages/IFramePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { useEditor } from 'pm/useEditor' 5 | 6 | export function IFramePage() { 7 | const editorDOMRef = useRef(null) 8 | useEditor(editorDOMRef) 9 | return ( 10 | 11 |
12 |

13 | 14 | prosemirror-dev-toolkit 15 | 16 |

17 |

18 | This page contains ProseMirror instances, two of them inside iframe to debug the Chrome 19 | extension. 20 |

21 |
22 | 23 | 24 |
25 | 26 | ) 27 | } 28 | 29 | const Container = styled.div` 30 | iframe { 31 | height: 100vh; 32 | width: 100%; 33 | } 34 | .m-top { 35 | margin-top: 1rem; 36 | } 37 | ` 38 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Github Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deployment: 8 | timeout-minutes: 15 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | with: 17 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 18 | fetch-depth: 0 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node_version }} 24 | 25 | - name: Install dependencies with pnpm 26 | uses: ./.github/actions/pnpm 27 | 28 | - name: Build 29 | run: pnpm --filter prosemirror-dev-toolkit build && pnpm --filter site build 30 | env: 31 | GH_PAGES: true 32 | 33 | - name: Deploy 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | deploy_key: ${{ secrets.GH_ACTIONS_DEPLOY_KEY }} 37 | publish_dir: ./packages/site/dist 38 | -------------------------------------------------------------------------------- /packages/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension", 3 | "version": "1.1.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "preview": "vite preview", 9 | "build": "vite build && pnpm build:inject && pnpm build:proxy && pnpm build:sw", 10 | "build:inject": "INPUT=inject vite build --config vite.config.input.ts", 11 | "build:proxy": "INPUT=proxy vite build --config vite.config.input.ts", 12 | "build:sw": "INPUT=sw vite build --config vite.config.input.ts" 13 | }, 14 | "devDependencies": { 15 | "@iconify-icons/mdi": "^1.2.48", 16 | "@sveltejs/vite-plugin-svelte": "^3.1.0", 17 | "@types/chrome": "^0.0.267", 18 | "@types/node": "^20.12.8", 19 | "prosemirror-view": "^1.33.6", 20 | "rimraf": "^5.0.5", 21 | "sass": "^1.76.0", 22 | "svelte": "^4.2.15", 23 | "svelte-check": "^3.7.1", 24 | "svelte-preprocess": "^5.1.4", 25 | "typescript": "5.4.5", 26 | "vite": "^5.4.2", 27 | "vite-tsconfig-paths": "^4.3.2" 28 | }, 29 | "dependencies": { 30 | "@iconify/svelte": "^4.0.1", 31 | "prosemirror-dev-toolkit": "workspace:*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vite' 4 | import { svelte } from '@sveltejs/vite-plugin-svelte' 5 | import tsconfigPaths from 'vite-tsconfig-paths' 6 | 7 | import { resolve } from 'path' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | svelte({ 13 | dynamicCompileOptions: () => { 14 | return { 15 | cssHash: ({ hash, css, name, filename }) => { 16 | return `s-${name}-${hash(css)}` 17 | } 18 | } 19 | } 20 | }), 21 | tsconfigPaths() 22 | ], 23 | resolve: { 24 | alias: { 25 | $components: resolve('./src/components'), 26 | $context: resolve('./src/context'), 27 | $stores: resolve('./src/stores'), 28 | $tabs: resolve('./src/tabs'), 29 | '$test-utils': resolve('./src/test-utils'), 30 | $typings: resolve('./src/typings') 31 | } 32 | }, 33 | test: { 34 | globals: true, 35 | environment: 'jsdom', 36 | include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], 37 | setupFiles: ['src/test-utils/setupTests.js'], 38 | cache: false 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/SplitView.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 42 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/ProseMirrorDevToolkit.ts: -------------------------------------------------------------------------------- 1 | import type { EditorView } from 'prosemirror-view' 2 | import DevTools from './components/DevTools.svelte' 3 | import type { SvelteComponent } from 'svelte' 4 | 5 | import { DevToolsOpts } from './types' 6 | 7 | // Inspired by https://www.colorglare.com/svelte-components-as-web-components-b400d1253504 8 | // Using a web component allows toolkit to encapsulate its DOM and CSS styles without affecting 9 | // the site or being affected by its global stylesheets 10 | export class ProseMirrorDevToolkit extends HTMLElement { 11 | private component?: SvelteComponent 12 | 13 | constructor() { 14 | super() 15 | const shadowRoot = this.attachShadow({ mode: 'open' }) 16 | 17 | this.addEventListener('init-dev-toolkit', (event: Event) => { 18 | const { 19 | detail: { view, opts } 20 | } = event as CustomEvent<{ view: EditorView; opts: DevToolsOpts }> 21 | this.component = new DevTools({ 22 | target: shadowRoot, 23 | props: { 24 | view, 25 | ...opts 26 | } 27 | }) 28 | }) 29 | } 30 | 31 | disconnectedCallback(): void { 32 | this.component?.$destroy() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/structure/colors.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'prosemirror-model' 2 | 3 | const nodeColors = [ 4 | '#EA7C7F', // red 5 | '#67B0C6', // cyan 400 6 | '#94BB7F', // green 7 | '#CA9EDB', // deep purple 8 | '#DCDC5D', // lime 9 | '#B9CC7C', // light green 10 | '#DD97D8', // purple 11 | '#FFB761', // orange 12 | '#4D8FD1', // light blue 13 | '#F36E98', // pink 14 | '#E45F44', // deep orange 15 | '#A6A4AE', // blue grey 16 | '#FCC047', // yellow 17 | '#FFC129', // amber 18 | '#D3929C', // can can 19 | '#4CBCD4', // cyan 20 | '#8D7BC0' // indigo 21 | ] 22 | 23 | export function calculateSafeIndex(index: number, total: number) { 24 | const quotient = index / total 25 | return Math.round(total * (quotient - Math.floor(quotient))) 26 | } 27 | 28 | export function buildColors(schema: Schema) { 29 | return Object.keys(schema.nodes).reduce( 30 | (acc, node, index) => { 31 | const safeIndex = 32 | index >= nodeColors.length ? calculateSafeIndex(index, nodeColors.length) : index 33 | 34 | acc[node] = nodeColors[safeIndex] 35 | return acc 36 | }, 37 | {} as { [key: string]: (typeof nodeColors)[number] } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/state/getActiveMarks.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state' 2 | 3 | // From https://github.com/PierBover/prosemirror-cookbook 4 | export function getActiveMarks(state: EditorState): string[] { 5 | if (state.selection.empty) { 6 | const $from = state.selection.$from 7 | const storedMarks = state.storedMarks 8 | 9 | // Return either the stored marks, or the marks at the cursor position. 10 | // Stored marks are the marks that are going to be applied to the next input 11 | // if you dispatched a mark toggle with an empty cursor. 12 | if (storedMarks) { 13 | return storedMarks.map(mark => mark.type.name) 14 | } else { 15 | return $from.marks().map(mark => mark.type.name) 16 | } 17 | } else { 18 | const $head = state.selection.$head 19 | const $anchor = state.selection.$anchor 20 | 21 | // We're using a Set to not get duplicate values 22 | const activeMarks = new Set() 23 | 24 | // Here we're getting the marks at the head and anchor of the selection 25 | $head.marks().forEach(mark => activeMarks.add(mark.type.name)) 26 | $anchor.marks().forEach(mark => activeMarks.add(mark.type.name)) 27 | 28 | return Array.from(activeMarks) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["svelte3", "@typescript-eslint", "eslint-plugin-import", "eslint-plugin-prettier"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 6 | "env": { 7 | "es6": true, 8 | "browser": true, 9 | "node": true 10 | }, 11 | "overrides": [ 12 | { 13 | "files": ["*.svelte"], 14 | "processor": "svelte3/svelte3" 15 | } 16 | ], 17 | "settings": { 18 | "svelte3/typescript": true 19 | }, 20 | "rules": { 21 | "@typescript-eslint/explicit-function-return-type": 0, 22 | "@typescript-eslint/explicit-member-accessibility": 0, 23 | "@typescript-eslint/indent": 0, 24 | "@typescript-eslint/member-delimiter-style": 0, 25 | "@typescript-eslint/no-explicit-any": 0, 26 | "@typescript-eslint/explicit-module-boundary-types": "off", 27 | "@typescript-eslint/no-var-requires": 0, 28 | "@typescript-eslint/no-use-before-define": 0, 29 | "@typescript-eslint/no-unused-vars": [ 30 | 2, 31 | { 32 | "argsIgnorePattern": "^_" 33 | } 34 | ], 35 | "no-console": [ 36 | 2, 37 | { 38 | "allow": ["warn", "error"] 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/state/selection.ts: -------------------------------------------------------------------------------- 1 | import { Selection } from 'prosemirror-state' 2 | 3 | const defaultProperties = ['jsonID', 'empty', 'anchor', 'from', 'head', 'to'] 4 | const resolvedPosProperties = ['$anchor', '$head', '$cursor', '$to', '$from'] 5 | const resolvedPosSubProperties = ['nodeAfter', 'nodeBefore', 'textOffset'] 6 | 7 | export function createSelection(selection: Selection) { 8 | return defaultProperties.reduce( 9 | (acc, key) => { 10 | // @ts-ignore 11 | acc[key] = selection[key] 12 | return acc 13 | }, 14 | {} as { [key: string]: any } 15 | ) 16 | } 17 | 18 | export function createFullSelection(selection: Selection) { 19 | return defaultProperties.concat(resolvedPosProperties).reduce( 20 | (acc, key) => { 21 | // @ts-ignore 22 | let val = selection[key] 23 | if (val && resolvedPosProperties.includes(key)) { 24 | const additionalProperties = {} 25 | resolvedPosSubProperties.forEach(subKey => { 26 | // @ts-ignore 27 | additionalProperties[subKey] = val[subKey] 28 | }) 29 | val = { ...val, ...additionalProperties } 30 | } 31 | acc[key] = val 32 | return acc 33 | }, 34 | {} as { [key: string]: any } 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ProseMirror Developer Tools", 4 | "version": "1.0.0", 5 | "description": "Run prosemirror-dev-toolkit as Chrome extension", 6 | "minimum_chrome_version": "102", 7 | "homepage_url": "https://github.com/TeemuKoivisto/prosemirror-dev-toolkit", 8 | "icons": { 9 | "16": "devtools-16.png", 10 | "32": "devtools-32.png", 11 | "48": "devtools-48.png", 12 | "128": "devtools-128.png" 13 | }, 14 | "action": { 15 | "default_popup": "pop-up.html", 16 | "default_icon": "devtools-128.png" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "run_at": "document_end", 21 | "matches": [""], 22 | "all_frames": true, 23 | "js": ["pop-up.js", "proxy.js"] 24 | } 25 | ], 26 | "web_accessible_resources": [ 27 | { 28 | "resources": ["inject.js"], 29 | "matches": [""], 30 | "extension_ids": [] 31 | } 32 | ], 33 | "externally_connectable": { 34 | "ids": ["*"] 35 | }, 36 | "background": { 37 | "service_worker": "sw.js" 38 | }, 39 | "permissions": ["storage", "scripting", "tabs"], 40 | "host_permissions": [""], 41 | "content_security_policy": { 42 | "extension_pages": "script-src 'self'; object-src 'self'" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | This is a loose list of features I'd like to implement into `prosemirror-dev-toolkit`. 4 | 5 | - add depth & stuff to shown Selection 6 | - ~~track appendedTransactions. Because it's a must.~~ 7 | - ~~show appendedTransactions in History tab & make it easier to access transactions~~ 8 | - ~~fix missing borders for nodes in DocView~~ 9 | - ~~add textarea to directly paste snapshots since importing files is cumbersome~~ 10 | - make panel adjustable in Structure tab between DocView and node info 11 | - figure out why importing snapshots sometimes dont work after first failing to import 12 | - special case for class-objects in svelte-tree-view 13 | - ~~improve the injection script (basically enhance the hack to gain access to view)~~ 14 | - enhance structure tab on large docs. It could be faster 15 | - add tab to execute commands. Basically a (primitive) prosemirror REPL 16 | - ~~make Chrome extension~~ 17 | - ~~migrate to pnpm? need to workout the deployment flow with releases though first~~ 18 | - ~~use css reset? just so styles are not affected by global stylesheets~~ 19 | - checkbox to filter empty/selection transactions 20 | - OR just meta-key based filter of transactions 21 | - switch Rollup to Vite (and maybe use PostCSS) 22 | - ~~either upgrade Cypress or switch to say Playwright~~ 23 | 24 | No time schedules set. Just for reference for my and yours sake. 25 | -------------------------------------------------------------------------------- /packages/extension/src/inject/utils.ts: -------------------------------------------------------------------------------- 1 | import type { InjectMessageMap, InjectState } from '../types' 2 | 3 | export function sleep(ms: number) { 4 | return new Promise(resolve => { 5 | setTimeout(() => { 6 | resolve(true) 7 | }, ms) 8 | }) 9 | } 10 | 11 | export function send(type: K, data: InjectMessageMap[K]['data']) { 12 | window.postMessage({ source: 'pm-dev-tools', origin: 'inject', type, data }) 13 | } 14 | 15 | export async function tryQueryIframe(iframe: HTMLIFrameElement, selector: string) { 16 | try { 17 | const doc = iframe.contentDocument 18 | if (!doc) return [] 19 | let tries = 0 20 | while (doc?.readyState === 'loading' || tries < 5) { 21 | await sleep(500) 22 | tries += 1 23 | } 24 | return Array.from(doc.querySelectorAll(selector) || []) 25 | } catch (err) { 26 | // Probably "Blocked a frame with origin from accessing a cross-origin frame." error 27 | return [] 28 | } 29 | } 30 | 31 | export function shouldRerun(oldState: InjectState, newState: InjectState) { 32 | return ( 33 | !newState.disabled && 34 | (oldState.devToolsOpts.devToolsExpanded !== newState.devToolsOpts.devToolsExpanded || 35 | oldState.devToolsOpts.buttonPosition !== newState.devToolsOpts.buttonPosition || 36 | oldState.inject.instance !== newState.inject.instance || 37 | oldState.inject.selector !== newState.inject.selector) 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "private": true, 4 | "version": "0.0.15", 5 | "type": "module", 6 | "homepage": "https://teemukoivisto.github.io/prosemirror-dev-toolkit/", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview" 11 | }, 12 | "devDependencies": { 13 | "@types/lodash.debounce": "^4.0.9", 14 | "@types/node": "^20.12.8", 15 | "@types/react": "18.3.1", 16 | "@types/react-dom": "18.3.0", 17 | "@types/react-icons": "^3.0.0", 18 | "@types/react-router": "^5.1.20", 19 | "@types/react-router-dom": "^5.3.3", 20 | "@types/styled-components": "^5.1.34", 21 | "@vitejs/plugin-react-refresh": "^1.3.6", 22 | "vite": "^5.4.2", 23 | "vite-tsconfig-paths": "^4.3.2" 24 | }, 25 | "dependencies": { 26 | "lodash.debounce": "^4.0.8", 27 | "prosemirror-dev-toolkit": "workspace:*", 28 | "prosemirror-dev-tools": "^4.1.0", 29 | "prosemirror-example-setup": "^1.2.2", 30 | "prosemirror-keymap": "^1.2.2", 31 | "prosemirror-model": "^1.20.0", 32 | "prosemirror-state": "^1.4.3", 33 | "prosemirror-transform": "^1.8.0", 34 | "prosemirror-view": "^1.33.6", 35 | "react": "^18.3.1", 36 | "react-dom": "^18.3.1", 37 | "react-icons": "^5.2.0", 38 | "react-router": "^6.23.0", 39 | "react-router-dom": "^6.23.0", 40 | "styled-components": "^6.1.9", 41 | "typescript": "5.4.5", 42 | "y-prosemirror": "^1.2.3", 43 | "y-protocols": "^1.0.6", 44 | "yjs": "^13.6.15" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/List.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 | {#each listItems as item} 9 |
  • 10 | 15 |
  • 16 | {/each} 17 |
18 | 19 | 61 | -------------------------------------------------------------------------------- /packages/extension/src/inject/findEditorViews.ts: -------------------------------------------------------------------------------- 1 | import type { EditorView } from 'prosemirror-view' 2 | 3 | import type { InjectState } from '../types' 4 | 5 | import { getEditorView } from './pmViewDescHack' 6 | import { sleep, tryQueryIframe } from './utils' 7 | 8 | const MAX_ATTEMPTS = 10 9 | 10 | export async function findEditorViews( 11 | state: InjectState, 12 | attempts = 0 13 | ): Promise { 14 | await sleep(1000 * attempts) 15 | try { 16 | const { 17 | disabled, 18 | inject: { selector } 19 | } = state 20 | if (disabled) { 21 | return [] 22 | } 23 | const views = await Promise.all( 24 | Array.from(document.querySelectorAll(selector)).map(el => 25 | getEditorView(el as HTMLElement).catch(err => { 26 | return undefined 27 | }) 28 | ) 29 | ) 30 | const iframes = ( 31 | await Promise.all( 32 | Array.from(document.querySelectorAll('iframe')).map(iframe => 33 | tryQueryIframe(iframe, selector) 34 | ) 35 | ) 36 | ).flat() 37 | const iframeViews = await Promise.all( 38 | iframes.map(el => 39 | getEditorView(el as HTMLElement).catch(err => { 40 | return undefined 41 | }) 42 | ) 43 | ) 44 | const filtered = views.concat(iframeViews).filter((v): v is EditorView => v !== undefined) 45 | if (filtered.length === 0 && attempts < MAX_ATTEMPTS) { 46 | return findEditorViews(state, attempts + 1) 47 | } 48 | return filtered 49 | } catch (err) { 50 | console.error(err) 51 | return undefined 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/actions/build-test/action.yml: -------------------------------------------------------------------------------- 1 | name: build-test 2 | description: Build & run unit and Cypress tests 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Build packages 8 | run: pnpm -r build 9 | shell: bash 10 | 11 | - name: Run type check and unit tests 12 | run: | 13 | pnpm --filter prosemirror-dev-toolkit svelte-check 14 | pnpm --filter prosemirror-dev-toolkit test:unit 15 | shell: bash 16 | 17 | - name: Run tests with Cypress 18 | id: cypress 19 | uses: cypress-io/github-action@v5 20 | with: 21 | browser: chrome 22 | cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 23 | command: pnpm --filter prosemirror-dev-toolkit test:e2e 24 | install: false 25 | quiet: false 26 | start: pnpm --filter site dev --host 27 | wait-on: http://localhost:3300 28 | working-directory: ./packages/prosemirror-dev-toolkit 29 | 30 | - name: Export screenshots (on failure only) 31 | uses: actions/upload-artifact@v3 32 | if: failure() 33 | with: 34 | name: cypress-screenshots 35 | path: | 36 | ./packages/prosemirror-dev-toolkit/cypress/screenshots 37 | ./packages/prosemirror-dev-toolkit/cypress/e2e/**/*.png 38 | retention-days: 7 39 | 40 | - name: Export screen recordings (on failure only) 41 | uses: actions/upload-artifact@v3 42 | if: failure() 43 | with: 44 | name: cypress-videos 45 | path: ./packages/prosemirror-dev-toolkit/cypress/videos 46 | retention-days: 7 47 | -------------------------------------------------------------------------------- /packages/extension/src/proxy/index.ts: -------------------------------------------------------------------------------- 1 | import { PAGE_PORT, POP_UP_PORT } from '../types/consts' 2 | 3 | /** 4 | * Resends messages from inject to sw 5 | * @param event 6 | * @returns 7 | */ 8 | function handleInjectMsgs(event: MessageEvent) { 9 | if ( 10 | typeof event.data !== 'object' || 11 | !('source' in event.data) || 12 | event.data.source !== 'pm-dev-tools' 13 | ) { 14 | return 15 | } 16 | pagePort?.postMessage(event.data) 17 | } 18 | 19 | /** 20 | * Resends messages from sw to inject and pop-up 21 | * @param msg 22 | * @returns 23 | */ 24 | function handleSWMsgs(msg: any) { 25 | if (typeof msg !== 'object' || !('source' in msg) || msg.source !== 'pm-dev-tools') { 26 | return 27 | } 28 | window.postMessage(msg, '*') 29 | popUpPort?.postMessage(msg) 30 | } 31 | 32 | /** 33 | * Resends messages from pop-up to sw 34 | * @param msg 35 | * @returns 36 | */ 37 | function handlePopUpMsgs(msg: any) { 38 | if (typeof msg !== 'object' || !('source' in msg) || msg.source !== 'pm-dev-tools') { 39 | return 40 | } 41 | pagePort?.postMessage(msg) 42 | } 43 | 44 | window.addEventListener('message', handleInjectMsgs) 45 | let pagePort: chrome.runtime.Port | undefined = chrome.runtime.connect({ 46 | name: PAGE_PORT 47 | }) 48 | pagePort.onMessage.addListener(handleSWMsgs) 49 | pagePort.onDisconnect.addListener(() => { 50 | pagePort = undefined 51 | }) 52 | let popUpPort: chrome.runtime.Port | undefined = chrome.runtime.connect({ 53 | name: POP_UP_PORT 54 | }) 55 | popUpPort.onMessage.addListener(handlePopUpMsgs) 56 | popUpPort.onDisconnect.addListener(() => { 57 | popUpPort = undefined 58 | }) 59 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/SchemaTab.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 |
16 |
17 |

Nodes

18 | 19 |
20 | 29 |
30 |
31 |
32 |

Marks

33 | 34 |
35 | 44 |
45 |
46 | 47 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "format": "pnpm pretty && pnpm lint", 6 | "pretty": "prettier --write \"**/*.+(js|jsx|json|html|css|less|scss|ts|tsx|svelte|yml|yaml|md|graphql|mdx)\" .", 7 | "lint": "eslint --ignore-path .gitignore --fix --ext .js,.cjs,.ts,.tsx,.svelte .", 8 | "ext": "pnpm --filter extension build", 9 | "start": "concurrently --kill-others 'pnpm --filter prosemirror-dev-toolkit watch' 'sleep 10 && pnpm --filter site dev'", 10 | "site": "pnpm --filter site dev", 11 | "kit": "pnpm --filter prosemirror-dev-toolkit watch", 12 | "test": "pnpm --filter prosemirror-dev-toolkit test", 13 | "cs": "changeset && git add .changeset", 14 | "ci:version": "changeset version", 15 | "ci:publish": "changeset publish && git push --follow-tags", 16 | "prepare": "husky install" 17 | }, 18 | "engines": { 19 | "node": ">=16", 20 | "pnpm": ">=7.0.0" 21 | }, 22 | "devDependencies": { 23 | "@changesets/cli": "^2.27.1", 24 | "@typescript-eslint/eslint-plugin": "^7.8.0", 25 | "@typescript-eslint/parser": "^7.8.0", 26 | "concurrently": "^8.2.2", 27 | "eslint": "^8.56.0", 28 | "eslint-config-prettier": "^9.1.0", 29 | "eslint-import-resolver-typescript": "^3.6.1", 30 | "eslint-plugin-import": "^2.29.1", 31 | "eslint-plugin-jsx-a11y": "^6.8.0", 32 | "eslint-plugin-node": "^11.1.0", 33 | "eslint-plugin-prettier": "^5.1.3", 34 | "eslint-plugin-promise": "^6.1.1", 35 | "eslint-plugin-svelte3": "^4.0.0", 36 | "husky": "^9.0.11", 37 | "prettier": "^3.2.5", 38 | "prettier-plugin-svelte": "^3.2.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/site/src/pm/example-plugin/findChangedNodes.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from 'prosemirror-state' 2 | import { Node as PMNode, NodeType, Slice } from 'prosemirror-model' 3 | import { Step } from 'prosemirror-transform' 4 | 5 | import type { Operation, TrackedNodes } from './types' 6 | 7 | export const findAddedOrRemovedNodes = (tr: Transaction, oldDoc: PMNode): TrackedNodes => { 8 | const nodesMap: Map = new Map() 9 | const typesSet: Set = new Set() 10 | const steps = (tr.steps || []) as (Step & { 11 | from: number 12 | to: number 13 | slice: Slice 14 | })[] 15 | steps.forEach(step => { 16 | const { to, from, slice } = step 17 | const sliceSize = slice.size || 0 18 | const isInsert = from === to 19 | const isReplace = !isInsert && sliceSize !== 0 20 | const isDelete = !isInsert && sliceSize === 0 21 | const operation = isInsert ? 'insert' : isReplace ? 'replace' : 'delete' 22 | if (isReplace || isDelete) { 23 | // go through the nodes inside from to 24 | oldDoc.nodesBetween(from, to, (n, pos) => { 25 | if (!nodesMap.has(n)) { 26 | nodesMap.set(n, { pos, operation }) 27 | typesSet.add(n.type) 28 | return true 29 | } 30 | }) 31 | } 32 | if (isInsert || isReplace) { 33 | // go through the nodes inside slice 34 | slice.content.descendants((n, pos) => { 35 | nodesMap.set(n, { pos, operation }) 36 | typesSet.add(n.type) 37 | }) 38 | } 39 | }) 40 | return { 41 | tr, 42 | changedNodesMap: nodesMap, 43 | changedNodesTypesSet: typesSet 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/TabsMenu.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
  • 7 | 8 |
  • 9 |
  • 10 | 13 |
  • 14 |
  • 15 | 18 |
  • 19 |
  • 20 | 21 |
  • 22 |
  • 23 | 26 |
  • 27 |
  • 28 | 31 |
  • 32 |
33 | 34 | 60 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/tabs/snapshots/SnapshotsTab.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 |
28 | {#if $snapshots.length === 0} 29 |
Save snapshots by clicking "Save" button.
30 | {:else} 31 | toggleViewSnapshot(view, snap)} 36 | onRestore={handleRestoreSnapshot} 37 | onExport={exportSnapshot} 38 | onDelete={deleteSnapshot} 39 | /> 40 | {/if} 41 |
42 |
43 | 44 | 58 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/components/__tests__/DevTools.spec.ts: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/svelte' 2 | import { EditorView } from 'prosemirror-view' 3 | import { vi } from 'vitest' 4 | 5 | import DevTools from '../DevTools.svelte' 6 | import { setupEditor } from '$test-utils/setupEditor' 7 | 8 | describe('DevTools component', () => { 9 | let editor: EditorView 10 | const div = document.createElement('div') 11 | div.id = 'pm-editor' 12 | document.body.appendChild(div) 13 | // polyfillDom() 14 | 15 | beforeAll(() => { 16 | vi.stubGlobal('prompt', (str: string) => undefined) 17 | }) 18 | 19 | afterEach(() => { 20 | if (editor) { 21 | editor.destroy() 22 | } 23 | vi.restoreAllMocks() 24 | }) 25 | 26 | it('should render', () => { 27 | editor = setupEditor(div) 28 | const results = render(DevTools, { 29 | props: { 30 | view: editor, 31 | buttonPosition: 'top-left', 32 | devToolsExpanded: true 33 | } 34 | }) 35 | 36 | expect(results.container).toBeInTheDocument() 37 | expect(document.body).toMatchSnapshot() 38 | }) 39 | 40 | it('should open by clicking the floating button', async () => { 41 | editor = setupEditor(div) 42 | const results = render(DevTools, { props: { view: editor } }) 43 | const button = results.container.querySelector('button') 44 | expect(button).not.toBeNull() 45 | 46 | await fireEvent.click(button as HTMLElement) 47 | const snapshotsTabBtn = await results.findByText('SNAPSHOTS') 48 | await fireEvent.click(snapshotsTabBtn as HTMLElement) 49 | 50 | expect(results.container).toBeInTheDocument() 51 | expect(document.body).toMatchSnapshot() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/extension/src/inject/store.ts: -------------------------------------------------------------------------------- 1 | import { applyDevTools } from 'prosemirror-dev-toolkit' 2 | 3 | import { DEFAULT_INJECT_STATE } from '../types' 4 | import type { FoundInstance, InjectState, InjectStatus } from '../types' 5 | 6 | import { findEditorViews } from './findEditorViews' 7 | import { send } from './utils' 8 | 9 | export let mounted = false 10 | export let state: InjectState = DEFAULT_INJECT_STATE 11 | 12 | export const injectActions = { 13 | setMounted(val: boolean) { 14 | mounted = val 15 | }, 16 | setState(val: InjectState) { 17 | state = val 18 | }, 19 | updateStatus(status: InjectStatus) { 20 | state.inject.status = status 21 | send('inject-status', status) 22 | }, 23 | async findInstances() { 24 | this.updateStatus('finding') 25 | const views = await findEditorViews(state) 26 | if (!views) { 27 | this.updateStatus('error') 28 | } else if (views.length > 0) { 29 | let applied = false 30 | // If any ProseMirror instances are found, apply toolkit to the first one (which doesn't error) 31 | // since there can be only one toolkit dock at a time 32 | const instances: FoundInstance[] = views.map((v, idx) => { 33 | if (idx === state.inject.instance || (!applied && idx === views.length - 1)) { 34 | try { 35 | applyDevTools(v, state.devToolsOpts) 36 | applied = true 37 | } catch (err) { 38 | console.error(err) 39 | } 40 | } 41 | return { 42 | size: v.dom.innerHTML.length, 43 | element: v.dom.innerHTML.slice(0, 100) 44 | } 45 | }) 46 | send('inject-found-instances', { instances }) 47 | } 48 | this.updateStatus('finished') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/site/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | import styled from 'styled-components' 4 | 5 | interface IProps { 6 | className?: string 7 | } 8 | 9 | export function NavBar(props: IProps) { 10 | const { className } = props 11 | return ( 12 | 13 | 33 | 34 | ) 35 | } 36 | 37 | const Container = styled.div` 38 | background: var(--color-primary); 39 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.18); 40 | padding: 1rem; 41 | ` 42 | const Nav = styled.nav` 43 | align-items: center; 44 | color: #fff; 45 | display: flex; 46 | ` 47 | const Link = styled(NavLink)` 48 | box-sizing: border-box; 49 | color: #fff; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | padding: 0.5rem 1rem; 53 | text-decoration: none; 54 | transition: 0.2s hover; 55 | &:hover { 56 | text-decoration: underline; 57 | } 58 | &.current { 59 | font-weight: 600; 60 | } 61 | ` 62 | -------------------------------------------------------------------------------- /packages/site/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { BrowserRouter, Navigate, Route, Routes as RouterRoutes } from 'react-router-dom' 3 | 4 | import { DefaultLayout } from './components/Layout' 5 | 6 | import { FrontPage } from './pages/FrontPage' 7 | import { DevToolsPage } from './pages/DevToolsPage' 8 | import { PlainPMPage } from './pages/PlainPMPage' 9 | import { YjsPage } from './pages/YjsPage' 10 | import { IFramePage } from 'pages/IFramePage' 11 | import { NoEditorPage } from './pages/NoEditorPage' 12 | 13 | export const Routes = () => ( 14 | 15 | 16 | 20 | 21 | 22 | } 23 | /> 24 | 28 | 29 | 30 | } 31 | /> 32 | 36 | 37 | 38 | } 39 | /> 40 | 44 | 45 | 46 | } 47 | /> 48 | 52 | 53 | 54 | } 55 | /> 56 | 60 | 61 | 62 | } 63 | /> 64 | } /> 65 | 66 | 67 | ) 68 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | import '@frsource/cypress-plugin-visual-regression-diff' 11 | 12 | Cypress.Commands.add('interrupt', () => { 13 | eval("window.top.document.body.querySelector('header button.stop').click()") 14 | }) 15 | 16 | Cypress.Commands.add('devTools', () => { 17 | return cy.get('prosemirror-dev-toolkit').shadow() 18 | }) 19 | 20 | // Reset devTools with applyDevTools and dispatch a complete deletion of the document. 21 | Cypress.Commands.add('resetDoc', () => { 22 | return cy.window().then(async window => { 23 | const { applyDevTools, editorView: view } = window 24 | const tr = view.state.tr 25 | view.dispatch(tr.delete(1, view.state.doc.nodeSize - 2)) 26 | applyDevTools(view) 27 | }) 28 | }) 29 | 30 | // This is a bit awkward but didn't find a better way 31 | Cypress.Commands.add('includesStringCount', { prevSubject: true }, (subject: any, str: string) => { 32 | let count = 0 33 | return cy 34 | .wrap(subject) 35 | .each($element => { 36 | if ($element.text().includes(str)) count += 1 37 | }) 38 | .then(() => { 39 | return count 40 | }) 41 | }) 42 | 43 | Cypress.Commands.add('pmInsParagraphBolded', (text: string) => { 44 | return cy.window().then(window => { 45 | const { editorView: view } = window 46 | const tr = view.state.tr 47 | const schema = view.state.schema 48 | tr.insert( 49 | 1, 50 | schema.nodes.paragraph.create(null, schema.text(text, [schema.marks.bold.create()])) 51 | ) 52 | view.dispatch(tr) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/extension/src/inject/index.ts: -------------------------------------------------------------------------------- 1 | import { removeDevTools } from 'prosemirror-dev-toolkit' 2 | import type { EditorView } from 'prosemirror-view' 3 | 4 | import type { SWMessageMap } from '../types' 5 | 6 | import { mounted, state, injectActions } from './store' 7 | import { shouldRerun } from './utils' 8 | 9 | declare global { 10 | interface Element { 11 | pmViewDesc?: globalThis.Node['pmViewDesc'] & { 12 | updateChildren: (view: EditorView, pos: number) => void 13 | selectNode: () => void 14 | deselectNode: () => void 15 | } 16 | } 17 | } 18 | 19 | async function handleMessages(event: MessageEvent) { 20 | if ( 21 | typeof event.data !== 'object' || 22 | !('source' in event.data) || 23 | event.data.source !== 'pm-dev-tools' 24 | ) { 25 | return 26 | } 27 | if (event.data.origin !== 'sw') { 28 | return 29 | } 30 | // console.log('RECEIVED IN INJECT', event.data) 31 | const msg = event.data 32 | switch (msg.type) { 33 | case 'inject-state': 34 | // Check shouldRerun first before over-writing the state 35 | const rerun = shouldRerun(state, msg.data) 36 | injectActions.setState(msg.data) 37 | if ((!mounted && !msg.data.disabled) || rerun) { 38 | // If toolkit wasn't mounted and it's not disabled -> run 39 | // Otherwise check whether the toolkit is still enabled and an option has changed 40 | injectActions.findInstances() 41 | injectActions.setMounted(true) 42 | } else if (msg.data.disabled) { 43 | // If toolkit is mounted and it is being disabled -> remove it 44 | removeDevTools() 45 | injectActions.setMounted(false) 46 | } 47 | break 48 | case 'rerun-inject': 49 | removeDevTools() 50 | injectActions.setMounted(false) 51 | injectActions.findInstances() 52 | break 53 | } 54 | } 55 | 56 | window.addEventListener('message', handleMessages) 57 | 58 | export {} 59 | -------------------------------------------------------------------------------- /packages/site/src/pm/prosemirror-example-setup.css: -------------------------------------------------------------------------------- 1 | /* From https://github.com/ProseMirror/prosemirror-example-setup/blob/master/style/style.css */ 2 | /* Add space around the hr to make clicking it easier */ 3 | 4 | .ProseMirror-example-setup-style hr { 5 | padding: 2px 10px; 6 | border: none; 7 | margin: 1em 0; 8 | } 9 | 10 | .ProseMirror-example-setup-style hr:after { 11 | content: ""; 12 | display: block; 13 | height: 1px; 14 | background-color: silver; 15 | line-height: 2px; 16 | } 17 | 18 | .ProseMirror ul, 19 | .ProseMirror ol { 20 | padding-left: 30px; 21 | } 22 | 23 | .ProseMirror blockquote { 24 | padding-left: 1em; 25 | border-left: 3px solid #eee; 26 | margin-left: 0; 27 | margin-right: 0; 28 | } 29 | 30 | .ProseMirror-example-setup-style img { 31 | cursor: default; 32 | } 33 | 34 | .ProseMirror-prompt { 35 | background: white; 36 | padding: 5px 10px 5px 15px; 37 | border: 1px solid silver; 38 | position: fixed; 39 | border-radius: 3px; 40 | z-index: 11; 41 | box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2); 42 | } 43 | 44 | .ProseMirror-prompt h5 { 45 | margin: 0; 46 | font-weight: normal; 47 | font-size: 100%; 48 | color: #444; 49 | } 50 | 51 | .ProseMirror-prompt input[type="text"], 52 | .ProseMirror-prompt textarea { 53 | background: #eee; 54 | border: none; 55 | outline: none; 56 | } 57 | 58 | .ProseMirror-prompt input[type="text"] { 59 | padding: 0 4px; 60 | } 61 | 62 | .ProseMirror-prompt-close { 63 | position: absolute; 64 | left: 2px; 65 | top: 1px; 66 | color: #666; 67 | border: none; 68 | background: transparent; 69 | padding: 0; 70 | } 71 | 72 | .ProseMirror-prompt-close:after { 73 | content: "✕"; 74 | font-size: 12px; 75 | } 76 | 77 | .ProseMirror-invalid { 78 | background: #ffc; 79 | border: 1px solid #cc7; 80 | border-radius: 4px; 81 | padding: 5px 10px; 82 | position: absolute; 83 | min-width: 10em; 84 | } 85 | 86 | .ProseMirror-prompt-buttons { 87 | margin-top: 5px; 88 | display: none; 89 | } 90 | -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a script to inject prosemirror-dev-toolkit into any live editor instance 3 | * 4 | * Basically what you do is inspect the element ProseMirror editor is mounted to, 5 | * a div with .ProseMirror class is a good guess. Then you just copy-paste this script 6 | * to the browser console and change the `viewOrSelector` parameter as needed. 7 | * 8 | * If there is a strict CSP enabled you might need to disable it. I've used this: 9 | * https://chrome.google.com/webstore/detail/disable-content-security/ieelmcmcagommplceebfedjlakkhpden 10 | */ 11 | ;(async viewOrSelector => { 12 | function getEditorView(selector) { 13 | const el = document.querySelector(selector) 14 | const oldFn = el.pmViewDesc.updateChildren 15 | const childWithSelectNode = Array.from(el.children).find( 16 | child => child.pmViewDesc && child.pmViewDesc.selectNode !== undefined 17 | ) 18 | 19 | if (childWithSelectNode === undefined) { 20 | console.error( 21 | 'Failed to find a ProseMirror child NodeViewDesc with selectNode function (which is strange)' 22 | ) 23 | } else { 24 | childWithSelectNode.pmViewDesc.selectNode() 25 | childWithSelectNode.pmViewDesc.deselectNode() 26 | } 27 | return new Promise((res, rej) => { 28 | el.pmViewDesc.updateChildren = (view, pos) => { 29 | el.pmViewDesc.updateChildren = oldFn 30 | res(view) 31 | return Function.prototype.bind.apply(oldFn, view, pos) 32 | } 33 | }) 34 | } 35 | 36 | const editorView = 37 | typeof viewOrSelector === 'string' ? await getEditorView(viewOrSelector) : viewOrSelector 38 | 39 | if (editorView) { 40 | fetch('https://unpkg.com/prosemirror-dev-toolkit/dist/bundle.umd.min.js') 41 | .then(response => response.text()) 42 | .then(data => { 43 | eval(data) 44 | window.applyDevTools(editorView, { buttonPosition: 'bottom-right' }) 45 | }) 46 | } else { 47 | console.error('No EditorView found or provided') 48 | } 49 | })('.ProseMirror') 50 | -------------------------------------------------------------------------------- /packages/site/src/pm/PMEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef } from 'react' 2 | import { EditorView } from 'prosemirror-view' 3 | import { EditorState, Transaction } from 'prosemirror-state' 4 | import { exampleSetup } from 'prosemirror-example-setup' 5 | 6 | import { schema } from './schema' 7 | import { examplePlugin } from './example-plugin' 8 | 9 | interface EditorProps { 10 | className?: string 11 | onEditorReady?: (view: EditorView) => void 12 | onEdit?: (state: EditorState) => void 13 | } 14 | 15 | export function PMEditor(props: EditorProps) { 16 | const { className = '' } = props 17 | const editorDOMRef = useRef(null) 18 | const editorViewRef = useRef(null) 19 | 20 | useLayoutEffect(() => { 21 | const state = createEditorState() 22 | const editorViewDOM = editorDOMRef.current 23 | if (editorViewDOM) { 24 | editorViewRef.current = createEditorView(editorViewDOM, state) 25 | props.onEditorReady && props?.onEditorReady(editorViewRef.current) 26 | } 27 | return () => { 28 | editorViewRef.current?.destroy() 29 | } 30 | // eslint-disable-next-line 31 | }, []) 32 | 33 | function createEditorState() { 34 | return EditorState.create({ 35 | schema, 36 | plugins: exampleSetup({ schema }).concat(examplePlugin()) 37 | }) 38 | } 39 | 40 | function createEditorView(element: HTMLDivElement, state: EditorState) { 41 | const view = new EditorView( 42 | { mount: element }, 43 | { 44 | state, 45 | dispatchTransaction 46 | } 47 | ) 48 | return view 49 | } 50 | 51 | function dispatchTransaction(transaction: Transaction) { 52 | if (!editorViewRef.current) { 53 | return 54 | } 55 | const editorState = editorViewRef.current.state.apply(transaction) 56 | editorViewRef.current.updateState(editorState) 57 | if (props.onEdit) { 58 | props.onEdit(editorState) 59 | } 60 | } 61 | 62 | return
63 | } 64 | -------------------------------------------------------------------------------- /packages/prosemirror-dev-toolkit/src/components/PasteModal.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |