Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/prosemirror-schema",
"version": "1.3.15",
"version": "1.3.16",
"description": "Schema setup for using prosemirror in chatwoot. Based on 👉 https://github.com/ProseMirror/prosemirror-example-setup/",
"main": "dist/index.es.js",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export const buildEditor = ({
gapCursor(),
schema.nodes.table ? tableEditing() : null,
schema.nodes.table ? tableControlsPlugin(schema) : null,
// Message editors (image, no table): keep each image alone on its line.
schema.nodes.image && !schema.nodes.table ? isolateImagesPlugin() : null,
// editor with images (messages and articles): keep each image alone on its line.
schema.nodes.image ? isolateImagesPlugin() : null,
Placeholder(placeholder),
menuBar({
floating: true,
Expand Down
2 changes: 1 addition & 1 deletion src/keymap.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function baseKeyMaps(schema) {
selectNodeBackward,
];
// Message editors (image, no table): let Backspace clear an image above.
if (schema.nodes.image && !schema.nodes.table) {
if (schema.nodes.image) {
backspaceList.unshift(deleteImageBackward);
}
const backspaceComands = chainCommands(...backspaceList);
Expand Down
71 changes: 51 additions & 20 deletions src/plugins/isolateImages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,52 @@
import { Plugin } from "prosemirror-state";
import { EditorState, Plugin } from "prosemirror-state";

/**
* Positions at which paragraphs must be split so that every image ends up alone
* in its own paragraph (no text to its left or right). Returned deduped and
* sorted descending so callers can apply them without remapping earlier
* positions. (Adjacent images share a boundary, hence the dedupe.)
*
* @param {Node} doc - The ProseMirror document to inspect.
* @returns {number[]} - Split positions, sorted high → low.
*/
function imageIsolationSplits(doc) {
const splits = [];
doc.descendants((node, pos) => {
if (node.type.name !== "paragraph" || node.childCount < 2) return;

const contentStart = pos + 1;
const contentEnd = pos + node.nodeSize - 1;

node.forEach((child, offset) => {
if (child.type.name !== "image") return;
const imageStart = contentStart + offset;
const imageEnd = imageStart + child.nodeSize;
if (imageStart > contentStart) splits.push(imageStart);
if (imageEnd < contentEnd) splits.push(imageEnd);
});
});

return [...new Set(splits)].sort((a, b) => b - a);
}

/**
* Normalizes a document so every image sits alone in its own paragraph. Apply
* at parse time so freshly parsed markdown matches the editor's runtime layout
* (and therefore serializes back to identical markdown). Without this, a
* parse → serialize round-trip keeps images inline while the live editor
* isolates them, so the two outputs diverge.
*
* @param {Node} doc - The ProseMirror document to normalize.
* @returns {Node} - The document with images isolated (unchanged if none apply).
*/
export function isolateImagesInDoc(doc) {
const splits = imageIsolationSplits(doc);
if (!splits.length) return doc;

const { tr } = EditorState.create({ doc });
splits.forEach(p => tr.split(p));
return tr.doc;
}

/**
* Keeps every image on its own line — no text to its left or right.
Expand All @@ -13,28 +61,11 @@ export default function isolateImagesPlugin() {
appendTransaction(transactions, _oldState, newState) {
if (!transactions.some(tr => tr.docChanged)) return null;

const splits = [];
newState.doc.descendants((node, pos) => {
if (node.type.name !== "paragraph" || node.childCount < 2) return;

const contentStart = pos + 1;
const contentEnd = pos + node.nodeSize - 1;

node.forEach((child, offset) => {
if (child.type.name !== "image") return;
const imageStart = contentStart + offset;
const imageEnd = imageStart + child.nodeSize;
if (imageStart > contentStart) splits.push(imageStart);
if (imageEnd < contentEnd) splits.push(imageEnd);
});
});

const splits = imageIsolationSplits(newState.doc);
if (!splits.length) return null;

const tr = newState.tr;
// Dedupe (adjacent images share a boundary) and apply from the end so
// earlier positions stay valid.
[...new Set(splits)].sort((a, b) => b - a).forEach(p => tr.split(p));
splits.forEach(p => tr.split(p));
return tr;
},
});
Expand Down
5 changes: 4 additions & 1 deletion src/schema/markdown/articleParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
baseMarksMdToPmMapping,
filterMdToPmSchemaMapping,
} from './parser';
import { isolateImagesInDoc } from '../../plugins/isolateImages';

export const articleSchemaToMdMapping = {
nodes: {
Expand Down Expand Up @@ -101,6 +102,8 @@ export class ArticleMarkdownTransformer {
}

parse(content) {
return this.markdownParser.parse(content);
// Isolate images the same way the live editor does (isolateImagesPlugin),
// so a parse → serialize round-trip matches the editor's output.
return isolateImagesInDoc(this.markdownParser.parse(content));
}
}
5 changes: 4 additions & 1 deletion src/schema/markdown/messageParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
baseMarksMdToPmMapping,
filterMdToPmSchemaMapping,
} from './parser';
import { isolateImagesInDoc } from '../../plugins/isolateImages';

export const messageSchemaToMdMapping = {
nodes: { ...baseSchemaToMdMapping.nodes },
Expand Down Expand Up @@ -69,6 +70,8 @@ export class MessageMarkdownTransformer {
}

parse(content) {
return this.markdownParser.parse(content);
// Isolate images the same way the live editor does (isolateImagesPlugin),
// so a parse → serialize round-trip matches the editor's output.
return isolateImagesInDoc(this.markdownParser.parse(content));
}
}
133 changes: 91 additions & 42 deletions src/styles/article.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,97 +15,146 @@
}

.ProseMirror {
p {
font-size: var(--font-size-medium);
line-height: 1.75rem;
margin-bottom: var(--space-medium);
}
font-size: var(--font-size-default); // 16px
line-height: 1.75;

strong code {
font-size: var(--font-size-medium);
p {
font-size: var(--font-size-default);
margin-top: 20px; // 1.25em — no matching --space-* var
margin-bottom: 20px;
}

pre code {
font-size: var(--font-size-default);
font-weight: var(--font-weight-medium);
line-height: 1.6;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
}

blockquote {
padding-left: var(--space-normal);
margin: var(--space-medium) 0;
margin-top: 25.6px; // 1.6em — matches prose default
margin-bottom: 25.6px;
padding-inline-start: var(--space-normal);
font-style: italic;
}

a {
font-weight: var(--font-weight-medium);
font-size: var(--font-size-medium);
font-size: var(--font-size-default);
}

hr {
border-color: var(--s-100);
margin-bottom: var(--space-big);
margin-top: var(--space-larger); // 48px (~3em from prose default)
margin-bottom: var(--space-larger);
}

h1 {
font-size: var(--font-size-big);
font-weight: var(--font-weight-heavy);
margin-bottom: var(--space-medium);
font-size: 36px; // 2.25em (prose default; no matching --font-size-* var)
font-weight: 620;
line-height: 1.1111111;
margin-top: 0;
margin-bottom: var(--space-large); // 32px
}

h2 {
font-size: var(--font-size-large);
font-weight: var(--font-weight-black);
margin-bottom: var(--space-medium);
font-size: var(--font-size-big); // 24px (1.5em)
font-weight: 620;
line-height: 1.3333333;
margin-top: var(--space-large); // 32px (2em)
margin-bottom: var(--space-normal); // 16px (1em)
}

h3 {
font-size: var(--font-size-large); // 20px (1.25em)
font-weight: 620;
line-height: 1.6;
margin-top: var(--space-medium); // 24px (~1.6em)
margin-bottom: var(--space-slab); // 12px (0.6em)
}

h3,
h4 {
font-size: var(--font-size-medium);
font-weight: var(--font-weight-bold);
margin-bottom: var(--space-normal);
font-size: var(--font-size-default); // 16px
font-weight: 620;
line-height: 1.5;
margin-top: var(--space-medium); // 24px (1.5em)
margin-bottom: var(--space-small); // 8px (0.5em)
}

h5 {
font-size: var(--font-size-medium);
font-size: var(--font-size-default);
font-weight: 620;
margin-top: var(--space-normal);
margin-bottom: var(--space-small);
}

h6 {
font-size: var(--font-size-medium);
font-size: var(--font-size-default);
font-weight: 620;
margin-top: var(--space-normal);
margin-bottom: var(--space-smaller);
}

// Reset spacing on the first child so the editor doesn't start with a gap.
> :first-child {
margin-top: 0;
}

> :last-child {
margin-bottom: 0;
}

.ProseMirror-icon svg {
width: var(--space-medium) !important;
}

li::marker {
font-size: var(--font-size-medium);
font-size: var(--font-size-default);
}

ul,
ol {
margin-top: var(--space-medium);
margin-bottom: var(--space-medium);
padding-left: var(--space-large);
margin-top: 20px;
margin-bottom: 20px;
padding-inline-start: 26px; // 1.625em from prose default
}

ul, ul ul, ul ul ul { list-style-type: disc; }
ol, ol ol, ol ol ol { list-style-type: decimal; }

li {
ul,
ol {
margin-top: var(--space-normal);
margin-bottom: var(--space-normal);
}
p {
padding-left: var(--space-medium);
margin-top: var(--space-small);
margin-bottom: var(--space-small);
}
margin-top: var(--space-small); // 8px (0.5em)
margin-bottom: var(--space-small);
padding-inline-start: 6px; // 0.375em
}

// Nested lists get tighter vertical rhythm matching prose default.
ul ul,
ul ol,
ol ul,
ol ol {
margin-top: var(--space-slab); // 12px (0.75em)
margin-bottom: var(--space-slab);
}

// Portal collapses li > p margins (`[&_li>p]:m-0`). Mirror that here.
li > p {
margin-top: 0;
margin-bottom: 0;
}

pre {
padding: var(--space-normal);
margin-bottom: var(--space-normal);
font-size: var(--font-size-small); // 14px (0.875em)
line-height: 1.7142857;
margin-top: var(--space-medium); // 24px (~1.71em)
margin-bottom: var(--space-medium);
padding: var(--space-slab) var(--space-normal); // ~12px 16px
border-radius: var(--border-radius-medium);
}

p code,
li code,
strong code {
font-size: var(--font-size-small); // 14px (0.875em)
}


Expand Down
Loading