├── .gitignore ├── redocx.png ├── demo ├── Memory.docx ├── HelloWorld.docx ├── word.js └── word-memory.js ├── examples ├── concr.png ├── Hr.js ├── Image.js ├── Header.js ├── Footer.js ├── PageBreak.js ├── LineBreak.js ├── Text.js ├── List.js └── Table.js ├── .travis.yml ├── src ├── components │ ├── Footer.js │ ├── Header.js │ ├── WordDocument.js │ ├── LineBreak.js │ ├── PageBreak.js │ ├── Hr.js │ ├── index.js │ ├── Root.js │ ├── Bullet.js │ ├── Number.js │ ├── List.js │ ├── HOC.js │ ├── Image.js │ ├── Document.js │ ├── Table.js │ └── Text.js ├── utils │ ├── platform.js │ ├── nodes.js │ ├── renderUtils.js │ └── createElement.js ├── index.js ├── renderer │ ├── parse.js │ ├── render.js │ └── renderer.js ├── styles │ └── styles.js └── validators │ └── componentValidators.js ├── .babelrc ├── __tests__ ├── createElement.test.js ├── Hr.test.js ├── PageBreak.test.js ├── LineBreak.test.js ├── Footer.test.js ├── Header.test.js ├── Image.test.js ├── Document.test.js ├── __snapshots__ │ ├── styles.test.js.snap │ ├── createElement.test.js.snap │ ├── Hr.test.js.snap │ ├── PageBreak.test.js.snap │ ├── Table.test.js.snap │ ├── Image.test.js.snap │ └── LineBreak.test.js.snap ├── Table.test.js ├── styles.test.js ├── Text.test.js └── Bullets.test.js ├── .eslintrc.js ├── docs ├── misc.md ├── styling.md ├── README.md └── api.md ├── CONTRIBUTING.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | demo/text.docx -------------------------------------------------------------------------------- /redocx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/redocx/HEAD/redocx.png -------------------------------------------------------------------------------- /demo/Memory.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/redocx/HEAD/demo/Memory.docx -------------------------------------------------------------------------------- /examples/concr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/redocx/HEAD/examples/concr.png -------------------------------------------------------------------------------- /demo/HelloWorld.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/redocx/HEAD/demo/HelloWorld.docx -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | script: 7 | - npm run test -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import hoc from './HOC'; 2 | 3 | /** 4 | * Footer component 5 | */ 6 | export default hoc('Footer', 'getFooter'); 7 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import hoc from './HOC'; 2 | 3 | /** 4 | * Header component 5 | */ 6 | export default hoc('Header', 'getHeader'); 7 | -------------------------------------------------------------------------------- /src/utils/platform.js: -------------------------------------------------------------------------------- 1 | const Platform = { 2 | OS: 'word', 3 | select: obj => ('word' in obj ? obj.word : obj.default), 4 | }; 5 | 6 | export default Platform; 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "exclude": ["transform-regenerator"] 7 | } 8 | ], 9 | "stage-0", 10 | "react" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/createElement.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createElement} from '../src/utils/createElement'; 3 | 4 | it('should create an element', () => { 5 | const inst = createElement('ROOT') 6 | expect(inst).toMatchSnapshot(); 7 | }) -------------------------------------------------------------------------------- /__tests__/Hr.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Hr } from '../src/'; 3 | import { testRenderer as render } from '../src/renderer/render'; 4 | 5 | it('should draw a horizontal ruler', () => { 6 | expect(render(
)).toMatchSnapshot(); 7 | }) -------------------------------------------------------------------------------- /__tests__/PageBreak.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageBreak } from '../src/'; 3 | import { testRenderer as render } from '../src/renderer/render'; 4 | 5 | it('should put a page break', () => { 6 | expect(render()).toMatchSnapshot(); 7 | }) 8 | -------------------------------------------------------------------------------- /src/components/WordDocument.js: -------------------------------------------------------------------------------- 1 | import officegen from 'officegen'; 2 | 3 | /** 4 | * This creates the document instance 5 | */ 6 | class WordDocument { 7 | constructor() { 8 | this.doc = officegen('docx'); 9 | } 10 | } 11 | 12 | export default WordDocument; 13 | -------------------------------------------------------------------------------- /src/components/LineBreak.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This component adds a line break and should be called inside Text or List component 3 | */ 4 | class LineBreak { 5 | constructor(root, props) { 6 | this.root = root; 7 | this.prop = props; 8 | this.name = 'LineBreak'; 9 | } 10 | } 11 | 12 | export default LineBreak; 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-await-in-loop": 0, 6 | "no-param-reassign": 0, 7 | "no-return-assign": 0, 8 | "no-return-await": 0, 9 | "no-console": 0, 10 | "no-unneeded-ternary": 0 11 | } 12 | }; -------------------------------------------------------------------------------- /examples/Hr.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Hr, Document, Text } from '../src/'; 3 | 4 | class HrComponent extends Component { 5 | render () { 6 | return ( 7 | 8 | Below is a horizontal ruler 9 |
10 |
11 | ); 12 | } 13 | } 14 | 15 | export default HrComponent -------------------------------------------------------------------------------- /examples/Image.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Image, Document } from '../src/'; 3 | 4 | class ImageComponent extends Component { 5 | render () { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default ImageComponent -------------------------------------------------------------------------------- /examples/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Header, Document } from '../src/'; 3 | 4 | class HeaderComponent extends Component { 5 | render () { 6 | return ( 7 | 8 |
Heading
9 |
10 | ); 11 | } 12 | } 13 | 14 | export default HeaderComponent -------------------------------------------------------------------------------- /examples/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Footer, Document } from '../src/'; 3 | 4 | class FooterComponent extends Component { 5 | render () { 6 | return ( 7 | 8 |
Heading
9 |
10 | ); 11 | } 12 | } 13 | 14 | export default FooterComponent -------------------------------------------------------------------------------- /src/components/PageBreak.js: -------------------------------------------------------------------------------- 1 | import Root from './Root'; 2 | 3 | /** 4 | * This component adds a page break. 5 | */ 6 | class PageBreak extends Root { 7 | constructor(root, props) { 8 | super(root, props); 9 | this.root = root; 10 | this.adder = this.root.doc.putPageBreak(); 11 | } 12 | 13 | async render() { 14 | await this.adder; 15 | } 16 | } 17 | 18 | export default PageBreak; 19 | -------------------------------------------------------------------------------- /examples/PageBreak.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { PageBreak, Document, Text, Image } from '../src/'; 3 | 4 | class PageBreakComponent extends Component { 5 | render () { 6 | return ( 7 | 8 | Hello World! 9 | 10 | Hello World! 11 | 12 | ); 13 | } 14 | } 15 | 16 | export default PageBreakComponent -------------------------------------------------------------------------------- /src/components/Hr.js: -------------------------------------------------------------------------------- 1 | import Root from './Root'; 2 | 3 | /** 4 | * This component draw a horizontal line 5 | */ 6 | class Hr extends Root { 7 | constructor(root, props) { 8 | super(root, props); 9 | this.root = root; 10 | this.name = 'Hr'; 11 | } 12 | 13 | async render() { 14 | await (this.parent === null ? this.root.doc.createP().addHorizontalLine() : null); 15 | } 16 | } 17 | 18 | export default Hr; 19 | -------------------------------------------------------------------------------- /__tests__/LineBreak.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LineBreak, Text } from '../src/'; 3 | import { testRenderer as render } from '../src/renderer/render'; 4 | 5 | it('should add a line break', () => { 6 | expect(render()).toMatchSnapshot(); 7 | }) 8 | 9 | it('should add a line break when called inside a Text component', () => { 10 | expect(render()).toMatchSnapshot(); 11 | }) 12 | -------------------------------------------------------------------------------- /examples/LineBreak.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { LineBreak, Document, Text } from '../src/'; 3 | 4 | class LineBreakComponent extends Component { 5 | render () { 6 | return ( 7 | 8 | 9 | Hello World! 10 | 11 | This is a new line! 12 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default LineBreakComponent -------------------------------------------------------------------------------- /examples/Text.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Text, Document } from '../src/'; 3 | 4 | const TextStyles = { 5 | color: 'red', 6 | fontSize: 30 7 | }; 8 | 9 | class TextComponent extends Component { 10 | render() { 11 | return ( 12 | 13 | 14 | Hello World! 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default TextComponent; 22 | -------------------------------------------------------------------------------- /examples/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { List, BulletItem, Document } from '../src/'; 3 | 4 | class ListComponent extends Component { 5 | render () { 6 | return ( 7 | 8 | 9 | Item one 10 | Item two 11 | Item three 12 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default ListComponent -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import BulletItem from './Bullet'; 2 | import Document from './Document'; 3 | import Footer from './Footer'; 4 | import Header from './Header'; 5 | import Hr from './Hr'; 6 | import Image from './Image'; 7 | import NumberItem from './Number'; 8 | import LineBreak from './LineBreak'; 9 | import List from './List'; 10 | import PageBreak from './PageBreak'; 11 | import Table from './Table'; 12 | import Text from './Text'; 13 | import WordDocument from './WordDocument'; 14 | 15 | export { 16 | BulletItem, 17 | Document, 18 | Footer, 19 | Header, 20 | Hr, 21 | Image, 22 | NumberItem, 23 | LineBreak, 24 | List, 25 | PageBreak, 26 | Table, 27 | Text, 28 | WordDocument, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class that keeps track of parent and child nodes 3 | */ 4 | class Root { 5 | parent = null; 6 | children = []; 7 | 8 | constructor(root, props) { 9 | this.root = root; 10 | this.props = props; 11 | } 12 | 13 | appendChild(child) { 14 | child.parent = this; 15 | this.children.push(child); 16 | } 17 | 18 | removeChild(child) { 19 | const index = this.children.indexOf(child); 20 | 21 | child.parent = null; 22 | this.children.splice(index, 1); 23 | } 24 | 25 | async renderChildren() { 26 | for (let i = 0; i < this.children.length; i += 1) { 27 | await this.children[i].render(); 28 | } 29 | } 30 | } 31 | 32 | export default Root; 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from './renderer/render'; 2 | import Platform from './utils/platform'; 3 | 4 | /** 5 | * Component name (input to createElement function call after transpilation with Babel) 6 | */ 7 | const Text = 'TEXT'; 8 | const Image = 'IMAGE'; 9 | const List = 'LIST'; 10 | const NumberItem = 'NUMBERITEM'; 11 | const BulletItem = 'BULLETITEM'; 12 | const LineBreak = 'LINEBREAK'; 13 | const PageBreak = 'PAGEBREAK'; 14 | const Document = 'DOCUMENT'; 15 | const Hr = 'HR'; 16 | const Table = 'TABLE'; 17 | const Header = 'HEADER'; 18 | const Footer = 'FOOTER'; 19 | 20 | /** 21 | * Main export 22 | */ 23 | export { 24 | Text, 25 | Image, 26 | List, 27 | Document, 28 | NumberItem, 29 | BulletItem, 30 | PageBreak, 31 | Hr, 32 | LineBreak, 33 | Table, 34 | Header, 35 | Footer, 36 | Platform, 37 | render, 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Bullet.js: -------------------------------------------------------------------------------- 1 | import Root from './Root'; 2 | import { renderNodes } from '../utils/nodes'; 3 | 4 | /** 5 | * This component renders a list of dots 6 | */ 7 | class BulletItem extends Root { 8 | constructor(root, props) { 9 | super(root, props); 10 | this.root = root; 11 | this.props = props; 12 | this.adder = this.root.doc.createListOfDots(); 13 | } 14 | 15 | appendChild(child) { 16 | this.children.push(child); 17 | } 18 | 19 | removeChild(child) { 20 | const index = this.children.indexOf(child); 21 | this.children.splice(index, 1); 22 | } 23 | 24 | async renderChildren(align, styles) { 25 | await renderNodes(align, null, styles, this.adder, this.children, this.props); 26 | } 27 | 28 | async render(align, styles) { 29 | await this.renderChildren(align, styles); 30 | } 31 | } 32 | 33 | export default BulletItem; 34 | -------------------------------------------------------------------------------- /demo/word.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TextComponent from '../examples/Text'; 3 | import { render, Document, Text } from '../src/'; 4 | 5 | // Uncomment any of the below component to see what they render 6 | 7 | import FooterComponent from '../examples/Footer'; 8 | import HeaderComponent from '../examples/Header'; 9 | import HrComponent from '../examples/Hr'; 10 | import LineBreakComponent from '../examples/LineBreak'; 11 | import PageBreakComponent from '../examples/PageBreak'; 12 | import TableComponent from '../examples/Table'; 13 | import ListComponent from '../examples/List'; 14 | import ImageComponent from '../examples/Image'; 15 | 16 | class MyDocument extends Component { 17 | render() { 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | render(, `${__dirname}/HelloWorld.docx`); 27 | -------------------------------------------------------------------------------- /src/components/Number.js: -------------------------------------------------------------------------------- 1 | import Root from './Root'; 2 | import { renderNodes } from '../utils/nodes'; 3 | 4 | /** 5 | * Creates list of numbers (currently unstable) 6 | */ 7 | class NumberItem extends Root { 8 | constructor(root, props) { 9 | super(root, props); 10 | this.root = root; 11 | this.props = props; 12 | this.adder = this.root.doc.createListOfNumbers(); 13 | } 14 | 15 | appendChild(child) { 16 | this.children.push(child); 17 | } 18 | 19 | removeChild(child) { 20 | const index = this.children.indexOf(child); 21 | this.children.splice(index, 1); 22 | } 23 | 24 | async renderChildren(align, styles) { 25 | await renderNodes(align, null, styles, this.adder, this.children, this.props); 26 | } 27 | 28 | async render(align, styles) { 29 | await this.renderChildren(align, styles); 30 | } 31 | } 32 | 33 | export default NumberItem; 34 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This component renders a list of dots or numbers 3 | */ 4 | class List { 5 | children = []; 6 | 7 | constructor(root, props) { 8 | this.root = root; 9 | this.props = props; 10 | } 11 | 12 | appendChild(child) { 13 | this.children.push(child); 14 | } 15 | 16 | removeChild(child) { 17 | const index = this.children.indexOf(child); 18 | this.children.splice(index, 1); 19 | } 20 | 21 | async renderChildren(align, styles) { 22 | // Override the styles and use List component styles 23 | // expected an assignment 24 | styles = this.props.style ? this.props.style : styles; 25 | for (let i = 0; i < this.children.length; i += 1) { 26 | await this.children[i].render(align, styles); 27 | } 28 | } 29 | 30 | async render(align, styles) { 31 | await this.renderChildren(align, styles); 32 | } 33 | } 34 | 35 | export default List; 36 | -------------------------------------------------------------------------------- /src/renderer/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Render the component (passed to docx generator instance) 3 | * This is similar to react-pdf https://github.com/diegomura/react-pdf/blob/master/packages/react-pdf/src/index.js 4 | * @param {Object} input Root component 5 | */ 6 | const parse = (input) => { 7 | async function parseComponent(inputComponent) { 8 | // This property is accessed due to https://github.com/nitin42/redocx/blob/master/src/renderer/renderer.js#L32 9 | const document = inputComponent.document; 10 | 11 | await document.render(); 12 | // Return the input component again because we rendered the children 13 | // which weren't wrapped inside a parent. 14 | // We async called the render method on all of the children. 15 | return inputComponent; 16 | } 17 | 18 | async function toBuffer() { 19 | return await parseComponent(input); 20 | } 21 | 22 | return { 23 | toBuffer, 24 | }; 25 | }; 26 | 27 | export default parse; 28 | -------------------------------------------------------------------------------- /__tests__/Footer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Footer, Document } from '../src/'; 3 | import { testRenderer as render } from '../src/renderer/render'; 4 | 5 | it('sanity check', () => { 6 | expect(render(