@gogitcms/editor 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/app/index.html +38 -0
  2. package/app/src/App.tsx +2665 -0
  3. package/app/src/apollo.ts +119 -0
  4. package/app/src/auth.ts +248 -0
  5. package/app/src/config.ts +88 -0
  6. package/app/src/main.tsx +18 -0
  7. package/app/src/media.ts +151 -0
  8. package/app/src/navigation.tsx +321 -0
  9. package/app/src/plugins.ts +11 -0
  10. package/app/src/previewHost.tsx +263 -0
  11. package/app/src/previewToken.ts +68 -0
  12. package/app/src/queries.ts +591 -0
  13. package/app/src/virtual-cms-plugins.d.ts +6 -0
  14. package/app/vendor/analytics/src/__tests__/config.test.ts +91 -0
  15. package/app/vendor/analytics/src/config.ts +94 -0
  16. package/app/vendor/analytics/src/events.ts +56 -0
  17. package/app/vendor/analytics/src/index.ts +23 -0
  18. package/app/vendor/analytics/src/provider.tsx +196 -0
  19. package/app/vendor/design-system/src/ThemeProvider.tsx +86 -0
  20. package/app/vendor/design-system/src/__tests__/ApplyChangesModal.test.tsx +148 -0
  21. package/app/vendor/design-system/src/__tests__/BranchImport.test.tsx +46 -0
  22. package/app/vendor/design-system/src/__tests__/Button.test.tsx +45 -0
  23. package/app/vendor/design-system/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
  24. package/app/vendor/design-system/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
  25. package/app/vendor/design-system/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
  26. package/app/vendor/design-system/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
  27. package/app/vendor/design-system/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
  28. package/app/vendor/design-system/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
  29. package/app/vendor/design-system/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
  30. package/app/vendor/design-system/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
  31. package/app/vendor/design-system/src/__tests__/ContentBrowser.media.test.tsx +140 -0
  32. package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
  33. package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
  34. package/app/vendor/design-system/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
  35. package/app/vendor/design-system/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
  36. package/app/vendor/design-system/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
  37. package/app/vendor/design-system/src/__tests__/ContentBrowser.search.test.tsx +135 -0
  38. package/app/vendor/design-system/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
  39. package/app/vendor/design-system/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
  40. package/app/vendor/design-system/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
  41. package/app/vendor/design-system/src/__tests__/MediaBrowser.test.tsx +353 -0
  42. package/app/vendor/design-system/src/__tests__/MediaField.test.tsx +185 -0
  43. package/app/vendor/design-system/src/__tests__/Notifications.test.tsx +69 -0
  44. package/app/vendor/design-system/src/__tests__/Onboarding.test.tsx +287 -0
  45. package/app/vendor/design-system/src/__tests__/cssTokens.test.ts +201 -0
  46. package/app/vendor/design-system/src/__tests__/fieldComponents.test.ts +43 -0
  47. package/app/vendor/design-system/src/__tests__/reorder.test.ts +58 -0
  48. package/app/vendor/design-system/src/components/ApplyChangesModal.tsx +348 -0
  49. package/app/vendor/design-system/src/components/BranchImport.tsx +157 -0
  50. package/app/vendor/design-system/src/components/BranchMenu.tsx +192 -0
  51. package/app/vendor/design-system/src/components/Button.tsx +131 -0
  52. package/app/vendor/design-system/src/components/ChangeDetail.tsx +472 -0
  53. package/app/vendor/design-system/src/components/ChangeRequestSummary.tsx +173 -0
  54. package/app/vendor/design-system/src/components/CollabField.tsx +388 -0
  55. package/app/vendor/design-system/src/components/ContentBrowser.tsx +5073 -0
  56. package/app/vendor/design-system/src/components/Icon.tsx +28 -0
  57. package/app/vendor/design-system/src/components/Icon.web.tsx +31 -0
  58. package/app/vendor/design-system/src/components/Input.tsx +106 -0
  59. package/app/vendor/design-system/src/components/MediaBrowser.tsx +766 -0
  60. package/app/vendor/design-system/src/components/MediaField.tsx +670 -0
  61. package/app/vendor/design-system/src/components/MediaPreview.tsx +91 -0
  62. package/app/vendor/design-system/src/components/MediaPreview.web.tsx +169 -0
  63. package/app/vendor/design-system/src/components/NavRow.tsx +105 -0
  64. package/app/vendor/design-system/src/components/Notifications.tsx +301 -0
  65. package/app/vendor/design-system/src/components/Onboarding.tsx +751 -0
  66. package/app/vendor/design-system/src/components/ProjectMenu.tsx +124 -0
  67. package/app/vendor/design-system/src/components/Segment.tsx +87 -0
  68. package/app/vendor/design-system/src/components/Skeleton.tsx +216 -0
  69. package/app/vendor/design-system/src/components/Spinner.tsx +44 -0
  70. package/app/vendor/design-system/src/components/Text.tsx +85 -0
  71. package/app/vendor/design-system/src/components/documentDrafts.ts +213 -0
  72. package/app/vendor/design-system/src/components/layout.tsx +284 -0
  73. package/app/vendor/design-system/src/components/primitives.tsx +143 -0
  74. package/app/vendor/design-system/src/components/reorder.ts +40 -0
  75. package/app/vendor/design-system/src/fieldComponents.ts +63 -0
  76. package/app/vendor/design-system/src/icons.ts +102 -0
  77. package/app/vendor/design-system/src/index.ts +198 -0
  78. package/app/vendor/design-system/src/media.ts +229 -0
  79. package/app/vendor/design-system/src/theme.ts +116 -0
  80. package/app/vendor/design-system/src/web/Button.tsx +110 -0
  81. package/app/vendor/design-system/src/web/Icon.tsx +52 -0
  82. package/app/vendor/design-system/src/web/Input.tsx +39 -0
  83. package/app/vendor/design-system/src/web/index.ts +43 -0
  84. package/app/vendor/design-system/src/web/primitives.tsx +119 -0
  85. package/app/vendor/markdown-editor/src/MarkdownEditor.tsx +248 -0
  86. package/app/vendor/markdown-editor/src/Toolbar.tsx +157 -0
  87. package/app/vendor/markdown-editor/src/field.tsx +67 -0
  88. package/app/vendor/markdown-editor/src/flavors/commonmark/index.ts +27 -0
  89. package/app/vendor/markdown-editor/src/flavors/gfm/index.ts +30 -0
  90. package/app/vendor/markdown-editor/src/flavors/gfm/parser.ts +73 -0
  91. package/app/vendor/markdown-editor/src/flavors/gfm/schema.ts +70 -0
  92. package/app/vendor/markdown-editor/src/flavors/gfm/serializer.ts +82 -0
  93. package/app/vendor/markdown-editor/src/flavors/gfm/taskList.ts +39 -0
  94. package/app/vendor/markdown-editor/src/flavors/registry.ts +13 -0
  95. package/app/vendor/markdown-editor/src/flavors/shared/inputrules.ts +79 -0
  96. package/app/vendor/markdown-editor/src/flavors/shared/keymap.ts +89 -0
  97. package/app/vendor/markdown-editor/src/flavors/shared/placeholder.ts +24 -0
  98. package/app/vendor/markdown-editor/src/flavors/shared/plugins.ts +58 -0
  99. package/app/vendor/markdown-editor/src/flavors/types.ts +22 -0
  100. package/app/vendor/markdown-editor/src/formats/registry.ts +45 -0
  101. package/app/vendor/markdown-editor/src/icons.tsx +120 -0
  102. package/app/vendor/markdown-editor/src/index.ts +23 -0
  103. package/app/vendor/markdown-editor/src/theme.ts +249 -0
  104. package/app/vendor/markdown-editor/src/types.ts +58 -0
  105. package/app/vendor/plugin-sdk/src/__tests__/documentActions.test.ts +108 -0
  106. package/app/vendor/plugin-sdk/src/__tests__/loader.test.ts +75 -0
  107. package/app/vendor/plugin-sdk/src/__tests__/registry.test.ts +148 -0
  108. package/app/vendor/plugin-sdk/src/index.ts +33 -0
  109. package/app/vendor/plugin-sdk/src/loader.ts +45 -0
  110. package/app/vendor/plugin-sdk/src/navkeys.ts +17 -0
  111. package/app/vendor/plugin-sdk/src/react.tsx +155 -0
  112. package/app/vendor/plugin-sdk/src/registry.ts +337 -0
  113. package/app/vendor/plugin-sdk/src/types.ts +230 -0
  114. package/app/vendor/realtime/src/__tests__/pure.test.ts +58 -0
  115. package/app/vendor/realtime/src/__tests__/seeded-room.base64 +1 -0
  116. package/app/vendor/realtime/src/__tests__/seeding.test.ts +70 -0
  117. package/app/vendor/realtime/src/collab.ts +308 -0
  118. package/app/vendor/realtime/src/hooks.ts +57 -0
  119. package/app/vendor/realtime/src/index.ts +12 -0
  120. package/app/vendor/realtime/src/pure.ts +43 -0
  121. package/app/vite.config.mjs +59 -0
  122. package/bin/gogitcms-editor.mjs +168 -0
  123. package/npm-shrinkwrap.json +5696 -0
  124. package/package.json +74 -0
  125. package/src/commands/build.mjs +100 -0
  126. package/src/commands/dev.mjs +186 -0
  127. package/src/commands/init.mjs +144 -0
  128. package/src/commands/login.mjs +116 -0
  129. package/src/commands/status.mjs +68 -0
  130. package/src/lib/api.mjs +122 -0
  131. package/src/lib/appRoot.mjs +51 -0
  132. package/src/lib/baked.json +3 -0
  133. package/src/lib/config.mjs +108 -0
  134. package/src/lib/credentials.mjs +91 -0
  135. package/src/lib/defaults.mjs +26 -0
  136. package/src/lib/graphql.mjs +28 -0
  137. package/src/lib/localServer.mjs +206 -0
  138. package/src/lib/open.mjs +23 -0
  139. package/src/lib/plugins.mjs +164 -0
  140. package/src/lib/tokenBroker.mjs +85 -0
  141. package/src/tui/prompts.mjs +92 -0
  142. package/src/tui/screen.mjs +282 -0
  143. package/src/tui/theme.mjs +40 -0
@@ -0,0 +1,82 @@
1
+ import {
2
+ MarkdownSerializer,
3
+ MarkdownSerializerState,
4
+ defaultMarkdownSerializer,
5
+ } from "prosemirror-markdown";
6
+ import type { Node as PMNode } from "prosemirror-model";
7
+ import { gfmSchema } from "./schema";
8
+
9
+ const strikethrough = {
10
+ open: "~~",
11
+ close: "~~",
12
+ mixable: true,
13
+ expelEnclosingWhitespace: true,
14
+ };
15
+
16
+ // Inline-only serializer used to render a single table cell's content to a
17
+ // markdown string. Cells never contain tables, so there is no recursion back
18
+ // into the table handler.
19
+ const inlineSerializer = new MarkdownSerializer(defaultMarkdownSerializer.nodes, {
20
+ ...defaultMarkdownSerializer.marks,
21
+ strikethrough,
22
+ });
23
+
24
+ function cellText(cell: PMNode): string {
25
+ const para = gfmSchema.nodes.paragraph.createChecked(null, cell.content);
26
+ const doc = gfmSchema.topNodeType.createChecked(null, [para]);
27
+ // Collapse newlines and escape pipes so the cell stays on one table row.
28
+ return inlineSerializer
29
+ .serialize(doc)
30
+ .trim()
31
+ .replace(/\n+/g, " ")
32
+ .replace(/\|/g, "\\|");
33
+ }
34
+
35
+ function serializeTable(state: MarkdownSerializerState, node: PMNode) {
36
+ const rows: string[][] = [];
37
+ node.forEach((row) => {
38
+ const cells: string[] = [];
39
+ row.forEach((cell) => cells.push(cellText(cell)));
40
+ rows.push(cells);
41
+ });
42
+ if (rows.length === 0) return;
43
+
44
+ const cols = rows[0].length;
45
+ const line = (cells: string[]) => {
46
+ const padded = [...cells];
47
+ while (padded.length < cols) padded.push("");
48
+ return "| " + padded.slice(0, cols).join(" | ") + " |";
49
+ };
50
+
51
+ state.write(line(rows[0]) + "\n");
52
+ state.write("| " + Array(cols).fill("---").join(" | ") + " |\n");
53
+ for (let i = 1; i < rows.length; i++) state.write(line(rows[i]) + "\n");
54
+ state.closeBlock(node);
55
+ }
56
+
57
+ const nodes = {
58
+ ...defaultMarkdownSerializer.nodes,
59
+ // Prefix task items with `[x] `/`[ ] `; the surrounding list handler has
60
+ // already written the `- `/`1. ` bullet.
61
+ list_item(state: MarkdownSerializerState, node: PMNode) {
62
+ if (node.attrs.checked !== null) {
63
+ state.write(node.attrs.checked ? "[x] " : "[ ] ");
64
+ }
65
+ state.renderContent(node);
66
+ },
67
+ table: serializeTable,
68
+ // Never reached (serializeTable renders rows/cells itself) but required so the
69
+ // serializer has an entry for every node type it might look up.
70
+ table_row() {},
71
+ table_header() {},
72
+ table_cell() {},
73
+ };
74
+
75
+ const serializer = new MarkdownSerializer(nodes, {
76
+ ...defaultMarkdownSerializer.marks,
77
+ strikethrough,
78
+ });
79
+
80
+ export function serializeGfm(doc: PMNode): string {
81
+ return serializer.serialize(doc);
82
+ }
@@ -0,0 +1,39 @@
1
+ import { Plugin } from "prosemirror-state";
2
+ import type { EditorView } from "prosemirror-view";
3
+ import { gfmSchema } from "./schema";
4
+
5
+ // Makes the task-list checkboxes interactive: clicking one toggles the enclosing
6
+ // list_item's `checked` attribute. The checkbox itself is contenteditable=false,
7
+ // so we intercept the mousedown before selection handling.
8
+ export function taskListPlugin(): Plugin {
9
+ return new Plugin({
10
+ props: {
11
+ handleDOMEvents: {
12
+ mousedown(view: EditorView, event: Event) {
13
+ const target = event.target as HTMLElement | null;
14
+ const box = target?.closest?.(".task-list-item-checkbox");
15
+ if (!box) return false;
16
+ const li = box.closest("li");
17
+ if (!li) return false;
18
+
19
+ const pos = view.posAtDOM(li, 0);
20
+ const $pos = view.state.doc.resolve(pos);
21
+ for (let d = $pos.depth; d > 0; d--) {
22
+ const node = $pos.node(d);
23
+ if (node.type === gfmSchema.nodes.list_item && node.attrs.checked !== null) {
24
+ const itemPos = $pos.before(d);
25
+ const tr = view.state.tr.setNodeMarkup(itemPos, undefined, {
26
+ ...node.attrs,
27
+ checked: !node.attrs.checked,
28
+ });
29
+ view.dispatch(tr);
30
+ event.preventDefault();
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ },
36
+ },
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,13 @@
1
+ import type { Flavor } from "./types";
2
+ import { commonmark } from "./commonmark";
3
+ import { gfm } from "./gfm";
4
+
5
+ // All available flavors, keyed by id. Adding a dialect = one import + one entry.
6
+ export const FLAVORS: Record<string, Flavor> = {
7
+ [commonmark.id]: commonmark,
8
+ [gfm.id]: gfm,
9
+ };
10
+
11
+ export function resolveFlavor(id: string | undefined): Flavor {
12
+ return (id && FLAVORS[id]) || commonmark;
13
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ InputRule,
3
+ inputRules,
4
+ wrappingInputRule,
5
+ textblockTypeInputRule,
6
+ smartQuotes,
7
+ emDash,
8
+ ellipsis,
9
+ } from "prosemirror-inputrules";
10
+ import type { NodeType } from "prosemirror-model";
11
+ import type { Schema } from "prosemirror-model";
12
+ import type { Plugin } from "prosemirror-state";
13
+
14
+ // Markdown "type it and it transforms" shortcuts: `> ` for blockquote, `1. ` /
15
+ // `- ` for lists, ``` for a code block, `## ` for a heading. Mirrors the rules
16
+ // from prosemirror-example-setup but built from whatever nodes the flavor's
17
+ // schema actually declares.
18
+
19
+ function blockQuoteRule(nodeType: NodeType) {
20
+ return wrappingInputRule(/^\s*>\s$/, nodeType);
21
+ }
22
+
23
+ function orderedListRule(nodeType: NodeType) {
24
+ return wrappingInputRule(
25
+ /^(\d+)\.\s$/,
26
+ nodeType,
27
+ (match) => ({ order: +match[1] }),
28
+ (match, node) => node.childCount + node.attrs.order === +match[1],
29
+ );
30
+ }
31
+
32
+ function bulletListRule(nodeType: NodeType) {
33
+ return wrappingInputRule(/^\s*([-+*])\s$/, nodeType);
34
+ }
35
+
36
+ function codeBlockRule(nodeType: NodeType) {
37
+ return textblockTypeInputRule(/^```$/, nodeType);
38
+ }
39
+
40
+ function headingRule(nodeType: NodeType, maxLevel: number) {
41
+ return textblockTypeInputRule(
42
+ new RegExp("^(#{1," + maxLevel + "})\\s$"),
43
+ nodeType,
44
+ (match) => ({ level: match[1].length }),
45
+ );
46
+ }
47
+
48
+ // Typing `[ ] ` / `[x] ` at the start of a list item's first block turns the
49
+ // item into a task (GFM only — gated on list_item having a `checked` attr).
50
+ function taskListRule(listItem: NodeType) {
51
+ return new InputRule(/^\[([ xX])\]\s$/, (state, match, start, end) => {
52
+ const $start = state.doc.resolve(start);
53
+ let depth = $start.depth;
54
+ while (depth > 0 && $start.node(depth).type !== listItem) depth--;
55
+ // Only fire inside a list item, and only on the item's first block.
56
+ if (depth === 0 || $start.index(depth) !== 0) return null;
57
+ const item = $start.node(depth);
58
+ return state.tr
59
+ .delete(start, end)
60
+ .setNodeMarkup($start.before(depth), undefined, {
61
+ ...item.attrs,
62
+ checked: match[1] !== " ",
63
+ });
64
+ });
65
+ }
66
+
67
+ export function buildInputRules(schema: Schema): Plugin {
68
+ const rules = [...smartQuotes, ellipsis, emDash];
69
+ let type: NodeType | undefined;
70
+ if ((type = schema.nodes.blockquote)) rules.push(blockQuoteRule(type));
71
+ if ((type = schema.nodes.ordered_list)) rules.push(orderedListRule(type));
72
+ if ((type = schema.nodes.bullet_list)) rules.push(bulletListRule(type));
73
+ if ((type = schema.nodes.code_block)) rules.push(codeBlockRule(type));
74
+ if ((type = schema.nodes.heading)) rules.push(headingRule(type, 6));
75
+ if ((type = schema.nodes.list_item) && type.spec.attrs && "checked" in type.spec.attrs) {
76
+ rules.push(taskListRule(type));
77
+ }
78
+ return inputRules({ rules });
79
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ chainCommands,
3
+ exitCode,
4
+ setBlockType,
5
+ toggleMark,
6
+ wrapIn,
7
+ } from "prosemirror-commands";
8
+ import { redo, undo } from "prosemirror-history";
9
+ import { undoInputRule } from "prosemirror-inputrules";
10
+ import {
11
+ liftListItem,
12
+ sinkListItem,
13
+ splitListItem,
14
+ wrapInList,
15
+ } from "prosemirror-schema-list";
16
+ import type { Command } from "prosemirror-state";
17
+ import type { Schema } from "prosemirror-model";
18
+
19
+ // Editing keybindings derived from the flavor's schema — only bindings whose
20
+ // nodes/marks exist are installed, so the same builder serves CommonMark and
21
+ // GFM. Adapted from prosemirror-example-setup's buildKeymap.
22
+ export function buildKeymap(schema: Schema): Record<string, Command> {
23
+ const keys: Record<string, Command> = {};
24
+ const bind = (key: string, cmd: Command) => {
25
+ keys[key] = cmd;
26
+ };
27
+
28
+ bind("Mod-z", undo);
29
+ bind("Shift-Mod-z", redo);
30
+ bind("Mod-y", redo);
31
+ bind("Backspace", undoInputRule);
32
+
33
+ if (schema.marks.strong) {
34
+ bind("Mod-b", toggleMark(schema.marks.strong));
35
+ bind("Mod-B", toggleMark(schema.marks.strong));
36
+ }
37
+ if (schema.marks.em) {
38
+ bind("Mod-i", toggleMark(schema.marks.em));
39
+ bind("Mod-I", toggleMark(schema.marks.em));
40
+ }
41
+ if (schema.marks.code) bind("Mod-`", toggleMark(schema.marks.code));
42
+ if (schema.marks.strikethrough) {
43
+ bind("Mod-Shift-x", toggleMark(schema.marks.strikethrough));
44
+ bind("Mod-Shift-X", toggleMark(schema.marks.strikethrough));
45
+ }
46
+
47
+ if (schema.nodes.bullet_list) bind("Shift-Ctrl-8", wrapInList(schema.nodes.bullet_list));
48
+ if (schema.nodes.ordered_list) bind("Shift-Ctrl-9", wrapInList(schema.nodes.ordered_list));
49
+ if (schema.nodes.blockquote) bind("Ctrl->", wrapIn(schema.nodes.blockquote));
50
+
51
+ if (schema.nodes.hard_break) {
52
+ const br = schema.nodes.hard_break;
53
+ const cmd = chainCommands(exitCode, (state, dispatch) => {
54
+ if (dispatch) dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
55
+ return true;
56
+ });
57
+ bind("Mod-Enter", cmd);
58
+ bind("Shift-Enter", cmd);
59
+ }
60
+
61
+ if (schema.nodes.list_item) {
62
+ bind("Enter", splitListItem(schema.nodes.list_item));
63
+ bind("Mod-[", liftListItem(schema.nodes.list_item));
64
+ bind("Mod-]", sinkListItem(schema.nodes.list_item));
65
+ bind("Tab", sinkListItem(schema.nodes.list_item));
66
+ bind("Shift-Tab", liftListItem(schema.nodes.list_item));
67
+ }
68
+
69
+ if (schema.nodes.code_block) bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
70
+ if (schema.nodes.paragraph) bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
71
+ if (schema.nodes.heading) {
72
+ for (let i = 1; i <= 6; i++) {
73
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
74
+ }
75
+ }
76
+ if (schema.nodes.horizontal_rule) {
77
+ const hr = schema.nodes.horizontal_rule;
78
+ bind("Mod-_", (state, dispatch) => {
79
+ if (dispatch) dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
80
+ return true;
81
+ });
82
+ }
83
+
84
+ // Note: the defaults (Enter, Backspace, Delete, arrows, …) are NOT merged in
85
+ // here. basePlugins installs baseKeymap as a separate, lower-priority keymap
86
+ // so that when a binding above returns false (e.g. splitListItem outside a
87
+ // list, undoInputRule with nothing to undo), it falls through to baseKeymap.
88
+ return keys;
89
+ }
@@ -0,0 +1,24 @@
1
+ import { Plugin } from "prosemirror-state";
2
+ import { Decoration, DecorationSet } from "prosemirror-view";
3
+
4
+ // Shows `text` over an empty document (a single empty textblock). Rendered via
5
+ // the `.cms-editor-placeholder::before` rule in theme.ts.
6
+ export function placeholderPlugin(text: string): Plugin {
7
+ return new Plugin({
8
+ props: {
9
+ decorations(state) {
10
+ const doc = state.doc;
11
+ const only = doc.firstChild;
12
+ if (doc.childCount === 1 && only && only.isTextblock && only.content.size === 0) {
13
+ return DecorationSet.create(doc, [
14
+ Decoration.node(0, only.nodeSize, {
15
+ class: "cms-editor-placeholder",
16
+ "data-placeholder": text,
17
+ }),
18
+ ]);
19
+ }
20
+ return null;
21
+ },
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,58 @@
1
+ import { baseKeymap } from "prosemirror-commands";
2
+ import { dropCursor } from "prosemirror-dropcursor";
3
+ import { gapCursor } from "prosemirror-gapcursor";
4
+ import { history } from "prosemirror-history";
5
+ import { keymap } from "prosemirror-keymap";
6
+ import type { Plugin } from "prosemirror-state";
7
+ import type { Schema } from "prosemirror-model";
8
+ import type { XmlFragment } from "yjs";
9
+ import type { Awareness } from "y-protocols/awareness";
10
+ import { ySyncPlugin, yUndoPlugin, undo as yUndo, redo as yRedo } from "y-prosemirror";
11
+ import { buildInputRules } from "./inputrules";
12
+ import { buildKeymap } from "./keymap";
13
+
14
+ // Collaborative binding for the editor: the shared XML fragment the document is
15
+ // mirrored to, plus the awareness instance (reserved for future in-editor
16
+ // cursors; presence is currently shown as a border + labels around the whole
17
+ // editor, consistent with the other fields).
18
+ export type EditorCollabBinding = {
19
+ fragment: XmlFragment;
20
+ awareness: Awareness;
21
+ };
22
+
23
+ // The editing plugins shared by every flavor: markdown input rules, the
24
+ // schema-derived keymap, drop/gap cursors, and history. `extra` lets a flavor
25
+ // prepend/append its own plugins (e.g. GFM's tableEditing).
26
+ //
27
+ // When `collab` is given, the local history plugin is replaced by y-prosemirror's
28
+ // sync/cursor/undo plugins (per-client undo), and Mod-z/Mod-y are rebound to the
29
+ // collaborative undo manager — a higher-priority keymap that shadows the
30
+ // history-based bindings still present in buildKeymap (those become no-ops with
31
+ // no history plugin installed).
32
+ export function basePlugins(schema: Schema, extra: Plugin[] = [], collab?: EditorCollabBinding): Plugin[] {
33
+ const plugins: Plugin[] = [];
34
+ if (collab) {
35
+ plugins.push(
36
+ keymap({ "Mod-z": yUndo, "Mod-y": yRedo, "Shift-Mod-z": yRedo }),
37
+ );
38
+ }
39
+ plugins.push(
40
+ buildInputRules(schema),
41
+ // Flavor-specific bindings first; baseKeymap second so unhandled keys
42
+ // (Enter/Backspace/Delete/arrows/…) fall through to the ProseMirror defaults.
43
+ keymap(buildKeymap(schema)),
44
+ keymap(baseKeymap),
45
+ dropCursor(),
46
+ gapCursor(),
47
+ );
48
+ if (collab) {
49
+ // ySyncPlugin mirrors the doc to the shared fragment; yUndoPlugin gives
50
+ // per-client undo. Remote cursors (yCursorPlugin) are intentionally omitted —
51
+ // presence is shown as a border + labels around the editor by the host.
52
+ plugins.push(ySyncPlugin(collab.fragment), yUndoPlugin());
53
+ } else {
54
+ plugins.push(history());
55
+ }
56
+ plugins.push(...extra);
57
+ return plugins;
58
+ }
@@ -0,0 +1,22 @@
1
+ import type { Node as PMNode, Schema } from "prosemirror-model";
2
+ import type { Plugin } from "prosemirror-state";
3
+ import type { EditorCollabBinding } from "./shared/plugins";
4
+
5
+ // A Flavor is a self-contained ProseMirror configuration: schema, the
6
+ // markdown<->doc conversion, and the editing plugins (keymap, history, input
7
+ // rules, …). Each markdown dialect (CommonMark, GFM) is one Flavor; adding a
8
+ // new dialect is a new module plus a registry entry and nothing else.
9
+ export interface Flavor {
10
+ id: string;
11
+ label: string;
12
+ schema: Schema;
13
+ // Parse a markdown string into a ProseMirror document.
14
+ parse(markdown: string): PMNode;
15
+ // Serialize a ProseMirror document back to a markdown string.
16
+ serialize(doc: PMNode): string;
17
+ // Editing plugins for this flavor. The `reactKeys` plugin (required by
18
+ // @handlewithcare/react-prosemirror) is added by the editor component, not
19
+ // here, so flavors never depend on the React binding. When `collab` is given,
20
+ // the flavor swaps local history for the collaborative (Yjs) plugins.
21
+ plugins(collab?: EditorCollabBinding): Plugin[];
22
+ }
@@ -0,0 +1,45 @@
1
+ // The set of editable *formats* and, for each, the file extensions it can drive
2
+ // as a document body. This is the single TS source of truth for extension
3
+ // gating; the Go schema validator mirrors these lists (keep them in sync).
4
+ //
5
+ // Frontmatter fields are always plain strings and are never gated by extension —
6
+ // only the file-body binding consults `extensions`.
7
+
8
+ export type EditorFormat = "markdown";
9
+
10
+ export type MarkdownFlavor = "commonmark" | "gfm";
11
+
12
+ export type FormatMeta = {
13
+ id: EditorFormat;
14
+ label: string;
15
+ // Lower-cased file extensions (leading dot) whose body may be edited in this
16
+ // format. `.mdx` is intentionally excluded — it needs a different parser.
17
+ extensions: string[];
18
+ flavors: readonly string[];
19
+ defaultFlavor: string;
20
+ };
21
+
22
+ export const FORMATS: Record<EditorFormat, FormatMeta> = {
23
+ markdown: {
24
+ id: "markdown",
25
+ label: "Markdown",
26
+ extensions: [".md", ".markdown"],
27
+ flavors: ["commonmark", "gfm"] as const,
28
+ defaultFlavor: "commonmark",
29
+ },
30
+ };
31
+
32
+ // Normalize a path or filename to its lower-cased extension (with dot), e.g.
33
+ // "content/posts/hello.MD" -> ".md". Returns "" when there is no extension.
34
+ export function extensionOf(pathOrName: string): string {
35
+ const base = pathOrName.split(/[\\/]/).pop() ?? "";
36
+ const dot = base.lastIndexOf(".");
37
+ return dot <= 0 ? "" : base.slice(dot).toLowerCase();
38
+ }
39
+
40
+ // Whether the given file path may be edited as a body in the given format.
41
+ export function supportsFileBody(format: EditorFormat, pathOrName: string): boolean {
42
+ const meta = FORMATS[format];
43
+ if (!meta) return false;
44
+ return meta.extensions.includes(extensionOf(pathOrName));
45
+ }
@@ -0,0 +1,120 @@
1
+ import React from "react";
2
+
3
+ // Small stroked icons (Lucide-style) for the formatting toolbar. 24px viewBox,
4
+ // currentColor stroke so they inherit button color.
5
+ type IconProps = { size?: number };
6
+
7
+ const Svg = ({ size = 16, children }: IconProps & { children: React.ReactNode }) => (
8
+ <svg
9
+ width={size}
10
+ height={size}
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth={2}
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ aria-hidden="true"
18
+ >
19
+ {children}
20
+ </svg>
21
+ );
22
+
23
+ export const BoldIcon = (p: IconProps) => (
24
+ <Svg {...p}>
25
+ <path d="M6 4h8a4 4 0 0 1 0 8H6z" />
26
+ <path d="M6 12h9a4 4 0 0 1 0 8H6z" />
27
+ </Svg>
28
+ );
29
+
30
+ export const ItalicIcon = (p: IconProps) => (
31
+ <Svg {...p}>
32
+ <line x1="19" y1="4" x2="10" y2="4" />
33
+ <line x1="14" y1="20" x2="5" y2="20" />
34
+ <line x1="15" y1="4" x2="9" y2="20" />
35
+ </Svg>
36
+ );
37
+
38
+ export const StrikethroughIcon = (p: IconProps) => (
39
+ <Svg {...p}>
40
+ <path d="M16 4H9a3 3 0 0 0-2.83 4" />
41
+ <path d="M14 12a4 4 0 0 1 0 8H6" />
42
+ <line x1="4" y1="12" x2="20" y2="12" />
43
+ </Svg>
44
+ );
45
+
46
+ export const CodeIcon = (p: IconProps) => (
47
+ <Svg {...p}>
48
+ <polyline points="16 18 22 12 16 6" />
49
+ <polyline points="8 6 2 12 8 18" />
50
+ </Svg>
51
+ );
52
+
53
+ export const H1Icon = (p: IconProps) => (
54
+ <Svg {...p}>
55
+ <path d="M4 12h8" />
56
+ <path d="M4 18V6" />
57
+ <path d="M12 18V6" />
58
+ <path d="m17 12 3-2v8" />
59
+ </Svg>
60
+ );
61
+
62
+ export const H2Icon = (p: IconProps) => (
63
+ <Svg {...p}>
64
+ <path d="M4 12h8" />
65
+ <path d="M4 18V6" />
66
+ <path d="M12 18V6" />
67
+ <path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1" />
68
+ </Svg>
69
+ );
70
+
71
+ export const BulletListIcon = (p: IconProps) => (
72
+ <Svg {...p}>
73
+ <line x1="8" y1="6" x2="21" y2="6" />
74
+ <line x1="8" y1="12" x2="21" y2="12" />
75
+ <line x1="8" y1="18" x2="21" y2="18" />
76
+ <line x1="3" y1="6" x2="3.01" y2="6" />
77
+ <line x1="3" y1="12" x2="3.01" y2="12" />
78
+ <line x1="3" y1="18" x2="3.01" y2="18" />
79
+ </Svg>
80
+ );
81
+
82
+ export const OrderedListIcon = (p: IconProps) => (
83
+ <Svg {...p}>
84
+ <line x1="10" y1="6" x2="21" y2="6" />
85
+ <line x1="10" y1="12" x2="21" y2="12" />
86
+ <line x1="10" y1="18" x2="21" y2="18" />
87
+ <path d="M4 6h1v4" />
88
+ <path d="M4 10h2" />
89
+ <path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" />
90
+ </Svg>
91
+ );
92
+
93
+ export const QuoteIcon = (p: IconProps) => (
94
+ <Svg {...p}>
95
+ <path d="M17 6H3" />
96
+ <path d="M21 12H8" />
97
+ <path d="M21 18H8" />
98
+ <path d="M3 12v6" />
99
+ </Svg>
100
+ );
101
+
102
+ export const ExpandIcon = ({ expanded, size = 15 }: IconProps & { expanded: boolean }) => (
103
+ <Svg size={size}>
104
+ {expanded ? (
105
+ <>
106
+ <polyline points="4 14 10 14 10 20" />
107
+ <polyline points="20 10 14 10 14 4" />
108
+ <line x1="14" y1="10" x2="21" y2="3" />
109
+ <line x1="3" y1="21" x2="10" y2="14" />
110
+ </>
111
+ ) : (
112
+ <>
113
+ <polyline points="15 3 21 3 21 9" />
114
+ <polyline points="9 21 3 21 3 15" />
115
+ <line x1="21" y1="3" x2="14" y2="10" />
116
+ <line x1="3" y1="21" x2="10" y2="14" />
117
+ </>
118
+ )}
119
+ </Svg>
120
+ );
@@ -0,0 +1,23 @@
1
+ // Public API for @gogitcms/markdown-editor.
2
+ export { default as MarkdownEditor } from "./MarkdownEditor";
3
+ export { createMarkdownRenderField } from "./field";
4
+ export type { MarkdownFieldArgs, MarkdownFieldOptions } from "./field";
5
+ export type {
6
+ MarkdownEditorProps,
7
+ EditorFormat,
8
+ MarkdownFlavor,
9
+ EditorSource,
10
+ EditorTheme,
11
+ } from "./types";
12
+
13
+ // Format metadata + extension gating (shared with the schema layer).
14
+ export {
15
+ FORMATS,
16
+ extensionOf,
17
+ supportsFileBody,
18
+ } from "./formats/registry";
19
+ export type { FormatMeta } from "./formats/registry";
20
+
21
+ // Flavor registry + type, for hosts that want to introspect available dialects.
22
+ export { FLAVORS, resolveFlavor } from "./flavors/registry";
23
+ export type { Flavor } from "./flavors/types";