Skip to content

Commit 005e427

Browse files
committed
Add blocks to NotePlan syntax conversion
- add api methods editor - fix file and image links - add post processing for numbering list items - add diff view to check conversions
1 parent 928451c commit 005e427

5 files changed

Lines changed: 208 additions & 17 deletions

File tree

examples/editor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"devDependencies": {
1818
"@types/react": "^18.0.25",
1919
"@types/react-dom": "^18.0.9",
20+
"@types/diff": "^5.0.0",
2021
"@vitejs/plugin-react": "^3.1.0",
22+
"diff": "^5.0.0",
2123
"eslint": "^8.10.0",
2224
"eslint-config-react-app": "^7.0.0",
2325
"typescript": "^5.0.4",

examples/editor/src/App.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import "@blocknote/core/style.css";
33
import { BlockNoteView, useBlockNote } from "@blocknote/react";
44
import styles from "./App.module.css";
5-
import { parseNoteToBlocks } from "../../../packages/core/src/api/formatConversions/notePlanConversions";
5+
import {
6+
parseNoteToBlocks,
7+
serializeBlocksToNote,
8+
} from "../../../packages/core/src/api/formatConversions/notePlanConversions";
9+
import { diffChars } from "diff";
610

711
import { useEffect, useState } from "react";
812
import { BlockNoteEditor, BlockSchema, PartialBlock } from "@blocknote/core";
@@ -14,18 +18,28 @@ function App() {
1418
const [input, setInput] = useState<string>("");
1519
const [markdown, setMarkdown] = useState<string>("");
1620
const [json, setJSON] = useState<string>("");
21+
const [equals, setEquals] = useState<boolean>(false);
22+
const [diff, setDiff] = useState<string>("");
1723

1824
const editor: BlockNoteEditor | null = useBlockNote({
1925
onEditorContentChange: (editor: BlockNoteEditor) => {
20-
const saveBlocksAsMarkdown = async () => {
21-
const markdown: string = await editor.blocksToMarkdown(
22-
editor.topLevelBlocks
23-
);
24-
setMarkdown(markdown);
25-
const json: string = JSON.stringify(editor.topLevelBlocks, null, 2);
26-
setJSON(json);
27-
};
28-
saveBlocksAsMarkdown();
26+
const note = serializeBlocksToNote(editor.topLevelBlocks);
27+
setMarkdown(note);
28+
const json: string = JSON.stringify(editor.topLevelBlocks, null, 2);
29+
setJSON(json);
30+
if (input === note) {
31+
setEquals(true);
32+
} else {
33+
// calculate diff
34+
const diffResult = diffChars(input, note);
35+
let diffString = "";
36+
diffResult.forEach((part) => {
37+
const color = part.added ? "blue" : part.removed ? "red" : "grey";
38+
diffString += `<span style="color:${color}">${part.value}</span>`;
39+
});
40+
console.log(diffString);
41+
setDiff(diffString);
42+
}
2943
},
3044
editorDOMAttributes: {
3145
class: styles.editor,
@@ -61,9 +75,15 @@ function App() {
6175
/>
6276
<h3>Editor</h3>
6377
<BlockNoteView editor={editor} />
78+
{diff && (
79+
<div>
80+
<h3>Diff</h3>
81+
<pre dangerouslySetInnerHTML={{ __html: diff }} />
82+
</div>
83+
)}
6484
<h3>Output</h3>
65-
<pre>{json}</pre>
6685
<pre>{markdown}</pre>
86+
<pre>{json}</pre>
6787
</div>
6888
);
6989
}

package-lock.json

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/BlockNoteEditor.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import {
2323
Block,
2424
BlockIdentifier,
2525
BlockSchema,
26+
BlockSpec,
2627
PartialBlock,
28+
PropSchema,
2729
} from "./extensions/Blocks/api/blockTypes";
2830
import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
2931
import {
@@ -41,6 +43,10 @@ import {
4143
BaseSlashMenuItem,
4244
defaultSlashMenuItems,
4345
} from "./extensions/SlashMenu";
46+
import {
47+
parseNoteToBlocks,
48+
serializeBlocksToNote,
49+
} from "./api/formatConversions/notePlanConversions";
4450

4551
export type BlockNoteEditorOptions<BSchema extends BlockSchema> = {
4652
// TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them.
@@ -722,6 +728,15 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
722728
return blocksToMarkdown(blocks, this._tiptapEditor.schema);
723729
}
724730

731+
/**
732+
* Serializes blocks into a NotePlan string.
733+
* @param blocks An array of blocks that should be serialized into a NotePlan string.
734+
* @returns The blocks, serialized as a NotePlan string.
735+
*/
736+
public blocksToNotePlan(blocks: Block<BlockSchema>[]): string {
737+
return serializeBlocksToNote(blocks);
738+
}
739+
725740
/**
726741
* Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on
727742
* Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it
@@ -733,6 +748,15 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {
733748
return markdownToBlocks(markdown, this.schema, this._tiptapEditor.schema);
734749
}
735750

751+
/**
752+
* Creates a list of blocks from a NotePlan string.
753+
* @param notePlan The NotePlan string to parse blocks from.
754+
* @returns The blocks parsed from the NotePlan string.
755+
*/
756+
public notePlanToBlocks(notePlan: string): PartialBlock<BlockSchema>[] {
757+
return parseNoteToBlocks(notePlan);
758+
}
759+
736760
/**
737761
* Updates the user info for the current user that's shown to other collaborators.
738762
*/

packages/core/src/api/formatConversions/notePlanConversions.ts

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import {
2+
Block,
23
PartialBlock,
34
BlockSchema,
45
PropSchema,
56
Props,
67
} from "../../extensions/Blocks/api/blockTypes";
7-
import { PartialInlineContent } from "../../extensions/Blocks/api/inlineContentTypes";
8+
import {
9+
InlineContent,
10+
PartialInlineContent,
11+
} from "../../extensions/Blocks/api/inlineContentTypes";
812
import { DefaultBlockSchema } from "../..";
913
import Tokenizr from "tokenizr";
1014

@@ -275,15 +279,15 @@ function createInlineContent(text: string): PartialInlineContent[] {
275279
inlineContent.push({
276280
type: "link",
277281
href: token.value,
278-
content: token.value,
282+
content: "file",
279283
});
280284
downloadFile(token.value, false);
281285
break;
282286
case "image-link":
283287
inlineContent.push({
284288
type: "link",
285289
href: token.value,
286-
content: token.value,
290+
content: "image",
287291
});
288292
downloadFile(token.value, true);
289293
break;
@@ -745,3 +749,136 @@ export function parseNoteToBlocks(note: string): PartialBlock<BlockSchema>[] {
745749
postProcessBlocks(blocks);
746750
return blocks;
747751
}
752+
753+
function serializeTaskStates(prop: Props<PropSchema>): string {
754+
if (prop.checked === "true") {
755+
return "[x]";
756+
} else if (prop.canceled === "true") {
757+
return "[-]";
758+
} else if (prop.scheduled === "true") {
759+
return "[>]";
760+
} else {
761+
return "[ ]";
762+
}
763+
}
764+
765+
function serializeBlockContent(content: InlineContent[]): string {
766+
let text = "";
767+
let contentLength = content.length;
768+
for (let i = 0; i < contentLength; ++i) {
769+
let contentItem = content[i];
770+
switch (contentItem.type) {
771+
case "text":
772+
// serialize styles
773+
if (contentItem.styles.bold) {
774+
text += "**" + contentItem.text + "**";
775+
} else if (contentItem.styles.italic) {
776+
text += "*" + contentItem.text + "*";
777+
} else if (contentItem.styles.strike) {
778+
text += "~~" + contentItem.text + "~~";
779+
} else if (contentItem.styles.backgroundColor === "highlight-color") {
780+
text += "::" + contentItem.text + "::";
781+
} else if (contentItem.styles.code) {
782+
text += "`" + contentItem.text + "`";
783+
} else {
784+
text += contentItem.text;
785+
}
786+
break;
787+
case "link":
788+
const linkContent = serializeBlockContent(contentItem.content);
789+
switch (linkContent) {
790+
case "file":
791+
case "image":
792+
text += "![" + linkContent + "](" + contentItem.href + ")";
793+
break;
794+
default:
795+
text += "[" + linkContent + "](" + contentItem.href + ")";
796+
break;
797+
}
798+
break;
799+
}
800+
}
801+
return text;
802+
}
803+
804+
export function serializeBlock(
805+
block: Block<BlockSchema>,
806+
depth: number
807+
): string {
808+
let text = " ".repeat(depth);
809+
// serialize block types
810+
switch (block.type) {
811+
case "heading":
812+
text += "#".repeat(parseInt(block.props?.level || "1")) + " ";
813+
break;
814+
case "quoteListItem":
815+
text += "> ";
816+
break;
817+
case "bulletListItem":
818+
text += "- ";
819+
break;
820+
case "numberedListItem":
821+
text += "1. ";
822+
break;
823+
case "taskListItem":
824+
text += "* " + serializeTaskStates(block.props || {}) + " ";
825+
break;
826+
case "checkListItem":
827+
text += "+ " + serializeTaskStates(block.props || {}) + " ";
828+
break;
829+
case "paragraph":
830+
break;
831+
}
832+
833+
// serialize content array with InlineContent
834+
text += serializeBlockContent(block.content);
835+
836+
// end block with newline
837+
text += "\n";
838+
let children = block.children || [];
839+
let childrenLength = children.length;
840+
841+
for (let i = 0; i < childrenLength; ++i) {
842+
text += serializeBlock(children[i], depth + 1);
843+
}
844+
845+
return text;
846+
}
847+
848+
function postProcessNote(note: string): string {
849+
// adjust numbering of numbered list items according to their level
850+
let lines = note.split(/\r?\n/);
851+
let linesLength = lines.length;
852+
let currentNumber = 0;
853+
let previousLevel = 0;
854+
for (let i = 0; i < linesLength; ++i) {
855+
let line = lines[i];
856+
let matches = /^(\s*?)(\d+)\.\s+(.*)/.exec(line);
857+
console.log(matches);
858+
if (matches != null) {
859+
let leadingWhitespace = matches[1].length;
860+
let level = 0;
861+
if (leadingWhitespace > 1) {
862+
level = Math.floor(leadingWhitespace / 2);
863+
}
864+
if (level === previousLevel) {
865+
currentNumber++;
866+
} else {
867+
currentNumber = 1;
868+
}
869+
lines[i] = matches[1] + currentNumber.toString() + ". " + matches[3];
870+
previousLevel = level;
871+
}
872+
}
873+
return lines.join("\n");
874+
}
875+
876+
export function serializeBlocksToNote(blocks: Block<BlockSchema>[]): string {
877+
let text = "";
878+
let blocksLength = blocks.length;
879+
for (let i = 0; i < blocksLength; ++i) {
880+
text += serializeBlock(blocks[i], 0);
881+
}
882+
// post process text
883+
return postProcessNote(text);
884+
}

0 commit comments

Comments
 (0)