fountainjs-editor 0.1.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fountainjs-editor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A modular, extensible rich text editor library for React and other frameworks",
5
5
  "author": "Your Name <your.email@example.com>",
6
6
  "license": "MIT",
@@ -73,4 +73,4 @@
73
73
  "vite": "^4.3.9",
74
74
  "vite-tsconfig-paths": "^5.1.4"
75
75
  }
76
- }
76
+ }
@@ -0,0 +1,118 @@
1
+ import { EditorState } from '../../core/state';
2
+ import { Node } from '../../core/schema/node';
3
+
4
+ /**
5
+ * Export editor content to HTML
6
+ * Supports all node types with syntax highlighting for code blocks
7
+ */
8
+ export class HTMLExporter {
9
+ private highlightCode(code: string, language: string): string {
10
+ // Using a simple approach - in production, use Highlight.js or Prism.js
11
+ // For now, just escape and wrap
12
+ const escaped = this.escapeHtml(code);
13
+ return `<pre><code class="language-${language}">${escaped}</code></pre>`;
14
+ }
15
+
16
+ private escapeHtml(text: string): string {
17
+ const map: { [key: string]: string } = {
18
+ '&': '&amp;',
19
+ '<': '&lt;',
20
+ '>': '&gt;',
21
+ '"': '&quot;',
22
+ "'": '&#039;',
23
+ };
24
+ return text.replace(/[&<>"']/g, (char) => map[char]);
25
+ }
26
+
27
+ private nodeToHtml(node: Node): string {
28
+ switch (node.type.name) {
29
+ case 'doc':
30
+ return node.content.map((child) => this.nodeToHtml(child)).join('\n');
31
+
32
+ case 'heading':
33
+ const level = node.attrs.level || 1;
34
+ const headingContent = node.content
35
+ .map((child) => this.nodeToHtml(child))
36
+ .join('');
37
+ return `<h${level}>${headingContent}</h${level}>`;
38
+
39
+ case 'paragraph':
40
+ const pContent = node.content.map((child) => this.nodeToHtml(child)).join('');
41
+ return `<p>${pContent}</p>`;
42
+
43
+ case 'text':
44
+ let text = node.text || '';
45
+ // Apply marks
46
+ if (node.marks) {
47
+ if (node.marks.some((m) => m.type === 'strong')) {
48
+ text = `<strong>${text}</strong>`;
49
+ }
50
+ if (node.marks.some((m) => m.type === 'em')) {
51
+ text = `<em>${text}</em>`;
52
+ }
53
+ }
54
+ return text;
55
+
56
+ case 'code-block':
57
+ const code = node.content.map((child) => child.text || '').join('\n');
58
+ const language = node.attrs.language || 'javascript';
59
+ return this.highlightCode(code, language);
60
+
61
+ case 'bullet-list':
62
+ const items = node.content
63
+ .map((child) => this.nodeToHtml(child))
64
+ .join('');
65
+ return `<ul>${items}</ul>`;
66
+
67
+ case 'list-item':
68
+ const liContent = node.content.map((child) => this.nodeToHtml(child)).join('');
69
+ return `<li>${liContent}</li>`;
70
+
71
+ case 'table':
72
+ const rows = node.content
73
+ .map((child) => this.nodeToHtml(child))
74
+ .join('');
75
+ return `<table><tbody>${rows}</tbody></table>`;
76
+
77
+ case 'table-row':
78
+ const cells = node.content
79
+ .map((child) => this.nodeToHtml(child))
80
+ .join('');
81
+ return `<tr>${cells}</tr>`;
82
+
83
+ case 'table-cell':
84
+ const cellContent = node.content.map((child) => this.nodeToHtml(child)).join('');
85
+ return `<td>${cellContent}</td>`;
86
+
87
+ case 'image':
88
+ return `<img src="${node.attrs.src}" alt="${node.attrs.alt || ''}" style="max-width: 100%; border-radius: 8px; margin: 10px 0;" />`;
89
+
90
+ default:
91
+ return '';
92
+ }
93
+ }
94
+
95
+ export(state: EditorState): string {
96
+ const htmlContent = this.nodeToHtml(state.doc);
97
+ return `<!DOCTYPE html>
98
+ <html>
99
+ <head>
100
+ <meta charset="UTF-8">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
102
+ <style>
103
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
104
+ h1, h2, h3 { margin-top: 24px; margin-bottom: 16px; }
105
+ code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; }
106
+ pre { background: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; }
107
+ table { border-collapse: collapse; width: 100%; margin: 16px 0; }
108
+ table td, table th { border: 1px solid #ddd; padding: 8px; }
109
+ img { max-width: 100%; height: auto; }
110
+ ul, ol { margin: 16px 0; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ ${htmlContent}
115
+ </body>
116
+ </html>`;
117
+ }
118
+ }
@@ -0,0 +1,47 @@
1
+ import { EditorState } from '../../core/state';
2
+ import { Node } from '../../core/schema/node';
3
+
4
+ /**
5
+ * Export editor content to JSON format
6
+ * Preserves full structure for reimporting
7
+ */
8
+ export class JSONExporter {
9
+ private nodeToJSON(node: Node): any {
10
+ const json: any = {
11
+ type: node.type.name,
12
+ };
13
+
14
+ if (node.attrs && Object.keys(node.attrs).length > 0) {
15
+ json.attrs = node.attrs;
16
+ }
17
+
18
+ if (node.text) {
19
+ json.text = node.text;
20
+ }
21
+
22
+ if (node.marks && node.marks.length > 0) {
23
+ json.marks = node.marks.map((m) => ({
24
+ type: m.type,
25
+ attrs: m.attrs,
26
+ }));
27
+ }
28
+
29
+ if (node.content && node.content.length > 0) {
30
+ json.content = node.content.map((child) => this.nodeToJSON(child));
31
+ }
32
+
33
+ return json;
34
+ }
35
+
36
+ export(state: EditorState): string {
37
+ const json = this.nodeToJSON(state.doc);
38
+ return JSON.stringify(json, null, 2);
39
+ }
40
+
41
+ /**
42
+ * Import from JSON (for round-trip serialization)
43
+ */
44
+ static import(json: string): any {
45
+ return JSON.parse(json);
46
+ }
47
+ }
@@ -0,0 +1,83 @@
1
+ import { EditorState } from '../../core/state';
2
+ import { Node } from '../../core/schema/node';
3
+
4
+ /**
5
+ * Export editor content to Markdown
6
+ * Preserves all formatting and supports code blocks
7
+ */
8
+ export class MarkdownExporter {
9
+ private nodeToMarkdown(node: Node, depth: number = 0): string {
10
+ const indent = ' '.repeat(depth);
11
+
12
+ switch (node.type.name) {
13
+ case 'doc':
14
+ return node.content.map((child) => this.nodeToMarkdown(child, depth)).join('\n\n');
15
+
16
+ case 'heading':
17
+ const level = node.attrs.level || 1;
18
+ const headingContent = node.content
19
+ .map((child) => this.nodeToMarkdown(child, depth))
20
+ .join('');
21
+ return `${'#'.repeat(level)} ${headingContent}`;
22
+
23
+ case 'paragraph':
24
+ return node.content.map((child) => this.nodeToMarkdown(child, depth)).join('');
25
+
26
+ case 'text':
27
+ let text = node.text || '';
28
+ // Apply marks
29
+ if (node.marks) {
30
+ if (node.marks.some((m) => m.type === 'strong')) {
31
+ text = `**${text}**`;
32
+ }
33
+ if (node.marks.some((m) => m.type === 'em')) {
34
+ text = `*${text}*`;
35
+ }
36
+ }
37
+ return text;
38
+
39
+ case 'code-block':
40
+ const code = node.content.map((child) => child.text || '').join('\n');
41
+ const language = node.attrs.language || '';
42
+ return `\`\`\`${language}\n${code}\n\`\`\``;
43
+
44
+ case 'bullet-list':
45
+ return node.content
46
+ .map((child) => {
47
+ const content = this.nodeToMarkdown(child, depth + 1);
48
+ return `${indent}- ${content}`;
49
+ })
50
+ .join('\n');
51
+
52
+ case 'list-item':
53
+ return node.content.map((child) => this.nodeToMarkdown(child, depth)).join('');
54
+
55
+ case 'table':
56
+ let table = '';
57
+ const rows = node.content as Node[];
58
+ rows.forEach((row, rowIdx) => {
59
+ const cells = (row.content as Node[])
60
+ .map((cell) => this.nodeToMarkdown(cell, depth))
61
+ .join(' | ');
62
+ table += `| ${cells} |\n`;
63
+ if (rowIdx === 0) {
64
+ table += `| ${cells.split(' | ').map(() => '---').join(' | ')} |\n`;
65
+ }
66
+ });
67
+ return table;
68
+
69
+ case 'table-cell':
70
+ return node.content.map((child) => this.nodeToMarkdown(child, depth)).join('');
71
+
72
+ case 'image':
73
+ return `![${node.attrs.alt || 'image'}](${node.attrs.src})`;
74
+
75
+ default:
76
+ return '';
77
+ }
78
+ }
79
+
80
+ export(state: EditorState): string {
81
+ return this.nodeToMarkdown(state.doc);
82
+ }
83
+ }
@@ -10,6 +10,7 @@ import { tableRow } from './nodes/table-row';
10
10
  import { tableCell } from './nodes/table-cell';
11
11
  import { bulletList } from './nodes/bullet-list';
12
12
  import { listItem } from './nodes/list-item';
13
+ import { codeBlock } from './nodes/code-block';
13
14
 
14
15
  // All Mark imports
15
16
  import { strong } from './marks/strong';
@@ -18,6 +19,8 @@ import { em } from './marks/em';
18
19
  // All Plugin imports
19
20
  import { historyPlugin } from './plugins/history';
20
21
  import { markdownShortcutsPlugin } from './plugins/markdown-shortcuts';
22
+ import { SyntaxHighlightPlugin } from './plugins/syntax-highlight';
23
+ import { MCPIntegration } from './plugins/mcp-integration';
21
24
 
22
25
  // Core imports for defining the schema
23
26
  import { SchemaSpec } from '../core';
@@ -34,12 +37,15 @@ export { tableRow } from './nodes/table-row';
34
37
  export { tableCell } from './nodes/table-cell';
35
38
  export { bulletList } from './nodes/bullet-list';
36
39
  export { listItem } from './nodes/list-item';
40
+ export { codeBlock } from './nodes/code-block';
37
41
 
38
42
  export { strong } from './marks/strong';
39
43
  export { em } from './marks/em';
40
44
 
41
45
  export { historyPlugin, undo, redo } from './plugins/history';
42
46
  export { markdownShortcutsPlugin } from './plugins/markdown-shortcuts';
47
+ export { SyntaxHighlightPlugin } from './plugins/syntax-highlight';
48
+ export { MCPIntegration, type ContentTransformRequest, type MCPTool, generateContentWithAI } from './plugins/mcp-integration';
43
49
 
44
50
 
45
51
  // --- Define and Export the Core Schema Spec (ONCE) ---
@@ -56,6 +62,7 @@ export const CoreSchemaSpec: SchemaSpec = {
56
62
  table_cell: tableCell,
57
63
  bullet_list: bulletList,
58
64
  list_item: listItem,
65
+ code_block: codeBlock,
59
66
  },
60
67
  marks: {
61
68
  strong,
@@ -0,0 +1,40 @@
1
+ import { NodeSpec } from '../../core/schema/node-spec';
2
+
3
+ export const CodeBlockNodeSpec: NodeSpec = {
4
+ name: 'code-block',
5
+ group: 'block',
6
+ atom: false,
7
+ code: true,
8
+ attrs: {
9
+ language: { default: 'javascript' },
10
+ lineNumbers: { default: false },
11
+ },
12
+ parseDOM: [
13
+ {
14
+ tag: 'pre',
15
+ preserveWhitespace: 'full',
16
+ getAttrs(dom: any) {
17
+ return {
18
+ language: dom.getAttribute('data-language') || 'javascript',
19
+ lineNumbers: dom.getAttribute('data-line-numbers') === 'true',
20
+ };
21
+ },
22
+ },
23
+ ],
24
+ toDOM() {
25
+ return [
26
+ 'pre',
27
+ {
28
+ 'data-language': this.attrs.language,
29
+ 'data-line-numbers': this.attrs.lineNumbers ? 'true' : 'false',
30
+ class: `language-${this.attrs.language}`,
31
+ },
32
+ ['code', 0],
33
+ ];
34
+ },
35
+ };
36
+
37
+ export const codeBlock = {
38
+ ...CodeBlockNodeSpec,
39
+ isInline: false,
40
+ };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Integration
3
+ *
4
+ * Allows FountainJS to work with ANY AI system that supports MCP
5
+ * AI-agnostic, language-agnostic, and framework-agnostic
6
+ *
7
+ * Example MCP servers:
8
+ * - OpenAI (via MCP bridges)
9
+ * - Anthropic Claude
10
+ * - Google Gemini
11
+ * - Open source LLMs
12
+ * - Custom enterprise LLMs
13
+ */
14
+
15
+ export interface MCPToolInput {
16
+ [key: string]: string | number | boolean;
17
+ }
18
+
19
+ export interface MCPTool {
20
+ name: string;
21
+ description: string;
22
+ inputSchema?: {
23
+ type: 'object';
24
+ properties: { [key: string]: any };
25
+ required: string[];
26
+ };
27
+ }
28
+
29
+ export interface MCPContentBlock {
30
+ type: 'text' | 'tool_use';
31
+ text?: string;
32
+ id?: string;
33
+ name?: string;
34
+ input?: MCPToolInput;
35
+ }
36
+
37
+ export interface MCPRequest {
38
+ content: string;
39
+ tools?: MCPTool[];
40
+ systemPrompt?: string;
41
+ }
42
+
43
+ export interface MCPResponse {
44
+ content: MCPContentBlock[];
45
+ stopReason: string;
46
+ }
47
+
48
+ /**
49
+ * AI-agnostic content transformation request
50
+ * Send this to any MCP server, get back improved content
51
+ */
52
+ export interface ContentTransformRequest {
53
+ content: string;
54
+ contentType: 'markdown' | 'json' | 'html' | 'fountain';
55
+ operation: 'generate' | 'improve' | 'transform' | 'summarize' | 'expand';
56
+ context?: string;
57
+ language?: string;
58
+ }
59
+
60
+ /**
61
+ * MCP Integration Plugin
62
+ * Works with any AI service that supports Model Context Protocol
63
+ */
64
+ export class MCPIntegration {
65
+ private mcpServerUrl?: string;
66
+ private tools: MCPTool[] = [];
67
+
68
+ constructor(mcpServerUrl?: string) {
69
+ this.mcpServerUrl = mcpServerUrl;
70
+ this.registerDefaultTools();
71
+ }
72
+
73
+ private registerDefaultTools(): void {
74
+ this.tools = [
75
+ {
76
+ name: 'generate_content',
77
+ description: 'Generate new content in specified format',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ prompt: {
82
+ type: 'string',
83
+ description: 'What to generate',
84
+ },
85
+ format: {
86
+ type: 'string',
87
+ enum: ['markdown', 'html', 'json', 'fountain'],
88
+ description: 'Output format',
89
+ },
90
+ language: {
91
+ type: 'string',
92
+ description: 'Programming language (if code)',
93
+ },
94
+ },
95
+ required: ['prompt', 'format'],
96
+ },
97
+ },
98
+ {
99
+ name: 'improve_content',
100
+ description: 'Improve existing content',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ content: {
105
+ type: 'string',
106
+ description: 'Content to improve',
107
+ },
108
+ aspect: {
109
+ type: 'string',
110
+ enum: ['clarity', 'grammar', 'tone', 'structure'],
111
+ description: 'What to improve',
112
+ },
113
+ },
114
+ required: ['content', 'aspect'],
115
+ },
116
+ },
117
+ {
118
+ name: 'transform_format',
119
+ description: 'Transform content between formats',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ content: {
124
+ type: 'string',
125
+ description: 'Content to transform',
126
+ },
127
+ fromFormat: {
128
+ type: 'string',
129
+ enum: ['markdown', 'html', 'json', 'fountain', 'text'],
130
+ },
131
+ toFormat: {
132
+ type: 'string',
133
+ enum: ['markdown', 'html', 'json', 'fountain'],
134
+ },
135
+ },
136
+ required: ['content', 'fromFormat', 'toFormat'],
137
+ },
138
+ },
139
+ ];
140
+ }
141
+
142
+ /**
143
+ * Connect to an MCP server
144
+ * Server can be hosted anywhere - local, cloud, enterprise
145
+ */
146
+ async connectToMCPServer(url: string): Promise<void> {
147
+ this.mcpServerUrl = url;
148
+ // In real implementation, establish WebSocket/HTTP connection
149
+ console.log(`Connected to MCP server: ${url}`);
150
+ }
151
+
152
+ /**
153
+ * Register custom tools for specific AI use cases
154
+ */
155
+ registerTool(tool: MCPTool): void {
156
+ this.tools.push(tool);
157
+ }
158
+
159
+ /**
160
+ * Get available tools for this AI
161
+ */
162
+ getAvailableTools(): MCPTool[] {
163
+ return this.tools;
164
+ }
165
+
166
+ /**
167
+ * Transform content using AI through MCP
168
+ * Works with ANY MCP-compatible AI service
169
+ */
170
+ async transformContent(request: ContentTransformRequest): Promise<string> {
171
+ if (!this.mcpServerUrl) {
172
+ throw new Error('MCP server not configured. Call connectToMCPServer() first.');
173
+ }
174
+
175
+ const systemPrompt = this.buildSystemPrompt(request);
176
+ const userPrompt = this.buildUserPrompt(request);
177
+
178
+ const mcpRequest: MCPRequest = {
179
+ content: userPrompt,
180
+ systemPrompt,
181
+ tools: this.tools,
182
+ };
183
+
184
+ // Send to MCP server (implementation depends on server)
185
+ const response = await this.sendToMCP(mcpRequest);
186
+
187
+ // Extract content from MCP response
188
+ return this.extractContent(response);
189
+ }
190
+
191
+ private buildSystemPrompt(request: ContentTransformRequest): string {
192
+ return `You are a helpful content transformation AI.
193
+ The user has content in ${request.contentType} format.
194
+ Help them ${request.operation} their content.
195
+ ${request.language ? `Programming language: ${request.language}` : ''}
196
+ ${request.context ? `Context: ${request.context}` : ''}`;
197
+ }
198
+
199
+ private buildUserPrompt(request: ContentTransformRequest): string {
200
+ switch (request.operation) {
201
+ case 'generate':
202
+ return `Generate new content: ${request.content}`;
203
+ case 'improve':
204
+ return `Improve this content:\n${request.content}`;
205
+ case 'transform':
206
+ return `Transform this content to a better format:\n${request.content}`;
207
+ case 'summarize':
208
+ return `Summarize this content:\n${request.content}`;
209
+ case 'expand':
210
+ return `Expand on this content:\n${request.content}`;
211
+ default:
212
+ return request.content;
213
+ }
214
+ }
215
+
216
+ private async sendToMCP(request: MCPRequest): Promise<MCPResponse> {
217
+ // This is a placeholder - real implementation would:
218
+ // 1. Connect to MCP server (WebSocket or HTTP)
219
+ // 2. Send request in MCP format
220
+ // 3. Wait for response
221
+ // 4. Handle tool calls if needed
222
+
223
+ if (!this.mcpServerUrl) {
224
+ throw new Error('MCP server URL not set');
225
+ }
226
+
227
+ try {
228
+ const response = await fetch(`${this.mcpServerUrl}/messages`, {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify(request),
232
+ });
233
+
234
+ if (!response.ok) {
235
+ throw new Error(`MCP server error: ${response.statusText}`);
236
+ }
237
+
238
+ return await response.json();
239
+ } catch (error) {
240
+ console.error('MCP request failed:', error);
241
+ throw error;
242
+ }
243
+ }
244
+
245
+ private extractContent(response: MCPResponse): string {
246
+ const textBlocks = response.content.filter((block) => block.type === 'text');
247
+ return textBlocks.map((block) => block.text || '').join('\n');
248
+ }
249
+ }
250
+
251
+ /**
252
+ * AI-agnostic content generation
253
+ * Can be used with any MCP-compatible service
254
+ */
255
+ export async function generateContentWithAI(
256
+ prompt: string,
257
+ mcpServer: MCPIntegration,
258
+ outputFormat: 'markdown' | 'json' | 'html' = 'markdown'
259
+ ): Promise<string> {
260
+ const request: ContentTransformRequest = {
261
+ content: prompt,
262
+ contentType: outputFormat,
263
+ operation: 'generate',
264
+ };
265
+
266
+ return mcpServer.transformContent(request);
267
+ }