sibiraj-s / ngx-editor

šŸ–‹ļø Rich Text Editor for angular using ProseMirror
https://sibiraj-s.github.io/ngx-editor/
MIT License
423 stars 182 forks source link

[Feature request]: allow passing preserveWhiteSpace to ProseMirror #496

Closed ansemjo closed 6 days ago

ansemjo commented 8 months ago

Proposal

I think this is similar to #430. Even if you insert a <code> block, insert multiple spaces inside that block and then later re-parse the exported HTML with an ngx-editor (I'm using the "Reactive Forms" style, if that makes any difference), the spaces get trimmed down.

The need for retaining spaces is also discussed on the ProseMirror forum, with the apparent solution being that you could pass { preserveWhiteSpace: true } to PM. So yes, while the "editor itself does not [...] transform HTML", maybe it could help by passing this as an optional flag to ProseMirror?

(Or is there a way to configure this via attributes or something and I just haven't figured it out yet? šŸ˜¬)

Willing to submit a PR?

None


I see that preserveWhitespace is used on the documentation page on Schema, but I'm not sure how to use / apply this to the entirety of the HTML that is passed to the editor?

Edit: Overriding the default paragraph schema like this works for my use-case, too. I'm ignoring the align style here because I am manually aligning text with a monospace font anyway.

import { DOMOutputSpec, NodeSpec, Schema } from "prosemirror-model";
import { nodes as basicNodes, marks } from "ngx-editor";

// extend the prosemirror schema to always preserveWhitespace
const preserve: NodeSpec = {
  content: "inline*",
  group: "block",
  parseDOM: [{
    tag: "p",
    preserveWhitespace: "full",
  }],
  toDOM(): DOMOutputSpec {
    return ["p", { }, 0];
  },
};

const nodes = Object.assign({}, basicNodes, {
  "paragraph": preserve,
});

const schema = new Schema({ nodes, marks });

// then use this when instantiating editor:
this.editor = new Editor({
  schema: schema,
  // ...
});
ansemjo commented 8 months ago

Haven't actually tested this yet but maybe something as simple as passing ParseOptions straight down to toDoc works?

diff --git a/projects/ngx-editor/src/lib/Editor.ts b/projects/ngx-editor/src/lib/Editor.ts
index adcd76b..36263b9 100644
--- a/projects/ngx-editor/src/lib/Editor.ts
+++ b/projects/ngx-editor/src/lib/Editor.ts
@@ -1,4 +1,4 @@
-import { Schema } from 'prosemirror-model';
+import { ParseOptions, Schema } from 'prosemirror-model';
 import { EditorState, Plugin, Transaction } from 'prosemirror-state';
 import { EditorProps, EditorView } from 'prosemirror-view';
 import { Observable, Subject } from 'rxjs';
@@ -26,6 +26,7 @@ interface Options {
   features?: EditorFeatures;
   handleScrollToSelection?: EditorProps['handleScrollToSelection'];
   linkValidationPattern?: string;
+  parseOptions?: ParseOptions;
 }

 interface EditorFeatures {
@@ -50,6 +51,7 @@ const DEFAULT_OPTIONS: Options = {
   features: defaultFeatures,
   handleScrollToSelection: null,
   linkValidationPattern: '(https?://)?([\\da-z.-]+)\\.([a-z.]{2,6})[/\\w .-]*/??([^#\n\r]*)?#?([^\n\r]*)|(mailto:.*[@].*)',
+  parseOptions: undefined,
 };

 class Editor {
@@ -104,10 +106,10 @@ class Editor {

   private createEditor(): void {
     const { options, schema } = this;
-    const { content = null, nodeViews } = options;
+    const { content = null, nodeViews, parseOptions } = options;
     const { history = true, keyboardShortcuts = true, inputRules = true } = options;

-    const doc = parseContent(content, schema);
+    const doc = parseContent(content, schema, parseOptions);

     const plugins: Plugin[] = options.plugins ?? [];
     const attributes: EditorProps['attributes'] = options.attributes ?? {};
diff --git a/projects/ngx-editor/src/lib/parsers.ts b/projects/ngx-editor/src/lib/parsers.ts
index 0498ec4..b2be9c7 100644
--- a/projects/ngx-editor/src/lib/parsers.ts
+++ b/projects/ngx-editor/src/lib/parsers.ts
@@ -1,4 +1,4 @@
-import { DOMSerializer, Schema, DOMParser, Node as ProseMirrorNode } from 'prosemirror-model';
+import { DOMSerializer, Schema, DOMParser, Node as ProseMirrorNode, ParseOptions } from 'prosemirror-model';

 import defaultSchema from './schema';
 import { HTML, isHtml } from './trustedTypesUtil';
@@ -24,16 +24,16 @@ export const toHTML = (json: Record<string, any>, inputSchema?: Schema): string
   return div.innerHTML;
 };

-export const toDoc = (html: HTML, inputSchema?: Schema): Record<string, any> => {
+export const toDoc = (html: HTML, inputSchema?: Schema, options?: ParseOptions): Record<string, any> => {
   const schema = inputSchema ?? defaultSchema;

   const el = document.createElement('div');
   el.innerHTML = html as any;

-  return DOMParser.fromSchema(schema).parse(el).toJSON();
+  return DOMParser.fromSchema(schema).parse(el, options).toJSON();
 };

-export const parseContent = (value: HTML | Record<string, any> | null, schema: Schema): ProseMirrorNode => {
+export const parseContent = (value: HTML | Record<string, any> | null, schema: Schema, options?: ParseOptions): ProseMirrorNode => {
   if (!value) {
     return schema.nodeFromJSON(emptyDoc);
   }
@@ -42,6 +42,6 @@ export const parseContent = (value: HTML | Record<string, any> | null, schema: S
     return schema.nodeFromJSON(value);
   }

-  const docJson = toDoc(value as HTML, schema);
+  const docJson = toDoc(value as HTML, schema, options);
   return schema.nodeFromJSON(docJson);
 };
ansemjo commented 8 months ago

So, I figured out how to use a schema for my use-case (see the edit in the first comment above). I'm not sure how well that works for any nested nodes but so far it seems to work fine.

I'll leave the issue open for you to decide whether the API addition of ParseOptions makes sense ..


Edit: sorry for the spam šŸ˜¬ But I think this feature might still be necessary (or at least useful) because the above schema does not work when you pass plaintext content to the editor, which is not HTML yet. I can't figure out how to amend the doc or text schemas to correctly preserve whitespace either in that case. As a workaround you can just wrap the plaintext in <p>...</p> and replace any newlines (\n) with <br>.

sibiraj-s commented 8 months ago

HI. Thanks for creating the issue. Will look into this tomorrow.

lpepen-re commented 3 months ago

I've been running into the same issue. Since this issue is still open i assume it has yet to be implemented. Looking forward to this feature (or something similar that will allow the users to preserve white spaces when parsing from html to json with toDoc()

sibiraj-s commented 6 days ago

Added in v17.1.0