public release
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
src/vendor/
|
||||
node_modules/
|
67
.eslintrc
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"env": { "es6": true },
|
||||
"plugins": [ "html", "json", "react", "jsx" ],
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"comma-style": [ 2, "last" ],
|
||||
"comma-dangle": [ 2, "never" ],
|
||||
"curly": 0,
|
||||
"no-bitwise": 0,
|
||||
"no-console": 0,
|
||||
"no-control-regex": 0,
|
||||
"no-empty": 0,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-use-before-define": [ 1, {
|
||||
"functions":false, "classes":true, "variables":false
|
||||
}],
|
||||
"no-useless-escape": 0,
|
||||
"semi": [ 2, "always" ],
|
||||
|
||||
"react/display-name": 0,
|
||||
"react/jsx-boolean-value": 2,
|
||||
"react/jsx-closing-bracket-location": 0,
|
||||
"react/jsx-curly-spacing": [2, "never"],
|
||||
"react/jsx-indent-props": 0,
|
||||
"react/jsx-max-props-per-line": 0,
|
||||
"react/jsx-no-duplicate-props": 2,
|
||||
"react/jsx-no-literals": 0,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-quotes": 0,
|
||||
"react/jsx-sort-prop-types": 0,
|
||||
"react/jsx-sort-props": 0,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/no-danger": 0,
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-direct-mutation-state": 2,
|
||||
"react/no-multi-comp": 0,
|
||||
"react/no-set-state": 0,
|
||||
"react/no-unknown-property": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/require-extension": 0,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/sort-comp": [2, {
|
||||
"order": [
|
||||
"mixins",
|
||||
"displayName",
|
||||
"defaultProps",
|
||||
"constructor",
|
||||
"getDefaultProps",
|
||||
"getInitialState",
|
||||
"getChildContext",
|
||||
"componentWillMount",
|
||||
"componentDidMount",
|
||||
"componentWillReceiveProps",
|
||||
"shouldComponentUpdate",
|
||||
"componentWillUpdate",
|
||||
"componentDidUpdate",
|
||||
"componentWillUnmount",
|
||||
"everything-else",
|
||||
"render"
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/coverage
|
||||
/dist
|
||||
/node_modules
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*.foo
|
||||
*.map
|
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"node_modules/*": true,
|
||||
"dist/*": true,
|
||||
"src/vendor/*": true,
|
||||
"*.map": true,
|
||||
"*.foo": true
|
||||
}
|
||||
}
|
27
CONTRIBUTING.md
Normal file
@ -0,0 +1,27 @@
|
||||
## Prerequisites
|
||||
|
||||
[Node.js](http://nodejs.org/) >= v4 must be installed.
|
||||
|
||||
## Installation
|
||||
|
||||
- Running `npm install` in the app's root directory will install everything you need for development.
|
||||
|
||||
## Development Server
|
||||
|
||||
- `npm start` will run the app's development server at [http://localhost:3000](http://localhost:3000) with hot module reloading.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- `npm test` will run the tests once.
|
||||
|
||||
- `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
|
||||
|
||||
- `npm run test:watch` will run the tests on every change.
|
||||
|
||||
## Building
|
||||
|
||||
- `npm run build` creates a production build by default.
|
||||
|
||||
To create a development build, set the `NODE_ENV` environment variable to `development` while running this command.
|
||||
|
||||
- `npm run clean` will delete built resources.
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2017-present SheetJS LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
||||
.PHONY: dist
|
||||
dist:
|
||||
rm -f dist/*.{js,css,html,map}
|
||||
npm run build
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f *.map *.foo
|
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# cfb-editor
|
||||
|
||||
Archive (ZIP/CFB) Editor
|
||||
|
||||
See <https://sheetjs.com/cfb-editor/> for a live instance
|
13
nwb.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
type: 'react-app',
|
||||
webpack: {
|
||||
extra: {
|
||||
module: {
|
||||
rules: [
|
||||
// {test: /\.html$/, loader: 'html-loader'},
|
||||
{test: /\.md$/, use: 'raw-loader'}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
42
package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "cfb-editor",
|
||||
"version": "0.1.0",
|
||||
"description": "CFB / ZIP editor",
|
||||
"private": true,
|
||||
"homepage": "https://sheetjs.com/cfb-editor",
|
||||
"scripts": {
|
||||
"build": "nwb build-react-app",
|
||||
"clean": "nwb clean-app",
|
||||
"start": "nwb serve-react-app",
|
||||
"lint": "eslint --ext js,jsx,html src/",
|
||||
"test:coverage": "nwb test-react --coverage",
|
||||
"test:watch": "nwb test-react --server"
|
||||
},
|
||||
"dependencies": {
|
||||
"cfb": "^1.1.1",
|
||||
"crc-32": "^1.2.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"printj": "^1.1.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-easy-state": "^3.0.1",
|
||||
"react-fa": "^5.0.0",
|
||||
"react-markdown": "^2.5.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-spinkit": "^3.0.0",
|
||||
"react-split-pane": "^0.1.66",
|
||||
"react-syntax-highlighter": "^6.1.1",
|
||||
"react-tippy": "^1.2.2",
|
||||
"sweetalert": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nwb": "0.19.x"
|
||||
},
|
||||
"author": "sheetjs",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/SheetJSDev/cfb-editor.git"
|
||||
}
|
||||
}
|
BIN
public/CloseFile.gif
Normal file
After Width: | Height: | Size: 731 KiB |
BIN
public/DeleteFile.gif
Normal file
After Width: | Height: | Size: 788 KiB |
BIN
public/DownloadEntry.gif
Normal file
After Width: | Height: | Size: 1011 KiB |
BIN
public/DownloadFile.gif
Normal file
After Width: | Height: | Size: 811 KiB |
BIN
public/DragFile.gif
Normal file
After Width: | Height: | Size: 777 KiB |
BIN
public/NewFile.gif
Normal file
After Width: | Height: | Size: 683 KiB |
BIN
public/ReadFile.gif
Normal file
After Width: | Height: | Size: 751 KiB |
BIN
public/RenameEntry.gif
Normal file
After Width: | Height: | Size: 947 KiB |
BIN
public/RenameFile.gif
Normal file
After Width: | Height: | Size: 727 KiB |
BIN
public/UpdateFile.gif
Normal file
After Width: | Height: | Size: 847 KiB |
BIN
public/ViewFile.gif
Normal file
After Width: | Height: | Size: 775 KiB |
BIN
public/logo.png
Normal file
After Width: | Height: | Size: 37 KiB |
26
public/logo.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<clipPath id="grid"><polygon points="0,0 0,1024 1024,1024 1024,780"/></clipPath>
|
||||
<clipPath id="bars"><polygon points="0,0 1024,0 1024,780"/></clipPath>
|
||||
</defs>
|
||||
<polygon fill="#ffffff" points="0,0 0,1024 1024,1024 1024,0"/>
|
||||
<polygon fill="#63deab" stroke="#ffffff" stroke-width="10" points="0,0 0,1024 1024,1024 1024,780"/>
|
||||
<g clip-path="url(#bars)">
|
||||
<rect x="288" y="160" width="192" height="1000" fill="#a2c5f0" stroke="#ffffff" stroke-width="10" />
|
||||
<rect x="544" y="288" width="192" height="1000" fill="#ffbdbd" stroke="#ffffff" stroke-width="10" />
|
||||
<rect x="800" y="64" width="192" height="1000" fill="#a2c5f0" stroke="#ffffff" stroke-width="10" />
|
||||
</g>
|
||||
<g clip-path="url(#grid)">
|
||||
<line x1="0" y1="128" x2="1024" y2="128" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="256" x2="1024" y2="256" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="384" x2="1024" y2="384" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="512" x2="1024" y2="512" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="640" x2="1024" y2="640" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="768" x2="1024" y2="768" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="0" y1="896" x2="1024" y2="896" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="256" y1="0" x2="256" y2="1024" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="512" y1="0" x2="512" y2="1024" stroke="#ffffff" stroke-width="10" />
|
||||
<line x1="768" y1="0" x2="768" y2="1024" stroke="#ffffff" stroke-width="10" />
|
||||
</g>
|
||||
<line x1="0" y1="0" x2="1024" y2="780" stroke="#ffffff" stroke-width="10" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
48
public/styles.css
Normal file
@ -0,0 +1,48 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* react-split-pane */
|
||||
.Resizer {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
opacity: .2;
|
||||
z-index: 1;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
-webkit-transition: all 2s ease;
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
min-height:1b1px;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
min-width:11px;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
23
src/App.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
import React, {Component} from 'react';
|
||||
import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
|
||||
import SheetJSApp from './SheetJSApp';
|
||||
import HelpView from './views/HelpView';
|
||||
|
||||
class App extends Component {
|
||||
render() { return (
|
||||
<Router basename="/cfb-editor">
|
||||
<Switch>
|
||||
<Route path="/view/:idx/:mode" component={SheetJSApp} />
|
||||
<Route path="/view/:idx" component={SheetJSApp} />
|
||||
<Route exact path="/" component={SheetJSApp} />
|
||||
<Route exact path="/help" component={HelpView} />
|
||||
<Route render={() => (
|
||||
<Redirect to={`/`}/>
|
||||
)}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
); }
|
||||
}
|
||||
|
||||
export default App;
|
14
src/SheetJSApp.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
.Outer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Bottom {
|
||||
height: calc(100% - 55px);
|
||||
}
|
||||
.RightPane {
|
||||
height: calc(100vh - 55px);
|
||||
overflow-y: auto;
|
||||
}
|
168
src/SheetJSApp.js
Normal file
@ -0,0 +1,168 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* eslint-env browser */
|
||||
import React, { Component } from 'react';
|
||||
import { easyComp } from 'react-easy-state';
|
||||
import { /*BrowserRouter as Router,*/ Switch, Route, Redirect } from 'react-router-dom';
|
||||
import swal from 'sweetalert';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import './SheetJSApp.css';
|
||||
|
||||
import store from './state';
|
||||
import ToolBar from './views/ToolBar';
|
||||
import TreeList from './views/TreeList';
|
||||
import FileView from './views/FileView';
|
||||
import ManifestView from './views/ManifestView';
|
||||
import InitView from './views/InitView';
|
||||
import DragDropDiv from './components/DragDropDiv';
|
||||
//import { fix_string, unfix_string } from './utils/misc';
|
||||
import { MAX_SIZE } from './consts';
|
||||
|
||||
const renamePopup = (n) => swal("Enter the new file name", {content:{element:'input', attributes:{placeholder:n}}});
|
||||
|
||||
const _ul = (evt) => {
|
||||
if(!store.isDirty()) return;
|
||||
const msg = `Recent changes have not been saved`;
|
||||
swal(msg, {timer:1000});
|
||||
return evt.returnValue = msg;
|
||||
};
|
||||
|
||||
class SheetJSApp extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.hfoe = this.handleFileOrEntry.bind(this);
|
||||
this.flipViz = this.flip_viz.bind(this);
|
||||
this.force = this.force.bind(this);
|
||||
this.state = { viz: true };
|
||||
}
|
||||
componentDidMount() { window.removeEventListener("beforeunload", _ul); window.addEventListener("beforeunload", _ul, true); }
|
||||
|
||||
flip_viz() { this.setState({viz: !this.state.viz}); this.forceUpdate(); }
|
||||
force() { if(this.toolbar) this.toolbar.force(); }
|
||||
erase() { if(!store.isDirty()) return store.reset();
|
||||
swal(`Are you sure you want to close ${store.fname}?`, {buttons:['No', 'Yes']}).then(v => { if(v) store.reset(); });
|
||||
}
|
||||
renameFile() { renamePopup(store.fname).then(v => { if(v) store.setName(v); }); }
|
||||
handleFile(file/*:File|FileList*/) {
|
||||
if(!(file instanceof File)) return swal("Please drop only one file");
|
||||
return this.handleOneFile(file);
|
||||
}
|
||||
handleOneFile(file/*:File*/) {
|
||||
const doit = () => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const bstr = e.target.result;
|
||||
try {
|
||||
store.setBStr(bstr, (e) => { swal("Error:" + (e.message||e)); });
|
||||
store.setName(file.name);
|
||||
} catch(e) { swal("Error:" + (e.message||e)); }
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
};
|
||||
|
||||
let msg = [];
|
||||
if(file.size > MAX_SIZE) msg.push("File size " + file.size + " exceeds limit.");
|
||||
if(msg.length > 0) {
|
||||
msg.push("Operations may be slow. Shall we proceed?");
|
||||
const domelt = document.createElement("div");
|
||||
domelt.innerHTML = msg.join("<br/>\n");
|
||||
swal({content: domelt, buttons:['No', 'Yes']}).then(v => { if(v) doit(); });
|
||||
} else doit();
|
||||
}
|
||||
handleEntry(idx/*:number*/, file/*:File*/) {
|
||||
const ReadFileArrayBuffer = (file/*:File*/, cb, opts) => {
|
||||
/* Boilerplate to set up FileReader */
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
/* Parse data */
|
||||
const ab = e.target.result;
|
||||
cb(null, ab);
|
||||
};
|
||||
if(opts) {
|
||||
if(opts.size > 0 && file.size > opts.size) return cb(new Error("File size " + file.size + " exceeds limit"), null);
|
||||
}
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
ReadFileArrayBuffer(file, (err, ab) => {
|
||||
if(err) return swal("Error: " + err.message);
|
||||
store.setContentAB(idx, ab);
|
||||
}, { size: MAX_SIZE });
|
||||
}
|
||||
handleFileOrEntry(file/*:File|FileList*/) {
|
||||
if(store.isEmpty()) return this.handleFile(file);
|
||||
if(!(file instanceof File)) return swal("Please drop only one file");
|
||||
const idx = (!this.props.match || this.props.match.params.idx == null) ? -1 : this.props.match.params.idx;
|
||||
const opts = { buttons: { open: `Open`, add: "Add File" } };
|
||||
const fname = store.getFixedName(idx);
|
||||
if(idx > -1) opts.buttons.update = `Update contents`;
|
||||
swal(`${store.fname} ${fname && "(" + fname + ")"} is open. What do you want to do with ${file.name}?`, opts).then(v => {
|
||||
switch(v) {
|
||||
case 'open': {
|
||||
if(!store.isDirty()) { store.reset(); return this.handleFile(file); }
|
||||
swal(`There are unsaved changes. Are you sure you want to close ${store.fname}?`, {buttons:['No', 'Yes']}).then(v => { if(!v) return; store.reset(); this.handleFile(file); });
|
||||
} break;
|
||||
case 'add': {
|
||||
let fname = "";
|
||||
if(!store.find(file.name)) {
|
||||
fname = store.addNewFile(file.name);
|
||||
} else {
|
||||
fname = store.addNewFile();
|
||||
swal(`File ${file.name} exists! New file: ${fname}`);
|
||||
}
|
||||
const id = store.getIdByName(fname);
|
||||
this.handleEntry(id, file);
|
||||
} break;
|
||||
case 'update': this.handleEntry(idx, file); break;
|
||||
}
|
||||
});
|
||||
}
|
||||
newEntry() { const fname = store.addNewFile(); swal("New File: " + fname); }
|
||||
exportFile() { store.exportBStr(store.fname || "SheetJS.cfb"); }
|
||||
deleteEntry(idx) {
|
||||
swal(`Are you sure you want to delete ${store.getFixedName(idx)}`, {buttons:['No', 'Yes']}).then(v => {
|
||||
switch(v) {
|
||||
case true: store.delFileById(idx); break;
|
||||
default: return;
|
||||
}
|
||||
});
|
||||
}
|
||||
renameEntry(idx) { renamePopup(store.getFixedPath(idx)).then(v => { if(v) store.renFileById(idx,v); }); }
|
||||
|
||||
render() { return (
|
||||
<div className="Outer">
|
||||
<DragDropDiv handleFile={this.hfoe}>
|
||||
<ToolBar idx={this.props.match.params.idx} erase={this.erase} exportFile={this.exportFile} handleFile={this.hfoe} viz={this.state.viz} flipViz={this.flipViz} ref={(tb) => { this.toolbar = tb; }}/>
|
||||
</DragDropDiv>
|
||||
<div className="Bottom"><SplitPane split="vertical" minSize={50} maxSize={250}
|
||||
defaultSize={window.matchMedia && !window.matchMedia("(min-width: 650px)").matches ? 50 : 250}
|
||||
style={{height: 'calc(100% - 55px)'}}
|
||||
pane1Style={{display:this.state.viz ? "block" : "none"}}>
|
||||
{this.state.viz && ( <DragDropDiv handleFile={this.hfoe}>
|
||||
<TreeList idx={this.props.match.params.idx} fname={store.fname} exportFile={this.exportFile} erase={this.erase} renameFile={this.renameFile} addFile={this.newEntry} />
|
||||
</DragDropDiv>)}
|
||||
<DragDropDiv handleFile={this.hfoe}><div className="RightPane">
|
||||
{!store.isEmpty() ? (
|
||||
<Switch>
|
||||
<Route path="/view">
|
||||
<FileView idx={this.props.match.params.idx||0} mode={this.props.match.params.mode} deleteEntry={this.deleteEntry} renameEntry={this.renameEntry} handleFile={this.handleEntry} viz={this.state.viz} force={this.force} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<ManifestView exportFile={this.exportFile} erase={this.erase} renameFile={this.renameFile} addFile={this.newEntry} />
|
||||
</Route>
|
||||
<Route><Redirect to="/" /></Route>
|
||||
</Switch>
|
||||
) : (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<InitView handleFile={this.handleFile} />
|
||||
</Route>
|
||||
<Route><Redirect to="/" /></Route>
|
||||
</Switch>
|
||||
)}
|
||||
</div></DragDropDiv>
|
||||
</SplitPane></div>
|
||||
</div>
|
||||
); }
|
||||
}
|
||||
|
||||
export default easyComp(SheetJSApp);
|
29
src/components/DataInput.js
Normal file
@ -0,0 +1,29 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/*
|
||||
Simple HTML5 file input wrapper
|
||||
usage: <DataInput handleFile={callback} />
|
||||
handleFile(file:File):void;
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
|
||||
class DataInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
handleChange(e) {
|
||||
const files = e.target.files;
|
||||
if(files && files[0]) this.props.handleFile(files[0]);
|
||||
}
|
||||
render() { return (
|
||||
<form className="form-inline">
|
||||
<div className="form-group">
|
||||
{this.props.title && `${this.props.title}: `}
|
||||
<input type="file" className="form-control" id="file" onChange={this.handleChange} />
|
||||
</div>
|
||||
</form>
|
||||
); }
|
||||
}
|
||||
//<input type="file" className="form-control" id="file" accept={this.props.fmts || "*"} onChange={this.handleChange} />
|
||||
|
||||
export default DataInput;
|
16
src/components/DirtyBit.js
Normal file
@ -0,0 +1,16 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/*
|
||||
Simple Marker when a file is dirty
|
||||
usage: <DirtyBit isDirty={isdirty} />
|
||||
isDirty:boolean
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
class DirtyBit extends Component {
|
||||
constructor(props) { super(props); }
|
||||
render() { return this.props.isDirty ? ( <Tooltip title="File has unsaved changes" position="bottom">**</Tooltip> ) : ""; }
|
||||
}
|
||||
|
||||
export default DirtyBit;
|
23
src/components/DragDropDiv.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/*
|
||||
Simple HTML5 file drag-and-drop wrapper
|
||||
usage: <DragDropDiv handleFile={handleFile}>...</DragDropFile>
|
||||
handleFile(file:File):void;
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
|
||||
class DragDropDiv extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
}
|
||||
suppress(evt) { evt.stopPropagation(); evt.preventDefault(); }
|
||||
onDrop(evt) { evt.stopPropagation(); evt.preventDefault();
|
||||
const files = evt.dataTransfer.files;
|
||||
if(!files || files.length == 0) return;
|
||||
this.props.handleFile((files.length == 1) ? files[0] : files);
|
||||
}
|
||||
render() { return ( <div className="DDD" onDrop={this.onDrop} onDragEnter={this.suppress} onDragOver={this.suppress}>{this.props.children}</div> ); }
|
||||
}
|
||||
|
||||
export default DragDropDiv;
|
6
src/components/Tooltip.js
Normal file
@ -0,0 +1,6 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* This is a light-weight wrapper to hide the css import for react-tippy */
|
||||
import 'react-tippy/dist/tippy.css';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
export default Tooltip;
|
11
src/consts.js
Normal file
@ -0,0 +1,11 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
export const THRESH = 24*16;
|
||||
export const MAX_SIZE = 5<<20;
|
||||
|
||||
export const SheetJSFT = [
|
||||
"application/zip",
|
||||
"application/octet-stream"
|
||||
].join(",") + [
|
||||
"zip", "xlsx", "xlsb", "xlsm", "ods",
|
||||
"cfb", "xls", "qpw", "wb3", "ppt", "doc"
|
||||
].map(function(x) { return "." + x; }).join(",");
|
21
src/index.css
Normal file
@ -0,0 +1,21 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
}
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: auto;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
33
src/index.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- cfb-editor (C) 2017-present SheetJS http://sheetjs.com -->
|
||||
<!-- vim: set ts=2: -->
|
||||
<html lang="en" style="height: 100%">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'>
|
||||
<title>CFB Editor</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/tyleruebele/details-shim/ba9df5f3/details-shim.min.css" />
|
||||
<link href="styles.css" rel="stylesheet"/>
|
||||
<link rel="shortcut icon" type="image/png" href="logo.png"/>
|
||||
</head>
|
||||
<body class="Body">
|
||||
<script src="https://cdn.rawgit.com/tyleruebele/details-shim/ba9df5f3/details-shim.min.js"></script>
|
||||
<div id="app" class="root"></div>
|
||||
<script type="text/javascript">
|
||||
/* eslint-env browser */
|
||||
/* eslint-disable-next-line */
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-36810333-1']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
10
src/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* eslint-env browser */
|
||||
import './index.css';
|
||||
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
|
||||
import App from './App';
|
||||
|
||||
render(<App/>, document.querySelector('#app'));
|
169
src/state.js
Normal file
@ -0,0 +1,169 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* eslint-env browser */
|
||||
import { easyStore } from 'react-easy-state';
|
||||
|
||||
import * as CFB from 'cfb';
|
||||
import * as JSZIP from './vendor/jszip';
|
||||
import { SaveBArray, SaveBString } from './utils/SaveData';
|
||||
import { fix_string, unfix_string, ab2a, s2a, a2s } from './utils/misc';
|
||||
|
||||
export default easyStore({
|
||||
fname: "",
|
||||
file: null,
|
||||
dirty: false,
|
||||
loading: false,
|
||||
|
||||
isDirty() { return this.file && this.dirty; },
|
||||
|
||||
setLoading(x) { return this.loading = x; },
|
||||
getLoading() { return this.loading; },
|
||||
|
||||
/* Type tests */
|
||||
isEmpty() { return !this.file; },
|
||||
isCFB() { return this.file && this.file.FullPaths; },
|
||||
isZIP() { return this.file && !this.isCFB(); },
|
||||
getType() { return this.isCFB() ? "CFB" : this.isZIP() ? "ZIP" : "???"; },
|
||||
|
||||
/* File-level Accessors */
|
||||
getFileList() { return this.keys().map((x, i) => [x, this.getFileEntryById(i), i]).filter(x => this.isCFB() ? x[1].type == 2 : !x[1].dir); },
|
||||
getNextName(id) { return !id ? this.getNextName(1) : this.find(`SheetJS${id}`) ? this.getNextName(id+1) : `SheetJS${id}`; },
|
||||
getRootName() { return !this.isCFB() ? "" : this.getFileNameById(0); },
|
||||
getCLSID() { return !this.isCFB() ? "" : this.getFileEntryById(0).clsid; },
|
||||
|
||||
/* File-level Mutators */
|
||||
setName(name) { this.fname = name; },
|
||||
setBStr(bstr, cb) {
|
||||
this.loading = true;
|
||||
setTimeout(() => {try {
|
||||
switch(bstr.slice(0,4)) {
|
||||
case "\xD0\xCF\x11\xE0": this.type = "CFB"; this.file = CFB.read(bstr, {type: "binary"}); break;
|
||||
case "PK\x03\x04": case "PK\x05\x06": this.type = "ZIP"; this.file = new JSZIP(bstr, {base64: false}); break;
|
||||
default: throw new Error(`Invalid file (magic ${bstr.slice(0,4).split("").map(x => x.charCodeAt(0).toString(16).padStart(2,"0"))})`);
|
||||
}
|
||||
this.loading = false;
|
||||
} catch(e) { this.loading = false; if(cb) cb(e); } }, 100); },
|
||||
addNewFile(name) {
|
||||
const fname = name || this.getNextName(1);
|
||||
if(this.isCFB()) CFB.utils.cfb_add(this.file, fname, [0x37, 0x32, 0x36, 0x32]);
|
||||
else if(this.isZIP()) this.file.file(fname, "7262");
|
||||
const fi = this.find(fname);
|
||||
this.dirty = true;
|
||||
return this.isCFB() ? this.getFileNameById(this.entries().indexOf(fi)) : fname;
|
||||
},
|
||||
|
||||
/* Entry-level Accessors */
|
||||
getFileNameById(id) { return this.keys()[id] || ""; },
|
||||
getFileEntryById(id) { return this.entries()[id] || null; },
|
||||
getFixedName(id) { return this.isEmpty() ? "" : fix_string(this.getFileNameById(id)); },
|
||||
getFixedPath(id) { return this.isEmpty() ? ""
|
||||
: this.isCFB() ? fix_string(this.getFileNameById(id)).replace(/^[^/]*/,"")
|
||||
: fix_string(this.getFileNameById(id)); },
|
||||
getFileTime(id) { return (this.isEmpty() || !this.getFileEntryById(id)) ? new Date(NaN) : (this.getFileTimeEntry(id) || this.getFileTimeEntry(0) || new Date(1980,0,1)); },
|
||||
|
||||
/* Entry-level Mutators */
|
||||
setContentAB(id, ab) {
|
||||
const payload = ab2a(ab), fn = this.getFileNameById(id);
|
||||
if(this.isCFB()) {
|
||||
CFB.utils.prep_blob(payload);
|
||||
CFB.utils.cfb_add(this.file, fn, ab2a(payload));
|
||||
} else if(this.isZIP()) {
|
||||
this.file.file(fn, payload.map(x => String.fromCharCode(x)).join(""), {binary:"true"});
|
||||
}
|
||||
this.dirty = true;
|
||||
},
|
||||
renFileById(id, name) {
|
||||
const oldfn = this.getFileNameById(id), newfn = unfix_string(name).replace(/^\//,"");
|
||||
if(this.isCFB()) {
|
||||
CFB.utils.cfb_mov(this.file, oldfn, this.getFileNameById(0) + newfn);
|
||||
CFB.utils.cfb_gc(this.file);
|
||||
} else if(this.isZIP()) {
|
||||
this.file.file(newfn, a2s(this.getContentById(id)), {binary: true});
|
||||
this.file.remove(oldfn);
|
||||
}
|
||||
this.dirty = true;
|
||||
},
|
||||
delFileById(id) {
|
||||
const fn = this.getFileNameById(id);
|
||||
if(this.isCFB()) CFB.utils.cfb_del(this.file, fn);
|
||||
if(this.isZIP()) this.file.remove(fn);
|
||||
this.dirty = true;
|
||||
},
|
||||
|
||||
/* Download data */
|
||||
exportBStrById(id) {
|
||||
this.loading = true;
|
||||
setTimeout(() => {try {
|
||||
const FI = this.getFileEntryById(id);
|
||||
SaveBArray(this.getContentByEntry(FI), FI.name);
|
||||
this.loading = false;
|
||||
} catch(e) { this.loading = false; throw e; } }, 100);
|
||||
},
|
||||
exportBStr(/*name*/) {
|
||||
this.loading = true;
|
||||
setTimeout(() => {try {
|
||||
this.dirty = false;
|
||||
let o;
|
||||
if(this.isCFB()) o = SaveBString(CFB.write(this.file, {type:"binary"}), this.fname || "SheetJS.cfb");
|
||||
if(this.isZIP()) o = SaveBString(this.file.generate({type:"string", compression:"DEFLATE"}), this.fname || "SheetJS.zip");
|
||||
this.loading = false;
|
||||
return o;
|
||||
} catch(e) { this.loading = false; throw e; } }, 100); },
|
||||
|
||||
/* Initialization */
|
||||
newFile(name) {
|
||||
this.fname = name || "sheetjs.cfb";
|
||||
if(this.fname.match(/\.zip$/)) {
|
||||
const out = new JSZIP();
|
||||
out.file("Sh33tJ5", "7262");
|
||||
this.file = out;
|
||||
} else this.file = CFB.utils.cfb_new();
|
||||
this.dirty = true;
|
||||
},
|
||||
reset() { this.file = null; this.fname = ""; this.dirty = false; },
|
||||
|
||||
/* Utils */
|
||||
getContentById(id) { return this.getContentByEntry(this.getFileEntryById(id)); },
|
||||
getContentByEntry(fi) {
|
||||
if(!fi) return [];
|
||||
if(fi._data) return typeof fi._data == "string" ? s2a(fi._data) : ab2a(fi._data.getContent());
|
||||
return fi.content || []; },
|
||||
getContentSliceByEntry(fi, s, e) {
|
||||
return !fi ? []
|
||||
: fi.content ? fi.content.slice(s, e)
|
||||
: this.getContentByEntry(fi).slice(s, e);
|
||||
},
|
||||
getSizeByEntry(fi) {
|
||||
if(this.isCFB() && (!fi.content || !fi.content.length)) return 0;
|
||||
return fi && (fi.size || fi._data && (fi._data.uncompressedSize) || fi._data.length) || 0;
|
||||
},
|
||||
getTextByEntry(fi) {
|
||||
if(!fi || (!fi._data && !fi.content)) return "";
|
||||
if(fi._data) return (typeof fi._data == "string") ? fi._data : a2s(fi._data.getContent());
|
||||
return a2s(fi.content);
|
||||
},
|
||||
getFileTimeEntry(id) {
|
||||
const FI = this.getFileEntryById(id);
|
||||
return !FI ? new Date(NaN) : (FI.ct || FI.mt || FI.date || id != 0 && this.getFileTime(0) || new Date(1980,0,1));
|
||||
},
|
||||
find(path) { return this.isEmpty() ? null
|
||||
: this.isCFB() ? CFB.find(this.file, path)
|
||||
: this.file.filter((rp, f) => (path == rp || path == f.name))[0]; },
|
||||
keys() { return this.isEmpty() ? []
|
||||
: this.isCFB() ? this.file.FullPaths
|
||||
: Object.keys(this.file.files).filter(x => !this.file.files[x].dir);
|
||||
},
|
||||
entries() { return this.isEmpty() ? []
|
||||
: this.isCFB() ? this.file.FileIndex
|
||||
: this.keys().map(x => this.file.files[x]);
|
||||
},
|
||||
getIdByName(name) {
|
||||
if(this.isEmpty()) return -1;
|
||||
if(this.isZIP()) {
|
||||
const entries = Object.keys(this.file.files).filter(x => !this.file.files[x].dir);
|
||||
return entries.indexOf(name);
|
||||
}
|
||||
return this.entries().indexOf(this.find(name));
|
||||
},
|
||||
|
||||
Sheet: "JS"
|
||||
});
|
27
src/utils/SaveData.js
Normal file
@ -0,0 +1,27 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* eslint-env browser */
|
||||
/* Save workbook on client side */
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
/* see Browser download file example in docs */
|
||||
const s2ab = (s/*:string*/) => {
|
||||
const buf = new ArrayBuffer(s.length);
|
||||
const view = new Uint8Array(buf);
|
||||
for (let i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
|
||||
return buf;
|
||||
};
|
||||
|
||||
const a2ab = (a/*:Array<number>*/) => {
|
||||
const o = new ArrayBuffer(a.length);
|
||||
const view = new Uint8Array(o);
|
||||
for (let i = 0; i!=a.length; ++i) view[i] = a[i];
|
||||
return o;
|
||||
};
|
||||
|
||||
export const SaveBString = (str, fname = "sheetjs.out") => {
|
||||
saveAs(new Blob([s2ab(str)],{type:"application/octet-stream"}), fname || "sheetjs.out");
|
||||
};
|
||||
|
||||
export const SaveBArray = (arr, fname = "sheetjs.out") => {
|
||||
saveAs(new Blob([a2ab(arr)],{type:"application/octet-stream"}), fname || "sheetjs.out");
|
||||
};
|
63
src/utils/misc.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* global Uint8Array */
|
||||
import { sprintf, vsprintf } from 'printj';
|
||||
|
||||
const X = "%02hhx", Y = X + X + " ";
|
||||
const FMT = [...Array.from({length:16}).map((x,i) =>
|
||||
Y.repeat(i>>1) + (i%2 ? X:" ") + " " + " ".repeat(7 - (i >> 1)) + "|" + "%c".repeat(i) + " ".repeat(16-i) + "|\n"
|
||||
), Y.repeat(8) + "|" + "%c".repeat(16) + "|\n"];
|
||||
export const line = ([x,d]) => {
|
||||
if(!d || !d.length) return "";
|
||||
if(!FMT[d.length]) return "wtf";
|
||||
return vsprintf("%04x: " + FMT[d.length], [x, ...d, ...d.map(x => String.fromCharCode(x).replace(/[^\x20-\x7E]/g,"."))]);
|
||||
};
|
||||
|
||||
export const fix_string = (x) => x.replace(/[\u0000-\u001f]/, ($$) => sprintf("\\u%04X", $$.charCodeAt(0)));
|
||||
|
||||
export const unfix_string = (x) => x.replace(/\\u(\d{4})/g, ($$, $1) => String.fromCharCode(parseInt($1, 16)));
|
||||
|
||||
export const format_date = (date/*:Date*/) => sprintf("%02u-%02u-%02u %02u:%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100, date.getUTCHours(), date.getUTCMinutes());
|
||||
|
||||
export const ab2a = (ab) => {
|
||||
const o = [], u = new Uint8Array(ab);
|
||||
for(let i = 0; i < u.length; ++i) o[i] = u[i];
|
||||
return o;
|
||||
};
|
||||
|
||||
export const s2a = x => x.split("").map(x => x.charCodeAt(0));
|
||||
|
||||
export const a2s = x => {
|
||||
const a = new Array(x.length);
|
||||
for(let i = 0; i < x.length; ++i) a.push(String.fromCharCode(x[i]));
|
||||
return a.join("");
|
||||
};
|
||||
|
||||
const decode_msi_char = (x) => {
|
||||
switch(true) {
|
||||
case x<10: return x + 48;
|
||||
case x<36: return x + 55;
|
||||
case x<62: return x + 61;
|
||||
case x==62: return 46;
|
||||
}
|
||||
return 95;
|
||||
};
|
||||
|
||||
export const decode_msi_name = (instr) => {
|
||||
var ch = instr.charCodeAt(0);
|
||||
var idx = 0;
|
||||
var out = [];
|
||||
if(ch == 0x4840) { ++idx; /*out.push("*");*/ }
|
||||
while ((ch = instr.charCodeAt(idx++))) {
|
||||
if(ch >= 0x3800 && ch < 0x4840 ) {
|
||||
if(ch >= 0x4800) {
|
||||
ch = decode_msi_char(ch-0x4800);
|
||||
} else {
|
||||
ch -= 0x3800;
|
||||
out.push(String.fromCharCode(decode_msi_char(ch & 0x3f)));
|
||||
ch = decode_msi_char( (ch>>6) & 0x3f);
|
||||
}
|
||||
}
|
||||
out.push(String.fromCharCode(ch));
|
||||
}
|
||||
return out.join("");
|
||||
};
|
14
src/utils/xml.js
Normal file
@ -0,0 +1,14 @@
|
||||
var xlmlregex = /<(\/?)([^\s?>!/:]*:|)([^\s?>:/]+)[^>]*>/mg;
|
||||
export default function formatXml(xml) {
|
||||
var indent = 0, last = "";
|
||||
var formatted = xml.replace(xlmlregex, function($$, $1, $2, $3) {
|
||||
var old = indent;
|
||||
if($1 == "/") { --indent; --old; }
|
||||
else if($$.charAt($$.length - 2) == "/");
|
||||
else ++indent;
|
||||
var pad = ($3 == last && $1 == "/") ? "" : "\n" + " ".repeat(old);
|
||||
last = $3;
|
||||
return pad + $$;
|
||||
});
|
||||
return formatted.replace(/\n+\n</mg, "\n<");
|
||||
}
|
8988
src/vendor/jszip.js
vendored
Normal file
120
src/views/FileView.js
Normal file
@ -0,0 +1,120 @@
|
||||
/* cfb-editor (C) 2017-present SheetJS -- http://sheetjs.com */
|
||||
/* eslint-env browser */
|
||||
import React, { PureComponent } from 'react';
|
||||
import { easyComp } from 'react-easy-state';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import { Icon } from 'react-fa';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { arduinoLight } from 'react-syntax-highlighter/styles/hljs';
|
||||
import * as Spinner from 'react-spinkit';
|
||||
import './MainPane.css';
|
||||
|
||||
import { sprintf } from 'printj';
|
||||
import { buf } from 'crc-32';
|
||||
|
||||
import store from '../state';
|
||||
import DataInput from '../components/DataInput';
|
||||
import { line, fix_string, decode_msi_name } from '../utils/misc';
|
||||
import { THRESH } from '../consts';
|
||||
import xml from '../utils/xml';
|
||||
|
||||
/* TODO: load through state so screen doesn't block on render */
|
||||
class FileView extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {loading: true, FN:"", _size:0, _crc32:"", show_text:false, indexData:[]};
|
||||
["export", "delete", "rename", "handle", "stload", "common"].forEach(n => { this[n] = this[n].bind(this); });
|
||||
}
|
||||
componentWillMount() { this.common(this.props, true); }
|
||||
componentWillReceiveProps(nextProps) { this.common(nextProps, false); return true; }
|
||||
export() { store.exportBStrById(this.props.idx); }
|
||||
delete() { this.props.deleteEntry(this.props.idx); }
|
||||
rename() { this.props.renameEntry(this.props.idx); }
|
||||
handle(f) { this.props.handleFile(this.props.idx, f); }
|
||||
stload() { }
|
||||
common(props/*, mnt*/) {
|
||||
if(!this.state.loading) { this.setState({loading:true}); }
|
||||
//const doit = mnt || this.props.idx != props.idx || this.props.mode != props.mode;
|
||||
setTimeout(() => {
|
||||
const FP = store.getFileNameById(props.idx), FI = store.getFileEntryById(props.idx);
|
||||
if(!FP || !FI || store.isCFB() && FI.type != 2) return this.setState({loading:false});
|
||||
const FN = fix_string(FI.name);
|
||||
|
||||
const content = store.getContentByEntry(FI);
|
||||
const _size = store.getSizeByEntry(FI);
|
||||
const _crc32 = sprintf("%0.8X", buf(content));
|
||||
|
||||
const show_text = props.mode == "text";
|
||||
const T = show_text ? store.getTextByEntry(FI) : "";
|
||||
const doxml = T && (T.slice(0,6) == "<?xml " || T.slice(0,100).match(/^<\w*(:\w+)?[\s/>]/));
|
||||
|
||||
let show_link = props.mode || "text"; if(show_link == "full") show_link = "text";
|
||||
if(!props.mode || props.mode == "full") {
|
||||
const hdr = store.getContentSliceByEntry(FI,0,16);
|
||||
if(hdr[0] == 0xFF && hdr[1] == 0xD8 && hdr[2] == 0xFF) show_link = "imag";
|
||||
}
|
||||
|
||||
const show_imag = this.props.mode == "imag", I = show_imag ? "data:image/jpeg;base64," + btoa(store.getTextByEntry(FI)) : "";
|
||||
|
||||
const Target = this.props && this.props.mode ? store.getSizeByEntry(FI) : Math.min(store.getSizeByEntry(FI),THRESH);
|
||||
const indices = Array.from({length:(Target)+15>>4}).map((x,i) => 16*i);
|
||||
const indexData = (T || I) ? [] : indices.map(x => line([x, store.getContentSliceByEntry(FI, x,x+16)]));
|
||||
this.setState({ loading:false, FN, _size, _crc32, show_text, T, doxml, show_link, show_imag, I, indices, indexData });
|
||||
this.forceUpdate();
|
||||
}, 0);
|
||||
}
|
||||
render() {
|
||||
const FP = store.getFileNameById(this.props.idx), FI = store.getFileEntryById(this.props.idx);
|
||||
if(!FP || !FI || store.isCFB() && FI.type != 2) return ( <Redirect to={`/`} /> );
|
||||
|
||||
const _FP = fix_string(FP);
|
||||
|
||||
/* indices, indices.map(x => line([x, store.getContentSliceByEntry(FI, x,x+16)])) */
|
||||
const { FN, _size, _crc32, show_text, T, doxml, show_link, show_imag, I, indices, indexData } = this.state;
|
||||
const partial = _size > THRESH && !this.props.mode;
|
||||
|
||||
const show_link_txt = { "text": "Text", "imag": "Image" }[show_link];
|
||||
const out = (
|
||||
<div className="conpainer">
|
||||
<div><b>File Name:</b> {FN} {FN.charCodeAt(0) >= 0x3800 && FN.charCodeAt(0) <= 0x4840 && `MSI Name: ${decode_msi_name(FN)}`}</div>
|
||||
<div className="minor">{FN != FI.name ? `File name has non-display characters, which are rendered as "\\u" followed by character code` : `\u00A0`}</div>
|
||||
<div className="minor"> </div>
|
||||
<div className="flexrow">
|
||||
<div className="col-xs-4"><a onClick={this.export |