Просмотр исходного кода

Merge pull request #671 from weseek/feat/html-table-data-import

Feat/html table data import
Yuki Takei 7 лет назад
Родитель
Сommit
afc85aeffc

+ 4 - 11
src/client/js/components/PageEditor/HandsontableModal.jsx

@@ -8,7 +8,7 @@ import Handsontable from 'handsontable';
 import { HotTable } from '@handsontable/react';
 import { debounce } from 'throttle-debounce';
 
-import TableDataImportForm from './TableDataImportForm';
+import MarkdownTableDataImportForm from './MarkdownTableDataImportForm';
 import MarkdownTable from '../../models/MarkdownTable';
 
 const DEFAULT_HOT_HEIGHT = 300;
@@ -244,15 +244,8 @@ export default class HandsontableModal extends React.PureComponent {
     this.setState({ isDataImportAreaExpanded: !this.state.isDataImportAreaExpanded });
   }
 
-  importData(dataFormat, data) {
-    switch (dataFormat) {
-      case 'csv':
-        this.init(MarkdownTable.fromDSV(data, ','));
-        break;
-      case 'tsv':
-        this.init(MarkdownTable.fromDSV(data, '\t'));
-        break;
-    }
+  importData(markdownTable) {
+    this.init(markdownTable);
     this.toggleDataImportArea();
   }
 
@@ -315,7 +308,7 @@ export default class HandsontableModal extends React.PureComponent {
             </ButtonGroup>
             <Collapse in={this.state.isDataImportAreaExpanded}>
               <div> {/* This div is necessary for smoothing animations. (https://react-bootstrap.github.io/utilities/transitions/#transitions-collapse) */}
-                <TableDataImportForm onCancel={this.toggleDataImportArea} onImport={this.importData}/>
+                <MarkdownTableDataImportForm onCancel={this.toggleDataImportArea} onImport={this.importData}/>
               </div>
             </Collapse>
           </div>

+ 37 - 5
src/client/js/components/PageEditor/TableDataImportForm.jsx → src/client/js/components/PageEditor/MarkdownTableDataImportForm.jsx

@@ -4,22 +4,48 @@ import FormGroup from 'react-bootstrap/es/FormGroup';
 import ControlLabel from 'react-bootstrap/es/ControlLabel';
 import FormControl from 'react-bootstrap/es/FormControl';
 import Button from 'react-bootstrap/es/Button';
+import MarkdownTable from '../../models/MarkdownTable';
+import Collapse from 'react-bootstrap/es/Collapse';
 
-export default class TableDataImportForm extends React.Component {
+export default class MarkdownTableDataImportForm extends React.Component {
 
   constructor(props) {
     super(props);
 
     this.state = {
       dataFormat: 'csv',
-      data: ''
+      data: '',
+      parserErrorMessage: null
     };
 
     this.importButtonHandler = this.importButtonHandler.bind(this);
   }
 
   importButtonHandler() {
-    this.props.onImport(this.state.dataFormat, this.state.data);
+    try {
+      const markdownTable = this.convertFormDataToMarkdownTable();
+      this.props.onImport(markdownTable);
+      this.setState({parserErrorMessage: null});
+    }
+    catch (e) {
+      this.setState({parserErrorMessage: e.message});
+    }
+  }
+
+  convertFormDataToMarkdownTable() {
+    let result;
+    switch (this.state.dataFormat) {
+      case 'csv':
+        result = MarkdownTable.fromDSV(this.state.data, ',');
+        break;
+      case 'tsv':
+        result = MarkdownTable.fromDSV(this.state.data, '\t');
+        break;
+      case 'html':
+        result = MarkdownTable.fromHTMLTableTag(this.state.data);
+        break;
+    }
+    return result;
   }
 
   render() {
@@ -31,7 +57,7 @@ export default class TableDataImportForm extends React.Component {
                        value={this.state.dataFormat} onChange={e => this.setState({dataFormat: e.target.value})}>
             <option value="csv">CSV</option>
             <option value="tsv">TSV</option>
-            <option value="html">(TBD) HTML</option>
+            <option value="html">HTML</option>
           </FormControl>
         </FormGroup>
         <FormGroup>
@@ -39,6 +65,12 @@ export default class TableDataImportForm extends React.Component {
           <FormControl componentClass="textarea" placeholder="Paste table data" style={{ height: 200 }}
                        onChange={e => this.setState({data: e.target.value})}/>
         </FormGroup>
+        <Collapse in={this.state.parserErrorMessage != null}>
+          <FormGroup>
+            <ControlLabel>Parse Error</ControlLabel>
+            <FormControl componentClass="textarea" style={{ height: 100 }}  value={this.state.parserErrorMessage} readOnly/>
+          </FormGroup>
+        </Collapse>
         <div className="d-flex justify-content-end">
           <Button bsStyle="default" onClick={this.props.onCancel}>Cancel</Button>
           <Button bsStyle="primary" onClick={this.importButtonHandler}>Import</Button>
@@ -48,7 +80,7 @@ export default class TableDataImportForm extends React.Component {
   }
 }
 
-TableDataImportForm.propTypes = {
+MarkdownTableDataImportForm.propTypes = {
   onCancel: PropTypes.func,
   onImport: PropTypes.func
 };

+ 39 - 3
src/client/js/models/MarkdownTable.js

@@ -8,6 +8,9 @@ const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
 const tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
+// set up DOMParser
+const domParser = new (window.DOMParser)();
+
 /**
  * markdown table class for markdown-table module
  *   ref. https://github.com/wooorm/markdown-table
@@ -37,9 +40,42 @@ export default class MarkdownTable {
     return new MarkdownTable(newTable, this.options);
   }
 
-  static fromTableTag(str) {
-    // TODO impl
-    return new MarkdownTable();
+  /**
+   * return a MarkdownTable instance made from a string of HTML table tag
+   *
+   * If a parser error occurs, an error object with an error message is thrown.
+   * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
+   */
+  static fromHTMLTableTag(str) {
+    // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
+    const dom = domParser.parseFromString(str, 'application/xml');
+
+    if (dom.querySelector('parsererror')) {
+      throw new Error(dom.documentElement.innerHTML);
+    }
+
+    const tableElement = dom.querySelector('table');
+    const trElements = tableElement.querySelectorAll('tr');
+
+    let table = [];
+    let maxRowSize = 0;
+    for (let i = 0; i < trElements.length; i++) {
+      let row = [];
+      let cellElements = trElements[i].querySelectorAll('th,td');
+      for (let j = 0; j < cellElements.length; j++) {
+        row.push(cellElements[j].innerHTML);
+      }
+      table.push(row);
+
+      if (maxRowSize < row.length) maxRowSize = row.length;
+    }
+
+    let align = [];
+    for (let i = 0; i < maxRowSize; i++) {
+      align.push('');
+    }
+
+    return new MarkdownTable(table, {align: align});
   }
 
   /**