prosemirror 0.5.2

A Rust implementation of ProseMirror's document model and transform pipeline
Documentation
#![recursion_limit = "512"]
//! Regression test for a replaceAround step that was failing on a real-world
//! document using the "Standard Article" schema.
//!
//! The step changes the `hidden` attribute of the abstract `richtext_part`
//! from `true` to `false` by wrapping the existing content in a new node
//! with updated attributes.

use prosemirror::dynamic::types::Dyn;
use prosemirror::dynamic::DynamicSchema;
use prosemirror::transform::Step;

fn article_schema() -> DynamicSchema {
    DynamicSchema::from_json(&serde_json::json!({
        "nodes": {
            "doc": {"content":"title part*","selectable":false,"allowGapCursor":false,"attrs":{"documentstyle":{"default":""},"tracked":{"default":false},"citationstyle":{"default":"apa"},"citationstyles":{"default":["american-anthropological-association","apa","chicago-author-date","chicago-note-bibliography","harvard-cite-them-right","modern-language-association","nature","oxford-university-press-humsoc"]},"language":{"default":"en-US"},"languages":{"default":["af-ZA","sq-AL","ar","ast","be","br","bg","ca","ca-ES-Valencia","zh-CN","da","nl","en-AU","en-CA","en-NZ","en-ZA","en-GB","en-US","eo","fr","gl","de-DE","de-AU","de-CH","el","he","is","it","ja","km","lt","ml","nb-NO","nn-NO","fa","pl","pt-BR","pt-PT","ro","ru","tr","sr-SP-Cy","sr-SP-Lt","sk","sl","es","sv","ta","tl","uk"]},"papersize":{"default":"A4"},"papersizes":{"default":["A4","US Letter"]},"footnote_marks":{"default":["strong","em","link"]},"footnote_elements":{"default":["paragraph","heading1","heading2","heading3","heading4","heading5","heading6","figure","ordered_list","bullet_list","horizontal_rule","equation","citation","cross_reference","blockquote","table"]},"bibliography_header":{"default":{}},"template":{"default":""},"import_id":{"default":""},"copyright":{"default":{"holder":false,"year":false,"freeToRead":true,"licenses":[]}},"code_categories":{"default":{"listing":{"counter":0,"enabled":true},"example":{"counter":0,"enabled":true},"snippet":{"counter":0,"enabled":false},"tutorial":{"counter":0,"enabled":false},"exercise":{"counter":0,"enabled":false},"exercise_solution":{"counter":0,"enabled":false}}},"code_languages":{"default":["javascript","python","java","cpp","c","csharp","php","ruby","go","rust","swift","kotlin","typescript","html","css","sql","bash","shell","r","matlab","scala","perl","lua","haskell","xml","json","yaml","markdown"]},"id_types":{"default":[]}},"parseDOM":[{"tag":"div.doc"}]},
            "richtext_part": {"content":"block+","group":"part","marks":"annotation track","isolating":true,"attrs":{"title":{"default":""},"id":{"default":""},"locking":{"default":false},"language":{"default":false},"optional":{"default":false},"hidden":{"default":false},"help":{"default":false},"initial":{"default":false},"deleted":{"default":false},"elements":{"default":["paragraph","heading1","heading2","heading3","heading4","heading5","heading6","code_block","figure","ordered_list","bullet_list","horizontal_rule","equation","citation","cross_reference","blockquote","footnote","table"]},"marks":{"default":["strong","em","link"]},"metadata":{"default":false}},"parseDOM":[{"tag":"div.doc-richtext"}]},
            "heading_part": {"content":"heading","group":"part","marks":"annotation track","isolating":true,"attrs":{"title":{"default":""},"id":{"default":""},"locking":{"default":false},"language":{"default":false},"optional":{"default":false},"hidden":{"default":false},"help":{"default":false},"initial":{"default":false},"deleted":{"default":false},"elements":{"default":["heading1"]},"marks":{"default":["strong","em","link","anchor","sup","sub","code"]},"metadata":{"default":false}},"parseDOM":[{"tag":"div.doc-heading"}]},
            "contributors_part": {"content":"contributor*","group":"part","marks":"annotation track","isolating":true,"attrs":{"title":{"default":""},"id":{"default":""},"locking":{"default":false},"language":{"default":false},"optional":{"default":false},"hidden":{"default":false},"help":{"default":false},"initial":{"default":false},"deleted":{"default":false},"item_title":{"default":"Contributor"},"metadata":{"default":false}},"parseDOM":[{"tag":"div.doc-contributors"}]},
            "tags_part": {"content":"tag*","group":"part","marks":"annotation track","isolating":true,"attrs":{"title":{"default":""},"id":{"default":""},"locking":{"default":false},"language":{"default":false},"optional":{"default":false},"hidden":{"default":false},"help":{"default":false},"initial":{"default":false},"deleted":{"default":false},"item_title":{"default":"Tag"},"metadata":{"default":false}},"parseDOM":[{"tag":"div.doc-tags"}]},
            "table_part": {"content":"table","group":"part","marks":"annotation track","isolating":true,"attrs":{"title":{"default":""},"id":{"default":""},"locking":{"default":false},"language":{"default":false},"optional":{"default":false},"hidden":{"default":false},"help":{"default":false},"initial":{"default":false},"deleted":{"default":false},"elements":{"default":["paragraph","heading1","heading2","heading3","heading4","heading5","heading6","code_block","figure","ordered_list","bullet_list","horizontal_rule","equation","citation","blockquote","footnote"]},"marks":{"default":["strong","em","link","anchor","sup","sub","code"]}},"parseDOM":[{"tag":"div.doc-table"}]},
            "table_of_contents": {"group":"part","marks":"annotation track","defining":true,"parseDOM":[{"tag":"div.table-of-contents"}],"attrs":{"title":{"default":"Table of Contents"},"id":{"default":"toc"},"optional":{"default":false},"hidden":{"default":false}}},
            "separator_part": {"marks":"annotation track","group":"part","defining":true,"attrs":{"id":{"default":"separator"}},"parseDOM":[{"tag":"hr.doc-separator_part"}]},
            "title": {"content":"text*","marks":"annotation track","group":"fixedpart","defining":true,"isolating":true,"attrs":{"id":{"default":"title"}},"parseDOM":[{"tag":"div.doc-title"}]},
            "contributor": {"inline":true,"draggable":true,"attrs":{"firstname":{"default":false},"lastname":{"default":false},"email":{"default":false},"institution":{"default":false},"id_type":{"default":false},"id_value":{"default":false}},"parseDOM":[{"tag":"span.contributor"}]},
            "tag": {"inline":true,"draggable":true,"attrs":{"tag":{"default":""}},"parseDOM":[{"tag":"span.tag"}]},
            "paragraph": {"group":"block","content":"inline*","attrs":{"track":{"default":[]}},"parseDOM":[{"tag":"p"}]},
            "blockquote": {"content":"block+","group":"block","attrs":{"track":{"default":[]}},"marks":"annotation","defining":true,"parseDOM":[{"tag":"blockquote"}]},
            "horizontal_rule": {"group":"block","attrs":{"track":{"default":[]}},"parseDOM":[{"tag":"hr"}]},
            "figure": {"inline":false,"allowGapCursor":false,"selectable":true,"group":"block","attrs":{"category":{"default":"none"},"caption":{"default":false},"id":{"default":false},"track":{"default":[]},"aligned":{"default":"center"},"width":{"default":"100"}},"content":"figure_caption image|figure_caption figure_equation|image figure_caption|figure_equation figure_caption","parseDOM":[{"tag":"figure"}]},
            "image": {"selectable":false,"draggable":false,"attrs":{"image":{"default":false}},"parseDOM":[{"tag":"img"}]},
            "figure_equation": {"selectable":false,"draggable":false,"attrs":{"equation":{"default":false}},"parseDOM":[{"tag":"div.figure-equation[data-equation]"}]},
            "figure_caption": {"isolating":true,"defining":true,"content":"inline*","parseDOM":[{"tag":"figcaption span.text"}]},
            "heading1": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h1"}]},
            "heading2": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h2"}]},
            "heading3": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h3"}]},
            "heading4": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h4"}]},
            "heading5": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h5"}]},
            "heading6": {"group":"block heading","content":"inline*","marks":"_","defining":true,"attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"h6"}]},
            "code_block": {"content":"text*","marks":"_","group":"block","code":true,"defining":true,"attrs":{"track":{"default":[]},"language":{"default":""},"category":{"default":""},"title":{"default":""},"id":{"default":""}},"parseDOM":[{"tag":"pre","preserveWhitespace":"full"}]},
            "text": {"group":"inline"},
            "hard_break": {"inline":true,"group":"inline","selectable":false,"parseDOM":[{"tag":"br"}]},
            "citation": {"inline":true,"group":"inline","attrs":{"format":{"default":"autocite"},"references":{"default":[]}},"parseDOM":[{"tag":"span.citation"}]},
            "equation": {"inline":true,"group":"inline","attrs":{"equation":{"default":""}},"parseDOM":[{"tag":"span.equation"}]},
            "cross_reference": {"inline":true,"group":"inline","attrs":{"id":{"default":false},"title":{"default":null}},"parseDOM":[{"tag":"span.cross-reference[data-id][data-title]"}]},
            "footnote": {"inline":true,"group":"inline","attrs":{"footnote":{"default":[{"type":"paragraph"}]}},"parseDOM":[{"tag":"span.footnote-marker[data-footnote]"}]},
            "ordered_list": {"group":"block","content":"list_item+","attrs":{"id":{"default":false},"order":{"default":1},"track":{"default":[]}},"parseDOM":[{"tag":"ol"}]},
            "bullet_list": {"group":"block","content":"list_item+","attrs":{"id":{"default":false},"track":{"default":[]}},"parseDOM":[{"tag":"ul"}]},
            "list_item": {"content":"block+","marks":"annotation","attrs":{"track":{"default":[]}},"parseDOM":[{"tag":"li"}],"defining":true},
            "table": {"inline":false,"group":"block","tableRole":"table","attrs":{"id":{"default":false},"track":{"default":[]},"width":{"default":"100"},"aligned":{"default":"center"},"layout":{"default":"fixed"},"category":{"default":"none"},"caption":{"default":false}},"content":"table_caption table_body","parseDOM":[{"tag":"table"}]},
            "table_caption": {"content":"inline*","parseDOM":[{"tag":"caption span.text"}]},
            "table_body": {"content":"table_row+","tableRole":"table","isolating":true,"parseDOM":[{"tag":"tbody"}]},
            "table_row": {"content":"(table_cell | table_header)+","tableRole":"row","parseDOM":[{"tag":"tr"}]},
            "table_cell": {"marks":"annotation","content":"block+","attrs":{"colspan":{"default":1},"rowspan":{"default":1},"colwidth":{"default":null}},"tableRole":"cell","isolating":true,"parseDOM":[{"tag":"td"}]},
            "table_header": {"marks":"annotation","content":"block+","attrs":{"colspan":{"default":1},"rowspan":{"default":1},"colwidth":{"default":null}},"tableRole":"header_cell","isolating":true,"parseDOM":[{"tag":"th"}]}
        },
        "marks": {
            "em": {"parseDOM":[{"tag":"i"},{"tag":"em"},{"style":"font-style=italic"},{"style":"font-style=normal"}]},
            "strong": {"parseDOM":[{"tag":"strong"},{"tag":"b"},{"style":"font-weight=400"},{"style":"font-weight"}]},
            "link": {"attrs":{"href":{},"title":{"default":""}},"inclusive":false,"parseDOM":[{"tag":"a[href]"}]},
            "underline": {"parseDOM":[{"tag":"span.underline"}]},
            "sup": {"parseDOM":[{"tag":"sup"}],"excludes":"sub"},
            "sub": {"parseDOM":[{"tag":"sub"}],"excludes":"sup"},
            "code": {"parseDOM":[{"tag":"code"}],"excludes":"strong em underline link sup sub"},
            "comment": {"attrs":{"id":{"default":false}},"inclusive":false,"excludes":"","group":"annotation","parseDOM":[{"tag":"span.comment[data-id]"}]},
            "annotation_tag": {"attrs":{"type":{"default":""},"key":{"default":""},"value":{"default":""}},"inclusive":false,"excludes":"","group":"annotation","parseDOM":[{"tag":"span.annotation-tag[data-type]"}]},
            "anchor": {"attrs":{"id":{"default":false}},"inclusive":false,"group":"annotation","parseDOM":[{"tag":"span.anchor[data-id]"}]},
            "deletion": {"attrs":{"user":{"default":0},"username":{"default":""},"date":{"default":0}},"inclusive":false,"group":"track","parseDOM":[{"tag":"span.deletion"}]},
            "insertion": {"attrs":{"user":{"default":0},"username":{"default":""},"date":{"default":0},"approved":{"default":true}},"inclusive":false,"group":"track","parseDOM":[{"tag":"span.insertion"},{"tag":"span.approved-insertion"}]},
            "format_change": {"attrs":{"user":{"default":0},"username":{"default":""},"date":{"default":0},"before":{"default":[]},"after":{"default":[]}},"inclusive":false,"group":"track","parseDOM":[{"tag":"span.format-change"}]}
        }
    })).unwrap()
}

/// The document JSON from the failing example. The abstract `richtext_part`
/// (at positions 12..16) has `hidden: true` and contains a single empty
/// paragraph.
fn article_doc_json() -> serde_json::Value {
    serde_json::json!({
        "attrs":{"bibliography_header":{},"citationstyle":"apa","citationstyles":["american-anthropological-association","apa","chicago-author-date","chicago-note-bibliography","harvard-cite-them-right","modern-language-association","nature","oxford-university-press-humsoc"],"code_categories":{"example":{"counter":0,"enabled":true},"exercise":{"counter":0,"enabled":false},"exercise_solution":{"counter":0,"enabled":false},"listing":{"counter":0,"enabled":true},"snippet":{"counter":0,"enabled":false},"tutorial":{"counter":0,"enabled":false}},"code_languages":["javascript","python","java","cpp","c","csharp","php","ruby","go","rust","swift","kotlin","typescript","html","css","sql","bash","shell","r","matlab","scala","perl","lua","haskell","xml","json","yaml","markdown"],"copyright":{"freeToRead":true,"holder":false,"licenses":[],"year":false},"documentstyle":"elephant","footnote_elements":["paragraph","heading1","heading2","heading3","heading4","heading5","heading6","figure","ordered_list","bullet_list","horizontal_rule","equation","citation","cross_reference","blockquote","table"],"footnote_marks":["strong","em","link"],"id_types":[],"import_id":"standard-article","language":"en-US","languages":["af-ZA","sq-AL","ar","ast","be","br","bg","ca","ca-ES-Valencia","zh-CN","da","nl","en-AU","en-CA","en-NZ","en-ZA","en-GB","en-US","eo","fr","gl","de-DE","de-AU","de-CH","el","he","is","it","ja","km","lt","ml","nb-NO","nn-NO","fa","pl","pt-BR","pt-PT","ro","ru","tr","sr-SP-Cy","sr-SP-Lt","sk","sl","es","sv","ta","tl","uk"],"papersize":"A4","papersizes":["A4","US Letter"],"template":"Standard Article","tracked":false},
        "content":[
            {
                "content":[{"marks":[{"attrs":{"approved":true,"date":29655180,"user":1,"username":"Yeti"},"type":"insertion"}],"text":"Test","type":"text"}],
                "type":"title"
            },
            {
                "attrs":{"hidden":true,"id":"subtitle","initial":[{"attrs":{"id":"H6323428"},"type":"heading1"}],"marks":["strong","em","link"],"metadata":"subtitle","optional":"hidden","title":"Subtitle"},
                "content":[{"attrs":{"id":"H6323428"},"type":"heading1"}],
                "type":"heading_part"
            },
            {
                "attrs":{"hidden":true,"id":"authors","item_title":"Author","metadata":"authors","optional":"hidden","title":"Authors"},
                "type":"contributors_part"
            },
            {
                "attrs":{"hidden":true,"id":"abstract","marks":["strong","em","link"],"metadata":"abstract","optional":"hidden","title":"Abstract"},
                "content":[{"type":"paragraph"}],
                "type":"richtext_part"
            },
            {
                "attrs":{"hidden":true,"id":"keywords","item_title":"Keyword","metadata":"keywords","optional":"hidden","title":"Keywords"},
                "type":"tags_part"
            },
            {
                "attrs":{"id":"body","marks":["strong","em","link"],"title":"Body"},
                "content":[{"type":"paragraph"}],
                "type":"richtext_part"
            }
        ],
        "type":"doc"
    })
}

/// The replaceAround step that changes `hidden: true` -> `hidden: false` on
/// the abstract part by wrapping the existing paragraph in a new richtext_part
/// with updated attributes.
///
/// Positions in this document:
/// - title:              [0..6]   (node_size = 2 + 4 chars)
/// - heading_part:       [6..10]  (node_size = 2 + 2)
/// - contributors_part:  [10..12] (node_size = 2, no content)
/// - richtext_part (abstract): [12..16] (node_size = 2 + 2)
///   - paragraph:        [13..15] (node_size = 2, empty)
/// - tags_part:          [16..18] (node_size = 2)
/// - richtext_part (body): [18..22] (node_size = 2 + 2)
fn replace_around_step_json() -> serde_json::Value {
    serde_json::json!({
        "stepType": "replaceAround",
        "from": 12,
        "to": 16,
        "gapFrom": 13,
        "gapTo": 15,
        "insert": 1,
        "slice": {
            "content": [{
                "type": "richtext_part",
                "attrs": {
                    "title": "Abstract",
                    "id": "abstract",
                    "locking": false,
                    "language": false,
                    "optional": "hidden",
                    "hidden": false,
                    "help": false,
                    "initial": false,
                    "deleted": false,
                    "elements": ["paragraph","heading1","heading2","heading3","heading4","heading5","heading6","code_block","figure","ordered_list","bullet_list","horizontal_rule","equation","citation","cross_reference","blockquote","footnote","table"],
                    "marks": ["strong","em","link"],
                    "metadata": "abstract"
                }
            }]
        },
        "structure": true
    })
}

#[test]
fn replace_around_unhides_abstract_part() {
    let schema = article_schema();

    schema.with_types(|| {
        let doc = schema
            .node_from_json(&article_doc_json())
            .expect("document should parse");

        // Verify our assumed position layout before applying the step.
        // The abstract richtext_part is the 4th child (index 3) of the doc.
        use prosemirror::model::Node;
        let abstract_part = doc.child(3).expect("doc should have 4th child");
        assert_eq!(abstract_part.type_name, "richtext_part");
        let abstract_attrs = abstract_part.attrs_json();
        assert_eq!(
            abstract_attrs["hidden"], true,
            "abstract part should start hidden"
        );

        let step: Step<Dyn> =
            serde_json::from_value(replace_around_step_json()).expect("step should deserialize");

        let result = step.apply(&doc);

        let new_doc = result.expect("replaceAround step should succeed");

        // The abstract part should now have hidden: false.
        let new_abstract = new_doc.child(3).expect("doc should still have 4th child");
        assert_eq!(new_abstract.type_name, "richtext_part");
        let new_attrs = new_abstract.attrs_json();
        assert_eq!(
            new_attrs["hidden"], false,
            "abstract part should be unhidden after the step"
        );

        // The paragraph inside should be preserved.
        assert_eq!(new_abstract.child_count(), 1);
        let inner = new_abstract
            .child(0)
            .expect("abstract should still have a paragraph");
        assert_eq!(inner.type_name, "paragraph");
    });
}