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
5,554 changes: 5,081 additions & 473 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,13 @@
"private": true,
"workspaces": [
"packages/*"
]
],
"scripts": {
"reinstall": "rm -rf node_modules package-lock.json; npm install;",
"dev:superdoc": "cd packages/superdoc && npm run dev",
"dev:super-editor": "cd packages/super-editor && npm run dev"
},
"dependencies": {
"eventemitter3": "^5.0.1"
}
}
25 changes: 25 additions & 0 deletions packages/super-editor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
38 changes: 38 additions & 0 deletions packages/super-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Super Editor (docx editor)

This package exports the SuperEditor Vue 3 component, as well as various utils needed.

## Development

For development purposes:
```
npm install && npm run dev
```

## Prose Mirror

* [ProseMirror Guide](https://prosemirror.net/docs/guide/#schema)
* [Schema example](https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.ts)
* [ProseMirror Reference](https://prosemirror.net/docs/ref/)
* [ProseMirror Cookbook - Examples](https://github.com/PierBover/prosemirror-cookbook)
* [prosemirror-commands](https://github.com/ProseMirror/prosemirror-commands/blob/master/src/commands.ts#L745)
* [prosemirror-schema-basic](https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.ts)

## Key files

### src/classes/super-converter.js
This handles converting from XML document to prose mirror schema, and back.

Note: This is very much an experimental / POC version of this file. I want to make several improvements and abstractions here but use it as an example of how tags need to be processed in and out of the docx.

* When converting from XML to Schema, We need to make sure all xml tags are correctly handled here.
* When converting from Schema back to XML, we have to handle various special cases. Need to figure out how to abstract as much of this as possible.

### src/classes/docx-zipper.js
This util handles extracting a .docx file and returning a list of its xml files.
It can also handle creating a new .docx file from updated xml files

### src/schemas/docx-schema.js
This file defines the prose mirror schema.

* Need to add 'marks' for style tags. ie: ```<w:b>``` becomes a ```strong``` mark.
12 changes: 12 additions & 0 deletions packages/super-editor/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!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"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
44 changes: 44 additions & 0 deletions packages/super-editor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "super-editor",
"version": "0.0.1-alpha.0",
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/super-editor.es.js",
"require": "./dist/super-editor.cjs.js"
}
},
"main": "./dist/super-editor.cjs.js",
"module": "./dist/super-editor.es.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest"
},
"dependencies": {
"jszip": "^3.10.1",
"prosemirror-commands": "^1.5.2",
"prosemirror-history": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.21.0",
"prosemirror-schema-basic": "^1.2.2",
"prosemirror-schema-list": "^1.3.0",
"prosemirror-view": "^1.33.6",
"vue": "^3.4.21",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"@vue/test-utils": "^2.4.6",
"jsdom": "^24.1.0",
"naive-ui": "^2.38.2",
"postcss-nested": "^6.0.1",
"postcss-nested-import": "^1.3.0",
"vite": "^5.2.12",
"vite-plugin-node-polyfills": "^0.22.0",
"vitest": "^1.6.0"
}
}
72 changes: 72 additions & 0 deletions packages/super-editor/src/DeveloperPlayground.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
Dev app for the SuperEditor component

The super-editor package exports SuperEditor directly. Thus, this app simulates the process
of importing the component into another Vue 3 app (ie: superdoc) and using it.
-->

<script setup>
import { nextTick, ref } from 'vue';
import BasicUpload from './dev-components/BasicUpload.vue';

// Import the component the same you would in your app
import { SuperEditor } from '@/index';

// For testing a file from URL
import sampleDocxUrl from './tests/fixtures/sample/sample.docx?url';

const currentFile = ref(null);
const handleNewFile = (file) => {
currentFile.value = null;

// Generate a file url
const fileUrl = URL.createObjectURL(file);
nextTick(() => {
currentFile.value = fileUrl;
});
}
</script>

<template>
<div class="dev-app">
<div class="header">
<div class="title">
<h2>Super Editor Dev Area</h2>
</div>

<!--
A user using SuperEditor is expected to handle file uplodas and data sources on their own.
SuperEditor just expects a URL to a docx file. This basic uploader is here for testing.
You can also replace currentFile directly with a URL (ie: sampleDocxUrl).
-->
<div>
Upload docx
<BasicUpload @file-change="handleNewFile" />
</div>
</div>
<div class="content" v-if="currentFile">

<!-- SuperEditor expects its data to be a URL -->
<SuperEditor mode="docx" :data-url="currentFile" />

</div>
</div>
</template>

<style scoped>
.dev-app {
display: flex;
flex-direction: column;
}
.header {
display: flex;
flex-direction: column;
background-color: rgb(222, 237, 243);
padding: 20px;
margin-bottom: 20px;
}
.content {
display: flex;
justify-content: center;
}
</style>
75 changes: 75 additions & 0 deletions packages/super-editor/src/classes/docx-zipper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import JSZip from 'jszip';

/**
* Class to handle unzipping and zipping of docx files
*/
class DocxZipper {

constructor(params = {}) {
this.debug = params.debug || false;
this.zip = new JSZip();
this.files = [];
}

/**
* Get XML data from the zipped docx
*
* [Content_Types].xml
* _rels/.rels
* word/document.xml
* word/_rels/document.xml.rels
* word/footnotes.xml
* word/endnotes.xml
* word/header1.xml
* word/theme/theme1.xml
* word/settings.xml
* word/styles.xml
* word/webSettings.xml
* word/fontTable.xml
* docProps/core.xml
* docProps/app.xml
* */
async getXmlData(file) {
const extractedFiles = await this.unzip(file);
const files = Object.entries(extractedFiles.files);

for (const file of files) {
const [_, zipEntry] = file;
if (zipEntry.name.endsWith('.xml')) {
const content = await zipEntry.async("string")
this.files.push({
name: zipEntry.name,
content,
});
}
}
return this.files;
}

async unzip(file) {
const zip = await this.zip.loadAsync(file);
return zip;
}

async updateZip(originalZip, newDocumentXmlContent) {
const updatedZip = new JSZip();

// Create an array of promises to read all files
const filePromises = [];

originalZip.forEach((relativePath, zipEntry) => {
const promise = zipEntry.async("uint8array").then((content) => {
updatedZip.file(zipEntry.name, content);
});
filePromises.push(promise);
});

// Wait for all promises to resolve
await Promise.all(filePromises);

updatedZip.file("word/document.xml", newDocumentXmlContent);
return updatedZip;
}
}

export default DocxZipper;
52 changes: 52 additions & 0 deletions packages/super-editor/src/classes/docx-zipper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import path from 'path';
import fs from 'fs';
import { describe, it, expect } from 'vitest';
import DocxZipper from './docx-zipper';

async function readFileAsBuffer(filePath) {
const resolvedPath = path.resolve(__dirname, filePath);
return new Promise((resolve, reject) => {
fs.readFile(resolvedPath, (err, data) => {
if (err) {
reject(err);
} else {
// Convert file content to a Buffer
const buffer = Buffer.from(data);
resolve(buffer);
}
});
});
}

describe('DocxZipper - file extraction', () => {

let zipper;
beforeEach(() => {
zipper = new DocxZipper();
});

it('It can unzip a docx', async () => {
const fileContent = await readFileAsBuffer('../tests/fixtures/sample/sample.docx');
const fileObject = Buffer.from(fileContent);
const unzippedXml = await zipper.unzip(fileObject);
expect(unzippedXml).toHaveProperty('files');
});

it('It can extract xml files', async () => {
const fileContent = await readFileAsBuffer('../tests/fixtures/sample/sample.docx');
const fileObject = Buffer.from(fileContent);
const unzippedXml = await zipper.getXmlData(fileObject);
expect(unzippedXml).toBeInstanceOf(Array);

unzippedXml.forEach((file) => {
expect(file).toHaveProperty('name');
expect(file.name).toMatch(/\.xml$/);
expect(file).toHaveProperty('content');
expect(file.content).toMatch(/<\?xml/);
});

// Make sure we have document.xml
const documentXml = unzippedXml.find(file => file.name === 'word/document.xml');
expect(documentXml).toBeTruthy();
});
});
Loading