@medway-ui/notes-core 0.1.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 (57) hide show
  1. package/README.md +127 -0
  2. package/dist/document/normalize.d.ts +61 -0
  3. package/dist/document/normalize.d.ts.map +1 -0
  4. package/dist/document/normalize.js +373 -0
  5. package/dist/document/normalize.js.map +1 -0
  6. package/dist/document/types.d.ts +47 -0
  7. package/dist/document/types.d.ts.map +1 -0
  8. package/dist/document/types.js +9 -0
  9. package/dist/document/types.js.map +1 -0
  10. package/dist/editor/handle.d.ts +28 -0
  11. package/dist/editor/handle.d.ts.map +1 -0
  12. package/dist/editor/handle.js +34 -0
  13. package/dist/editor/handle.js.map +1 -0
  14. package/dist/editor/slashCommands.d.ts +21 -0
  15. package/dist/editor/slashCommands.d.ts.map +1 -0
  16. package/dist/editor/slashCommands.js +98 -0
  17. package/dist/editor/slashCommands.js.map +1 -0
  18. package/dist/editor/vocabulary.d.ts +110 -0
  19. package/dist/editor/vocabulary.d.ts.map +1 -0
  20. package/dist/editor/vocabulary.js +11 -0
  21. package/dist/editor/vocabulary.js.map +1 -0
  22. package/dist/index.d.ts +15 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +11 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/schema/contract.d.ts +14 -0
  27. package/dist/schema/contract.d.ts.map +1 -0
  28. package/dist/schema/contract.js +51 -0
  29. package/dist/schema/contract.js.map +1 -0
  30. package/dist/schema/extensions.d.ts +23 -0
  31. package/dist/schema/extensions.d.ts.map +1 -0
  32. package/dist/schema/extensions.js +81 -0
  33. package/dist/schema/extensions.js.map +1 -0
  34. package/dist/schema/image.d.ts +14 -0
  35. package/dist/schema/image.d.ts.map +1 -0
  36. package/dist/schema/image.js +28 -0
  37. package/dist/schema/image.js.map +1 -0
  38. package/dist/schema/index.d.ts +17 -0
  39. package/dist/schema/index.d.ts.map +1 -0
  40. package/dist/schema/index.js +18 -0
  41. package/dist/schema/index.js.map +1 -0
  42. package/dist/schema/slashCommand.d.ts +19 -0
  43. package/dist/schema/slashCommand.d.ts.map +1 -0
  44. package/dist/schema/slashCommand.js +146 -0
  45. package/dist/schema/slashCommand.js.map +1 -0
  46. package/package.json +134 -0
  47. package/src/document/normalize.ts +456 -0
  48. package/src/document/types.ts +74 -0
  49. package/src/editor/handle.ts +65 -0
  50. package/src/editor/slashCommands.ts +120 -0
  51. package/src/editor/vocabulary.ts +126 -0
  52. package/src/index.ts +52 -0
  53. package/src/schema/contract.ts +76 -0
  54. package/src/schema/extensions.ts +120 -0
  55. package/src/schema/image.ts +44 -0
  56. package/src/schema/index.ts +29 -0
  57. package/src/schema/slashCommand.ts +181 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Catalog behind the `/` menu. The editor filters ids by `keywords` as the
3
+ * student types; the host renders the labels and icons — natively on mobile, in
4
+ * the DOM on the web. Keeping one list is what stops the ends from disagreeing
5
+ * about which commands exist.
6
+ *
7
+ * Dependency-free on purpose: both the editor and every host read this list.
8
+ */
9
+
10
+ import type { SlashCommandId } from './vocabulary.js';
11
+
12
+ export type SlashCommandDefinition = {
13
+ id: SlashCommandId;
14
+ label: string;
15
+ description: string;
16
+ /** Lowercase, unaccented terms matched against the typed query. */
17
+ keywords: string[];
18
+ };
19
+
20
+ export const SLASH_COMMANDS: readonly SlashCommandDefinition[] = [
21
+ {
22
+ id: 'paragraph',
23
+ label: 'Texto',
24
+ description: 'Parágrafo simples',
25
+ keywords: ['texto', 'paragrafo', 'text', 'p'],
26
+ },
27
+ {
28
+ id: 'heading1',
29
+ label: 'Título 1',
30
+ description: 'Título de seção grande',
31
+ keywords: ['titulo1', 'titulo', 'h1', 'heading'],
32
+ },
33
+ {
34
+ id: 'heading2',
35
+ label: 'Título 2',
36
+ description: 'Título de seção médio',
37
+ keywords: ['titulo2', 'titulo', 'h2', 'heading'],
38
+ },
39
+ {
40
+ id: 'heading3',
41
+ label: 'Título 3',
42
+ description: 'Título de seção pequeno',
43
+ keywords: ['titulo3', 'titulo', 'h3', 'heading'],
44
+ },
45
+ {
46
+ id: 'bulletList',
47
+ label: 'Lista',
48
+ description: 'Lista com marcadores',
49
+ keywords: ['lista', 'bullet', 'marcador', 'ul'],
50
+ },
51
+ {
52
+ id: 'orderedList',
53
+ label: 'Lista numerada',
54
+ description: 'Lista ordenada',
55
+ keywords: ['lista', 'numerada', 'ordenada', 'ol', 'numero'],
56
+ },
57
+ {
58
+ id: 'taskList',
59
+ label: 'Checklist',
60
+ description: 'Lista de tarefas',
61
+ keywords: ['checklist', 'tarefa', 'todo', 'check', 'caixa'],
62
+ },
63
+ {
64
+ id: 'blockquote',
65
+ label: 'Citação',
66
+ description: 'Bloco de citação',
67
+ keywords: ['citacao', 'quote', 'aspas'],
68
+ },
69
+ {
70
+ id: 'codeBlock',
71
+ label: 'Código',
72
+ description: 'Bloco de código',
73
+ keywords: ['codigo', 'code', 'bloco'],
74
+ },
75
+ {
76
+ id: 'horizontalRule',
77
+ label: 'Divisor',
78
+ description: 'Linha divisória',
79
+ keywords: ['divisor', 'linha', 'separador', 'hr'],
80
+ },
81
+ {
82
+ id: 'image',
83
+ label: 'Imagem',
84
+ description: 'Insere uma imagem',
85
+ keywords: ['imagem', 'foto', 'image', 'picture', 'upload'],
86
+ },
87
+ ];
88
+
89
+ const ACCENTS = 'áàâãäéèêëíìîïóòôõöúùûüçñ';
90
+ const PLAIN = 'aaaaaeeeeiiiiooooouuuucn';
91
+
92
+ /** Lowercases and strips accents so "citacao" matches "citação". */
93
+ const normalizeTerm = (term: string) =>
94
+ term
95
+ .toLowerCase()
96
+ .trim()
97
+ .split('')
98
+ .map((char) => {
99
+ const accentIndex = ACCENTS.indexOf(char);
100
+ return accentIndex === -1 ? char : PLAIN[accentIndex];
101
+ })
102
+ .join('');
103
+
104
+ /** Filters the catalog by the text typed after `/`. Empty query lists all. */
105
+ export const filterSlashCommands = (query: string): SlashCommandId[] => {
106
+ const term = normalizeTerm(query);
107
+
108
+ if (!term) {
109
+ return SLASH_COMMANDS.map((command) => command.id);
110
+ }
111
+
112
+ return SLASH_COMMANDS.filter((command) =>
113
+ command.keywords.some((keyword) => normalizeTerm(keyword).startsWith(term))
114
+ ).map((command) => command.id);
115
+ };
116
+
117
+ export const getSlashCommand = (
118
+ id: SlashCommandId
119
+ ): SlashCommandDefinition | undefined =>
120
+ SLASH_COMMANDS.find((command) => command.id === id);
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The vocabulary a note editor speaks, independent of how it is hosted: what
3
+ * can be asked of an editor and what it reports back is identical on mobile
4
+ * (WebView + bridge) and web (in-process), so a toolbar written against these
5
+ * types works either way.
6
+ *
7
+ * The transport (envelopes, protocol version, injected global) must not land
8
+ * here: it only exists on React Native.
9
+ */
10
+
11
+ export type EditorMarkFlag =
12
+ | 'bold'
13
+ | 'italic'
14
+ | 'underline'
15
+ | 'strike'
16
+ | 'code'
17
+ | 'link';
18
+
19
+ export type EditorBlockFlag =
20
+ | 'paragraph'
21
+ | 'heading1'
22
+ | 'heading2'
23
+ | 'heading3'
24
+ | 'bulletList'
25
+ | 'orderedList'
26
+ | 'taskList'
27
+ | 'blockquote'
28
+ | 'codeBlock';
29
+
30
+ export type EditorActiveFlags = Record<
31
+ EditorMarkFlag | EditorBlockFlag,
32
+ boolean
33
+ >;
34
+
35
+ /** Small payload published on every selection change to drive the toolbar. */
36
+ export type EditorSelectionState = {
37
+ isFocused: boolean;
38
+ isEmpty: boolean;
39
+ canUndo: boolean;
40
+ canRedo: boolean;
41
+ canIndent: boolean;
42
+ canOutdent: boolean;
43
+ active: EditorActiveFlags;
44
+ /** `href` of the link under the cursor, when there is one. */
45
+ linkHref: string | null;
46
+ };
47
+
48
+ export type SlashCommandId =
49
+ | 'paragraph'
50
+ | 'heading1'
51
+ | 'heading2'
52
+ | 'heading3'
53
+ | 'bulletList'
54
+ | 'orderedList'
55
+ | 'taskList'
56
+ | 'blockquote'
57
+ | 'codeBlock'
58
+ | 'horizontalRule'
59
+ | 'image';
60
+
61
+ /**
62
+ * Caret rectangle in CSS pixels, relative to the editor's own viewport. The
63
+ * host places its menu from this — natively on mobile, in the DOM on the web.
64
+ */
65
+ export type EditorCaretRect = {
66
+ x: number;
67
+ y: number;
68
+ width: number;
69
+ height: number;
70
+ };
71
+
72
+ export type SlashMenuState = {
73
+ active: boolean;
74
+ query: string;
75
+ /** Ids matching `query`, in display order. Empty means "no match". */
76
+ commandIds: SlashCommandId[];
77
+ caret: EditorCaretRect | null;
78
+ };
79
+
80
+ /**
81
+ * Resolved design-system values handed to the editor surface. The editor never
82
+ * picks its own colors — the host resolves them (Restyle on mobile, CSS custom
83
+ * properties on web) so the document follows light/dark mode and the student's
84
+ * font-size preference.
85
+ */
86
+ export type EditorTheme = {
87
+ colorScheme: 'light' | 'dark';
88
+ background: string;
89
+ text: string;
90
+ heading: string;
91
+ muted: string;
92
+ placeholder: string;
93
+ border: string;
94
+ accent: string;
95
+ caret: string;
96
+ selection: string;
97
+ codeBackground: string;
98
+ codeText: string;
99
+ fontSize: number;
100
+ lineHeight: number;
101
+ paddingHorizontal: number;
102
+ paddingTop: number;
103
+ paddingBottom: number;
104
+ };
105
+
106
+ /** Everything a host may ask the editor to do. */
107
+ export type EditorCommand =
108
+ | { name: 'toggleBold' }
109
+ | { name: 'toggleItalic' }
110
+ | { name: 'toggleUnderline' }
111
+ | { name: 'toggleStrike' }
112
+ | { name: 'toggleCode' }
113
+ | { name: 'setParagraph' }
114
+ | { name: 'toggleHeading'; level: 1 | 2 | 3 }
115
+ | { name: 'toggleBulletList' }
116
+ | { name: 'toggleOrderedList' }
117
+ | { name: 'toggleTaskList' }
118
+ | { name: 'toggleBlockquote' }
119
+ | { name: 'toggleCodeBlock' }
120
+ | { name: 'setHorizontalRule' }
121
+ | { name: 'setLink'; href: string }
122
+ | { name: 'unsetLink' }
123
+ | { name: 'indent' }
124
+ | { name: 'outdent' }
125
+ | { name: 'undo' }
126
+ | { name: 'redo' };
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@medway-ui/notes-core` — the note editor's domain layer: the document model,
3
+ * its whitelist, and the vocabulary hosts drive an editor with.
4
+ *
5
+ * Hard constraint: this entry point is free of Tiptap, React, React Native and
6
+ * the DOM. Anything touching Tiptap lives behind the `/schema` subpath.
7
+ */
8
+
9
+ export type {
10
+ EditorAttributes,
11
+ EditorDocument,
12
+ EditorImageNode,
13
+ EditorLinkMark,
14
+ EditorMark,
15
+ EditorMarkType,
16
+ EditorNode,
17
+ EditorNodeType,
18
+ } from './document/types.js';
19
+
20
+ export {
21
+ EMPTY_DOCUMENT,
22
+ documentToPlainText,
23
+ documentsAreEqual,
24
+ isEditorDocument,
25
+ isEmptyDocument,
26
+ normalizeDocument,
27
+ sanitizeImageSrc,
28
+ sanitizeLinkHref,
29
+ toEditorContent,
30
+ } from './document/normalize.js';
31
+
32
+ export type {
33
+ EditorActiveFlags,
34
+ EditorBlockFlag,
35
+ EditorCaretRect,
36
+ EditorCommand,
37
+ EditorMarkFlag,
38
+ EditorSelectionState,
39
+ EditorTheme,
40
+ SlashCommandId,
41
+ SlashMenuState,
42
+ } from './editor/vocabulary.js';
43
+
44
+ export type { NoteEditorHandle } from './editor/handle.js';
45
+ export { EMPTY_SLASH_STATE, INITIAL_EDITOR_STATE } from './editor/handle.js';
46
+
47
+ export type { SlashCommandDefinition } from './editor/slashCommands.js';
48
+ export {
49
+ SLASH_COMMANDS,
50
+ filterSlashCommands,
51
+ getSlashCommand,
52
+ } from './editor/slashCommands.js';
@@ -0,0 +1,76 @@
1
+ import type { JSONContent } from '@tiptap/core';
2
+ import type { Schema } from '@tiptap/pm/model';
3
+
4
+ import type {
5
+ EditorDocument,
6
+ EditorMarkType,
7
+ EditorNodeType,
8
+ } from '../document/types.js';
9
+
10
+ /**
11
+ * Keeps the hand-written document types in `../document/types` honest: the
12
+ * type-level check below fails `tsc` if the mirror stops being assignable to
13
+ * Tiptap's `JSONContent`, and `assertSchemaContract` fails at runtime if a
14
+ * declared node or mark is missing from the built schema.
15
+ */
16
+ type AssignableToTiptap = EditorDocument extends JSONContent ? true : never;
17
+ const assignableToTiptap: AssignableToTiptap = true;
18
+
19
+ // `Record<Union, true>` instead of an array: adding a member without listing it
20
+ // here stops compiling.
21
+ const NODE_TYPE_MAP: Record<EditorNodeType, true> = {
22
+ doc: true,
23
+ paragraph: true,
24
+ text: true,
25
+ hardBreak: true,
26
+ heading: true,
27
+ bulletList: true,
28
+ orderedList: true,
29
+ listItem: true,
30
+ taskList: true,
31
+ taskItem: true,
32
+ blockquote: true,
33
+ codeBlock: true,
34
+ horizontalRule: true,
35
+ image: true,
36
+ };
37
+
38
+ const MARK_TYPE_MAP: Record<EditorMarkType, true> = {
39
+ bold: true,
40
+ italic: true,
41
+ underline: true,
42
+ strike: true,
43
+ code: true,
44
+ link: true,
45
+ };
46
+
47
+ export const NODE_TYPES = Object.keys(NODE_TYPE_MAP) as EditorNodeType[];
48
+
49
+ export const MARK_TYPES = Object.keys(MARK_TYPE_MAP) as EditorMarkType[];
50
+
51
+ /**
52
+ * Returns every disagreement between the model and the built schema, in both
53
+ * directions: declared but missing (`node:`/`mark:`), and present in the schema
54
+ * but not the model (`extra-node:`/`extra-mark:`, which is how `extra` in
55
+ * `buildExtensions` can smuggle a type past this file). Either direction means
56
+ * one client can write JSON another client's `normalizeDocument` silently
57
+ * drops — treat any result as fatal in dev, not just log it.
58
+ */
59
+ export const assertSchemaContract = (schema: Schema): string[] => {
60
+ void assignableToTiptap;
61
+
62
+ return [
63
+ ...NODE_TYPES.filter((name) => !(name in schema.nodes)).map(
64
+ (name) => `node:${name}`
65
+ ),
66
+ ...MARK_TYPES.filter((name) => !(name in schema.marks)).map(
67
+ (name) => `mark:${name}`
68
+ ),
69
+ ...Object.keys(schema.nodes)
70
+ .filter((name) => !(name in NODE_TYPE_MAP))
71
+ .map((name) => `extra-node:${name}`),
72
+ ...Object.keys(schema.marks)
73
+ .filter((name) => !(name in MARK_TYPE_MAP))
74
+ .map((name) => `extra-mark:${name}`),
75
+ ];
76
+ };
@@ -0,0 +1,120 @@
1
+ import type { AnyExtension } from '@tiptap/core';
2
+ import Blockquote from '@tiptap/extension-blockquote';
3
+ import Bold from '@tiptap/extension-bold';
4
+ import Code from '@tiptap/extension-code';
5
+ import CodeBlock from '@tiptap/extension-code-block';
6
+ import Document from '@tiptap/extension-document';
7
+ import HardBreak from '@tiptap/extension-hard-break';
8
+ import Heading from '@tiptap/extension-heading';
9
+ import HorizontalRule from '@tiptap/extension-horizontal-rule';
10
+ import Italic from '@tiptap/extension-italic';
11
+ import Link from '@tiptap/extension-link';
12
+ import Paragraph from '@tiptap/extension-paragraph';
13
+ import Strike from '@tiptap/extension-strike';
14
+ import Text from '@tiptap/extension-text';
15
+ import Underline from '@tiptap/extension-underline';
16
+ import {
17
+ BulletList,
18
+ ListItem,
19
+ ListKeymap,
20
+ OrderedList,
21
+ TaskItem,
22
+ TaskList,
23
+ } from '@tiptap/extension-list';
24
+ import {
25
+ Dropcursor,
26
+ Gapcursor,
27
+ Placeholder,
28
+ Selection,
29
+ TrailingNode,
30
+ UndoRedo,
31
+ } from '@tiptap/extensions';
32
+
33
+ import { Image } from './image.js';
34
+
35
+ const DEFAULT_PLACEHOLDER =
36
+ 'Comece a escrever ou digite "/" para comandos';
37
+ const DEFAULT_CARET_HINT = 'Digite "/" para comandos';
38
+
39
+ export type BuildExtensionsOptions = {
40
+ /** Shown while the whole note is blank. */
41
+ placeholder?: string;
42
+ /** Shown on whichever empty paragraph currently holds the caret. */
43
+ caretHint?: string;
44
+ /**
45
+ * Appended after the schema — the slash-command extension from
46
+ * `createSlashCommand`, and anything else a host adds. Nothing here may
47
+ * introduce a node or a mark: the schema is what this file declares, and
48
+ * `assertSchemaContract` checks that claim.
49
+ */
50
+ extra?: AnyExtension[];
51
+ };
52
+
53
+ /**
54
+ * The document schema, and the single place where it is declared: a second
55
+ * declaration would let two clients write documents the other reads wrong.
56
+ *
57
+ * Node and mark names end up in the persisted JSON and must stay in sync with
58
+ * `EditorNodeType` / `EditorMarkType` in `../document/types`.
59
+ */
60
+ export const buildExtensions = ({
61
+ placeholder = DEFAULT_PLACEHOLDER,
62
+ caretHint = DEFAULT_CARET_HINT,
63
+ extra = [],
64
+ }: BuildExtensionsOptions = {}): AnyExtension[] => [
65
+ Document,
66
+ Paragraph,
67
+ Text,
68
+ HardBreak,
69
+ Heading.configure({ levels: [1, 2, 3] }),
70
+ BulletList,
71
+ OrderedList,
72
+ ListItem,
73
+ TaskList,
74
+ TaskItem.configure({ nested: true }),
75
+ Blockquote,
76
+ CodeBlock,
77
+ HorizontalRule,
78
+ Image,
79
+
80
+ Bold,
81
+ Italic,
82
+ Underline,
83
+ Strike,
84
+ Code,
85
+ Link.configure({
86
+ // Clicks are reported to the host, which decides what to open. The editor
87
+ // surface must never navigate away from the document it is editing.
88
+ openOnClick: false,
89
+ autolink: true,
90
+ linkOnPaste: true,
91
+ protocols: ['http', 'https', 'mailto'],
92
+ HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
93
+ }),
94
+
95
+ UndoRedo,
96
+ ListKeymap,
97
+ Dropcursor,
98
+ Gapcursor,
99
+ // Keeps the selection visible while the toolbar holds focus instead of the
100
+ // editor — the normal case on mobile.
101
+ Selection,
102
+ // Always an empty paragraph to tap at the end of the note.
103
+ TrailingNode,
104
+ Placeholder.configure({
105
+ placeholder: ({ editor, node, pos, hasAnchor }) => {
106
+ if (node.type.name !== 'paragraph') return '';
107
+
108
+ // `pos === 0` is required: a blank note stays `isEmpty` after Enter adds
109
+ // a second paragraph, so without it both paragraphs show the note-level
110
+ // hint at once and it reads as duplicated/stuck placeholder text.
111
+ if (editor.isEmpty && pos === 0) return placeholder;
112
+
113
+ return hasAnchor ? caretHint : '';
114
+ },
115
+ showOnlyCurrent: false,
116
+ includeChildren: false,
117
+ }),
118
+
119
+ ...extra,
120
+ ];
@@ -0,0 +1,44 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+
3
+ // Reimplements the minimum of `@tiptap/extension-image` (not a dependency of
4
+ // this package); getting a `src` is host UI.
5
+ export const Image = Node.create({
6
+ name: 'image',
7
+ group: 'block',
8
+ atom: true,
9
+ selectable: true,
10
+ draggable: true,
11
+
12
+ addAttributes() {
13
+ return {
14
+ src: { default: null },
15
+ alt: { default: null },
16
+ };
17
+ },
18
+
19
+ parseHTML() {
20
+ return [{ tag: 'img[src]' }];
21
+ },
22
+
23
+ renderHTML({ HTMLAttributes }) {
24
+ return ['img', mergeAttributes(HTMLAttributes)];
25
+ },
26
+
27
+ addCommands() {
28
+ return {
29
+ setImage:
30
+ (attrs: { src: string; alt?: string | null }) =>
31
+ ({ commands }) =>
32
+ commands.insertContent({ type: this.name, attrs }),
33
+ };
34
+ },
35
+ });
36
+
37
+ declare module '@tiptap/core' {
38
+ interface Commands<ReturnType> {
39
+ image: {
40
+ /** Inserts an image node at the current selection. */
41
+ setImage: (attrs: { src: string; alt?: string | null }) => ReturnType;
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@medway-ui/notes-core/schema` — the Tiptap half, behind its own subpath so
3
+ * that importing the document model never pulls Tiptap in.
4
+ *
5
+ * `Editor`, `generateHTML` and `generateJSON` are re-exported to guarantee a
6
+ * single ProseMirror copy, and must always be passed the exact array
7
+ * `buildExtensions()` returns.
8
+ */
9
+
10
+ export { Editor, generateHTML, generateJSON } from '@tiptap/core';
11
+ export type { AnyExtension, JSONContent } from '@tiptap/core';
12
+
13
+ export type { BuildExtensionsOptions } from './extensions.js';
14
+ export { buildExtensions } from './extensions.js';
15
+
16
+ // Re-exported so hosts can build custom extension lists, and — less obviously
17
+ // — so `./image`'s `declare module '@tiptap/core'` augmentation (the
18
+ // `setImage` command) ships: `buildExtensions`'s signature never mentions
19
+ // `Image`, so declaration emission would otherwise drop the import.
20
+ export { Image } from './image.js';
21
+
22
+ export type { SlashCommandController } from './slashCommand.js';
23
+ export { createSlashCommand } from './slashCommand.js';
24
+
25
+ export {
26
+ MARK_TYPES,
27
+ NODE_TYPES,
28
+ assertSchemaContract,
29
+ } from './contract.js';