This commit is contained in:
2019-06-30 21:51:41 +08:00
commit eab01e3674
14 changed files with 11314 additions and 0 deletions

181
src/App.css Executable file
View File

@ -0,0 +1,181 @@
.App {
height: 100%;
width: 100%;
}
/* apply a natural box layout model to all elements */
*,
*:before,
*:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html {
height: 100%;
}
body {
margin: 0;
padding: 0;
height: 100%;
font-family: 'Anaheim', sans-serif;
}
#output,
#preview,
#input {
width: 100%;
display: block;
height: 40%;
border: none;
font-family: 'Anaheim', sans-serif;
}
#input {
height: 10%;
background-color: #7a9a95;
-webkit-transition: background-color 250ms;
-moz-transition: background-color 250ms;
transition: background-color 250ms;
text-align: center;
font-size: 20px;
color: #fff;
}
#output {
background-color: #e5e5e5;
}
#input:focus {
background-color: #90c5a9;
}
#preview {
text-align: center;
}
.Power {
text-align: center;
width: 100%;
padding: 16px 10px 10px 10px;
background-color: aliceblue;
}
.PowerType {
max-width: 320px;
width: 100%;
margin: auto;
text-align: left;
}
#loading {
background-color: #45b29d;
height: 100%;
width: 100%;
position: fixed;
z-index: 99999999999;
margin-top: 0px;
top: 0px;
display: none;
}
.loading-title {
width: 100%;
text-align: center;
color: #fff;
}
#loading-center {
width: 100%;
height: 100%;
position: relative;
}
#loading-center-absolute {
position: absolute;
left: 50%;
top: 50%;
height: 50px;
width: 150px;
margin-top: -25px;
margin-left: -75px;
}
.object {
width: 8px;
height: 50px;
margin-right: 5px;
background-color: #FFF;
-webkit-animation: animate 1s infinite;
animation: animate 1s infinite;
float: left;
}
.object:last-child {
margin-right: 0px;
}
.object:nth-child(10) {
-webkit-animation-delay: 0.9s;
animation-delay: 0.9s;
}
.object:nth-child(9) {
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.object:nth-child(8) {
-webkit-animation-delay: 0.7s;
animation-delay: 0.7s;
}
.object:nth-child(7) {
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
.object:nth-child(6) {
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
}
.object:nth-child(5) {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.object:nth-child(4) {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.object:nth-child(3) {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
.object:nth-child(2) {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s;
}
@-webkit-keyframes animate {
50% {
-ms-transform: scaleY(0);
-webkit-transform: scaleY(0);
transform: scaleY(0);
}
}
@keyframes animate {
50% {
-ms-transform: scaleY(0);
-webkit-transform: scaleY(0);
transform: scaleY(0);
}
}

444
src/App.js Executable file
View File

@ -0,0 +1,444 @@
import React, { Component } from 'react';
import './App.css';
import Axios from 'axios';
const removeUnused = require('postcss-remove-unused');
const postcss = require('postcss');
const prettier = require("prettier/standalone");
const plugins = [require("prettier/parser-html"), require("prettier/parser-postcss")];
const CSS = require('css')
const cssjs = require("jotform-css.js");
//initialize parser object
const parser = new cssjs.cssjs();
// 移除指定标签
function removeTags(tagName, el = document){
var tagElements = el.getElementsByTagName(tagName);
for(var m = tagElements.length - 1; m >= 0; m--){
tagElements[m].parentNode.removeChild( tagElements[m]);
}
}
// 移除所有CSS
function removeAllCSS (el = document) {
el.querySelectorAll('link').forEach(linkEl => linkEl.rel === 'stylesheet' && linkEl.remove());
el.querySelectorAll('style').forEach(styleEl => styleEl.remove());
(function recurse (node) {
node.childNodes.forEach(recurse);
if (node.removeAttribute) {
node.removeAttribute('height')
node.removeAttribute('border')
node.removeAttribute('cellpadding')
node.removeAttribute('cellspacing')
node.removeAttribute('class')
}
return node.removeAttribute && node.removeAttribute('style');
})(el.body);
}
// 移除表格所有宽度
function removeTableWidth (el = document) {
el.querySelectorAll('width').forEach(styleEl => styleEl.remove());
(function recurse (node) {
node.childNodes.forEach(recurse);
return node.removeAttribute && node.removeAttribute('width');
})(el.body);
}
// 为CSS的AST添加前缀
function scopeCSS(css, prefix) {
var ast = CSS.parse(css);
var stylesheet = ast.stylesheet;
if (stylesheet) {
var rules = stylesheet.rules;
// Append our container scope to rules
// Recursive rule appender
var ruleAppend = function(rules) {
rules.forEach(function(rule) {
if (rule.selectors !== undefined) {
rule.selectors = rule.selectors.map(function(selector) {
return prefix + " " + selector;
});
}
if (rule.rules !== undefined) {
ruleAppend(rule.rules);
}
});
};
ruleAppend(rules);
}
return CSS.stringify(ast);
}
// 移除指定的CSS样式
function removeCSS(ast, names) {
ast = ast.map(function(rule) {
if (rule.selector) {
for (const n in names) {
if (rule.selector === names[n]) {
return null
}
}
}
return rule
});
ast = ast.filter(function (s) {
return s != null
});
return ast;
}
// 移除包含指定字符串的CSS样式
function removeCSSIncludes(ast, names) {
ast = ast.map(function(rule) {
if (rule.selector) {
for (const n in names) {
if (rule.selector.includes(names[n])) {
return null
}
}
}
return rule
});
ast = ast.filter(function (s) {
return s != null
});
return ast;
}
// 移除包含指定字符串的CSS属性
function removeCSSatt(ast, names) {
ast = ast.map(function(rule) {
let rules = rule.rules
rules = rules.map((r) => {
if (r.directive) {
for (const n in names) {
if (r.directive.includes(names[n])) {
return null
}
}
}
if (r.value && r.value.length - 2 > 0 && r.value.lastIndexOf('pt') === r.value.length - 2) { // 判断以 pt 结尾
r.value = r.value.replaceAll('0cm', '0').replaceAll('pt', 'px')
}
return r
})
rules = rules.filter(function (s) {
return s != null
});
rule.rules = rules
return rule
});
ast = ast.filter(function (s) {
return s != null
});
return ast;
}
// 生成随机字符串
function makeid() {
var text = "";
var possible = "abcdefghijklmnopqrstuvwxyz";
for( var i=0; i < 8; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
// 表格宽度转百分比
function convertWidthToPercent (el = document) {
const tables = el.body.getElementsByTagName('table')
for (const n in tables) {
const table = tables[n]
if (table) {
const tableWidth = table.offsetWidth;
if (table.removeAttribute) {
table.removeAttribute('width')
}
if (table.style && table.style.width) table.style.width = '100%'
const tds = el.body.getElementsByTagName('td')
for (const i in tds) {
const node = tds[i]
if (node.removeAttribute) {
node.removeAttribute('width')
}
if (node.offsetWidth && node.offsetWidth > 0) {
node.style.width = (node.offsetWidth / tableWidth).toFixed(3) * 100 + '%'
}
}
}
}
}
// 移除Style中Office字体样式和专用样式
function removeStyleatt (el = document) {
const tables = el.body.getElementsByTagName('table');
(function recurse (nodes) {
for (const i in nodes) {
const node = nodes[i]
if (node.getAttribute) {
const style = node.getAttribute('style')
if (style) {
const classid = makeid()
let parsed = parser.parseCSS('.' + classid + '{' + style + '}');
parsed = removeCSSatt(parsed, ['font-family', 'mso-', 'page-break-inside'])
let newCSSString = parser.getCSSForEditor(parsed);
newCSSString = newCSSString.match(/\{[\S\s]+\}/)
if (newCSSString && newCSSString.length >= 1) {
newCSSString = newCSSString[0].substring(1, newCSSString[0].length-1).replace(/\s/g,'').replace(/<\/?.+?>/g,"").replace(/[\r\n]/g, "")
if (newCSSString !== '') {
node.setAttribute('style', newCSSString)
}
} else {
node.removeAttribute('style')
}
}
}
if (node.childNodes) recurse(node.childNodes)
}
})(tables);
for (const x in tables) {
if (tables[x] && tables[x].getAttribute) {
let style = tables[x].getAttribute('style')
if (style && style.includes('border:none;')) {
style = style.replaceAll('border:none;', '')
if (style !== '') {
tables[x].setAttribute('style', style)
} else {
tables[x].removeAttribute('style')
}
}
}
}
}
class App extends Component {
constructor(){
super();
this.state = {
value: '点击这里开始',
html: '',
type: 'excel',
mobile: true,
uncss: true,
removeOfficeCss: true,
compress: false,
removeAllCSS: false,
removeTableWidth: false,
percent: false
};
}
textareaClick = (e) => {
this.textareaInput.focus();
this.textareaInput.select();
}
inputFocus = (e) => {
this.setState(() => ({value: '现在请使用 CTRL+V 粘贴Excel表格计算量大请耐心等待结果'}));
this.textInput.style.backgroundColor="#90c5a9";
}
inputBlur = (e) => {
this.setState(() => ({value: '点击这里开始'}));
}
inputPaste = (e) => {
document.getElementById("loading").style.display = 'block'
// regexp
var toReg = e.clipboardData.getData('text/html');
setTimeout(async () => {
let preid = makeid()
var preview = document.getElementById("preview")
var p = (preview.contentDocument || preview.contentWindow);
p = p.document || p;
// console.log(toReg);
toReg = toReg.replace(/(\r\n|\n|\r)/gm,"");
var regstyle = /<STYLE*>.*<\/STYLE>/gi;
var reg = /<TABLE.*>.*<\/TABLE>/gi;
let styleCode = toReg.match(regstyle)
let tableCode = toReg.match(reg)
try {
// 移除问题标签
// console.log(styleCode)
if (styleCode !== null) styleCode = styleCode.toString().replaceAll('<!--table', 'table').replaceAll('}--></style>', '}</style>').replaceAll('<!-- /\\* Font Definitions \\*/', '').replaceAll('<!--\\[if gte mso 10\\]>', '');
// console.log(styleCode)
if (tableCode !== null) tableCode = tableCode.toString().replaceAll('<!--StartFragment-->', '').replaceAll('<!--EndFragment-->', '').replaceAll('<!--\\[endif\\]-->', '');
if (styleCode !== null) {
styleCode = styleCode.replaceAll('<style>', '').replaceAll('</style>', '');
var CleanCSS = require('clean-css');
var output = new CleanCSS({
compatibility: 'ie9,-properties.merging'
}).minify(styleCode);
styleCode = output.styles
// 避免CSS污染全局样式
// parse css string
let parsed = parser.parseCSS(styleCode);
parsed = removeCSS(parsed, ['html', 'body'])
parsed = removeCSSIncludes(parsed, ['@'])
// 移除Office字体样式和专用样式
if (this.state.removeOfficeCss) {
parsed = removeCSSatt(parsed, ['mso-', 'font-family'])
}
let newCSSString = parser.getCSSForEditor(parsed);
// console.log(newCSSString)
newCSSString = scopeCSS(newCSSString, '.' + preid)
// console.log(newCSSString)
styleCode = newCSSString
let style = ''
if (this.state.mobile) {
style = `style="width: 100%;height: 80%;overflow: auto;"`
}
tableCode = `<div class="${preid}" ${style}>` + tableCode + '</div>'
styleCode = prettier.format(styleCode, { parser: "css", plugins })
}
} catch (err) {
console.log(err)
}
var finalCode;
if(styleCode != null) {
if (this.state.uncss) {
const cssResult = await postcss([ // 移除未使用的CSS
removeUnused({html: tableCode})
]).process(styleCode);
styleCode = cssResult.css
}
finalCode = '<style>\n' + styleCode + '</style>\n\n' + tableCode;
} else {
finalCode = tableCode;
}
if(finalCode != null){
// iframe
p.body.innerHTML = finalCode;
// 移除多余标签
removeTags('col', p)
removeTags('colgroup', p)
if (this.state.removeAllCSS) removeAllCSS(p)
if (this.state.removeTableWidth) removeTableWidth(p)
// 转百分数
if (this.state.percent) convertWidthToPercent(p)
// 移除Office字体样式和专用样式
if (this.state.removeOfficeCss) {
removeStyleatt(p)
}
finalCode = p.body.innerHTML
finalCode = prettier.format(finalCode, { parser: "html", plugins });
// 压缩HTML
if (this.state.compress) {
const {
data
} = await Axios.post('https://minifier.yige.ink', {
code: finalCode
})
if (data.status === 1) {
finalCode = data.result
}
}
this.setState(() => ({html: finalCode}));
this.textInput.style.backgroundColor="#7a9a95";
this.textareaInput.focus();
this.textareaInput.select();
this.textareaInput.style.textAlign="left";
this.setState(() => ({value: '现在使用 CTRL+C 可以复制下面的代码,或者点击这里开始粘贴其它表格。'}));
this.textInput.style.backgroundColor="#fa8b60";
} else {
this.textInput.style.backgroundColor="#7a9a95";
this.textareaInput.focus();
this.setState(() => ({html: '看起来你似乎没有粘贴合适的表格内容'}));
this.textareaInput.style.textAlign="center";
}
document.getElementById("loading").style.display = 'none'
}, 50)
}
handleChange = (e) => {
this.setState({type: e.target.value})
}
boxChange = (e) => {
const state = {}
state[e.target.value] = !this.state[e.target.value]
this.setState(state)
}
render() {
return (
<div className="App">
<div id="loading">
<div id="loading-center">
<div id="loading-center-absolute">
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
<div className="object"></div>
</div>
<div className="loading-title"><h2>正在全力计算中</h2></div>
</div>
</div>
<div className="Power">
<strong>Office Table HTML Table</strong>
<div className="PowerType">
<label ><input type="radio" name='type' value="excel" checked={this.state.type === 'excel'}
onChange={this.handleChange}/>Excel</label><br/>
<label ><input type="radio" name='type' value="word" checked={this.state.type === 'word'}
onChange={this.handleChange}/>Word/PowerPoint</label><br/>
<label ><input type="checkbox" name='type' value="mobile" checked={this.state.mobile === true}
onChange={this.boxChange}/>添加移动端支持</label><br/>
<label ><input type="checkbox" name='type' value="uncss" checked={this.state.uncss === true}
onChange={this.boxChange}/>移除未使用的CSS</label><br/>
<label ><input type="checkbox" name='type' value="removeOfficeCss" checked={this.state.removeOfficeCss === true}
onChange={this.boxChange}/>移除Office字体样式和专用样式推荐</label><br/>
<label ><input type="checkbox" name='type' value="compress" checked={this.state.compress === true}
onChange={this.boxChange}/>压缩代码耗时较长</label><br/>
<label ><input type="checkbox" name='type' value="removeAllCSS" checked={this.state.removeAllCSS === true}
onChange={this.boxChange}/>移除所有样式</label><br/>
<label ><input type="checkbox" name='type' value="removeTableWidth" checked={this.state.removeTableWidth === true}
onChange={this.boxChange}/>移除表格宽度</label><br/>
<label ><input type="checkbox" name='type' value="percent" checked={this.state.percent === true}
onChange={this.boxChange}/>将表格宽度转换为百分比</label>
</div>
</div>
<input type="text" id="input" value={this.state.value} onFocus={this.inputFocus} onBlur={this.inputBlur} onPaste={this.inputPaste} ref={(input) => { this.textInput = input; }} readOnly/>
<textarea id="output" onClick={this.textareaClick} ref={(input) => { this.textareaInput = input; }} value={this.state.html} readOnly></textarea>
<iframe id="preview" title="preview"></iframe>
</div>
);
}
}
export default App;

9
src/App.test.js Executable file
View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

14
src/index.css Executable file
View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

26
src/index.js Executable file
View File

@ -0,0 +1,26 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
ReactDOM.render( < App / > , document.getElementById('root'))
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister()
// eslint-disable-next-line
String.prototype.replaceAll = function (s1, s2) {
// for (var j = 0; j < face.length; j++) { //考虑到含有特殊字符,不用正则
// while (data.indexOf(face[j][1]) + 1) {
// var index = data.indexOf(face[j][1]),
// len = face[j][1].length,
// str1 = data.substr(0, index),
// str2 = data.substr(index + len);
// data = str1 + '<img src="' + src + j + '.gif">' + str2;
// }
// }
return this.replace(new RegExp(s1, "gm"), s2);
}

135
src/serviceWorker.js Executable file
View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read http://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit http://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}