Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Paste from Office support for Safari #16

Merged
merged 25 commits into from
Nov 5, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a3c2ed
Tests: Safari input files added.
f1ames Sep 17, 2018
c2e7b96
Tests: fixtures loader added and used in basic styles integration tests.
f1ames Sep 17, 2018
6c2ea03
Tests: removed leftovers
f1ames Sep 17, 2018
569b7ca
Tests: Util for browser detection added.
f1ames Sep 18, 2018
a27aa01
Adjust 'isWordInput' method to Safari.
f1ames Sep 18, 2018
8f8032a
Tests: Refactoring basic styles tests and utils.
f1ames Sep 18, 2018
59306ff
Tests: Link tests refactor and Safari fixtures added.
f1ames Sep 18, 2018
4bdd9bb
Tests: Spacing tests for Safari.
f1ames Sep 19, 2018
499bf01
Tests: List Safari tests. Tests utils refactoring.
f1ames Sep 19, 2018
9070253
Normalize Safari specific spaces.
f1ames Sep 19, 2018
be70851
Tests: Adjust tests expected files to Safari space normalization.
f1ames Sep 19, 2018
027312c
Handle Safari specific spacing.
f1ames Sep 19, 2018
d6381cc
Tests: Refactoring - run all fixtures in each browser.
f1ames Sep 27, 2018
036083e
Merge branch 't/8' into t/12
f1ames Oct 18, 2018
9aa46f9
Tests: Add 'Clipboard' plugin to normalization tests.
f1ames Oct 18, 2018
c54fc33
Tests: Corrected unit test after wrong merge resolution.
f1ames Oct 18, 2018
8fe3bf4
Merge branch 't/8' into t/12
Reinmar Oct 26, 2018
3f5459d
Tests: Initial tests readme.
f1ames Oct 29, 2018
a0a7c24
Tests: Readme adjustments. [skip ci]
f1ames Oct 29, 2018
6f41c23
Tests: Added section on creating tests to README. [skip ci]
f1ames Oct 29, 2018
cf51bb0
Tests: Readme rewording. [skip ci]
f1ames Oct 29, 2018
550153e
Merge branch 't/8' into t/12
f1ames Nov 5, 2018
c5ff14c
Merge branch 'master' into t/12
Reinmar Nov 5, 2018
78426f3
Reviewed the tests readme.
Reinmar Nov 5, 2018
5833e28
Readme title.
Reinmar Nov 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/filters/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function parseHtml( htmlString ) {
const domParser = new DOMParser();

// Parse htmlString as native Document object.
const htmlDocument = domParser.parseFromString( normalizeEndTagsPrecedingSpace( htmlString ), 'text/html' );
const htmlDocument = domParser.parseFromString( normalizeSpacing( htmlString ), 'text/html' );

normalizeSpacerunSpans( htmlDocument );

Expand Down Expand Up @@ -96,12 +96,27 @@ function extractStyles( htmlDocument ) {
//
// @param {String} htmlString HTML string in which spacing should be normalized.
// @returns {String} Input HTML with spaces normalized.
function normalizeEndTagsPrecedingSpace( htmlString ) {
return htmlString
function normalizeSpacing( htmlString ) {
return normalizeSafariSpaceSpans( normalizeSafariSpaceSpans( htmlString ) ) // Run normalization two times to cover nested spans.
.replace( / <\//g, '\u00A0</' )
.replace( / <o:p><\/o:p>/g, '\u00A0<o:p></o:p>' );
}

// Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)
// by replacing all spaces sequences longer than 1 space with `&nbsp; ` pairs. This prevents spaces from being removed during
// further DOM/View processing (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
//
// This function is similar to {@link module:clipboard/utils/normalizeclipboarddata normalizeClipboardData util} but uses
// regular spaces / &nbsp; sequence for replacement.
//
// @param {String} htmlString HTML string in which spacing should be normalized
// @returns {String} Input HTML with spaces normalized.
function normalizeSafariSpaceSpans( htmlString ) {
return htmlString.replace( /<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, ( fullMatch, spaces ) => {
return spaces.length === 1 ? ' ' : Array( spaces.length + 1 ).join( '\u00A0 ' ).substr( 0, spaces.length );
} );
}

// Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
// all spaces with `&nbsp; ` pairs. This prevents spaces from being removed during further DOM/View processing
// (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
Expand Down
3 changes: 2 additions & 1 deletion src/pastefromoffice.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@ export default class PasteFromOffice extends Plugin {
// @param {String} html HTML string to test.
// @returns {Boolean} True if given HTML string is a Word HTML.
function isWordInput( html ) {
return !!( html && html.match( /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/gi ) );
return !!( html && ( html.match( /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/gi ) ||
html.match( /xmlns:o="urn:schemas-microsoft-com/gi ) ) );
}
221 changes: 221 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# Normalization and integration testing in `Paste from Office`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Normalization and integration testing in `Paste from Office`
# Testing this package


To test if content pasted from any external application is transformed correctly by the editor the test itself needs to
use data which is put into the native clipboard by the application. For test purpose, such data is stored in the single file
called `fixture` file.

The `fixture` file is usually a HTML file containing HTML content which was fetched from the native browser `dataTransfer`
object (`dataTransfer.getData( 'html/text' )`) when content was pasted to the browser. This ensures that `fixture`
file provides exactly same data as a real use scenario.

## Fixture files

_For example files see `_data/basic-style/bold-within-text/`_.

The `fixture` files are grouped per feature (which usually corresponds to editor plugins, for example `basic-styles`, `list`, etc).
All fixtures are stored in `_data/feature-name/` directory (for example `_data/basic-style/`). Each feature (which
will be called **group**) has a separate folder per fixture. Each fixture is used to create one normalization and one integration test.

Each fixture folder contains:

- original input file - `bold-with-text.docx`
- input fixture - `input.word2016.html`
- normalized output fixture - `normalized.word2016.html`
- model output fixture - `model.word2016.html`

In some cases, different browsers produces different input data. For such situations, additional fixtures are stored.
For example if input data is different for Safari, additional `input.safari.word2016.html` file will be present in fixture directory.

## Tests group index

_For example file see `_data/basic-style/index.js`_.

Each group of fixtures contains index file (`index.js` in group folder e.g. `_data/basic-styles/index.js`).
Its purpose is to simply import all fixture files from the group and expose them for further use. Index file has the following structure:


```
// Import default/generic fixtures.
// Input fixtures.
import boldWithinText from './bold-within-text/input.word2016.html';

// Expected normalized fixtures.
import boldWithinTextNormalized from './bold-within-text/normalized.word2016.html';

// Expected model fixtures.
import boldWithinTextModel from './bold-within-text/model.word2016.html';

// Export imported generic fixtures for future use.
export const fixtures = {
input: {
boldWithinText: boldWithinText
},
normalized: {
boldWithinText: boldWithinTextNormalized
},
model: {
boldWithinText: boldWithinTextModel
}
}
```

Such structure exports generic fixtures (the ones which are the same for more than one browser and will be used if no browser specific fixtures are present).

Index files must also export browser specific fixtures. In the simplest case if there are none, it exports empty object:


```
export browserFixtures = {};
```

If there are any browser specific fixtures, they are exported in a similar manner to generic ones (apart from being grouped by a browser):


```
// Export imported browser-specific fixtures for future use.
export const browserFixtures = {
safari: {
input: {
boldWithinText: boldWithinTextSafari
},
normalized: {
boldWithinText: boldWithinTextNormalizedSafari
},
model: {
boldWithinText: boldWithinTextModelSafari
}
}
}
```

### What if only input or one of the expected output fixtures are different for specific browser? Could fixtures be mixed?

There are cases when only some fixtures differ for a given browser. In such cases browser fixtures export reuses generic fixtures:

```
// Export imported browser-specific fixtures for future use.
export const browserFixtures = {
safari: {
input: {
boldWithinText: boldWithinText // generic
},
normalized: {
boldWithinText: boldWithinTextNormalizedSafari // Safari specific
},
model: {
boldWithinText: boldWithinTextModel // generic
}
}
}
```

## Fixtures aggregation

_See `_utils/fixtures.js`_.

All group indexes files are aggregated in the `fixtures` util (`_utils/fixtures.js`) and exposed for tests in a single
`fixtures` and `browserFixtures` objects:


```
// Import fixtures.
import { fixtures as basicStyles, browserFixtures as basicStylesBrowser } from '../_data/basic-styles/index.js';

// Generic fixtures.
export const fixtures = {
'basic-styles': basicStyles
};

// Browser specific fixtures.
export const browserFixtures = {
'basic-styles': basicStylesBrowser
};
```

## Tests generation

_See `data/normalization.js` and `data/integration.js`_.

Tests based on fixture files are generated by the special util function `generateTests()` (see `_utils/utils.js`). This function
is specifically designed to generate `normalization` (see `data/normalization.js`) or `integration` (see `data/integration.js`)
tests using provided fixtures group, for example:


```
generateTests( {
input: 'basic-styles', // Group name.
type: 'integration', // Tests type (integration or normalization).
browsers: [ 'chrome', 'firefox', 'safari', 'edge' ], // For which browsers generate tests.
editorConfig: { // Editor config which will be used during editor creation which is used in tests.
plugins: [ Clipboard, Paragraph, Heading, Bold, Italic, Underline, Strikethrough, PasteFromOffice ]
},
skip: { // Names of fixtures which tests should be skipped (object `key` is the name of the browser for which to skip tests).
safari: [ 'italicStartingText', 'multipleStylesSingleLine', 'multipleStylesMultiline' ] // Skip due to spacing issue (#13).
}
} );
```

## Adding new tests

### To an existing group

1. Create new fixtures directory in a group to which you plan to add tests (e.g. `_data/link/new-use-case/`).
2. Add all necessary fixture files to the above directory:
* original input file - `new-use-case.docx`
* input fixture - `input.word2016.html` (to acquire clipboard data you may use `integration.html` manual test which prints `text/html` on paste)
* normalized output fixture - `normalized.word2016.html`
* model output fixture - `model.word2016.html`
* any browser specific fixtures
3. Add new fixtures to group `index.js` file.

That's all, added fixtures will be now used to generate normalization and integration test.

### To a new group

1. Create new group directory, for example `_data/new-group/`.
2. Create new fixtures directories (one per input fixture file) in `_data/new-group/`, each containing:
* original input file - `new-use-case.docx`
* input fixture - `input.word2016.html` (to acquire clipboard data you may use `integration.html` manual test which prints `text/html` on paste)
* normalized output fixture - `normalized.word2016.html`
* model output fixture - `model.word2016.html`
* any browser specific fixtures
3. Create group `index.js` file (`_data/new-group/index.js`) importing all necessary fixtures.
4. Add new group to fixtures util `_utils/fixtures.js`:


```
// Import fixtures.
import { fixtures as newGroup, browserFixtures as newGroupBrowser } from '../_data/new-group/index.js';

// Generic fixtures.
export const fixtures = {
'new-group': newGroup
};

// Browser specific fixtures.
export const browserFixtures = {
'new-group': newGroupBrowser
};
```

5. Add `generateTests()` function call in `data/normalization.js` to generate normalization and in `data/integration.js`
to generate integration tests:

```
// normalization.js
generateTests( {
input: 'new-group',
type: 'normalization',
browsers: [ 'chrome', 'firefox', 'safari', 'edge' ]
editorConfig: { ... }
} );

// integration.js
generateTests( {
input: 'new-group',
type: 'integration',
browsers: [ 'chrome', 'firefox', 'safari', 'edge' ]
editorConfig: { ... }
} );

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="urn:schemas-microsoft-com:office:word"
xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"
xmlns="http://www.w3.org/TR/REC-html40"><p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><span>Some text<span class="Apple-converted-space"> </span><b>with bold</b>.<o:p></o:p></span></p></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<paragraph>Some text <$text bold="true">with bold</$text>.</paragraph>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><span>Some text <b>with bold</b>.<o:p></o:p></span></p>
97 changes: 97 additions & 0 deletions tests/_data/basic-styles/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

// Default.
import boldWithinText from './bold-within-text/input.word2016.html';
import italicStartingText from './italic-starting-text/input.word2016.html';
import underlinedText from './underlined-text/input.word2016.html';
import strikethroughEndingText from './strikethrough-ending-text/input.word2016.html';
import multipleStylesSingleLine from './multiple-styles-single-line/input.word2016.html';
import multipleStylesMultiline from './multiple-styles-multiline/input.word2016.html';

import boldWithinTextNormalized from './bold-within-text/normalized.word2016.html';
import italicStartingTextNormalized from './italic-starting-text/normalized.word2016.html';
import underlinedTextNormalized from './underlined-text/normalized.word2016.html';
import strikethroughEndingTextNormalized from './strikethrough-ending-text/normalized.word2016.html';
import multipleStylesSingleLineNormalized from './multiple-styles-single-line/normalized.word2016.html';
import multipleStylesMultilineNormalized from './multiple-styles-multiline/normalized.word2016.html';

import boldWithinTextModel from './bold-within-text/model.word2016.html';
import italicStartingTextModel from './italic-starting-text/model.word2016.html';
import underlinedTextModel from './underlined-text/model.word2016.html';
import strikethroughEndingTextModel from './strikethrough-ending-text/model.word2016.html';
import multipleStylesSingleLineModel from './multiple-styles-single-line/model.word2016.html';
import multipleStylesMultilineModel from './multiple-styles-multiline/model.word2016.html';

export const fixtures = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand what's in fixtures and browserFixtures and how it is used later on. I see they are combined somehow... Could you perhaps add a README.md to tests/ explaining what's the process of adding more tests using this system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added README.md file describing briefly each part of testing mechanism and some short step-by-step guide on adding new tests 👍

input: {
boldWithinText,
italicStartingText,
underlinedText,
strikethroughEndingText,
multipleStylesSingleLine,
multipleStylesMultiline
},
normalized: {
boldWithinText: boldWithinTextNormalized,
italicStartingText: italicStartingTextNormalized,
underlinedText: underlinedTextNormalized,
strikethroughEndingText: strikethroughEndingTextNormalized,
multipleStylesSingleLine: multipleStylesSingleLineNormalized,
multipleStylesMultiline: multipleStylesMultilineNormalized
},
model: {
boldWithinText: boldWithinTextModel,
italicStartingText: italicStartingTextModel,
underlinedText: underlinedTextModel,
strikethroughEndingText: strikethroughEndingTextModel,
multipleStylesSingleLine: multipleStylesSingleLineModel,
multipleStylesMultiline: multipleStylesMultilineModel
}
};

// Safari.
import boldWithinTextSafari from './bold-within-text/input.safari.word2016.html';
import italicStartingTextSafari from './italic-starting-text/input.safari.word2016.html';
import underlinedTextSafari from './underlined-text/input.safari.word2016.html';
import strikethroughEndingTextSafari from './strikethrough-ending-text/input.safari.word2016.html';
import multipleStylesSingleLineSafari from './multiple-styles-single-line/input.safari.word2016.html';
import multipleStylesMultilineSafari from './multiple-styles-multiline/input.safari.word2016.html';

import boldWithinTextNormalizedSafari from './bold-within-text/normalized.safari.word2016.html';
import italicStartingTextNormalizedSafari from './italic-starting-text/normalized.safari.word2016.html';
import underlinedTextNormalizedSafari from './underlined-text/normalized.safari.word2016.html';
import strikethroughEndingTextNormalizedSafari from './strikethrough-ending-text/normalized.safari.word2016.html';
import multipleStylesSingleLineNormalizedSafari from './multiple-styles-single-line/normalized.safari.word2016.html';
import multipleStylesMultilineNormalizedSafari from './multiple-styles-multiline/normalized.safari.word2016.html';

export const browserFixtures = {
safari: {
input: {
boldWithinText: boldWithinTextSafari,
italicStartingText: italicStartingTextSafari,
underlinedText: underlinedTextSafari,
strikethroughEndingText: strikethroughEndingTextSafari,
multipleStylesSingleLine: multipleStylesSingleLineSafari,
multipleStylesMultiline: multipleStylesMultilineSafari
},
normalized: {
boldWithinText: boldWithinTextNormalizedSafari,
italicStartingText: italicStartingTextNormalizedSafari,
underlinedText: underlinedTextNormalizedSafari,
strikethroughEndingText: strikethroughEndingTextNormalizedSafari,
multipleStylesSingleLine: multipleStylesSingleLineNormalizedSafari,
multipleStylesMultiline: multipleStylesMultilineNormalizedSafari
},
model: {
boldWithinText: boldWithinTextModel,
italicStartingText: italicStartingTextModel,
underlinedText: underlinedTextModel,
strikethroughEndingText: strikethroughEndingTextModel,
multipleStylesSingleLine: multipleStylesSingleLineModel,
multipleStylesMultiline: multipleStylesMultilineModel
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="urn:schemas-microsoft-com:office:word"
xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"
xmlns="http://www.w3.org/TR/REC-html40"><p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><i><span>Italic</span></i><span>text.<o:p></o:p></span></p></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<paragraph><$text italic="true">Italic</$text> text.</paragraph>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p class="MsoNormal" style="margin: 0cm 0cm 8pt; line-height: 15.693333625793457px; font-size: 11pt; font-family: Calibri, sans-serif; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none;"><i><span>Italic</span></i><span>text.<o:p></o:p></span></p>
Loading