diff options
-rw-r--r-- | editor/editor.css | 36 | ||||
-rwxr-xr-x | editor/editor.ml | 102 | ||||
-rwxr-xr-x | editor/prosemirror/bindings.ml | 262 | ||||
-rwxr-xr-x | editor/prosemirror/prosemirror.ml | 116 | ||||
-rwxr-xr-x | editor/prosemirror/prosemirror.mli | 56 |
5 files changed, 519 insertions, 53 deletions
diff --git a/editor/editor.css b/editor/editor.css index 8f29bde..fb58773 100644 --- a/editor/editor.css +++ b/editor/editor.css @@ -324,3 +324,39 @@ li.ProseMirror-selectednode:after { } .ProseMirror p { margin-bottom: 1em } + +.editor em::before, .editor em::after { + content: "//" +} + +.editor blockquote p::before { + content: "> " +} + +.editor strong::before, .editor strong::after { + content: "**" +} + +.editor h1::before { + content: "# " +} + +.editor h2::before { + content: "## " +} + +.editor h3::before { + content: "### " +} + +.editor h4::before { + content: "#### " +} + +.editor h5::before { + content: "##### " +} + +.editor h6::before { + content: "###### " +} diff --git a/editor/editor.ml b/editor/editor.ml index c32a5ba..64fb723 100755 --- a/editor/editor.ml +++ b/editor/editor.ml @@ -1,5 +1,96 @@ open Js_of_ocaml open Brr +module PM = Prosemirror + +let change_level + : PM.t -> PM.Model.resolved_pos Js.t -> PM.State.editor_state Js.t -> int -> (PM.State.transaction Js.t -> unit) -> (int -> bool) -> bool + = fun pm res state incr dispatch pred -> + let parent = res##.parent in + let attributes = parent##.attrs in + + (* There are some problems to update the view when the content is empty. + It looks like the comparaison does not comprae the level argument. + In order to prevent any error, juste return in such situation. + *) + let empty_content = parent##.content##eq (PM.Model.empty_fragment pm) + and level = attributes##.level in + if (pred level || empty_content) then false + else + (* Look the position for the previous element *) + let resolved = (res##.doc)##resolve (res##.pos -1) in + let selection = PM.State.node_selection pm resolved in + + let props = object%js + val level = level + incr + end in + + let element = parent##copy (PM.Model.empty_fragment pm) in + element##.attrs := props; + element##.content := parent##.content; + + (* Create a new transaction for replacing the selection *) + let tr = state##.tr in + let tr = tr##replaceRangeWith + selection##.from + selection##._to + element in + + (* Return at the initial position *) + let position = PM.State.create_text_selection + pm + tr##.doc + res##.pos in + let tr = tr##setSelection position in + dispatch (tr##scrollIntoView ()); + true + +let handle_backspace pm state dispatch = + + (* Get the currrent node *) + let res = PM.State.selection_to (state##.selection) in + + match Js.Opt.to_option res##.nodeBefore with + | Some _ -> false + | None -> (* Line start *) + let parent = res##.parent in + begin match Jstr.to_string parent##._type##.name with + | "heading" -> change_level pm res state (-1) dispatch (fun x -> x <= 1) + | _ -> false + end + + +(** Increase the title level by one when pressing # at the begining of a line *) +let handle_sharp pm state dispatch = + (* Get the currrent node *) + let res = PM.State.selection_to (state##.selection) in + + match Js.Opt.to_option res##.nodeBefore with + | Some _ -> false + | None -> (* Line start *) + let parent = res##.parent in + begin match Jstr.to_string parent##._type##.name with + | "heading" -> change_level pm res state (+1) dispatch (fun x -> x > 5) + | _ -> false + end + + +let default_plugins pm schema = + + let props = PM.Example.options schema in + props##.menuBar := Js.some false; + props##.floatingMenu := Js.some false; + let setup = PM.Example.example_setup pm props in + + let keymaps = + PM.Keymap.keymap pm + [| "Backspace", (handle_backspace pm) + ; "#", (handle_sharp pm) + |] in + + (* Add the custom keymaps in the list *) + let _ = setup##unshift keymaps in + + Js.some setup let create_new_state pm mySchema content = let module PM = Prosemirror in @@ -11,7 +102,7 @@ let create_new_state pm mySchema content = let props = PM.State.creation_prop () in props##.doc := Js.some doc; - props##.plugins := Js.some (PM.example_setup pm mySchema); + props##.plugins := default_plugins pm mySchema; PM.State.create pm @@ -50,23 +141,16 @@ let prosemirror id content = Console.(log [Jstr.v "Loading json"]); let history = PM.History.(history pm (history_prop ()) ) in - Console.(log [history]); let _ = history in let obj = PM.State.configuration_prop () in - obj##.plugins := Js.some (PM.example_setup pm mySchema); + obj##.plugins := default_plugins pm mySchema; obj##.schema := Js.some mySchema; PM.State.fromJSON pm obj json end in let props = PM.View.direct_editor_props () in - props##.dispatchTransaction := (Js.wrap_meth_callback (fun view transaction -> - Console.(log [ Jstr.v "Document size went from" - ; transaction##.before##.content##.size ]); - let state = view##.state##apply transaction in - view##updateState state - )); props##.state := state; diff --git a/editor/prosemirror/bindings.ml b/editor/prosemirror/bindings.ml index 08db819..f6d4223 100755 --- a/editor/prosemirror/bindings.ml +++ b/editor/prosemirror/bindings.ml @@ -1,5 +1,47 @@ open Js_of_ocaml.Js +module TypedObject : sig + + type 'a t + + val get + : 'a t -> Jv.prop -> 'a option + + val get' + : 'a t -> Jv.prop' -> 'a option + + val set + : 'a t -> Jv.prop -> 'a -> unit + + val set' + : 'a t -> Jv.prop' -> 'a -> unit + +end = struct + + type 'a t = Jv.t + + let get + : 'a t -> Jv.prop -> 'a + = fun t prop -> + Jv.to_option Jv.Id.of_jv (Jv.get t prop) + + let get' + : 'a t -> Jv.prop' -> 'a + = fun t prop -> + Jv.to_option Jv.Id.of_jv (Jv.get' t prop) + + let set + : 'a t -> Jv.prop -> 'a -> unit + = fun o prop v -> + Jv.set o prop (Jv.Id.to_jv v) + + let set' + : 'a t -> Jv.prop' -> 'a -> unit + = fun o prop v -> + Jv.set' o prop (Jv.Id.to_jv v) + +end + class type ['a] ordered_map = object ('this) method get: @@ -21,63 +63,131 @@ end module Model = struct + type domOutputSpec + + class type _node_props = object ('this) + + method inlineContent: + bool readonly_prop + (** True if this node type has inline content. *) + + method isBlock: + bool readonly_prop + + method isText: + bool readonly_prop + + method isInline: + bool readonly_prop + + method isTextblock: + bool readonly_prop + + method isLeaf: + bool readonly_prop + + method isAtom: + bool readonly_prop + + end + + type mark type node_spec - type mark_spec + type content_match - class type schema_spec = object ('this) + type slice - method nodes: - node_spec ordered_map t readonly_prop + class type resolved_pos = object ('this) + + method pos: + int readonly_prop + + method depth: + int readonly_prop + + method parent: + node t readonly_prop + + method doc: + node t readonly_prop + + method node: + int -> node t meth + + method index: + int -> int meth + + method nodeAfter: + node t opt readonly_prop + + method nodeBefore: + node t opt readonly_prop method marks: - mark_spec ordered_map t readonly_prop + unit -> mark t js_array t meth - method topNode: - Jstr.t opt readonly_prop + method sameParent: + 'this -> bool t meth + method max: + 'this -> 'this t meth + + method min: + 'this -> 'this t meth end - class type schema = object ('this) + and mark_spec = object ('this) - method spec: - schema_spec t prop + method toDOM: + (node t -> domOutputSpec t) callback writeonly_prop + + method inclusive: + bool t prop + + method spanning: + bool t prop end - type content_match + and schema_spec = object ('this) - type slice + method nodes: + node_spec ordered_map t readonly_prop - class type _node_props = object ('this) + method marks: + mark_spec ordered_map t readonly_prop - method inlineContent: - bool readonly_prop - (** True if this node type has inline content. *) + method topNode: + Jstr.t opt readonly_prop - method isBlock: - bool readonly_prop + end - method isText: - bool readonly_prop + and schema = object ('this) - method isInline: - bool readonly_prop + method spec: + schema_spec t prop - method isTextblock: - bool readonly_prop + method nodes: + node_type t TypedObject.t readonly_prop - method isLeaf: - bool readonly_prop + method marks: + mark_type t TypedObject.t readonly_prop - method isAtom: - bool readonly_prop + method typoNodeType: + node_type t readonly_prop + + method text: + Jstr.t -> node t meth + + method node: + Jstr.t -> < .. > t opt -> fragment t opt -> mark t js_array t opt -> node t meth end - class type node_type = object ('this) + and node_type = object ('this) inherit _node_props @@ -96,13 +206,30 @@ module Model = struct method hasRequiredAttrs: unit -> bool meth + method create_withFragment: + < .. > t -> fragment t opt -> mark t opt -> node t meth + end - class type mark_type = object ('this) + (** Signature for MarkType class + + https://prosemirror.net/docs/ref/#model.MarkType + *) + and mark_type = object ('this) + + method name: + Jstr.t readonly_prop + + method schema: + schema t readonly_prop + + method spec: + mark_spec t readonly_prop + end (** Common signature between fragment and node *) - class type _element = object ('this) + and _element = object ('this) method childCount: int readonly_prop @@ -164,16 +291,29 @@ module Model = struct node_type t readonly_prop method attrs: - < .. > t readonly_prop + < .. > t prop method content: - fragment t readonly_prop + fragment t prop + + method copy: + fragment t -> 'this t meth + + method resolve: + int -> resolved_pos t meth + + method nodeAt: + int -> 'this t opt meth method marks: mark t js_array t readonly_prop method sameMarkupd: node t -> bool meth + + method text: + Jstr.t opt prop + end end @@ -219,6 +359,12 @@ module Transform = struct method step: step t -> 'this t meth + method insert: + int -> Model.node t -> 'this t meth + + method replaceRangeWith: + int -> int -> Model.node t -> 'this t meth + end end @@ -244,12 +390,33 @@ module State = struct class type selection = object ('this) + method from: + int readonly_prop + + method _to: + int readonly_prop + method content: unit -> Model.slice t meth method replace: transaction t -> Model.slice t -> unit meth + method replaceWith: + transaction t -> Model.node t -> unit meth + + end + + and text_selection = object ('this) + + inherit selection + + end + + and node_selection = object ('this) + + inherit selection + end and transaction = object ('this) @@ -295,12 +462,19 @@ module State = struct method replaceSelection: Model.slice t -> 'this t meth + method replaceSelectionWith: + Model.node t -> bool t -> 'this t meth + method selectionSet: bool readonly_prop method before: Model.node t readonly_prop + method scrollIntoView : + unit -> 'this t meth + + end class type configuration_prop = object ('this) @@ -398,6 +572,9 @@ module View = struct method updateState: State.editor_state t -> unit meth + method dispatch: + State.transaction t -> unit meth + end end @@ -464,3 +641,22 @@ module History = struct end end + +module Example = struct + + class type options = object ('this) + + method schema: + Model.schema t prop + + method menuBar: + bool opt prop + + method floatingMenu: + bool opt prop + + method history: + bool opt prop + + end +end diff --git a/editor/prosemirror/prosemirror.ml b/editor/prosemirror/prosemirror.ml index c19abe0..e97fa9b 100755 --- a/editor/prosemirror/prosemirror.ml +++ b/editor/prosemirror/prosemirror.ml @@ -8,12 +8,15 @@ let v = fun () -> Jv.get Jv.global "PM" +module O = Bindings.TypedObject + module Model = struct include Bindings.Model module DOMParser = struct + type parser = Jv.t let from_schema @@ -63,6 +66,46 @@ module Model = struct Jv.get fragment "empty" |> Jv.Id.of_jv + module Dom_output_spec = struct + + let v + : ?attrs:< .. > -> string -> domOutputSpec Js.t list -> domOutputSpec Js.t + = fun ?attrs name elems -> + + let elems = match attrs with + | None -> elems + | Some v -> Jv.Id.(of_jv @@ to_jv @@ v)::elems in + + let elems = (Jv.Id.of_jv @@ Jv.of_string name)::elems in + (Jv.of_list Jv.Id.to_jv elems) + |> Jv.Id.to_jv + |> Jv.Id.of_jv + + let hole + : domOutputSpec Js.t + = 0 + |> Jv.Id.to_jv + |> Jv.Id.of_jv + + let of_ + : 'a -> domOutputSpec Js.t + = fun elem -> + elem + |> Jv.Id.to_jv + |> Jv.Id.of_jv + + let of_el + : Brr.El.t -> domOutputSpec Js.t + = of_ + + let of_jstr + : Jstr.t -> domOutputSpec Js.t + = of_ + + let of_obj + : < dom: node Js.t Js.readonly_prop ; contentDOM : node Js.t Js.opt Js.readonly_prop > Js.t -> domOutputSpec Js.t + = of_ + end end module State = struct @@ -92,6 +135,47 @@ module State = struct let editor_state = Jv.get state "EditorState" in Jv.call editor_state "fromJSON" [|Jv.Id.to_jv config ; json |] |> Jv.Id.of_jv + + let selection_to + : selection Js.t -> Model.resolved_pos Js.t + = fun selection -> + Jv.Id.(of_jv @@ Jv.get (to_jv selection) "$to") + + let node_selection + : t -> Model.resolved_pos Js.t -> node_selection Js.t + = fun t pos -> + let state = Jv.get t "state" in + Jv.new' (Jv.get state "NodeSelection") [| Jv.Id.to_jv pos |] + |> Jv.Id.of_jv + + let is_selectable + : t -> Model.node Js.t -> bool Js.t + = fun t node -> + let selection = Jv.get (Jv.get t "state") "NodeSelection" in + Jv.call selection "isSelectable" [|Jv.Id.to_jv node|] + |> Jv.Id.of_jv + + let selection_at_start + : t-> Model.node Js.t -> selection Js.t + = fun t node -> + let selection = Jv.get (Jv.get t "state") "NodeSelection" in + Jv.call selection "atStart" [|Jv.Id.to_jv node|] + |> Jv.Id.of_jv + + + let create_node_selection + : t -> Model.node Js.t -> int -> node_selection Js.t + = fun t doc number -> + let state = Jv.get t "state" in + Jv.call (Jv.get state "NodeSelection") "create" Jv.Id.[|to_jv doc; Jv.of_int number|] + |> Jv.Id.of_jv + + let create_text_selection + : t -> Model.node Js.t -> int -> node_selection Js.t + = fun t doc number -> + let state = Jv.get t "state" in + Jv.call (Jv.get state "TextSelection") "create" Jv.Id.[|to_jv doc; Jv.of_int number|] + |> Jv.Id.of_jv end (* Editor view *) @@ -142,6 +226,12 @@ module SchemaBasic = struct Jv.get (Jv.get t "schema_basic") "schema" |> Jv.Id.of_jv + let nodes + : t -> nodes Js.t + = fun t -> + Jv.get (Jv.get t "schema_basic") "nodes" + |> Jv.Id.of_jv + end module History = struct @@ -174,7 +264,7 @@ end module Keymap = struct let keymap - : t -> (string * (State.editor_state Js.t -> (State.transaction -> unit) -> bool)) array -> State.plugin Js.t + : t -> (string * (State.editor_state Js.t -> (State.transaction Js.t -> unit) -> bool)) array -> State.plugin Js.t = fun t props -> let props = Jv.obj @@ Array.map (fun (id, f) -> (id, Jv.repr f)) props in Jv.call (Jv.get t "keymap") "keymap" [|props|] @@ -194,10 +284,20 @@ end (* Example Setup *) -let example_setup - : t -> Model.schema Js.t -> State.plugin Js.t Js.js_array Js.t - = fun t schema -> - let setup = Jv.get t "example_setup" in - let props = Jv.obj [|("schema", Jv.Id.to_jv schema)|] in - Jv.call setup "exampleSetup" [|props|] - |> Jv.Id.of_jv +module Example = struct + + include Bindings.Example + + let options + : Model.schema Js.t -> options Js.t + = fun schema -> + Jv.obj [|("schema", Jv.Id.to_jv schema)|] + |> Jv.Id.of_jv + + let example_setup + : t -> options Js.t -> State.plugin Js.t Js.js_array Js.t + = fun t options -> + let setup = Jv.get t "example_setup" in + Jv.call setup "exampleSetup" [|Jv.Id.to_jv options|] + |> Jv.Id.of_jv +end diff --git a/editor/prosemirror/prosemirror.mli b/editor/prosemirror/prosemirror.mli index aa27bf4..a4c5d35 100755 --- a/editor/prosemirror/prosemirror.mli +++ b/editor/prosemirror/prosemirror.mli @@ -6,6 +6,8 @@ type t val v : unit -> t +module O = Bindings.TypedObject + module rec Model : sig include module type of Bindings.Model @@ -34,6 +36,24 @@ module rec Model : sig val empty_fragment : t -> fragment Js.t + module Dom_output_spec : sig + + val v + : ?attrs:< .. > -> string -> domOutputSpec Js.t list -> domOutputSpec Js.t + + (** Hole element inside an output_spec element *) + val hole + : domOutputSpec Js.t + + val of_el + : Brr.El.t -> domOutputSpec Js.t + + val of_jstr + : Jstr.t -> domOutputSpec Js.t + + val of_obj + : < dom: node Js.t Js.readonly_prop ; contentDOM : node Js.t Js.opt Js.readonly_prop > Js.t -> domOutputSpec Js.t + end end @@ -62,6 +82,24 @@ and State : sig val fromJSON : t -> configuration_prop Js.t -> Brr.Json.t -> editor_state Js.t + val selection_to + : selection Js.t -> Model.resolved_pos Js.t + + val selection_at_start + : t-> Model.node Js.t -> selection Js.t + + val is_selectable + : t -> Model.node Js.t -> bool Js.t + + val node_selection + : t -> Model.resolved_pos Js.t -> node_selection Js.t + + val create_node_selection + : t -> Model.node Js.t -> int -> node_selection Js.t + + val create_text_selection + : t -> Model.node Js.t -> int -> text_selection Js.t + end (* Editor view *) @@ -91,6 +129,10 @@ module SchemaBasic : sig val schema : t -> Model.schema Js.t + val nodes + : t -> nodes Js.t + + end module History : sig @@ -113,7 +155,7 @@ end module Keymap : sig val keymap - : t -> (string * (State.editor_state Js.t -> (State.transaction -> unit) -> bool)) array -> State.plugin Js.t + : t -> (string * (State.editor_state Js.t -> (State.transaction Js.t -> unit) -> bool)) array -> State.plugin Js.t end @@ -126,5 +168,13 @@ end (* Example Setup *) -val example_setup - : t -> Model.schema Js.t -> State.plugin Js.t Js.js_array Js.t +module Example : sig + + include module type of Bindings.Example + + val options + : Model.schema Js.t -> options Js.t + + val example_setup + : t -> options Js.t -> State.plugin Js.t Js.js_array Js.t +end |