That’s an interesting issue actually. Most of the movement methods work with three classes of positions: “raw” (not explicitly mentioned in the code, but basically any node/offset), “ok for movement” (a place where the cursor is allowed to be) and “ok for insertion” (a place where it’s ok to insert text). This is implemented in position.ts. The algorithm for determining what is and isn’t a valid position for insertion of movement is crazily complicated, but it works. A while ago I came across an excellent article which describes a better way to do it, by one of the guys that wrote the blog editor for Medium:
https://medium.com/medium-eng/why-contenteditable-is-terrible-122d8a40e480
Much of the complexity regarding paragraphs in our editor comes from the fact that in HTML you can potentially have text that resides *between* paragraph elements. In rendering terms, any sequence of inline elements (e.g. text, spans) that exists between two block elements (p, div, h1 etc) is effectively treated as a block - I think the proper term is an “anonymous block box”. See
https://www.w3.org/TR/CSS2/visuren.html for all the gory details.
The editor code tries to keep everything in block-level elements, with a strong preference for <p>. So if you try to insert text between two <p> elements, or at the very start or end positions of the document (that is, node = document.body, offset = 0 or document.body.childNodes.length), it will actually search forwards or backwards for the nearest block-level element and do the insertion there. If there is any inline content that precedes (respectively, follows) the block element, it will wrap that in a paragraph. There are several instances in which the editor tries to force content to be in block elements - have a look at the code in hierarchy.ts, especially ensureInlineNodesInParagraph(), for where it does this.
In another email you mentioned you had several <p> elements inside a <div>. When I wrote the code, I didn’t really take this into consideration, and I think that depending on the circumstances, this may cause problems of the form of it either getting rid of the div, or treating the paragraphs incorrectly. I’ll have to take a look at the code to figure out exactly what it does in this situation and why (it’s been a long time since I touched that part).
I made an implicit assumption when writing much of the code that the documents the editor would deal with would have only a single level of block elements inside the body. This stems in part from the model of Word/OOXML documents, where this is always the case (well, excluding special stuff like frames), and also for the LaTeX’s concept of environments (though I realised just now these can actually be nested). In ODF, if I remember correctly, has a more flexible model regarding nesting compared to OOXML.
I agree that the editor should respect the nesting of block elements where a document already has them. We should try to fix this; I think the solution will likely only need to involve the code in hierarchy.ts and cursor.ts, possibly also formatting.ts.
Regarding the <script> element at the end of the document… I think what we need to do here is remove the element during initialisation, specifically in Main.init(). Originally when i wrote the code the scripts were always injected through “out of band” mechanisms like eval() that didn’t involve adding <script> elements to the DOM as we now do with require.js. So I think simply removing the script element as part of initialisation will solve the problem. I suggest adding a function called something like removeScripts() and call it from Main.init() to deal with this. In the particular case where we’re using require.js, it should be sufficient to just check the last element in the body.
(fingerprint 5435 6718 59F0 DD1F BFA0 5E46 2523 BAA1 44AE 2966)