├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── prosemirror_dark.svg └── prosemirror_light.svg ├── bin ├── pm └── pm.js ├── demo ├── bench │ ├── example.js │ ├── index.html │ ├── index.js │ ├── mutate.js │ └── type.js ├── demo.css ├── demo.ts ├── img.png ├── index.html ├── parent └── test │ ├── mocha.css │ └── mocha.js ├── package.json └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: marijn 2 | custom: ['https://www.paypal.com/paypalme/marijnhaverbeke', 'https://marijnhaverbeke.nl/fund/'] 3 | github: marijnh -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tern-port 2 | /node_modules 3 | /model 4 | /transform 5 | /state 6 | /view 7 | /history 8 | /collab 9 | /commands 10 | /inputrules 11 | /keymap 12 | /search 13 | /schema-basic 14 | /schema-list 15 | /schema-table 16 | /menu 17 | /markdown 18 | /dropcursor 19 | /test-builder 20 | /gapcursor 21 | /changeset 22 | /website 23 | /example-setup 24 | /tables 25 | /notes.txt 26 | /yarn.lock 27 | /bin/.pm-dev.pid 28 | /demo/demo.js 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | - [Getting help](#getting-help) 4 | - [Submitting bug reports](#submitting-bug-reports) 5 | - [Contributing code](#contributing-code) 6 | 7 | ## Getting help 8 | 9 | Community discussion, questions, and informal bug reporting is done on the 10 | [discuss.ProseMirror forum](http://discuss.prosemirror.net). 11 | 12 | ## Submitting bug reports 13 | 14 | Report bugs on the 15 | [GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues). 16 | Before reporting a bug, please read these pointers. 17 | 18 | - The issue tracker is for *bugs*, not requests for help. Questions 19 | should be asked on the [forum](http://discuss.prosemirror.net). 20 | 21 | - Include information about the version of the code that exhibits the 22 | problem. For browser-related issues, include the browser and browser 23 | version on which the problem occurred. 24 | 25 | - Mention very precisely what went wrong. "X is broken" is not a good 26 | bug report. What did you expect to happen? What happened instead? 27 | Describe the exact steps a maintainer has to take to make the 28 | problem occur. A screencast can be useful, but is no substitute for 29 | a textual description. 30 | 31 | - A great way to make it easy to reproduce your problem, if it can not 32 | be trivially reproduced on the website demos, is to submit a script 33 | that triggers the issue. 34 | 35 | ## Contributing code 36 | 37 | - Make sure you have a [GitHub Account](https://github.com/signup/free) 38 | 39 | - Fork the relevant repository 40 | ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) 41 | 42 | - Create a local checkout of the code. You can use the 43 | [main repository](https://github.com/prosemirror/prosemirror) to 44 | easily check out all core modules. 45 | 46 | - Make your changes, and commit them 47 | 48 | - Follow the code style of the rest of the project (see below). Run 49 | `npm run lint` (in the main repository checkout) to make sure that 50 | the linter is happy. 51 | 52 | - If your changes are easy to test or likely to regress, add tests in 53 | the relevant `test/` directory. Either put them in an existing 54 | `test-*.js` file, if they fit there, or add a new file. 55 | 56 | - Make sure all tests pass. Run `npm run test` to verify tests pass 57 | (you will need Node.js v6+). 58 | 59 | - Submit a pull request ([how to create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)). 60 | Don't put more than one feature/fix in a single pull request. 61 | 62 | By contributing code to ProseMirror you 63 | 64 | - Agree to license the contributed code under the project's [MIT 65 | license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE). 66 | 67 | - Confirm that you have the right to contribute and license the code 68 | in question. (Either you hold all rights on the code, or the rights 69 | holder has explicitly granted the right to use it like this, 70 | through a compatible open source license or through a direct 71 | agreement with you.) 72 | 73 | ### Coding standards 74 | 75 | - TypeScript, targeting an ES5 runtime (i.e. don't use library 76 | elements added by ES6, don't use ES7/ES.next syntax). 77 | 78 | - 2 spaces per indentation level, no tabs. 79 | 80 | - No semicolons except when necessary. 81 | 82 | - Follow the surrounding code when it comes to spacing, brace 83 | placement, etc. 84 | 85 | - Brace-less single-statement bodies are encouraged (whenever they 86 | don't impact readability). 87 | 88 | - [getdocs](https://github.com/marijnh/getdocs-ts)-style doc comments 89 | above items that are part of the public API. 90 | 91 | - ProseMirror does *not* follow JSHint or JSLint prescribed style. 92 | Patches that try to 'fix' code to pass one of these linters will not 93 | be accepted. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2017 by Marijn Haverbeke and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prosemirror 2 | 3 | [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) ] 4 | 5 | ProseMirror is a well-behaved rich semantic content editor based on 6 | contentEditable, with support for collaborative editing and custom 7 | document schemas. 8 | 9 | The ProseMirror library consists of a number of separate 10 | [modules](https://github.com/prosemirror/). This repository just 11 | serves as a central issue tracker, and holds a script to help easily 12 | check out all the core modules for development. 13 | 14 | The [project page](https://prosemirror.net) has more information, a 15 | number of [examples](https://prosemirror.net/examples/) and the 16 | [documentation](https://prosemirror.net/docs/). 17 | 18 | This code is released under an 19 | [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). 20 | There's a [forum](http://discuss.prosemirror.net) for general 21 | discussion and support requests, and the 22 | [Github bug tracker](https://github.com/prosemirror/prosemirror/issues) 23 | is the place to report issues. 24 | 25 | **STOP READING HERE IF YOU'RE SIMPLY _USING_ PROSEMIRROR. YOU CAN 26 | INSTALL THE SEPARATE [NPM 27 | MODULES](https://www.npmjs.com/search?q=prosemirror-) FOR THAT. THE 28 | INSTRUCTIONS BELOW ONLY APPLY WHEN _DEVELOPING_ PROSEMIRROR!** 29 | 30 | ## Setting up a dev environment 31 | 32 | Clone this repository, and make sure you have 33 | [node](https://nodejs.org/en/) and [yarn](https://yarnpkg.com/) (due 34 | to a string of issues with NPM 5, NPM is not currently supported) 35 | installed. Next, from the cloned directory run: 36 | 37 | bin/pm install 38 | 39 | This will fetch the submodules, install their dependencies, and build 40 | them. 41 | 42 | The `bin/pm` script in this repository provides functionality for 43 | working with the repositories: 44 | 45 | * `bin/pm build` rebuilds all the modules 46 | 47 | * `bin/pm watch` sets up a process that automatically rebuilds the 48 | modules when they change 49 | 50 | * `bin/pm status` prints the git status of all submodules 51 | 52 | * `bin/pm commit ` runs `git commit` with the given arguments 53 | in all submodules that have pending changes 54 | 55 | * `bin/pm test` runs the (non-browser) tests in all modules 56 | 57 | * `bin/pm push` runs `git push` in all modules 58 | 59 | * `bin/pm grep ` greps through the source code for the 60 | modules for the given pattern 61 | 62 | * `bin/pm dev-start` starts a server that rebuilds the packages 63 | whenever their sources change, and exposes the demo (`demo/*`) 64 | under a webserver on port 8080 65 | 66 | (Functionality for managing releases will be added in the future.) 67 | 68 | ## Community 69 | 70 | Development of ProseMirror happens in the various repositories exposed 71 | under the [ProseMirror](https://github.com/ProseMirror) organization 72 | on GitHub. Bugs for core packages are tracked in the [bug 73 | tracker](https://github.com/prosemirror/prosemirror/issues) for the 74 | meta repository. 75 | 76 | We aim to be an inclusive, welcoming community. To make that explicit, 77 | we have a [code of 78 | conduct](http://contributor-covenant.org/version/1/1/0/) that applies 79 | to communication around the project. 80 | -------------------------------------------------------------------------------- /assets/prosemirror_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/prosemirror_light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/pm: -------------------------------------------------------------------------------- 1 | pm.js -------------------------------------------------------------------------------- /bin/pm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // NOTE: Don't require anything from node_modules here, since the 4 | // install script has to be able to run _before_ that exists. 5 | let child = require("child_process"), fs = require("fs"), path = require("path") 6 | 7 | const {join} = path 8 | 9 | let main = ["model", "transform", "state", "view", 10 | "keymap", "inputrules", "history", "collab", "commands", "gapcursor", 11 | "schema-basic", "schema-list"] 12 | let mods = main.concat(["menu", "example-setup", "markdown", "dropcursor", "test-builder", "changeset", "search"]) 13 | let modsAndWebsite = mods.concat("website") 14 | 15 | let projectDir = join(__dirname, "..") 16 | 17 | function joinP(...args) { 18 | return join(projectDir, ...args) 19 | } 20 | 21 | function mainFile(pkg) { 22 | let index = joinP(pkg, "src", "index.ts"), self = joinP(pkg, "src", pkg + ".ts") 23 | if (fs.existsSync(index)) return index 24 | if (fs.existsSync(self)) return self 25 | throw new Error("Couldn't find a main file for " + pkg) 26 | } 27 | 28 | function start() { 29 | let command = process.argv[2] 30 | if (command && !["install", "--help", "modules"].includes(command)) assertInstalled() 31 | let args = process.argv.slice(3) 32 | let cmdFn = { 33 | "status": status, 34 | "commit": commit, 35 | "install": install, 36 | "build": build, 37 | "test": test, 38 | "push": push, 39 | "pull": pull, 40 | "grep": grep, 41 | "run": runCmd, 42 | "watch": watch, 43 | "changes": changes, 44 | "modules": listModules, 45 | "release": release, 46 | "unreleased": unreleased, 47 | "dev-start": devStart, 48 | "dev-stop": devStop, 49 | "mass-change": massChange, 50 | "--help": showHelp 51 | }[command] 52 | if (!cmdFn || cmdFn.length > args.length) help(1) 53 | cmdFn.apply(null, args) 54 | } 55 | 56 | function showHelp() { 57 | help(0) 58 | } 59 | 60 | function help(status) { 61 | console.log(`Usage: 62 | pm install [--ssh] Clone and symlink the packages, install dependencies, build 63 | pm build Build all modules 64 | pm status Print out the git status of packages 65 | pm commit Run git commit in all packages that have changes 66 | pm push Run git push in packages that have new commits 67 | pm pull Run git pull in all packages 68 | pm test Run the tests from all packages 69 | pm watch Set up a process that rebuilds the packages on change 70 | pm grep Grep through the source code for all packages 71 | pm run Run the given command in each of the package dirs 72 | pm changes Show commits since the last release for all packages 73 | pm mass-change 74 | Run a regexp-replace on the matching files in each package 75 | pm release Generate a new release for the given module. 76 | pm unreleased List committed but unreleased changes. 77 | pm modules [--core] Emit a list of all package names 78 | pm dev-start Start development server 79 | pm dev-stop Stop development server, if running 80 | pm --help`) 81 | process.exit(status) 82 | } 83 | 84 | function assertInstalled() { 85 | modsAndWebsite.forEach(repo => { 86 | if (!fs.existsSync(joinP(repo))) { 87 | console.error("module `%s` is missing. Did you forget to run `pm install`?", repo) 88 | process.exit(1) 89 | } 90 | }) 91 | } 92 | 93 | function run(cmd, args, pkg) { 94 | return child.execFileSync(cmd, args, { 95 | cwd: pkg === null ? undefined : pkg ? joinP(pkg) : projectDir, 96 | encoding: "utf8", 97 | stdio: ["ignore", "pipe", process.stderr] 98 | }) 99 | } 100 | 101 | function status() { 102 | modsAndWebsite.forEach(repo => { 103 | let output = run("git", ["status", "-sb"], repo) 104 | if (output != "## master...origin/master\n" && output != "## main...origin/main\n") 105 | console.log(repo + ":\n" + run("git", ["status"], repo)) 106 | }) 107 | } 108 | 109 | function commit(...args) { 110 | modsAndWebsite.forEach(repo => { 111 | if (run("git", ["diff"], repo) || run("git", ["diff", "--cached"], repo)) 112 | console.log(repo + ":\n" + run("git", ["commit"].concat(args), repo)) 113 | }) 114 | } 115 | 116 | function install(arg = null) { 117 | let base = "https://github.com/prosemirror/" 118 | if (arg == "--ssh") { base = "git@github.com:ProseMirror/" } 119 | else if (arg != null) help(1) 120 | 121 | modsAndWebsite.forEach(repo => { 122 | if (fs.existsSync(joinP(repo))) { 123 | console.warn("Skipping cloning of " + repo + " (directory exists)") 124 | return 125 | } 126 | let origin = base + (repo == "website" ? "" : "prosemirror-") + repo + ".git" 127 | run("git", ["clone", origin, repo]) 128 | }) 129 | 130 | console.log("Running npm install") 131 | run("npm", ["install"]) 132 | console.log("Building modules") 133 | build() 134 | } 135 | 136 | async function build() { 137 | console.info("Building...") 138 | let t0 = Date.now() 139 | await require("@marijn/buildtool").build(mods.map(mainFile), buildOptions) 140 | console.info(`Done in ${((Date.now() - t0) / 1000).toFixed(2)}s`) 141 | } 142 | 143 | function test(...args) { 144 | let runTests = require("@marijn/testtool") 145 | let {tests, browserTests} = runTests.gatherTests(mods.map(m => joinP(m))) 146 | let browsers = [], grep, noBrowser = false 147 | for (let i = 0; i < args.length; i++) { 148 | if (args[i] == "--firefox") browsers.push("firefox") 149 | if (args[i] == "--chrome") browser.push("chrome") 150 | if (args[i] == "--no-browser") noBrowser = true 151 | if (args[i] == "--grep") grep = args[++i] 152 | } 153 | if (!browsers.length && !noBrowser) browsers.push("chrome") 154 | runTests.runTests({tests, browserTests, browsers, grep}).then(failed => process.exit(failed ? 1 : 0)) 155 | } 156 | 157 | function push() { 158 | modsAndWebsite.forEach(repo => { 159 | if (/\bahead\b/.test(run("git", ["status", "-sb"], repo))) 160 | run("git", ["push"], repo) 161 | }) 162 | } 163 | 164 | function pull() { 165 | modsAndWebsite.forEach(repo => run("git", ["pull"], repo)) 166 | } 167 | 168 | function grep(pattern) { 169 | let files = [] 170 | const {globSync: glob} = require("glob") 171 | mods.forEach(repo => { 172 | files = files.concat(glob(joinP(repo, "src", "*.ts"))).concat(glob(joinP(repo, "test", "*.ts"))) 173 | }) 174 | files = files.concat(glob(joinP("website", "src", "**", "*.js"))) 175 | .concat(glob(joinP("website", "pages", "examples", "*", "*.js"))) 176 | try { 177 | console.log(run("grep", ["--color", "-nH", "-e", pattern].concat(files.map(f => path.relative(process.cwd(), f))), null)) 178 | } catch(e) { 179 | process.exit(1) 180 | } 181 | } 182 | 183 | function runCmd(cmd, ...args) { 184 | mods.forEach(repo => { 185 | console.log(repo + ":") 186 | try { 187 | console.log(run(cmd, args, repo)) 188 | } catch (e) { 189 | console.log(e.toString()) 190 | process.exit(1) 191 | } 192 | }) 193 | } 194 | 195 | function changes() { 196 | mods.forEach(repo => { 197 | let lastTag = run("git", ["describe", "HEAD", "--tags", "--abbrev=0"], repo).trim() 198 | if (!lastTag) return console.log("No previous tag for " + repo + "\n") 199 | let history = run("git", ["log", lastTag + "..HEAD"], repo).trim() 200 | if (history) console.log(repo + ":\n" + "=".repeat(repo.length + 1) + "\n\n" + history + "\n") 201 | }) 202 | } 203 | 204 | function editReleaseNotes(notes) { 205 | let noteFile = join(projectDir, "notes.txt") 206 | fs.writeFileSync(noteFile, notes.head + notes.body) 207 | run(process.env.EDITOR || "emacs", [noteFile], null) 208 | let edited = fs.readFileSync(noteFile) 209 | fs.unlinkSync(noteFile) 210 | if (!/\S/.test(edited)) process.exit(0) 211 | let split = /^(.*)\n+([^]*)/.exec(edited) 212 | return {head: split[1] + "\n\n", body: split[2]} 213 | } 214 | 215 | function version(mod) { 216 | return require(join("..", mod, "package.json")).version 217 | } 218 | 219 | function release(mod, ...args) { 220 | let currentVersion = version(mod) 221 | let noteArg = args.indexOf("--notes") 222 | let extra = noteArg > -1 ? args[noteArg + 1] : null 223 | let changes = changelog(mod, currentVersion, extra) 224 | let newVersion = bumpVersion(currentVersion, changes) 225 | console.log(`Creating prosemirror-${mod} ${newVersion}`) 226 | 227 | let notes = releaseNotes(mod, changes, newVersion) 228 | if (args.indexOf("--edit") > -1) nodes = editReleaseNotes(notes) 229 | 230 | setModuleVersion(mod, newVersion) 231 | if (changes.breaking.length) setDepVersion(mod, newVersion) 232 | fs.writeFileSync(joinP(mod, "CHANGELOG.md"), notes.head + notes.body + fs.readFileSync(joinP(mod, "CHANGELOG.md"), "utf8")) 233 | run("git", ["add", "package.json"], mod) 234 | run("git", ["add", "CHANGELOG.md"], mod) 235 | run("git", ["commit", "-m", `Mark version ${newVersion}`], mod) 236 | run("git", ["tag", newVersion, "-m", `Version ${newVersion}\n\n${notes.body}`, "--cleanup=verbatim"], mod) 237 | } 238 | 239 | function unreleased() { 240 | mods.forEach(mod => { 241 | let ver = version(mod), changes = changelog(mod, ver) 242 | if (changes.fix.length || changes.feature.length || changes.breaking.length) 243 | console.log(mod + ":\n\n", releaseNotes(mod, changes, "xxx").body) 244 | }) 245 | } 246 | 247 | function changelog(repo, since, extra) { 248 | let commits = run("git", ["log", "--format=%B", "--reverse", since + "..HEAD"], repo) 249 | if (extra) commits += "\n\n" + extra 250 | let result = {fix: [], feature: [], breaking: []} 251 | let re = /\n\r?\n(BREAKING|FIX|FEATURE):\s*([^]*?)(?=\r?\n\r?\n|\r?\n?$)/g, match 252 | while (match = re.exec(commits)) result[match[1].toLowerCase()].push(match[2].replace(/\r?\n/g, " ")) 253 | return result 254 | } 255 | 256 | function bumpVersion(version, changes) { 257 | let [major, minor, patch] = version.split(".") 258 | if (changes.breaking.length) return `${Number(major) + 1}.0.0` 259 | if (changes.feature.length) return `${major}.${Number(minor) + 1}.0` 260 | if (changes.fix.length) return `${major}.${minor}.${Number(patch) + 1}` 261 | throw new Error("No new release notes!") 262 | } 263 | 264 | function releaseNotes(mod, changes, version) { 265 | let pad = n => n < 10 ? "0" + n : n 266 | let d = new Date, date = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) 267 | 268 | let types = {breaking: "Breaking changes", fix: "Bug fixes", feature: "New features"} 269 | 270 | let refTarget = "https://prosemirror.net/docs/ref/" 271 | let head = `## ${version} (${date})\n\n`, body = "" 272 | for (let type in types) { 273 | let messages = changes[type] 274 | if (messages.length) body += `### ${types[type]}\n\n` 275 | messages.forEach(message => body += message.replace(/\]\(##/g, "](" + refTarget + "#") + "\n\n") 276 | } 277 | return {head, body} 278 | } 279 | 280 | function setModuleVersion(mod, version) { 281 | let file = joinP(mod, "package.json") 282 | fs.writeFileSync(file, fs.readFileSync(file, "utf8").replace(/"version":\s*".*?"/, `"version": "${version}"`)) 283 | } 284 | 285 | function setDepVersion(mod, version) { 286 | modsAndWebsite.forEach(repo => { 287 | if (repo == mod) return 288 | let file = joinP(repo, "package.json"), text = fs.readFileSync(file, "utf8") 289 | let result = text.replace(/"prosemirror-(.*?)":\s*".*?"/g, (match, dep) => { 290 | return dep == mod ? `"prosemirror-${mod}": "^${version}"` : match 291 | }) 292 | if (result != text) { 293 | fs.writeFileSync(file, result) 294 | run("git", ["add", "package.json"], repo) 295 | run("git", ["commit", "-m", `Upgrade prosemirror-${mod} dependency`], repo) 296 | } 297 | }) 298 | } 299 | 300 | function listModules() { 301 | console.log((process.argv.includes("--core") ? main : mods).join("\n")) 302 | } 303 | 304 | const buildOptions = {} 305 | 306 | function watch() { 307 | require("@marijn/buildtool").watch(mods.map(mainFile), [join(__dirname, "..", "demo", "demo.ts")], buildOptions) 308 | } 309 | 310 | const pidFile = join(__dirname, ".pm-dev.pid") 311 | function devPID() { 312 | try { return JSON.parse(fs.readFileSync(pidFile, "utf8")) } 313 | catch(_) { return null } 314 | } 315 | 316 | function startServer() { 317 | let serve = path.resolve(join(__dirname, "..", "demo")) 318 | let port = +(process.env.PORT || 8080) 319 | let moduleserver = new (require("esmoduleserve/moduleserver"))({root: serve, maxDepth: 2}) 320 | let serveStatic = require("serve-static")(serve) 321 | require("http").createServer((req, resp) => { 322 | if (/^\/test\/?($|\?)/.test(req.url)) { 323 | let runTests = require("@marijn/testtool") 324 | let {browserTests} = runTests.gatherTests(mods.map(m => joinP(m))) 325 | resp.writeHead(200, {"content-type": "text/html"}) 326 | resp.end(runTests.testHTML(browserTests.map(f => path.relative(serve, f)), false)) 327 | } else { 328 | moduleserver.handleRequest(req, resp) || serveStatic(req, resp, _err => { 329 | resp.statusCode = 404 330 | resp.end('Not found') 331 | }) 332 | } 333 | }).listen(port, process.env.OPEN ? undefined : "127.0.0.1") 334 | console.log(`Dev server listening on ${port}`) 335 | } 336 | 337 | 338 | function devStart() { 339 | let pid = devPID() 340 | if (pid != null) { 341 | try { run("ps", ["-p", String(pid)]) } 342 | catch (_) { pid = null } 343 | } 344 | if (pid != null) { 345 | console.log("Dev server already running at pid " + pid) 346 | return 347 | } 348 | 349 | fs.writeFileSync(pidFile, process.pid + "\n") 350 | function del() { fs.unlink(pidFile, () => {}); console.log("Stop") } 351 | function delAndExit() { del(); process.exit() } 352 | process.on("exit", del) 353 | process.on("SIGINT", delAndExit) 354 | process.on("SIGTERM", delAndExit) 355 | 356 | startServer() 357 | watch() 358 | } 359 | 360 | function devStop() { 361 | let pid = devPID() 362 | if (pid == null) { 363 | console.log("Dev server not running") 364 | } else { 365 | process.kill(pid, "SIGTERM") 366 | console.log("Killed dev server with pid " + pid) 367 | } 368 | } 369 | 370 | function massChange(file, pattern, replacement = "") { 371 | let re = new RegExp(pattern, "g") 372 | modsAndWebsite.forEach(repo => { 373 | let {globSync: glob} = require("glob") 374 | glob(joinP(repo, file)).forEach(file => { 375 | let content = fs.readFileSync(file, "utf8"), changed = content.replace(re, replacement) 376 | if (changed != content) { 377 | console.log("Updated " + file) 378 | fs.writeFileSync(file, changed) 379 | } 380 | }) 381 | }) 382 | } 383 | 384 | start() 385 | -------------------------------------------------------------------------------- /demo/bench/example.js: -------------------------------------------------------------------------------- 1 | const {schema, doc, p, ol, ul, li, h1, h2, blockquote, em, code, a} = require("prosemirror-model/test/build") 2 | 3 | exports.schema = schema 4 | 5 | let example = doc( 6 | h1("Collaborative Editing in ProseMirror"), 7 | p("This post describes the algorithm used to make collaborative editing work in ", a("ProseMirror"), ". For an introduction to ProseMirror, see ", a("another post"), " here."), 8 | h2("The Problem"), 9 | p("A real-time collaborative editing system is one where multiple people may work on a document at the same time. The system ensures that the documents stay synchronized—changes made by individual users are sent to other users, and show up in their representation of the document."), 10 | p("Since transmitting changes over any kind of network is going to take time, the complexity of such systems lies in the way they handle concurrent updates. One solution is to allow users to lock the document (or parts of it) and thus prevent concurrent changes from happening at all. But this forces users to think about locks, and to wait when the lock they need is not available. We'd prefer not to do that."), 11 | p("If we allow concurrent updates, we get situations where user A and user B both did something, unaware of the other user's actions, and now those things they did have to be reconciled. The actions might not interact at all—when they are editing different parts of the document—or interact very much—when they are trying to change the same word."), 12 | h2("Operational Transformation"), 13 | p("A lot of research has gone into this problem. And I must admit that, though I did read a bunch of papers, I definitely do not have a deep knowledge of this research, and if you find that I misrepresent something or am missing an interesting reference, I am very interested in an ", a("email"), " that tells me about it."), 14 | p("A lot of this research is about truly distributed systems, where a group of nodes exchange messages among themselves, without a central point of control. The classical approach to the problem, which is called ", a("Operational Transformation"), ", is such a distributed algorithm. It defines a way to describe changes that has two properties:"), 15 | ol(li(p("You can transform changes relative to other changes. So if user A inserted an “O” at offset 1, and user B concurrently inserted a “T” at offset 10, user A can transform B's change relative to its own change, an insert the “T” at offset 11, because an extra character was added in front of the change's offset.")), 16 | li(p("No matter in which order concurrent changes are applied, you end up with the same document. This allows A to transform B's change relative to its own change, and B to transform A's change similarly, without the two users ending up with different documents."))), 17 | p("An Operational Transformation (OT) based system applies local changes to the local document immediately, and broadcasts them to other users. Those users will transform and apply them when they get them. In order to know exactly which local changes a remote change should be transformed through, such a system also has to send along some representation of the state of the document at the time the change was made."), 18 | p("That sounds relatively simple. But it is a nightmare to implement. Once you support more than a few trivial types of changes (things like “insert” and “delete”), ensuring that applying changes in any order produces the same document becomes very hard."), 19 | p("Joseph Gentle, one of the engineers who worked on Google Wave, ", a("stated"), "..."), blockquote(p("Unfortunately, implementing OT sucks. There's a million algorithms with different trade-offs, mostly trapped in academic papers. The algorithms are really hard and time consuming to implement correctly.")), 20 | h2("Centralization"), 21 | p("The design decisions that make the OT mechanism complex largely stem from the need to have it be truly distributed. Distributed systems have nice properties, both practically and politically, and they tend to be interesting to work on."), 22 | p("But you can save oh so much complexity by introducing a central point. I am, to be honest, extremely bewildered by Google's decision to use OT for their Google Docs—a centralized system."), 23 | p("ProseMirror's algorithm is centralized, in that it has a single node (that all users are connected to) making decisions about the order in which changes are applied. This makes it relatively easy to implement and to reason about."), 24 | p("And I don't actually believe that this property represents a huge barrier to actually running the algorithm in a distributed way. Instead of a central server calling the shots, you could use a consensus algorithm like ", a("Raft"), " to pick an arbiter. (But note that I have not actually tried this.)"), 25 | h2("The Algorithm"), 26 | p("Like OT, ProseMirror uses a change-based vocabulary and transforms changes relative to each other. Unlike OT, it does not try to guarantee that applying changes in a different order will produce the same document."), 27 | p("By using a central server, it is possible—easy even—to have clients all apply changes in the same order. You can use a mechanism much like the one used in code versioning systems. When a client has made a change, they try to ", em("push"), " that change to the server. If the change was based on the version of the document that the server considers current, it goes through. If not, the client must ", em("pull"), " the changes that have been made by others in the meantime, and ", em("rebase"), " their own changes on top of them, before retrying the push."), 28 | p("Unlike in git, the history of the document is linear in this model, and a given version of the document can simply be denoted by an integer."), 29 | p("Also unlike git, all clients are constantly pulling (or, in a push model, listening for) new changes to the document, and track the server's state as quickly as the network allows."), 30 | p("The only hard part is rebasing changes on top of others. This is very similar to the transforming that OT does. But it is done with the client's ", em("own"), " changes, not remote changes."), 31 | p("Because applying changes in a different order might create a different document, rebasing isn't quite as easy as transforming all of our own changes through all of the remotely made changes."), 32 | h2("Position Mapping"), 33 | p("Whereas OT transforms changes relative to ", em("other changes"), ", ProseMirror transforms them using a derived data structure called a ", em("position map"), ". Whenever you apply a change to a document, you get a new document and such a map, which you can use to convert positions in the old document to corresponding positions in the new document. The most obvious use case of such a map is adjusting the cursor position so that it stays in the same conceptual place—if a character was inserted before it, it should move forward along with the surrounding text."), 34 | p("Transforming changes is done entirely in terms of mapping positions. This is nice—it means that we don't have to write change-type-specific transformation code. Each change has one to three positions associated with it, labeled ", code("from"), ", ", code("to"), ", and ", code("at"), ". When transforming the change relative to a given other change, those positions get mapped through the other change's position map."), 35 | p("For example, if a character is inserted at position 5, the change “delete from 10 to 14” would become “delete from 11 to 15” when transformed relative to that insertion."), 36 | p("Every change's positions are meaningful only in the exact document version that it was originally applied to. A position map defines a mapping between positions in the two document versions before and after a change. To be able to apply a change to a different version, it has to be mapped, step by step, through the changes that lie between its own version and the target version."), 37 | p("(For simplicity, examples will use integers for positions. Actual positions in ProseMirror consist of an integer offset in a paragraph plus the path of that paragraph in the document tree.)"), 38 | h2("Rebasing Positions"), 39 | p("An interesting case comes up when a client has multiple unpushed changes buffered. If changes from a peer come in, all of the locally buffered changes have to be moved on top of those changes. Say we have local changes ", em("L1"), " and ", em("L2"), ", and are rebasing them onto remote changes ", em("R1"), " and ", em("R2"), ", where ", em("L1"), " and ", em("R1"), " start from the same version of the document."), 40 | p("First, we apply R1 and R2 to our representation of that original version (clients must track both the document version they are currently displaying, which includes unsent changes, and the version that does not yet include those changes). This creates two position maps ", em("mR1"), " and ", em("mR2"), "."), 41 | p("We can simply map ", em("L1"), " forward through those maps to arrive at ", em("L1⋆"), ", the remapped version of ", em("L1"), ". But ", em("L2"), " was based on the document that existed after applying ", em("L1"), ", so we first have to map it ", em("backwards"), " through ", em("mL1"), ", the original map created by applying ", em("L1"), ". Now it refers to the same version that ", em("R1"), " starts in, so we can map it forward through ", em("mR1"), " and ", em("mR2"), ", and then finally though ", em("mL1⋆"), ", the map created by applying ", em("L1⋆"), ". Now we have ", em("L2⋆"), ", and can apply it to the output of applying ", em("L1⋆"), ", and ", em("voila"), ", we have rebased two changes onto two other changes."), 42 | p("Except that mapping through deletions or backwards through insertions loses information. If you insert two characters at position 5, and then another one at position 6 (between the two previously inserted characters), mapping backwards and then forward again through the first insertion will leave you before or after the characters, because the position between them could not be expressed in the coordinate space of a document that did not yet have these characters."), 43 | p("To fix this, the system uses mapping pipelines that are not just a series of maps, but also keep information about which of those maps are mirror images of each other. When a position going through such a pipeline encounters a map that deletes the content around the position, the system scans ahead in the pipeline looking for a mirror images of that map. If such a map is found, we skip forward to it, and restore the position in the content that is inserted by this map, using the relative offset that the position had in the deleted content. A mirror image of a map that deletes content must insert content with the same shape."), 44 | h2("Mapping Bias"), 45 | p("Whenever content gets inserted, a position at the exact insertion point can be meaningfully mapped to two different positions: before the inserted content, or after it. Sometimes the first is appropriate, sometimes the second. The system allows code that maps a position to choose what bias it prefers."), 46 | p("This is also why the positions associated with a change are labeled. If a change with ", code("from"), " and ", code("to"), " positions, such as deleting or styling a piece of the document, has content inserted directly before or after it, that content should not be included in the change. So ", code("from"), " positions get mapped with a forward bias, and ", code("to"), " positions with a backward bias."), 47 | p("When a change is mapped through a map that completely contains it, for example when inserting a character at position 5 is mapped through the map created by deleting from position 2 to 10, the whole change is simply dropped, since the context in which it was made no longer exists."), 48 | h2("Types of Changes"), 49 | p("An atomic change in ProseMirror is called a ", em("step"), ". Some things that look like single changes from a user interface perspective are actually decomposed into several steps. For example, if you select text and press enter, the editor will generate a ", em("delete"), " step that removes the selected text, and then a ", em("split"), " step that splits the current paragraph."), 50 | p("These are the step types that exist in ProseMirror:"), 51 | ul(li(p("", code("addStyle"), " and ", code("removeStyle"), " add and remove inline styling to or from a piece of the document. They take ", code("from"), " and ", code("to"), " positions.")), 52 | li(p("", code("split"), " splits a node in two. It can be used, for example, to split a paragraph when the user presses enter. It takes a single ", code("at"), " position.")), 53 | li(p("", code("join"), " joins two adjacent nodes. This only works if they contain the same type of content. It takes ", code("from"), " and ", code("to"), " positions that should refer to the end and start of the nodes to be joined. (This is to make sure that the nodes that were actually intended are being joined. The step is ignored when another node has been inserted between them in the meantime.)")), 54 | li(p("", code("ancestor"), " is used to change the type of a node and to add or remove nodes above it. It can be used to wrap something in a list, or to convert from a paragraph to a heading. It takes ", code("from"), " and ", code("to"), " positions pointing at the start and end of the node.")), 55 | li(p("", code("replace"), " replaces a piece of the document with zero or more replacement nodes, and optionally stitches up compatible nodes at the edges of the cut. Its ", code("from"), " and ", code("to"), " positions define the range that should be deleted, and its ", code("at"), " position gives the place where the new nodes should be inserted."))), 56 | p("The last type is more complex than the other ones, and my initial impulse was to split it up into steps that remove and insert content. But because the position map created by a replace step needs to treat the step as atomic (positions have to be pushed out of ", em("all"), " replaced content), I got better results with making it a single step."), 57 | h2("Intention"), 58 | p("An essential property of real-time collaborative systems is that they try to preserve the ", em("intention"), " of a change. Because “merging” of changes happens automatically, without user interaction, it would get very annoying when the changes you make are, through rebasing, reinterpreted in a way that does not match what you were trying to do."), 59 | p("I've tried to define the steps and the way in which they are rebased in so that rebasing yields unsurprising behavior. Most of the time, changes don't overlap, and thus don't really interact. But when they overlap, we must make sure that their combined effect remains sane."), 60 | p("Sometimes a change must simply be dropped. When you type into a paragraph, but another user deleted that paragraph before your change goes through, the context in which your input made sense is gone, and inserting it in the place where the paragraph used to be would create a meaningless fragment."), 61 | p("If you tried to join two lists together, but somebody has added a paragraph between them, your change becomes impossible to execute (you can't join nodes that aren't adjacent), so it is dropped."), 62 | p("In other cases, a change is modified but stays meaningful. If you made characters 5 to 10 strong, and another user inserted a character at position 7, you end up making characters 5 to 11 strong."), 63 | p("And finally, some changes can overlap without interacting. If you make a word a link and another user makes it emphasized, both of your changes to that same word can happen in their original form."), 64 | h2("Offline Work"), 65 | p("Silently reinterpreting or dropping changes is fine for real-time collaboration, where the feedback is more or less immediate—you see the paragraph that you were editing vanish, and thus know that someone deleted it, and your changes are gone."), 66 | p("For doing offline work (where you keep editing when not connected) or for a branching type of work flow, where you do a bunch of work and ", em("then"), " merge it with whatever other people have done in the meantime, the model I described here is useless (as is OT). It might silently throw away a lot of work (if its context was deleted), or create a strange mishmash of text when two people edited the same sentence in different ways."), 67 | p("In cases like this, I think a diff-based approach is more appropriate. You probably can't do automatic merging—you have to identify conflicts had present them to the user to resolve. I.e. you'd do what git does."), 68 | h2("Undo History"), 69 | p("How should the undo history work in a collaborative system? The widely accepted answer to that question is that it definitely should ", em("not"), " use a single, shared history. If you undo, the last edit that ", em("you"), " made should be undone, not the last edit in the document."), 70 | p("This means that the easy way to implement history, which is to simply roll back to a previous state, does not work. The state that is created by undoing your change, if other people's changes have come in after it, is a new one, not seen before."), 71 | p("To be able to implement this, I had to define changes (steps) in such a way that they can be inverted, producing a new step that represents the change that cancels out the original step."), 72 | p("ProseMirror's undo history accumulates inverted steps, and also keeps track of all position maps between them and the current document version. These are needed to be able to map the inverted steps to the current document version."), 73 | p("A downside of this is that if a user has made a change but is now idle while other people are editing the document, the position maps needed to move this user's change to the current document version pile up without bound. To address this, the history periodically ", em("compacts"), " itself, mapping the inverted changes forward so that they start at the current document again. It can then discard the intermediate position maps.") 74 | ) 75 | exports.example = example 76 | -------------------------------------------------------------------------------- /demo/bench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProseMirror benchmarks 4 | 5 |
6 | 7 |

8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/bench/index.js: -------------------------------------------------------------------------------- 1 | const {Fragment} = require("prosemirror-model") 2 | const {doc, blockquote, p} = require("prosemirror-model/test/build") 3 | const {EditorState} = require("prosemirror-state") 4 | const {EditorView} = require("prosemirror-view") 5 | const {history} = require("prosemirror-history") 6 | 7 | const {example} = require("./example") 8 | const {typeDoc} = require("./type") 9 | const {mutateDoc} = require("./mutate") 10 | 11 | function button(name, run) { 12 | var dom = document.createElement("button") 13 | dom.textContent = name 14 | dom.addEventListener("click", run) 15 | return dom 16 | } 17 | 18 | function group(name, ...buttons) { 19 | var wrap = document.querySelector("#buttons").appendChild(document.createElement("p")) 20 | wrap.textContent = name 21 | wrap.append(document.createElement("br")) 22 | buttons.forEach(b => wrap.append(" ", b)) 23 | } 24 | 25 | function run(bench, options) { 26 | var t0 = Date.now(), steps = 0 27 | var startState = (options.state || options.view) && EditorState.create({doc: options.doc, plugins: options.plugins}) 28 | var view = options.view && new EditorView(document.querySelector("#workspace"), {state: startState}) 29 | var state, callback = tr => { 30 | ++steps 31 | if (state) { 32 | state = state.applyAction({type: "transform", time: Date.now(), transform: tr}) 33 | if (view) view.updateState(state) 34 | } 35 | } 36 | var profile = document.querySelector("#profile").checked 37 | if (profile) console.profile(options.name) 38 | for (var i = 0, e = options.repeat || 1; i < e; i++) { 39 | state = startState 40 | bench(options, callback) 41 | } 42 | if (profile) console.profileEnd(options.name) 43 | console.log("'" + options.name + "' took " + (Date.now() - t0) + "ms for " + steps + " steps") 44 | } 45 | 46 | group("Type out a document", button("Plain", () => { 47 | run(typeDoc, {doc: example, name: "Type plain", profile: true, repeat: 6}) 48 | }), button("State", () => { 49 | run(typeDoc, {doc: example, name: "Type with state", profile: true, repeat: 6, state: true}) 50 | }), button("State + History", () => { 51 | run(typeDoc, {doc: example, name: "Type with state + history", profile: true, repeat: 6, state: true, plugins: [history()]}) 52 | }), button("View", () => { 53 | run(typeDoc, {doc: example, name: "Type with view", profile: true, repeat: 6, state: true, view: true}) 54 | })) 55 | 56 | group("Mutate inside a document", button("small + shallow", () => { 57 | run(mutateDoc, {doc: doc(p("a"), p("b"), p("c")), 58 | pos: 4, n: 100000, name: "Mutate small + shallow"}) 59 | }), button("small + deep", () => { 60 | run(mutateDoc, {doc: doc(p("a"), blockquote(blockquote(blockquote(blockquote(blockquote(blockquote(p("b"))))))), p("c")), 61 | pos: 10, n: 100000, name: "Mutate small + deep"}) 62 | }), button("large + shallow", () => { 63 | var d = doc(p("a")), many = [] 64 | for (var i = 0; i < 1000; i++) many.push(d.firstChild) 65 | run(mutateDoc, {doc: d.copy(Fragment.from(many)), 66 | pos: 4, n: 100000, name: "Mutate large + shallow"}) 67 | })) 68 | -------------------------------------------------------------------------------- /demo/bench/mutate.js: -------------------------------------------------------------------------------- 1 | const {Slice, Fragment} = require("prosemirror-model") 2 | const {Transform} = require("prosemirror-transform") 3 | 4 | function mutateDoc(options, callback) { 5 | var doc = options.doc, pos = options.pos, slice = new Slice(Fragment.from(doc.type.schema.text("X")), 0, 0) 6 | for (var i = 0; i < options.n; i++) { 7 | var add = new Transform(doc).replace(pos, pos, slice) 8 | callback(add) 9 | var rem = new Transform(add.doc).delete(pos, pos + 1) 10 | callback(rem) 11 | doc = rem.doc 12 | } 13 | } 14 | exports.mutateDoc = mutateDoc 15 | -------------------------------------------------------------------------------- /demo/bench/type.js: -------------------------------------------------------------------------------- 1 | const {Transform} = require("prosemirror-transform") 2 | 3 | function typeDoc(options, callback) { 4 | var example = options.doc, schema = example.type.schema 5 | var doc = schema.nodes.doc.createAndFill(), pos = 0 6 | 7 | function scan(node, depth) { 8 | if (node.isText) { 9 | for (var i = 0; i < node.text.length; i++) { 10 | var tr = new Transform(doc).replaceRangeWith(pos, pos, schema.text(node.text.charAt(i), node.marks)) 11 | callback(tr) 12 | doc = tr.doc 13 | pos++ 14 | } 15 | } else if (pos < doc.content.size - depth) { 16 | pos++ 17 | scanContent(node, depth + 1) 18 | pos++ 19 | } else { 20 | if (node.isLeaf) { 21 | var tr = new Transform(doc).replaceRangeWith(pos, pos, node) 22 | callback(tr) 23 | doc = tr.doc 24 | pos += node.nodeSize 25 | } else { 26 | var tr = new Transform(doc).replaceRangeWith(pos, pos, node.type.createAndFill()) 27 | callback(tr) 28 | doc = tr.doc 29 | pos++ 30 | scanContent(node, depth + 1) 31 | pos++ 32 | } 33 | } 34 | } 35 | function scanContent(node, depth) { 36 | node.forEach(child => scan(child, depth)) 37 | } 38 | scanContent(example, 0) 39 | } 40 | exports.typeDoc = typeDoc 41 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia; 3 | margin: 0 1em 2em; 4 | } 5 | 6 | textarea { 7 | width: 100%; 8 | border: 1px solid silver; 9 | min-height: 40em; 10 | padding: 4px 8px; 11 | } 12 | 13 | .left, .right { 14 | width: 50%; 15 | float: left; 16 | } 17 | 18 | .full { 19 | max-width: 50em; 20 | } 21 | 22 | .marked { 23 | background: #ff6 24 | } 25 | 26 | .ProseMirror-menubar-wrapper { 27 | border: 1px solid silver; 28 | } 29 | 30 | .ProseMirror { 31 | padding: 4px 8px 4px 14px; 32 | line-height: 1.2; 33 | } 34 | -------------------------------------------------------------------------------- /demo/demo.ts: -------------------------------------------------------------------------------- 1 | import {Schema, DOMParser} from "prosemirror-model" 2 | import {EditorView} from "prosemirror-view" 3 | import {EditorState} from "prosemirror-state" 4 | import {schema} from "prosemirror-schema-basic" 5 | import {addListNodes} from "prosemirror-schema-list" 6 | import {exampleSetup} from "prosemirror-example-setup" 7 | 8 | const demoSchema = new Schema({ 9 | nodes: addListNodes(schema.spec.nodes as any, "paragraph block*", "block"), 10 | marks: schema.spec.marks 11 | }) 12 | 13 | let state = EditorState.create({doc: DOMParser.fromSchema(demoSchema).parse(document.querySelector("#content")!), 14 | plugins: exampleSetup({schema: demoSchema})}) 15 | 16 | ;(window as any).view = new EditorView(document.querySelector(".full"), {state}) 17 | -------------------------------------------------------------------------------- /demo/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProseMirror/prosemirror/0126a40c14d408a275743058d8cd04d3fc64834c/demo/img.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ProseMirror demo page 6 | 7 | 8 | 9 | 10 | 11 | 12 |

ProseMirror demo page

13 | 14 |
15 | 16 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /demo/parent: -------------------------------------------------------------------------------- 1 | .. -------------------------------------------------------------------------------- /demo/test/mocha.css: -------------------------------------------------------------------------------- 1 | ../../node_modules/mocha/mocha.css -------------------------------------------------------------------------------- /demo/test/mocha.js: -------------------------------------------------------------------------------- 1 | ../../node_modules/mocha/mocha.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prosemirror", 3 | "version": "0.0.0", 4 | "description": "Structured WYSIWYM editor", 5 | "license": "MIT", 6 | "maintainers": [ 7 | { 8 | "name": "Marijn Haverbeke", 9 | "email": "marijn@haverbeke.berlin", 10 | "web": "http://marijnhaverbeke.nl" 11 | } 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/prosemirror/prosemirror.git" 16 | }, 17 | "dependencies": { 18 | "glob": "^10.3.0", 19 | "esmoduleserve": "^0.2.0", 20 | "serve-static": "^1.14.1", 21 | "@marijn/buildtool": "^1.0.0", 22 | "@marijn/testtool": "^0.1.0" 23 | }, 24 | "scripts": { 25 | "test": "bin/pm test" 26 | }, 27 | "workspaces": [ 28 | "*" 29 | ], 30 | "private": true 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom", "scripthost"], 4 | "types": ["mocha"], 5 | "stripInternal": true, 6 | "typeRoots": ["./node_modules/@types"], 7 | "noUnusedLocals": true, 8 | "strict": true, 9 | "target": "es6", 10 | "module": "es2020", 11 | "newLine": "lf", 12 | "moduleResolution": "node", 13 | "noEmit": true, 14 | "paths": { 15 | "prosemirror-model": ["./model/src/index.ts"], 16 | "prosemirror-schema-basic": ["./schema-basic/src/schema-basic.ts"], 17 | "prosemirror-schema-list": ["./schema-list/src/schema-list.ts"], 18 | "prosemirror-test-builder": ["./test-builder/src/index.ts"], 19 | "prosemirror-transform": ["./transform/src/index.ts"], 20 | "prosemirror-view": ["./view/src/index.ts"], 21 | "prosemirror-state": ["./state/src/index.ts"], 22 | "prosemirror-commands": ["./commands/src/commands.ts"], 23 | "prosemirror-history": ["./history/src/history.ts"], 24 | "prosemirror-dropcursor": ["./dropcursor/src/dropcursor.ts"], 25 | "prosemirror-inputrules": ["./inputrules/src/index.ts"], 26 | "prosemirror-keymap": ["./keymap/src/keymap.ts"], 27 | "prosemirror-search": ["./search/src/search.ts"], 28 | "prosemirror-changeset": ["./changeset/src/changeset.ts"], 29 | "prosemirror-markdown": ["./markdown/src/markdown.ts"], 30 | "prosemirror-collab": ["./collab/src/collab.ts"], 31 | "prosemirror-menu": ["./menu/src/index.ts"], 32 | "prosemirror-gapcursor": ["./gapcursor/src/index.ts"], 33 | "prosemirror-example-setup": ["./example-setup/src/index.ts"] 34 | } 35 | }, 36 | "include": ["*/src/*.ts", "*/test/*.ts", "demo/demo.ts"] 37 | } 38 | --------------------------------------------------------------------------------