gzkx-editor 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE.md +9 -0
- package/ARCHITECTURE.md +1011 -0
- package/CONTRIBUTING.md +104 -0
- package/LICENSE +19 -0
- package/PUBLISH.md +286 -0
- package/README.md +80 -0
- package/STARTUP.md +535 -0
- package/USAGE.md +789 -0
- package/assets/prosemirror_dark.svg +1 -0
- package/assets/prosemirror_light.svg +1 -0
- package/bin/pm +1 -0
- package/bin/pm.js +384 -0
- package/core/build.mjs +95 -0
- package/core/core-entry.mjs +19 -0
- package/core/dist/index.cjs +20951 -0
- package/core/dist/index.js +20717 -0
- package/core/package.json +24 -0
- package/core/src/index.ts +21 -0
- package/core/tsconfig.json +35 -0
- package/demo/bench/example.js +75 -0
- package/demo/bench/index.html +11 -0
- package/demo/bench/index.js +67 -0
- package/demo/bench/mutate.js +14 -0
- package/demo/bench/type.js +40 -0
- package/demo/demo.css +33 -0
- package/demo/demo.ts +16 -0
- package/demo/example-setup/style/style.css +83 -0
- package/demo/gapcursor/style/gapcursor.css +25 -0
- package/demo/img.png +0 -0
- package/demo/index.html +75 -0
- package/demo/menu/style/menu.css +169 -0
- package/demo/test/mocha.css +1 -0
- package/demo/test/mocha.js +1 -0
- package/demo/view/style/prosemirror.css +54 -0
- package/package.json +36 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gzkx-editor/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GZKX Editor — all modules in one bundle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"orderedmap": "^2.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node build.mjs"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Re-export everything from all workspace packages
|
|
2
|
+
// The build script maps prosemirror-* to pre-compiled dist-src/*.js files
|
|
3
|
+
export * from "prosemirror-model"
|
|
4
|
+
export * from "prosemirror-transform"
|
|
5
|
+
export * from "prosemirror-state"
|
|
6
|
+
export * from "prosemirror-view"
|
|
7
|
+
export * from "prosemirror-commands"
|
|
8
|
+
export * from "prosemirror-keymap"
|
|
9
|
+
export * from "prosemirror-inputrules"
|
|
10
|
+
export * from "prosemirror-history"
|
|
11
|
+
export * from "prosemirror-dropcursor"
|
|
12
|
+
export * from "prosemirror-gapcursor"
|
|
13
|
+
export * from "prosemirror-schema-basic"
|
|
14
|
+
export * from "prosemirror-schema-list"
|
|
15
|
+
export * from "prosemirror-menu"
|
|
16
|
+
export * from "prosemirror-collab"
|
|
17
|
+
export * from "prosemirror-example-setup"
|
|
18
|
+
export * from "prosemirror-markdown"
|
|
19
|
+
export * from "prosemirror-search"
|
|
20
|
+
export * from "prosemirror-changeset"
|
|
21
|
+
export * from "prosemirror-test-builder"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": false,
|
|
10
|
+
"declaration": false,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"paths": {
|
|
13
|
+
"prosemirror-model": ["./src/model-shim.ts"],
|
|
14
|
+
"prosemirror-transform": ["./src/transform-shim.ts"],
|
|
15
|
+
"prosemirror-state": ["./src/state-shim.ts"],
|
|
16
|
+
"prosemirror-view": ["./src/view-shim.ts"],
|
|
17
|
+
"prosemirror-commands": ["./src/commands-shim.ts"],
|
|
18
|
+
"prosemirror-keymap": ["./src/keymap-shim.ts"],
|
|
19
|
+
"prosemirror-inputrules": ["./src/inputrules-shim.ts"],
|
|
20
|
+
"prosemirror-history": ["./src/history-shim.ts"],
|
|
21
|
+
"prosemirror-dropcursor": ["./src/dropcursor-shim.ts"],
|
|
22
|
+
"prosemirror-gapcursor": ["./src/gapcursor-shim.ts"],
|
|
23
|
+
"prosemirror-schema-basic": ["./src/schema-basic-shim.ts"],
|
|
24
|
+
"prosemirror-schema-list": ["./src/schema-list-shim.ts"],
|
|
25
|
+
"prosemirror-menu": ["./src/menu-shim.ts"],
|
|
26
|
+
"prosemirror-collab": ["./src/collab-shim.ts"],
|
|
27
|
+
"prosemirror-example-setup": ["./src/example-setup-shim.ts"],
|
|
28
|
+
"prosemirror-markdown": ["./src/markdown-shim.ts"],
|
|
29
|
+
"prosemirror-search": ["./src/search-shim.ts"],
|
|
30
|
+
"prosemirror-changeset": ["./src/changeset-shim.ts"],
|
|
31
|
+
"prosemirror-test-builder": ["./src/test-builder-shim.ts"]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"include": ["src/**/*.ts"]
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset=utf8>
|
|
3
|
+
<title>ProseMirror benchmarks</title>
|
|
4
|
+
|
|
5
|
+
<div id="buttons"></div>
|
|
6
|
+
|
|
7
|
+
<p><label><input type=checkbox id=profile> Profile</label></p>
|
|
8
|
+
|
|
9
|
+
<div id="workspace" style="height: 0; overflow: hidden"></div>
|
|
10
|
+
|
|
11
|
+
<script src="/moduleserve/load.js" data-module="./index" data-require></script>
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}))
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
package/demo/demo.css
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
package/demo/demo.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Add space around the hr to make clicking it easier */
|
|
2
|
+
|
|
3
|
+
.ProseMirror-example-setup-style hr {
|
|
4
|
+
padding: 2px 10px;
|
|
5
|
+
border: none;
|
|
6
|
+
margin: 1em 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ProseMirror-example-setup-style hr:after {
|
|
10
|
+
content: "";
|
|
11
|
+
display: block;
|
|
12
|
+
height: 1px;
|
|
13
|
+
background-color: silver;
|
|
14
|
+
line-height: 2px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ProseMirror ul, .ProseMirror ol {
|
|
18
|
+
padding-left: 30px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.ProseMirror blockquote {
|
|
22
|
+
padding-left: 1em;
|
|
23
|
+
border-left: 3px solid #eee;
|
|
24
|
+
margin-left: 0; margin-right: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ProseMirror-example-setup-style img {
|
|
28
|
+
cursor: default;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ProseMirror-prompt {
|
|
32
|
+
background: white;
|
|
33
|
+
padding: 5px 10px 5px 15px;
|
|
34
|
+
border: 1px solid silver;
|
|
35
|
+
position: fixed;
|
|
36
|
+
border-radius: 3px;
|
|
37
|
+
z-index: 11;
|
|
38
|
+
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ProseMirror-prompt h5 {
|
|
42
|
+
margin: 0;
|
|
43
|
+
font-weight: normal;
|
|
44
|
+
font-size: 100%;
|
|
45
|
+
color: #444;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ProseMirror-prompt input[type="text"],
|
|
49
|
+
.ProseMirror-prompt textarea {
|
|
50
|
+
background: #eee;
|
|
51
|
+
border: none;
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ProseMirror-prompt input[type="text"] {
|
|
56
|
+
padding: 0 4px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ProseMirror-prompt-close {
|
|
60
|
+
position: absolute;
|
|
61
|
+
left: 2px; top: 1px;
|
|
62
|
+
color: #666;
|
|
63
|
+
border: none; background: transparent; padding: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ProseMirror-prompt-close:after {
|
|
67
|
+
content: "✕";
|
|
68
|
+
font-size: 12px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ProseMirror-invalid {
|
|
72
|
+
background: #ffc;
|
|
73
|
+
border: 1px solid #cc7;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
padding: 5px 10px;
|
|
76
|
+
position: absolute;
|
|
77
|
+
min-width: 10em;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ProseMirror-prompt-buttons {
|
|
81
|
+
margin-top: 5px;
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.ProseMirror-gapcursor {
|
|
2
|
+
display: none;
|
|
3
|
+
pointer-events: none;
|
|
4
|
+
position: absolute;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.ProseMirror-gapcursor:after {
|
|
8
|
+
content: "";
|
|
9
|
+
display: block;
|
|
10
|
+
position: absolute;
|
|
11
|
+
top: -2px;
|
|
12
|
+
width: 20px;
|
|
13
|
+
border-top: 1px solid black;
|
|
14
|
+
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes ProseMirror-cursor-blink {
|
|
18
|
+
to {
|
|
19
|
+
visibility: hidden;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ProseMirror-focused .ProseMirror-gapcursor {
|
|
24
|
+
display: block;
|
|
25
|
+
}
|
package/demo/img.png
ADDED
|
Binary file
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
|
|
3
|
+
<meta charset="utf-8"/>
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>GZKX Editor demo page</title>
|
|
6
|
+
<link rel=stylesheet href="demo.css">
|
|
7
|
+
<link rel=stylesheet href="view/style/prosemirror.css">
|
|
8
|
+
<link rel=stylesheet href="menu/style/menu.css">
|
|
9
|
+
<link rel=stylesheet href="example-setup/style/style.css">
|
|
10
|
+
<link rel=stylesheet href="gapcursor/style/gapcursor.css">
|
|
11
|
+
|
|
12
|
+
<h1>GZKX Editor demo page</h1>
|
|
13
|
+
|
|
14
|
+
<div class="full"></div>
|
|
15
|
+
|
|
16
|
+
<div id=content style="display: none">
|
|
17
|
+
<h2>Demonstration Text</h2>
|
|
18
|
+
|
|
19
|
+
<p>A GZKX Editor document is based on a schema, which determines the
|
|
20
|
+
kind of elements that may occur in it, and the relation they have to
|
|
21
|
+
each other. This one is based on the basic schema, with lists and
|
|
22
|
+
tables added. It allows the usual <strong>strong</strong>
|
|
23
|
+
and <em>emphasized</em> text, <code>code font</code>,
|
|
24
|
+
and <a href="http://marijnhaverbeke.nl">links</a>. There are also
|
|
25
|
+
images: <img alt="demo picture" src="img.png">.</p>
|
|
26
|
+
|
|
27
|
+
<p>On the block level you can have:</p>
|
|
28
|
+
|
|
29
|
+
<ol>
|
|
30
|
+
<li>Ordered lists (such as this one)</li>
|
|
31
|
+
<li>Bullet lists</li>
|
|
32
|
+
<li>Blockquotes</li>
|
|
33
|
+
<li>Code blocks</li>
|
|
34
|
+
<li>Tables</li>
|
|
35
|
+
<li>Horizontal rules</li>
|
|
36
|
+
</ol>
|
|
37
|
+
|
|
38
|
+
<p>It isn't hard to define your own custom elements, and include them
|
|
39
|
+
in your schema. These can be opaque 'leaf' nodes, that the user
|
|
40
|
+
manipulates through extra interfaces you provide, or nodes with
|
|
41
|
+
regular editable child nodes.</p>
|
|
42
|
+
|
|
43
|
+
<hr>
|
|
44
|
+
|
|
45
|
+
<h2>The Model</h2>
|
|
46
|
+
|
|
47
|
+
<p>Nodes can nest arbitrarily deep. Thus, the document forms a tree,
|
|
48
|
+
not dissimilar to the browser's DOM tree.</p>
|
|
49
|
+
|
|
50
|
+
<p>At the inline level, the model works differently. Each block of
|
|
51
|
+
text is a single node containing a flat series of inline elements.
|
|
52
|
+
These are serialized as a tree structure when outputting HTML.</p>
|
|
53
|
+
|
|
54
|
+
<p>Positions in the document are represented as a path (an array of
|
|
55
|
+
offsets) through the block tree, and then an offset into the inline
|
|
56
|
+
content of the block. Blocks that have no inline content (such as
|
|
57
|
+
horizontal rules and HTML blocks) can not have the cursor inside of
|
|
58
|
+
them. User-exposed operations on the document preserve the invariant
|
|
59
|
+
that there is always at least a single valid cursor position.</p>
|
|
60
|
+
|
|
61
|
+
<hr>
|
|
62
|
+
|
|
63
|
+
<h2>Examples</h2>
|
|
64
|
+
|
|
65
|
+
<blockquote><blockquote><p>We did not see a nested blockquote
|
|
66
|
+
yet.</p></blockquote></blockquote>
|
|
67
|
+
|
|
68
|
+
<pre><code class="lang-markdown">Nor did we see a code block
|
|
69
|
+
|
|
70
|
+
Note that the content of a code block can't be styled.</code></pre>
|
|
71
|
+
|
|
72
|
+
<p>This paragraph has<br>a hard break inside of it.</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script type=module src="_m/demo.js"></script>
|