Skip to content
Closed
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 docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ cd packages/superdoc
npm install && npm run dev
```

This will run **SuperdocDev.vue**, with a Vue 3 based example of how to instantiate SuperDoc.
This will run **Playground.vue**, with a Vue 3 based example of how to instantiate SuperDoc.

### SuperEditor Package

Expand Down
2,421 changes: 1,448 additions & 973 deletions package-lock.json

Large diffs are not rendered by default.

23 changes: 13 additions & 10 deletions packages/super-editor/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Super Editor - Dev mode</title>
</head>
<body>
<div id="app" role="application"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SuperEditor - Playground</title>
</head>

<body>
<div id="app" role="application"></div>
<script type="module" src="/src/main.js"></script>
</body>

</html>
1 change: 1 addition & 0 deletions packages/super-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"eventemitter3": "^5.0.1",
"he": "^1.2.0",
"jszip": "^3.10.1",
"marked": "^14.1.4",
"prosemirror-commands": "^1.5.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
Expand Down
179 changes: 155 additions & 24 deletions packages/super-editor/src/core/Editor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { DOMParser, DOMSerializer } from 'prosemirror-model';
import { marked } from 'marked';
import { yXmlFragmentToProseMirrorRootNode } from 'y-prosemirror';
import { helpers } from '@core/index.js';
import { EventEmitter } from './EventEmitter.js';
Expand Down Expand Up @@ -131,6 +132,7 @@ import { SuperValidator } from '@core/super-validator/index.js';
* @property {boolean} [suppressDefaultDocxStyles] - Prevent default styles from being applied in docx mode
* @property {boolean} [jsonOverride] - Whether to override content with provided json
* @property {string} [html] - HTML content to initialize the editor with
* @property {string} [markdown] - Markdown content to initialize the editor with
*/

/**
Expand Down Expand Up @@ -249,6 +251,29 @@ export class Editor extends EventEmitter {
focusTarget: null,
};

/**
* Normalize content options to use consistent mode + content pattern
* @private
*/
#normalizeContentOptions() {
// Handle markdown convenience attribute
if (this.options.markdown) {
this.options.mode = 'markdown';
this.options.content = this.options.markdown;
}

// Future: Can add other formats here
// if (this.options.rst) {
// this.options.mode = 'rst';
// this.options.content = this.options.rst;
// }

// if (this.options.latex) {
// this.options.mode = 'latex';
// this.options.content = this.options.latex;
// }
}

/**
* Create a new Editor instance
* @param {EditorOptions} options - Editor configuration options
Expand All @@ -261,10 +286,14 @@ export class Editor extends EventEmitter {
this.#checkHeadless(options);
this.setOptions(options);

// Normalize content options for consistency
this.#normalizeContentOptions();

let modes = {
docx: () => this.#init(),
text: () => this.#initRichText(),
html: () => this.#initRichText(),
markdown: () => this.#initRichText(),
default: () => {
console.log('Not implemented.');
},
Expand Down Expand Up @@ -934,43 +963,87 @@ export class Editor extends EventEmitter {
* @returns {Object} ProseMirror data
*/
#generatePmData() {
let doc;

try {
const { mode, fragment, content, loadFromSchema } = this.options;

if (mode === 'docx') {
if (loadFromSchema) {
doc = this.schema.nodeFromJSON(content);
doc = this.#prepareDocumentForImport(doc);
} else {
doc = createDocument(this.converter, this.schema, this);

// Perform any additional document processing prior to finalizing the doc here
doc = this.#prepareDocumentForImport(doc);
// Handle schema loading (same for all modes)
if (loadFromSchema && content) {
return this.#prepareDocumentForImport(this.schema.nodeFromJSON(content));
}

// If we have a new doc, and have html data, we initialize from html
if (this.options.html) doc = this.#createDocFromHTML(this.options.html);
else if (this.options.jsonOverride) doc = this.schema.nodeFromJSON(this.options.jsonOverride);
// Route to appropriate handler based on mode
let doc = this.#createDocumentForMode(mode);

if (fragment) doc = yXmlFragmentToProseMirrorRootNode(fragment, this.schema);
}
}
// Apply any overrides or fragments (mostly for docx mode)
doc = this.#applyDocumentOverrides(doc, fragment);

// If we are in HTML mode, we initialize from either content or html (or blank)
else if (mode === 'text' || mode === 'html') {
if (loadFromSchema) doc = this.schema.nodeFromJSON(content);
else if (content) doc = this.#createDocFromHTML(content);
else doc = this.schema.topNodeType.createAndFill();
}
return doc;
} catch (err) {
console.error(err);
console.error('[SuperDoc] Document generation failed:', err);
this.emit('contentError', { editor: this, error: err });
// Return empty doc as fallback
return this.schema.topNodeType.createAndFill();
}
}

/**
* Create document based on mode
* @private
* @param {string} mode - The document mode
* @returns {Object} ProseMirror document
*/
#createDocumentForMode(mode) {
const { content } = this.options;

switch (mode) {
case 'markdown':
return this.#createDocFromMarkdown(content || this.options.markdown);

case 'html':
case 'text':
return content ? this.#createDocFromHTML(content) : this.schema.topNodeType.createAndFill();

case 'docx':
return this.#createDocxDocument();

default:
return this.schema.topNodeType.createAndFill();
}
}

/**
* Handle DOCX-specific document creation
* @private
* @returns {Object} ProseMirror document
*/
#createDocxDocument() {
let doc = createDocument(this.converter, this.schema, this);
doc = this.#prepareDocumentForImport(doc);

// Handle HTML override for DOCX mode
if (this.options.html) {
doc = this.#createDocFromHTML(this.options.html);
} else if (this.options.jsonOverride) {
doc = this.schema.nodeFromJSON(this.options.jsonOverride);
}

return doc;
}

/**
* Apply any final overrides or fragments
* @private
* @param {Object} doc - The document to modify
* @param {Object} fragment - Optional fragment to apply
* @returns {Object} Final ProseMirror document
*/
#applyDocumentOverrides(doc, fragment) {
if (fragment && this.options.mode === 'docx') {
return yXmlFragmentToProseMirrorRootNode(fragment, this.schema);
}
return doc;
}

/**
* Create a document from HTML content
* @private
Expand All @@ -989,6 +1062,64 @@ export class Editor extends EventEmitter {
return DOMParser.fromSchema(this.schema).parse(parsedContent);
}

/**
* Create a document from Markdown content
* @private
* @param {string} content - Markdown content
* @returns {Object} Document node
*/
#createDocFromMarkdown(content) {
try {
const html = this.#convertMarkdownToHTML(content);
return this.#createDocFromHTML(html);
} catch (error) {
console.error('[SuperDoc] Markdown import failed:', error);
// Return empty doc so editor still initializes
return this.schema.nodeFromJSON({ type: 'doc', content: [] });
}
}

/**
* Convert Markdown to HTML with SuperDoc compatibility
* @private
* @param {string} markdown - Markdown content
* @returns {string} HTML content
*/
#convertMarkdownToHTML(markdown) {
marked.setOptions({
breaks: true, // Respect line breaks
gfm: true, // GitHub flavored markdown (tables, etc)
});

// Convert and add SuperDoc compatibility
let html = marked.parse(markdown);
return this.#addSuperDocAttributes(html);
}

/**
* Add attributes SuperDoc requires
* @private
* @param {string} html - HTML from markdown parser
* @returns {string} HTML with SuperDoc attributes
*/
#addSuperDocAttributes(html) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;

// 1. Headings need data-level
tempDiv.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((h) => {
h.setAttribute('data-level', h.tagName[1]);
});

// 2. Code blocks benefit from language info
tempDiv.querySelectorAll('pre code').forEach((code) => {
const lang = code.className.replace('language-', '');
if (lang) code.parentElement.setAttribute('data-language', lang);
});

return tempDiv.innerHTML;
}

/**
* Create the PM editor view
* @private
Expand Down
14 changes: 0 additions & 14 deletions packages/super-editor/src/dev/components/BasicUpload.vue

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import '@harbour-enterprises/common/styles/common-styles.css';

import { ref, shallowRef, computed, onMounted } from 'vue';
import { SuperEditor } from '@/index.js';
import { getFileObject } from '@harbour-enterprises/common/helpers/get-file-object';
import { getFileObject, FileHandler, BasicUpload } from '@harbour-enterprises/common';
import { DOCX } from '@harbour-enterprises/common';
import { SuperToolbar } from '@components/toolbar/super-toolbar';
import { PaginationPluginKey } from '@extensions/pagination/pagination-helpers.js';
import BasicUpload from './BasicUpload.vue';
import BlankDOCX from '@harbour-enterprises/common/data/blank.docx?url';
import { Telemetry } from '@harbour-enterprises/common/Telemetry.js';

Expand All @@ -21,8 +20,7 @@ const telemetry = shallowRef(null);

const handleNewFile = async (file) => {
currentFile.value = null;
const fileUrl = URL.createObjectURL(file);
currentFile.value = await getFileObject(fileUrl, file.name, file.type);
currentFile.value = await FileHandler.processFile(file);
};

const onCreate = ({ editor }) => {
Expand Down Expand Up @@ -65,7 +63,7 @@ const user = {
};

const editorOptions = computed(() => {
return {
const options = {
documentId: 'dev-123',
user,
rulers: true,
Expand All @@ -78,6 +76,15 @@ const editorOptions = computed(() => {
telemetry: telemetry.value,
annotations: true,
};

// Add content based on file type using FileHandler helpers
if (FileHandler.isMarkdown(currentFile.value)) {
options.markdown = currentFile.value.content;
} else if (FileHandler.isHtml(currentFile.value)) {
options.html = currentFile.value.content;
}

return options;
});

const onCommentsLoaded = ({ comments }) => {
Expand Down Expand Up @@ -136,11 +143,11 @@ onMounted(async () => {
<div class="dev-app__header">
<div class="dev-app__header-side dev-app__header-side--left">
<div class="dev-app__header-title">
<h2>Super Editor Dev Area</h2>
<h2>🦋 SuperEditor - Playground</h2>
</div>
<div class="dev-app__header-upload">
Upload docx
<BasicUpload @file-change="handleNewFile" accept=".docx" />
Upload docx, html or markdown
<BasicUpload @file-change="handleNewFile" accept=".docx,.html,.md" />
</div>
</div>
<div class="dev-app__header-side dev-app__header-side--right">
Expand Down Expand Up @@ -189,9 +196,11 @@ onMounted(async () => {
display: flex;
justify-content: center;
}

.page-spacer:nth-child(odd) {
background-color: #aa000055;
}

.dev-app {
--header-height: 154px;
--toolbar-height: 39px;
Expand All @@ -218,9 +227,11 @@ onMounted(async () => {
.dev-app__header-side {
display: flex;
}

.dev-app__header-side--left {
flex-direction: column;
}

.dev-app__header-side--right {
align-items: flex-end;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/super-editor/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Mark } from '@core/Mark.js';
import SlashMenu from './components/slash-menu/SlashMenu.vue';

import SuperEditor from './components/SuperEditor.vue';
import BasicUpload from './dev/components/BasicUpload.vue';
import BasicUpload from '@harbour-enterprises/common/components/BasicUpload.vue';
import Toolbar from './components/toolbar/Toolbar.vue';
import SuperInput from './components/SuperInput.vue';
import AIWriter from './components/toolbar/AIWriter.vue';
Expand Down
Loading
Loading