Switch from Vue to React

This commit is contained in:
Johannes Zillmann 2017-01-06 20:49:37 +01:00
parent 409e9a070f
commit b9da57ed5b
22 changed files with 677 additions and 321 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
node_modules/
dist/
build/
npm-debug.log

View File

@ -1,11 +1,12 @@
{
"name": "pdf-to-markdown",
"version": "0.0.1",
"description": "A PDF to Markdown converter",
"description": "A PDF to Markdown Converter",
"main": "main.js",
"scripts": {
"watch": "webpack -d --watch",
"build": "webpack"
"build": "webpack",
"lint": "eslint . --ext .js --ext .jsx --cache"
},
"keywords": [
"PDF",
@ -13,15 +14,20 @@
"Converter"
],
"author": "Johannes Zillmann",
"license": "ISC",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/jzillmann/pdf-to-markdown"
},
"dependencies": {
"bootstrap": "^3.3.7",
"enumify": "^1.0.4",
"pdfjs-dist": "^1.6.317",
"vue": "^2.0.5",
"vue-material": "^0.3.3"
"react": "^15.3.2",
"react-bootstrap": "^0.30.3",
"react-dom": "^15.3.2",
"react-dropzone": "^3.6.0",
"react-icons": "^2.2.1"
},
"devDependencies": {
"babel-core": "^6.18.2",
@ -29,13 +35,18 @@
"babel-loader": "^6.2.7",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.25.0",
"esformatter-jsx": "^7.0.1",
"eslint": "^3.7.0",
"eslint-plugin-react": "^6.3.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.24.1",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"vue-loader": "^9.8.1",
"webpack": "^1.13.3"
}
}

View File

@ -1,47 +0,0 @@
<template>
<div id="app">
<hello v-if="state.uploaded"/>
<!--img src="./assets/logo.png"-->
<dropzone v-else id="myVueDropzone" url="https://httpbin.org/post" v-on:vdropzone-success="showSuccess" v-on:vdropzone-fileAdded="fileAdded"></dropzone>
</div>
</template>
<script>
import store from './store.js'
import Hello from './components/Hello'
import Dropzone from './components/Dropzone'
export default {
name: 'app',
components: {
Hello,
Dropzone,
},
data() {
return {
state: store.state
}
},
methods: {
'fileAdded': function (file) {
console.log('A file was added: ')
console.log(file)
},
'showSuccess': function (file) {
console.log('A file was successfully uploaded: ')
console.log(file)
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,174 +0,0 @@
<template>
<div id="wrapper">
<div class="dropzone-area" @dragenter="hovering = true" @dragleave="hovering = false" :class="{hovered: hovering}">
<div class="dropzone-text">
<span class="dropzone-title">Drop image here or click to select</span>
</div>
<input type="file" @change="onFileChange">
</div>
<div class="dropzone-preview">
<img :src="image" />
<button @click="removeImage" v-if="image">Remove</button>
</div>
</div>
</template>
<script>
import store from '../store.js'
import pdfjs from 'pdfjs-dist';
import Page from '../models/Page.js';
import TextItem from '../models/TextItem.js';
export default {
props : {
multiple : {
default : false
},
path : {
default : '/document/upload-unlinked/'
},
file : {
default : 'user_file'
},
files : {
default : function () { return [] }
},
target : {
default : 'dropzone'
},
clickable : {
default : false
},
previewTemplate : {
default : '<div style="display:none"></div>'
},
createImageThumbnails : {
default : false
}
},
data() {
return {
hovering : false,
image: null,
}
},
computed: {
multipleUploads() {
return this.multiple ? true : false;
}
},
methods: {
onFileChange(e) {
console.debug('upload');
var files = e.target.files || e.dataTransfer.files;
console.debug(files);
if (!files.length) return;
const reader = new FileReader();
reader.onload = (evt) => {
console.debug("Loaded");
const buffer = evt.target.result;
PDFJS.getDocument(buffer).then(function (pdfDocument) {
console.log('Number of pages: ' + pdfDocument.numPages);
// console.debug(pdfDocument);
const numPages = pdfDocument.numPages;
// const numPages = 3;
store.preparePageUpload(numPages);
for (var i = 0; i <= numPages; i++) {
pdfDocument.getPage(i).then(function(page){
page.getTextContent().then(function(textContent) {
//console.debug(textContent);
const textItems = textContent.items.map(function(item) {
const transform = item.transform;
return new TextItem({
x: transform[4],
y: transform[5],
width: item.width,
height: item.height,
text: item.str
});
});
store.uploadPage(page.pageIndex, textItems);
});
});
}
});
};
reader.readAsArrayBuffer(files[0]);
},
removeImage: function (e) {
this.image = '';
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang='sass' scoped>
.dropzone-area {
width: 80%;
height: 200px;
position: relative;
border: 2px dashed #CBCBCB;
&.hovered {
border: 2px dashed #2E94C4;
.dropzone-title {
color: #1975A0;
}
}
}
.dropzone-area input {
position: absolute;
cursor: pointer;
top: 0px;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.dropzone-text {
position: absolute;
top: 50%;
text-align: center;
transform: translate(0, -50%);
width: 100%;
span {
display: block;
font-family: Arial, Helvetica;
line-height: 1.9;
}
}
.dropzone-title {
font-size: 13px;
color: #787878;
letter-spacing: 0.4px;
}
.dropzone-button {
position: absolute;
top: 10px;
right: 10px;
display: none;
}
.dropzone-preview {
width: 80%;
position: relative;
&:hover .dropzone-button {
display: block;
}
img {
display: block;
height: auto;
max-width: 100%;
}
}
</style>

View File

@ -1,47 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<div>
Uploaded: {{state.uploaded}}
</div>
<div v-for="line in state.pages">
<textarea :value="line" rows="50" cols="150"></textarea>
</div>
</div>
</template>
<script>
import store from '../store.js'
export default {
name: 'hello',
data () {
return {
msg: 'Welcome to Your!! Vue.js App',
state: store.state
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
textarea { font-size: 18px; }
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta charset="UTF-8">
<title>PDF to Markdown</title>
<meta name="description" content="Converts PDF files to Markdown." />
<meta name="keywords" content="PDF, Markdown, converter">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<div id="main"</div>
</body>
</html>

View File

@ -0,0 +1,46 @@
import React from 'react';
import Grid from 'react-bootstrap/lib/Grid'
import TopBar from './TopBar.jsx';
import { View } from '../models/AppState.jsx';
import PdfUploadView from './PdfUploadView.jsx';
import LoadingView from './LoadingView.jsx';
import PdfView from './PdfView.jsx';
export default class App extends React.Component {
static propTypes = {
appState: React.PropTypes.object.isRequired,
};
render() {
console.debug(this.props.appState);
var mainView;
switch (this.props.appState.mainView) {
case View.UPLOAD:
mainView = <PdfUploadView uploadPdfFunction={ this.props.appState.uploadPdf } />
break;
case View.LOADING:
mainView = <LoadingView/>
break;
case View.PDF_VIEW:
mainView = <PdfView pdfPages={ this.props.appState.pdfPages } />
break;
}
return (
<div>
<TopBar/>
<Grid>
<div>
{ mainView }
</div>
</Grid>
</div>
);
}
}

View File

@ -0,0 +1,28 @@
import React, { Component } from 'react';
import FaFilePdfO from 'react-icons/lib/fa/file-pdf-o'
export default class AppLogo extends Component {
static propTypes = {
onClick: React.PropTypes.func,
};
constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
this.props.onClick(e);
}
render() {
return (
<a href="" onClick={ this.handleClick }>
<FaFilePdfO/> PDF To Markdown Converter</a>
);
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import Spinner from './lib/Spinner.jsx';
export default class LoadingView extends React.Component {
render() {
return (
<div style={ { textAlign: 'center' } }>
<br/>
<br/>
<br/>
<br/>
<Spinner/>
<br/>
<br/>
<div>
Uploading and parsing PDF...
</div>
</div>);
}
}

View File

@ -0,0 +1,66 @@
import React from 'react';
import Table from 'react-bootstrap/lib/Table'
export default class PdfPageView extends React.Component {
static propTypes = {
pdfPage: React.PropTypes.object.isRequired,
};
render() {
const header = "Page " + this.props.pdfPage.index;
return (
<div>
<h2>{ header }</h2>
<Table responsive>
<thead>
<tr>
<th>
#
</th>
<th>
Text
</th>
<th>
X
</th>
<th>
Y
</th>
<th>
Width
</th>
<th>
Height
</th>
</tr>
</thead>
<tbody>
{ this.props.pdfPage.textItems.map((textItem, i) => <tr key={ i }>
<td>
{ i }
</td>
<td>
{ textItem.text }
</td>
<td>
{ textItem.x }
</td>
<td>
{ textItem.y }
</td>
<td>
{ textItem.width }
</td>
<td>
{ textItem.height }
</td>
</tr>
) }
</tbody>
</Table>
</div>
);
}
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import Dropzone from 'react-dropzone'
import FaCloudUpload from 'react-icons/lib/fa/cloud-upload'
export default class PdfUploadView extends React.Component {
static propTypes = {
uploadPdfFunction: React.PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
uploadPdfFunction: props.uploadPdfFunction,
};
}
onDrop(files) {
console.debug(files.length);
if (files.length > 1) {
alert(`Maximum one file allowed to upload, but not ${files.length}!`)
return
}
const reader = new FileReader();
const uploadFunction = this.state.uploadPdfFunction;
reader.onload = (evt) => {
const fileBuffer = evt.target.result;
uploadFunction(fileBuffer);
};
reader.readAsArrayBuffer(files[0]);
}
render() {
return (
<div>
<Dropzone onDrop={ this.onDrop.bind(this) } multiple={ false } style={ { width: 400, height: 500, borderWidth: 2, borderColor: '#666', borderStyle: 'dashed', borderRadius: 5, display: 'table-cell', textAlign: 'center', verticalAlign: 'middle' } }>
<div className="container">
<h2>Drop your PDF file here!</h2>
</div>
<h1><FaCloudUpload width={ 100 } height={ 100 } /></h1>
</Dropzone>
</div>
);
}
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import PdfPageView from './PdfPageView.jsx';
// A view which displays the TextItems of multiple PdfPages
export default class PdfView extends React.Component {
static propTypes = {
pdfPages: React.PropTypes.array.isRequired,
};
render() {
console.debug(this.props.pdfPages);
const header = "Parsed " + this.props.pdfPages.length + " pages!"
return (
<div>
<div>
{ header }
</div>
<hr/>
{ this.props.pdfPages.map((page) => <PdfPageView key={ page.index } pdfPage={ page } />) }
</div>
);
}
}

View File

@ -0,0 +1,49 @@
import React from 'react';
import Navbar from 'react-bootstrap/lib/Navbar'
import MenuItem from 'react-bootstrap/lib/MenuItem'
import Dropdown from 'react-bootstrap/lib/Dropdown'
import Popover from 'react-bootstrap/lib/Popover'
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
import AppLogo from './AppLogo.jsx';
export default class TopBar extends React.Component {
render() {
const aboutPopover = (
<Popover id="popover-trigger-click-root-close" title={ `About PDF to Markdown Converter - ${ process.env.version }` }>
<p>
<i>PDF to Markdown Converter</i> will convert your uploaded PDF to Markdown format.
</p>
</Popover>
);
return (
<Navbar inverse>
<Navbar.Header>
<Navbar.Brand>
<Dropdown id="logo-dropdown">
<AppLogo bsRole="toggle" />
<Dropdown.Menu>
<MenuItem divider />
<MenuItem href="http://github.com/jzillmann/pdf-to-markdown" target="_blank"> Github
</MenuItem>
<OverlayTrigger
trigger="click"
rootClose
placement="bottom"
overlay={ aboutPopover }>
<MenuItem eventKey="3"> About
</MenuItem>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
</Navbar.Brand>
</Navbar.Header>
</Navbar>
);
}
}

View File

@ -0,0 +1,209 @@
import React from 'react';
// Spinner like loading indicator
export default class Spinner extends React.Component {
render() {
return (
<svg
width='120px'
height='120px'
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid">
<rect
x="0"
y="0"
width="100"
height="100"
fill="none"></rect>
<g transform="translate(50 50)">
<g transform="rotate(0) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(45) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.12s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.12s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(90) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.25s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.25s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(135) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.37s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.37s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(180) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.5s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.5s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(225) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.62s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.62s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(270) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.75s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.75s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
<g transform="rotate(315) translate(34 0)">
<circle
cx="0"
cy="0"
r="8"
fill="#000">
<animate
attributeName="opacity"
from="1"
to="0.1"
begin="0.87s"
dur="1s"
repeatCount="indefinite"></animate>
<animateTransform
attributeName="transform"
type="scale"
from="1.5"
to="1"
begin="0.87s"
dur="1s"
repeatCount="indefinite"></animateTransform>
</circle>
</g>
</g>
</svg>
);
}
}

View File

@ -0,0 +1,32 @@
import pdfjs from 'pdfjs-dist';
import AppState from '../models/AppState.jsx';
import TextItem from '../models/TextItem.jsx';
export function pdfToTextItemsAsync(fileBuffer:ArrayBuffer, appState:AppState) {
PDFJS.getDocument(fileBuffer).then(function(pdfDocument) {
console.log('Number of pages: ' + pdfDocument.numPages);
// console.debug(pdfDocument);
const numPages = pdfDocument.numPages;
// const numPages = 3;
appState.setPageCount(numPages);
for (var i = 0; i <= numPages; i++) {
pdfDocument.getPage(i).then(function(page) {
page.getTextContent().then(function(textContent) {
// console.debug(textContent);
const textItems = textContent.items.map(function(item) {
const transform = item.transform;
return new TextItem({
x: transform[4],
y: transform[5],
width: item.width,
height: item.height,
text: item.str
});
});
appState.setPdfPage(page.pageIndex, textItems);
});
});
}
});
}

17
src/javascript/index.jsx Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import App from './components/App.jsx';
import AppState from './models/AppState.jsx';
function render(appState) {
ReactDOM.render(<App appState={ appState } />, document.getElementById('main'));
}
const appState = new AppState({
renderFunction: render,
});
appState.render()

View File

@ -0,0 +1,57 @@
import { Enum } from 'enumify';
import { pdfToTextItemsAsync } from '../functions/pdfToTextItems.jsx'
import PdfPage from './PdfPage.jsx';
// Holds the state of the Application
export default class AppState {
constructor(options) {
this.renderFunction = options.renderFunction;
this.mainView = View.UPLOAD;
this.pagesToUpload = 0;
this.uploadedPages = 0;
this.pdfPages = [];
//bind functions
this.render = this.render.bind(this);
this.uploadPdf = this.uploadPdf.bind(this);
this.setPageCount = this.setPageCount.bind(this);
this.setPdfPage = this.setPdfPage.bind(this);
}
render() {
this.renderFunction(this)
}
uploadPdf(fileBuffer:ArrayBuffer) {
pdfToTextItemsAsync(fileBuffer, this);
this.mainView = View.LOADING;
this.render()
}
setPageCount(numPages) {
this.pagesToUpload = numPages;
for (var i = 0; i < numPages; i++) {
this.pdfPages.push(new PdfPage({
index: i
}));
}
}
setPdfPage(pageIndex, textItems) {
console.debug("Upload " + pageIndex);
this.pdfPages[pageIndex].textItems = textItems;
this.uploadedPages++;
if (this.uploadedPages == this.pagesToUpload) {
console.debug("Fin");
this.mainView = View.PDF_VIEW;
this.render();
}
}
}
export class View extends Enum {
}
View.initEnum(['UPLOAD', 'LOADING', 'PDF_VIEW'])

View File

@ -1,4 +1,5 @@
export default class Page {
// A page which holds TextItems displayable via PdfPageView
export default class PdfPage {
constructor(options) {
this.index = options.index;

View File

@ -1,11 +0,0 @@
import Vue from 'vue'
import App from './App'
/* eslint-disable no-new */
new Vue({
el: '#app',
template: '<App/>',
components: {
App
}
})

View File

@ -1,15 +1,18 @@
var path = require('path')
var sourceDir = path.resolve(__dirname, 'src');
var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var SOURCE_DIR = path.resolve(__dirname, 'src');
var BUILD_DIR = path.resolve(__dirname, 'build');
var NODEMODULES_DIR = path.resolve(__dirname, 'node_modules');
var JAVASCRIPT_DIR = SOURCE_DIR + '/javascript';
module.exports = {
entry: './src/main.js',
entry: JAVASCRIPT_DIR + '/index.jsx',
output: {
// To the `dist` folder
path: './dist',
// With the filename `build.js` so it's dist/build.js
filename: 'build.js'
path: BUILD_DIR,
filename: 'bundle.js'
},
resolve: {
extensions: ['', '.js', '.vue'],
@ -29,34 +32,55 @@ module.exports = {
loaders: [
{
// Ask webpack to check: If this file ends with .js, then apply some transforms
test: /\.js$/,
test: /\.jsx?$/,
// Transform it with babel
loader: 'babel',
// don't transform node_modules folder (which don't need to be compiled)
exclude: /node_modules/
include: [JAVASCRIPT_DIR],
query: {
plugins: ['transform-runtime'],
presets: ['es2015', 'stage-0', 'react'],
}
},
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.scss$/,
loaders: ["style", "css", "sass"]
test: /\.css$/,
loader: "style-loader!css-loader"
},
{
test: /\.png$/,
loader: "url-loader?limit=100000"
},
]
{
test: /\.jpg$/,
loader: "file-loader"
},
vue: {
loaders: {
js: 'babel'
{
test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/font-woff'
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/octet-stream'
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file'
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=image/svg+xml'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: sourceDir + '/index.html'
})
template: SOURCE_DIR + '/index.html'
}),
new CopyWebpackPlugin([
{
from: NODEMODULES_DIR + '/pdfjs-dist/build/pdf.worker.js',
to: 'bundle.worker.js'
},
])
]
}