├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc-md.js
├── .eslintrc.js
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── release.yml
│ ├── strings.yml
│ └── test.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .lintstagedrc.json
├── .markdownlint.json
├── .npmpackagejsonlintrc.json
├── .npmrc
├── .nvmrc
├── .prettierrc.js
├── .stylelintrc.js
├── .wp-env.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── bin
├── build-language-pack.js
├── export-from-har.js
├── lib
│ └── get-wxr-from-wix-har.js
└── packages
│ ├── build-worker.js
│ ├── build.js
│ ├── get-babel-config.js
│ ├── get-packages.js
│ ├── lint-staged-typecheck.js
│ ├── validate-typescript-version.js
│ └── watch.js
├── distribution
├── action.css
├── action.html
├── icon.png
└── manifest.json
├── docs
├── contributors
│ ├── command-line.md
│ ├── design.md
│ ├── develop.md
│ ├── getting-started.md
│ ├── readme.md
│ └── testing-overview.md
└── roadmap.md
├── jest.config.js
├── jsconfig.json
├── languages
└── free-as-in-speech.pot
├── package-lock.json
├── package.json
├── packages
├── fetch-from-har
│ ├── README.md
│ ├── bin
│ │ └── anonymize-har.js
│ ├── package.json
│ └── src
│ │ ├── index.js
│ │ └── test
│ │ ├── empty-json.har
│ │ ├── index.js
│ │ └── test.har
├── gutenberg-for-node
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ └── index.js
├── site-parsers
│ ├── .eslintrc.js
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── index.js
│ │ ├── parsers
│ │ └── wix
│ │ │ ├── components
│ │ │ ├── anchor.js
│ │ │ ├── audio.js
│ │ │ ├── button-stylable.js
│ │ │ ├── button.js
│ │ │ ├── document-media.js
│ │ │ ├── google-map.js
│ │ │ ├── html.js
│ │ │ ├── image-list.js
│ │ │ ├── image-vector.js
│ │ │ ├── image.js
│ │ │ ├── link-bar.js
│ │ │ ├── menu.js
│ │ │ ├── separator.js
│ │ │ ├── sound-cloud.js
│ │ │ ├── spotify.js
│ │ │ ├── tpa-widget.js
│ │ │ ├── twitter-follow.js
│ │ │ └── video.js
│ │ │ ├── containers
│ │ │ ├── column.js
│ │ │ ├── columns.js
│ │ │ ├── cover.js
│ │ │ ├── form.js
│ │ │ └── mobile.js
│ │ │ ├── data.js
│ │ │ ├── index.js
│ │ │ ├── links.js
│ │ │ ├── mappers.js
│ │ │ ├── menu.js
│ │ │ └── pages.js
│ │ └── utils
│ │ ├── idfactory.js
│ │ ├── index.js
│ │ ├── logger.js
│ │ └── register-blocks.js
└── wxr
│ ├── .npmrc
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ └── src
│ ├── 1.2
│ ├── index.js
│ ├── schema.js
│ └── test
│ │ ├── __snapshots__
│ │ ├── authors.js.snap
│ │ ├── basic.js.snap
│ │ ├── comments.js.snap
│ │ ├── posts.js.snap
│ │ ├── sitemeta.js.snap
│ │ └── terms.js.snap
│ │ ├── authors.js
│ │ ├── basic.js
│ │ ├── comments.js
│ │ ├── posts.js
│ │ ├── sitemeta.js
│ │ └── terms.js
│ ├── 1.3
│ ├── index.js
│ ├── objects
│ │ ├── contact-form.json
│ │ ├── index.js
│ │ ├── map.json
│ │ └── test
│ │ │ ├── __snapshots__
│ │ │ └── contact-form.js.snap
│ │ │ └── contact-form.js
│ ├── schema.js
│ └── test
│ │ ├── __snapshots__
│ │ ├── authors.js.snap
│ │ ├── basic.js.snap
│ │ ├── comments.js.snap
│ │ ├── objects.js.snap
│ │ ├── posts.js.snap
│ │ ├── sitemeta.js.snap
│ │ └── terms.js.snap
│ │ ├── authors.js
│ │ ├── basic.js
│ │ ├── comments.js
│ │ ├── objects.js
│ │ ├── posts.js
│ │ ├── sitemeta.js
│ │ └── terms.js
│ ├── index.js
│ └── test
│ └── index.js
├── source
├── action.js
├── background.js
├── content.js
└── services
│ ├── index.js
│ └── wix
│ ├── extractors
│ ├── communities-blog-app
│ │ ├── block-mapping.js
│ │ ├── index.js
│ │ └── test
│ │ │ ├── __snapshots__
│ │ │ └── block-mapping.js.snap
│ │ │ ├── block-mapping.js
│ │ │ └── index.js
│ ├── easy-blog-app
│ │ └── index.js
│ ├── index.js
│ ├── media-manager
│ │ └── index.js
│ ├── site-meta-app
│ │ └── index.js
│ └── static-pages
│ │ ├── data.js
│ │ └── index.js
│ └── index.js
├── test
├── integration
│ └── wix
│ │ ├── __snapshots__
│ │ └── fetch-from-wix-har.test.js.snap
│ │ ├── fetch-from-wix-har.test.js
│ │ └── fixtures
│ │ ├── easyblog.har
│ │ ├── empty.har
│ │ ├── personal-website.har
│ │ ├── rockfield.har
│ │ └── wix-basic.har
├── setup-env.js
└── web-ext-profile
│ └── .gitkeep
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "@wordpress/default" ],
3 | "sourceType": "unambiguous"
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.yml]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | bin/packages/*.js
2 |
3 | build
4 | build-module
5 | node_modules
6 | test/web-ext-profile
7 | !.*.js
8 |
--------------------------------------------------------------------------------
/.eslintrc-md.js:
--------------------------------------------------------------------------------
1 | // eslint config for markdown documentation
2 |
3 | // This configuration is used when parsing JS code blocks
4 | // in documentation. It attempts to allow for snippets of
5 | // codes which may define variables unused, or use variables
6 | // that are assumed to be defined.
7 | module.exports = {
8 | root: true,
9 | plugins: [ 'markdown' ],
10 | extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ],
11 | rules: {
12 | 'import/no-extraneous-dependencies': 'off',
13 | 'no-undef': 'off',
14 | 'no-unused-vars': 'off',
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const eslintConfig = {
2 | root: true,
3 | extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ],
4 | globals: {
5 | browser: 'readonly',
6 | Blob: 'readonly',
7 | i18n: 'readonly',
8 | WritableStreamDefaultWriter: 'readonly',
9 | },
10 | rules: {
11 | '@wordpress/no-global-event-listener': 'off',
12 | },
13 | };
14 |
15 | module.exports = eslintConfig;
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.ai binary
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 | ## How has this been tested?
5 |
6 |
7 |
8 |
9 | ## Screenshots
10 |
11 | ## Types of changes
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | env: {}
2 |
3 | # DO NOT EDIT BELOW, USE: npx ghat fregante/ghatemplates/webext --set 'on.schedule=[{"cron": "31 12 * * 6"}]'
4 |
5 | name: Release
6 | on:
7 | workflow_dispatch: null
8 | schedule:
9 | - cron: 31 12 * * 6
10 | jobs:
11 | Version:
12 | outputs:
13 | created: '${{ steps.daily-version.outputs.created }}'
14 | version: '${{ steps.daily-version.outputs.version }}'
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 20
20 | - name: install
21 | run: npm ci || npm install
22 | - run: npm test
23 | - uses: fregante/daily-version-action@v1
24 | name: Create tag if necessary
25 | id: daily-version
26 | - uses: notlmn/release-with-changelog@v3
27 | if: steps.daily-version.outputs.created
28 | with:
29 | token: '${{ secrets.GITHUB_TOKEN }}'
30 | exclude: true
31 | Submit:
32 | needs: Version
33 | if: github.event_name == 'workflow_dispatch' || needs.Version.outputs.created
34 | strategy:
35 | fail-fast: false
36 | matrix:
37 | command:
38 | - firefox
39 | - chrome
40 | runs-on: ubuntu-latest
41 | steps:
42 | - uses: actions/checkout@v2
43 | - name: install
44 | run: npm ci || npm install
45 | - run: npm run build
46 | - name: Update extension’s meta
47 | run: >-
48 | npx dot-json distribution/manifest.json version ${{
49 | needs.Version.outputs.version }}
50 | - run: 'npm run release:${{ matrix.command }}'
51 | env:
52 | EXTENSION_ID: '${{ secrets.EXTENSION_ID }}'
53 | CLIENT_ID: '${{ secrets.CLIENT_ID }}'
54 | CLIENT_SECRET: '${{ secrets.CLIENT_SECRET }}'
55 | REFRESH_TOKEN: '${{ secrets.REFRESH_TOKEN }}'
56 | WEB_EXT_API_KEY: '${{ secrets.WEB_EXT_API_KEY }}'
57 | WEB_EXT_API_SECRET: '${{ secrets.WEB_EXT_API_SECRET }}'
--------------------------------------------------------------------------------
/.github/workflows/strings.yml:
--------------------------------------------------------------------------------
1 | name: update-translation-strings
2 |
3 | on:
4 | - pull_request
5 |
6 | jobs:
7 | update-translation-strings:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Check out the code
12 | uses: actions/checkout@v2
13 | with:
14 | ref: ${{ github.head_ref }}
15 |
16 | - name: Generate the POT file
17 | run: |
18 | docker run -v $PWD:/var/www/html --user 1001 --rm wordpress:cli i18n make-pot --ignore-domain ./source languages/free-as-in-speech.pot
19 | ! git diff --quiet -G'(msgid|msgstr)' || git reset --hard
20 |
21 | - name: Merge changes into the PO files
22 | if: steps.auto-commit-action.outputs.changes_detected == 'true'
23 | run: |
24 | sudo apt-get install gettext
25 | for po in languages/*.po; do
26 | echo $po;
27 | msgmerge -NUv --no-wrap --backup=none $po languages/free-as-in-speech.pot;
28 | printf '%s\n' "$(cat $po | sed '/^#~ msgid/,/^\s*$/d')" > $po;
29 | done
30 |
31 | - name: Commit changes
32 | uses: stefanzweifel/git-auto-commit-action@v4
33 | with:
34 | commit_message: Update translation string files.
35 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | - pull_request
5 | - push
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - run: npm ci
13 | - run: npm run build
14 | - run: npm run lint
15 | - run: npm test
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Directories/files that may be generated by this project.
2 | build
3 | build-module
4 | node_modules
5 |
6 | # Directories/files that may appear in your environment.
7 | .DS_Store
8 | *.log
9 | yarn.lock
10 | .idea
11 |
12 | # Local browser profiles.
13 | test/web-ext-profile
14 | !test/web-ext-profile/.gitkeep
15 |
16 | *.har
17 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install lint-staged
5 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "package.json,packages/*/package.json": [
3 | "npm run lint:pkg-json"
4 | ],
5 | "*.scss,*.css": [
6 | "npm run lint:css"
7 | ],
8 | "*.js": [
9 | "npm run lint:js"
10 | ],
11 | "*.md": [
12 | "npm run lint:md-js",
13 | "npm run lint:md-docs"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "MD003": { "style": "atx" },
4 | "MD004": { "style": "dash" },
5 | "MD007": { "indent": 4 },
6 | "MD010": { "code_blocks": false },
7 | "MD013": { "line_length": 9999 },
8 | "MD029": { "style": "ordered" },
9 | "MD033": { "allowed_elements": [ "br", "p", "img" ] },
10 | "MD035": { "style": "---" },
11 | "MD048": { "style": "backtick" }
12 | }
13 |
--------------------------------------------------------------------------------
/.npmpackagejsonlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@wordpress/npm-package-json-lint-config",
3 | "rules": {
4 | "description-format": [
5 | "error",
6 | {
7 | "requireCapitalFirstLetter": true,
8 | "requireEndingPeriod": true
9 | }
10 | ],
11 | "require-publishConfig": "error",
12 | "require-repository-directory": "error",
13 | "valid-values-author": [
14 | "error",
15 | [
16 | "The WordPress Contributors"
17 | ]
18 | ],
19 | "valid-values-publishConfig": [
20 | "error",
21 | [
22 | {
23 | "access": "public"
24 | }
25 | ]
26 | ]
27 | },
28 | "overrides": [
29 | {
30 | "patterns": [ "./package.json" ],
31 | "rules": {
32 | "require-publishConfig": "off",
33 | "require-repository-directory": "off"
34 | }
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact = true
2 | legacy-peer-deps = true
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | // Import the default config file and expose it in the project root.
2 | // Useful for editor integrations.
3 | module.exports = require( '@wordpress/prettier-config' );
4 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | // Import the default config file and expose it in the project root.
2 | // Useful for editor integrations.
3 | module.exports = require( '@wordpress/stylelint-config/scss' );
4 |
--------------------------------------------------------------------------------
/.wp-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [ "WordPress/wordpress-importer" ],
3 | "mappings": {
4 | "free-as-in-speech": "."
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | This project comes under the WordPress [Etiquette](https://wordpress.org/about/etiquette/):
4 |
5 | In the WordPress open source project, we realize that our biggest asset is the community that we foster. The project, as a whole, follows these basic philosophical principles from The Cathedral and The Bazaar.
6 |
7 | - Contributions to the WordPress open source project are for the benefit of the WordPress community as a whole, not specific businesses or individuals. All actions taken as a contributor should be made with the best interests of the community in mind.
8 | - Participation in the WordPress open source project is open to all who wish to join, regardless of ability, skill, financial status, or any other criteria.
9 | - The WordPress open source project is a volunteer-run community. Even in cases where contributors are sponsored by companies, that time is donated for the benefit of the entire open source community.
10 | - Any member of the community can donate their time and contribute to the project in any form including design, code, documentation, community building, etc. For more information, go to make.wordpress.org.
11 | - The WordPress open source community cares about diversity. We strive to maintain a welcoming environment where everyone can feel included, by keeping communication free of discrimination, incitement to violence, promotion of hate, and unwelcoming behavior.
12 |
13 | The team involved will block any user who causes any breach in this.
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Welcome to WordPress' *Free (as in Freedom)* project! We hope you join us in helping folks free their content from CMSes that try to lock them in; all are welcome here.
4 |
5 | ## How To Contribute
6 |
7 | To learn all about contributing to the *Free (as in Freedom)* project, see the [Contributor Guide](/docs/contributors/readme.md). The handbook includes all the details you need to get setup and start shaping the future of web publishing.
8 |
9 | - Code? See the [developer section](/docs/contributors/develop.md).
10 |
11 | - Design? See the [design section](/docs/contributors/design.md).
12 |
13 | ## Guidelines
14 |
15 | - As with all WordPress projects, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [Code of Conduct](/CODE_OF_CONDUCT.md).
16 |
17 | - All WordPress projects are [licensed under the GPLv2+](/LICENSE.md), and all contributions to *Free (as in Freedom)* will be released under the GPLv2+ license. You maintain copyright over any contribution you make, and by submitting a pull request, you are agreeing to release that contribution under the GPLv2+ license.
18 |
19 | ## Reporting Security Issues
20 |
21 | Please see [SECURITY.md](/SECURITY.md).
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Free (as in Speech)
2 |
3 | > You own the content you create. When a CMS tries to lock you in, this extension helps you break free.
4 |
5 | Welcome to the *Free (as in Speech)* development hub! *FaiS* is a browser extension written for Firefox and Chrome, which allows you to export your site from Content Management Systems that don't otherwise provide you with an export.
6 |
7 |
8 |
9 |
10 |
11 | ## Getting Started
12 |
13 | ### Using Free (as in Speech)
14 |
15 | *FaiS* is currently under heavy development, and isn't recommended for general use. If you're comfortable with building software from source, you're welcome to experiment with it, but please be aware that it may not function as expected for you.
16 |
17 | If you do try out *FaiS*, please let us know how you go! We love to hear feedback from everyone.
18 |
19 | ### Contribute to Free (as in Speech)
20 |
21 | *FaiS* is an open-source project and welcomes all contributors from code to design, from documentation to triage. We'd love your help building it!
22 |
23 | See the [Contributing Guidelines](/CONTRIBUTING.md) for information on getting started.
24 |
25 | As with all WordPress projects, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [Code of Conduct](/CODE_OF_CONDUCT.md).
26 |
27 | ## Get Involved
28 |
29 | You can join us in the `#core-importers` channel in Slack, see the [WordPress Slack page](https://make.wordpress.org/chat/) for signup information; it is free to join.
30 |
31 | ## License
32 |
33 | WordPress is free software, and is released under the terms of the GNU General Public License version 2 or (at your option) any later version. See [LICENSE.md](/LICENSE.md) for the complete license.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Security Issues
2 |
3 | The *Free (as in Freedom)* team and WordPress community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
4 |
5 | To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.
6 |
--------------------------------------------------------------------------------
/bin/build-language-pack.js:
--------------------------------------------------------------------------------
1 | const { parseFileSync } = require( 'po2json' );
2 | const glob = require( 'glob' );
3 | const { mkdirSync, writeFileSync } = require( 'fs' );
4 |
5 | const languageFiles = glob.sync( 'languages/*.po' );
6 |
7 | let languagePackData = 'const i18n = {};\n';
8 |
9 | languageFiles.forEach( ( file ) => {
10 | const languageJson = parseFileSync( file, { format: 'jed1.x' } );
11 |
12 | languagePackData += `i18n.${
13 | languageJson.locale_data.messages[ '' ].lang
14 | } = ${ JSON.stringify( languageJson.locale_data.messages ) };\n`;
15 | } );
16 |
17 | mkdirSync( 'distribution/build', { recursive: true } );
18 | writeFileSync( 'distribution/build/language-pack.js', languagePackData );
19 |
--------------------------------------------------------------------------------
/bin/export-from-har.js:
--------------------------------------------------------------------------------
1 | require( 'fake-indexeddb/auto' );
2 | require( 'gutenberg-for-node' );
3 | const { registerBlocks } = require( 'site-parsers' ).utils;
4 |
5 | registerBlocks();
6 |
7 | const fetchFromHAR = require( 'fetch-from-har' );
8 | const getWXRFromWixHAR = require( './lib/get-wxr-from-wix-har' );
9 | const fs = require( 'fs' );
10 | const { startExport } = require( '../source/services/wix' );
11 |
12 | const { Command } = require( 'commander' );
13 | const program = new Command();
14 | program.version( '1.0.0' );
15 | program
16 | .command( 'wix' )
17 | .option(
18 | '-a, --appDefinitionId ',
19 | 'Which Wix module to extract',
20 | 'all'
21 | )
22 | .option( '-m, --metaSiteId ', 'The UUID representing', null )
23 | .arguments( '', 'The file to import' )
24 | .arguments( '', 'Write to this WXR file' )
25 | .description( 'Extract from Wix' )
26 | .action( ( harfile, wxrfile, options ) => {
27 | const config = {
28 | initialState: {
29 | siteMetaData: {
30 | metaSiteId: options.metaSiteId,
31 | },
32 | },
33 | };
34 |
35 | if ( typeof options.appDefinitionId === 'string' ) {
36 | if ( 'all' === options.appDefinitionId ) {
37 | config.extractAll = true;
38 | options.appDefinitionId = [];
39 | } else {
40 | options.appDefinitionId = options.appDefinitionId.split( /,/ );
41 | }
42 | }
43 |
44 | config.initialState.embeddedServices = options.appDefinitionId.map(
45 | ( appDefinitionId ) => {
46 | return {
47 | appDefinitionId,
48 | };
49 | }
50 | );
51 |
52 | getWXRFromWixHAR(
53 | fetchFromHAR,
54 | JSON.parse( fs.readFileSync( harfile ) ),
55 | config,
56 | startExport
57 | )
58 | .then( ( WXRDriver ) => WXRDriver.export() )
59 | .then( ( wxr ) => fs.writeFileSync( wxrfile, wxr ) )
60 | .then( () => {
61 | if ( fs.existsSync( wxrfile ) ) {
62 | process.exit( 0 );
63 | }
64 | } );
65 | } );
66 |
67 | program.parse( process.argv );
68 |
--------------------------------------------------------------------------------
/bin/lib/get-wxr-from-wix-har.js:
--------------------------------------------------------------------------------
1 | async function getWXRFromWixHAR(
2 | fetchFromHAR,
3 | har,
4 | config,
5 | startExport,
6 | fallback
7 | ) {
8 | if ( typeof fallback === 'undefined' ) {
9 | fallback = ( url, entry ) => {
10 | console.error( 'Missing URL in HAR:', url ); // eslint-disable-line no-console
11 | return entry;
12 | };
13 | }
14 | window.fetch = fetchFromHAR( har, {
15 | queryComparison: ( requestValue, harValue, key, url ) => {
16 | if (
17 | 'manage.wix.com' === url.host &&
18 | '/_api/communities-blog-node-api/_api/posts' === url.pathname
19 | ) {
20 | if ( 'status' === key && requestValue !== harValue ) {
21 | // The values must match, so this is not ok.
22 | return false;
23 | }
24 | }
25 |
26 | // Parameters don't need to match.
27 | return true;
28 | },
29 | fallback,
30 | } );
31 |
32 | // We're ignoring the status reports in these tests for now.
33 | const statusReport = () => {};
34 |
35 | return await startExport( config, statusReport );
36 | }
37 |
38 | module.exports = getWXRFromWixHAR;
39 |
--------------------------------------------------------------------------------
/bin/packages/build-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const { promisify } = require( 'util' );
5 | const fs = require( 'fs' );
6 | const path = require( 'path' );
7 | const babel = require( '@babel/core' );
8 | const makeDir = require( 'make-dir' );
9 | const sass = require( 'sass' );
10 | const postcss = require( 'postcss' );
11 | /**
12 | * Internal dependencies
13 | */
14 | const getBabelConfig = require( './get-babel-config' );
15 |
16 | /**
17 | * Path to packages directory.
18 | *
19 | * @type {string}
20 | */
21 | const PACKAGES_DIR = path.resolve( __dirname, '../../packages' );
22 |
23 | /**
24 | * Mapping of JavaScript environments to corresponding build output.
25 | *
26 | * @type {Object}
27 | */
28 | const JS_ENVIRONMENTS = {
29 | main: 'build',
30 | module: 'build-module',
31 | };
32 |
33 | /**
34 | * Promisified fs.readFile.
35 | *
36 | * @type {Function}
37 | */
38 | const readFile = promisify( fs.readFile );
39 |
40 | /**
41 | * Promisified fs.writeFile.
42 | *
43 | * @type {Function}
44 | */
45 | const writeFile = promisify( fs.writeFile );
46 |
47 | /**
48 | * Promisified sass.render.
49 | *
50 | * @type {Function}
51 | */
52 | const renderSass = promisify( sass.render );
53 |
54 | /**
55 | * Get the package name for a specified file
56 | *
57 | * @param {string} file File name
58 | * @return {string} Package name
59 | */
60 | function getPackageName( file ) {
61 | return path.relative( PACKAGES_DIR, file ).split( path.sep )[ 0 ];
62 | }
63 |
64 | /**
65 | * Get Build Path for a specified file.
66 | *
67 | * @param {string} file File to build
68 | * @param {string} buildFolder Output folder
69 | * @return {string} Build path
70 | */
71 | function getBuildPath( file, buildFolder ) {
72 | const pkgName = getPackageName( file );
73 | const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, 'src' );
74 | const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder );
75 | const relativeToSrcPath = path.relative( pkgSrcPath, file );
76 | return path.resolve( pkgBuildPath, relativeToSrcPath );
77 | }
78 |
79 | /**
80 | * Object of build tasks per file extension.
81 | *
82 | * @type {Object}
83 | */
84 | const BUILD_TASK_BY_EXTENSION = {
85 | async '.scss'( file ) {
86 | const outputFile = getBuildPath(
87 | file.replace( '.scss', '.css' ),
88 | 'build-style'
89 | );
90 | const outputFileRTL = getBuildPath(
91 | file.replace( '.scss', '-rtl.css' ),
92 | 'build-style'
93 | );
94 |
95 | const [ , contents ] = await Promise.all( [
96 | makeDir( path.dirname( outputFile ) ),
97 | readFile( file, 'utf8' ),
98 | ] );
99 | const builtSass = await renderSass( {
100 | file,
101 | includePaths: [ path.join( PACKAGES_DIR, 'base-styles' ) ],
102 | data:
103 | [
104 | 'colors',
105 | 'breakpoints',
106 | 'variables',
107 | 'mixins',
108 | 'animations',
109 | 'z-index',
110 | ]
111 | // Editor styles should be excluded from the default CSS vars output.
112 | .concat(
113 | file.includes( 'editor-styles.scss' )
114 | ? []
115 | : [ 'default-custom-properties' ]
116 | )
117 | .map( ( imported ) => `@import "${ imported }";` )
118 | .join( ' ' ) + contents,
119 | } );
120 |
121 | const result = await postcss(
122 | require( '@wordpress/postcss-plugins-preset' )
123 | ).process( builtSass.css, {
124 | from: 'src/app.css',
125 | to: 'dest/app.css',
126 | } );
127 |
128 | const resultRTL = await postcss( [ require( 'rtlcss' )() ] ).process(
129 | result.css,
130 | {
131 | from: 'src/app.css',
132 | to: 'dest/app.css',
133 | }
134 | );
135 |
136 | await Promise.all( [
137 | writeFile( outputFile, result.css ),
138 | writeFile( outputFileRTL, resultRTL.css ),
139 | ] );
140 | },
141 |
142 | async '.js'( file ) {
143 | for ( const [ environment, buildDir ] of Object.entries(
144 | JS_ENVIRONMENTS
145 | ) ) {
146 | const destPath = getBuildPath( file, buildDir );
147 | const babelOptions = getBabelConfig(
148 | environment,
149 | file.replace( PACKAGES_DIR, '@wordpress' )
150 | );
151 |
152 | const [ , transformed ] = await Promise.all( [
153 | makeDir( path.dirname( destPath ) ),
154 | babel.transformFileAsync( file, babelOptions ),
155 | ] );
156 |
157 | await Promise.all( [
158 | writeFile(
159 | destPath + '.map',
160 | JSON.stringify( transformed.map )
161 | ),
162 | writeFile(
163 | destPath,
164 | transformed.code +
165 | '\n//# sourceMappingURL=' +
166 | path.basename( destPath ) +
167 | '.map'
168 | ),
169 | ] );
170 | }
171 | },
172 |
173 | async '.json'( file ) {
174 | for ( const [ , buildDir ] of Object.entries(
175 | JS_ENVIRONMENTS
176 | ) ) {
177 | const destPath = getBuildPath( file, buildDir );
178 |
179 | const [ , content ] = await Promise.all( [
180 | makeDir( path.dirname( destPath ) ),
181 | readFile( file, 'utf8' ),
182 | ] );
183 |
184 | await writeFile( destPath, content );
185 | }
186 | }
187 | };
188 |
189 | module.exports = async ( file, callback ) => {
190 | const extension = path.extname( file );
191 | const task = BUILD_TASK_BY_EXTENSION[ extension ];
192 |
193 | if ( ! task ) {
194 | return;
195 | }
196 |
197 | try {
198 | await task( file );
199 | callback();
200 | } catch ( error ) {
201 | callback( error );
202 | }
203 | };
204 |
--------------------------------------------------------------------------------
/bin/packages/get-babel-config.js:
--------------------------------------------------------------------------------
1 | module.exports = ( environment = '', file ) => {
2 | /*
3 | * Specific options to be passed using the caller config option:
4 | * https://babeljs.io/docs/en/options#caller
5 | *
6 | * The caller options can only be 'boolean', 'string', or 'number' by design:
7 | * https://github.com/babel/babel/blob/bd0c62dc0c30cf16a4d4ef0ddf21d386f673815c/packages/babel-core/src/config/validation/option-assertions.js#L122
8 | */
9 | const callerOpts = {
10 | caller: {
11 | name: `WP_BUILD_${ environment.toUpperCase() }`,
12 | },
13 | };
14 | switch ( environment ) {
15 | case 'main':
16 | // to be merged as a presetEnv option
17 | callerOpts.caller.modules = 'commonjs';
18 | break;
19 | case 'module':
20 | // to be merged as a presetEnv option
21 | callerOpts.caller.modules = false;
22 | // to be merged as a pluginTransformRuntime option
23 | callerOpts.caller.useESModules = true;
24 | break;
25 | default:
26 | // preventing measure, this shouldn't happen ever
27 | delete callerOpts.caller;
28 | }
29 |
30 | // Sourcemaps options
31 | const sourceMapsOpts = {
32 | sourceMaps: true,
33 | sourceFileName: file,
34 | };
35 |
36 | return {
37 | ...callerOpts,
38 | ...sourceMapsOpts,
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/bin/packages/get-packages.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const fs = require( 'fs' );
5 | const path = require( 'path' );
6 | const { isEmpty, overEvery } = require( 'lodash' );
7 |
8 | /**
9 | * Absolute path to packages directory.
10 | *
11 | * @type {string}
12 | */
13 | const PACKAGES_DIR = path.resolve( __dirname, '../../packages' );
14 |
15 | /**
16 | * Returns true if the given base file name for a file within the packages
17 | * directory is itself a directory.
18 | *
19 | * @param {string} file Packages directory file.
20 | *
21 | * @return {boolean} Whether file is a directory.
22 | */
23 | function isDirectory( file ) {
24 | return fs.lstatSync( path.resolve( PACKAGES_DIR, file ) ).isDirectory();
25 | }
26 |
27 | /**
28 | * Returns true if the given packages has "module" field.
29 | *
30 | * @param {string} file Packages directory file.
31 | *
32 | * @return {boolean} Whether file is a directory.
33 | */
34 | function hasModuleField( file ) {
35 | let pkg;
36 | try {
37 | pkg = require( path.resolve( PACKAGES_DIR, file, 'package.json' ) );
38 | } catch {
39 | // If, for whatever reason, the package's `package.json` cannot be read,
40 | // consider it as an invalid candidate. In most cases, this can happen
41 | // when lingering directories are left in the working path when changing
42 | // to an older branch where a package did not yet exist.
43 | return false;
44 | }
45 |
46 | return ! isEmpty( pkg.module );
47 | }
48 |
49 | /**
50 | * Filter predicate, returning true if the given base file name is to be
51 | * included in the build.
52 | *
53 | * @param {string} pkg File base name to test.
54 | *
55 | * @return {boolean} Whether to include file in build.
56 | */
57 | const filterPackages = overEvery( isDirectory, hasModuleField );
58 |
59 | /**
60 | * Returns the absolute path of all WordPress packages
61 | *
62 | * @return {Array} Package paths
63 | */
64 | function getPackages() {
65 | return fs
66 | .readdirSync( PACKAGES_DIR )
67 | .filter( filterPackages )
68 | .map( ( file ) => path.resolve( PACKAGES_DIR, file ) );
69 | }
70 |
71 | module.exports = getPackages;
72 |
--------------------------------------------------------------------------------
/bin/packages/lint-staged-typecheck.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const _ = require( 'lodash' );
5 | const path = require( 'path' );
6 | const fs = require( 'fs' );
7 | const execa = require( 'execa' );
8 |
9 | /**
10 | * Internal dependencies
11 | */
12 | require( './validate-typescript-version' );
13 |
14 | const repoRoot = path.join( __dirname, '..', '..' );
15 | const tscPath = path.join( repoRoot, 'node_modules', '.bin', 'tsc' );
16 |
17 | // lint-staged passes full paths to staged changes
18 | const changedFiles = process.argv.slice( 2 );
19 |
20 | // Transform changed files to package directories containing tsconfig.json
21 | const changedPackages = _.uniq(
22 | changedFiles.map( ( fullPath ) => {
23 | const relativePath = path.relative( repoRoot, fullPath );
24 | return path.join( ...relativePath.split( path.sep ).slice( 0, 2 ) );
25 | } )
26 | ).filter( ( packageRoot ) =>
27 | fs.existsSync( path.join( packageRoot, 'tsconfig.json' ) )
28 | );
29 |
30 | try {
31 | execa.sync( tscPath, [ '--build', ...changedPackages ] );
32 | } catch ( err ) {
33 | console.error( err.stdout );
34 | process.exitCode = 1;
35 | }
36 |
--------------------------------------------------------------------------------
/bin/packages/validate-typescript-version.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const tscDetectedVersion = require( 'typescript' ).version;
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const tscDependencyVersion = require( '../../package.json' ).devDependencies
10 | .typescript;
11 |
12 | if ( tscDependencyVersion !== tscDetectedVersion ) {
13 | console.error(
14 | [
15 | 'TypeScript dependency out of date.',
16 | '\tDetected: %o',
17 | '\tRequired: %o',
18 | 'Please ensure dependencies are up to date.',
19 | ].join( require( 'os' ).EOL ),
20 | tscDetectedVersion,
21 | tscDependencyVersion
22 | );
23 | process.exit( 1 );
24 | }
25 |
--------------------------------------------------------------------------------
/bin/packages/watch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const fs = require( 'fs' );
5 | const watch = require( 'node-watch' );
6 | const { spawn } = require( 'child_process' );
7 | const path = require( 'path' );
8 | const chalk = require( 'chalk' );
9 |
10 | /**
11 | * Internal dependencies
12 | */
13 | const getPackages = require( './get-packages' );
14 |
15 | const BUILD_SCRIPT = path.resolve( __dirname, './build.js' );
16 |
17 | let filesToBuild = new Map();
18 |
19 | const exists = ( filename ) => {
20 | try {
21 | return fs.statSync( filename ).isFile();
22 | } catch ( e ) {}
23 | return false;
24 | };
25 |
26 | // Exclude test files including .js files inside of __tests__ or test folders
27 | // and files with a suffix of .test or .spec (e.g. blocks.test.js),
28 | // and deceitful source-like files, such as editor swap files.
29 | const isSourceFile = ( filename ) => {
30 | return (
31 | ! [
32 | /\/(benchmark|__mocks__|__tests__|test|storybook|stories)\/.+.js$/,
33 | /.\.(spec|test)\.js$/,
34 | ].some( ( regex ) => regex.test( filename ) ) &&
35 | /.\.(js|json|scss)$/.test( filename )
36 | );
37 | };
38 |
39 | const rebuild = ( filename ) => filesToBuild.set( filename, true );
40 |
41 | getPackages().forEach( ( p ) => {
42 | const srcDir = path.resolve( p, 'src' );
43 | try {
44 | fs.accessSync( srcDir, fs.F_OK );
45 | watch(
46 | path.resolve( p, 'src' ),
47 | { recursive: true, delay: 500 },
48 | ( event, filename ) => {
49 | if ( ! isSourceFile( filename ) ) {
50 | return;
51 | }
52 |
53 | const filePath = path.resolve( srcDir, filename );
54 | if ( event === 'update' && exists( filePath ) ) {
55 | console.log(
56 | chalk.green( '->' ),
57 | `${ event }: ${ filename }`
58 | );
59 | rebuild( filePath );
60 | } else {
61 | const buildFile = path.resolve(
62 | srcDir,
63 | '..',
64 | 'build',
65 | filename
66 | );
67 | try {
68 | fs.unlinkSync( buildFile );
69 | process.stdout.write(
70 | chalk.red( ' \u2022 ' ) +
71 | path.relative(
72 | path.resolve( srcDir, '..', '..' ),
73 | buildFile
74 | ) +
75 | ' (deleted)' +
76 | '\n'
77 | );
78 | } catch ( e ) {}
79 | }
80 | }
81 | );
82 | } catch ( e ) {
83 | // doesn't exist
84 | }
85 | } );
86 |
87 | setInterval( () => {
88 | const files = Array.from( filesToBuild.keys() );
89 | if ( files.length ) {
90 | filesToBuild = new Map();
91 | try {
92 | spawn( 'node', [ BUILD_SCRIPT, ...files ], { stdio: [ 0, 1, 2 ] } );
93 | } catch ( e ) {}
94 | }
95 | }, 100 );
96 |
97 | console.log( chalk.red( '->' ), chalk.cyan( 'Watching for changes...' ) );
98 |
--------------------------------------------------------------------------------
/distribution/action.css:
--------------------------------------------------------------------------------
1 | body {
2 | width: 300px;
3 | }
4 |
--------------------------------------------------------------------------------
/distribution/action.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/distribution/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pento/free-as-in-speech/91db70bbd0cdcabe33f18d19ac2f99cd427b7a3d/distribution/icon.png
--------------------------------------------------------------------------------
/distribution/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Free (as in Speech)",
3 | "version": "0.0.0",
4 | "description": "An awesome new browser extension",
5 | "homepage_url": "https://github.com/pento/free-as-in-speech",
6 | "manifest_version": 2,
7 | "minimum_chrome_version": "74",
8 | "applications": {
9 | "gecko": {
10 | "id": "free-as-in-speech@pento.github.io",
11 | "strict_min_version": "67.0"
12 | }
13 | },
14 | "icons": {
15 | "128": "icon.png"
16 | },
17 | "permissions": [
18 | "activeTab",
19 | "downloads",
20 | "https://easy-blog-production.myeasyappsserver.com/*",
21 | "https://*.wix.com/*"
22 | ],
23 | "browser_action": {
24 | "default_icon": "icon.png",
25 | "default_popup": "action.html",
26 | "default_title": "Free your content!"
27 | },
28 | "background": {
29 | "scripts": [
30 | "build/polyfills/webextension.js",
31 | "build/polyfills/web-streams.js",
32 | "build/background.js"
33 | ]
34 | },
35 | "content_scripts": [
36 | {
37 | "matches": [ "https://manage.wix.com/dashboard/*" ],
38 | "js": [
39 | "build/polyfills/webextension.js",
40 | "build/content.js"
41 | ],
42 | "run_at": "document_start"
43 | }
44 | ],
45 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';"
46 | }
47 |
--------------------------------------------------------------------------------
/docs/contributors/command-line.md:
--------------------------------------------------------------------------------
1 | # Working with the Command-line
2 |
3 | The command-line script `bin/export-from-har.js` can be a convenient way to iterate quickly if you have previously saved a [HAR file](https://en.wikipedia.org/wiki/HAR_\(file_format\)) as the source to fulfill the HTTP requests the extension would normally make in the browser using your credentials.
4 |
5 | ## Creating a HAR file
6 |
7 | The developer tools of modern browsers offer a section called _Network_ which will list all the network requests the browser made in order to display the associated tab in the browser.
8 |
9 | This list of requests can be saved as a HAR file:
10 |
11 | - In Firefox, click on any request in the list (or the cog-wheel on the top right) and select the option _Save All As HAR_. It will prompt you where to save the HAR file.
12 | - In Chrome, click on any request in the list and select the option _Save all as HAR with content_. It will prompt you where to save the HAR file.
13 |
14 | The resulting file is a text file with JSON content. You can now use this to work with the command-line.
15 |
16 | **Warning: the HAR file might contain private information. You can use the [anonymize-har.js](/packages/fetch-from-har/bin/anonymize-har.js) tool to anonymize it before sharing.**
17 |
18 | Tools like [Charles proxy](https://www.charlesproxy.com/) can be used to manipulate HAR files with a UI.
19 |
20 | ### How to get the right requests for a HAR file
21 |
22 | There are two main strategies to retrieve the requests for your development work:
23 |
24 | #### Exploratory use by browsing
25 |
26 | If you enable the option _Persist logs_ (Firefox, in the cog-wheel menu in the Network tab) or _Preserve logs_ (Chrome, a checkbox at the top of the Network tab), the requests in the network tab will accumulate until you use the trash icon on the top left.
27 |
28 | This allows to gather larger amounts of HTTP requests that can serve as a repository of requests to be used later in development.
29 |
30 | Now save the HAR file as described above.
31 |
32 | #### Saving the specific requests the extension already makes
33 |
34 | By opening an inspect window specifically for the extension, you can just save the specific set of requests the extension already makes in its normal operation:
35 |
36 | - In Firefox, go to the URL `about:addons` (or use the _Tools_ menu and select _Add-ons_), click the cog-wheel and select _Debug Add-ons_ from the dropdown. Now find the _Free as in Speech_ extension and click the _Inspect_ button.
37 | - In Chrome, go to the URL `chrome://extensions` (or use the _Window_ menu and select _Extensions_), toggle the _Developer Mode_ on the top right. Now find the _Free as in Speech_ extension and click the _background page_ link_.
38 |
39 | Then, run the extension once as normal in the browser, i.e. click the extension button the desired website.
40 |
41 | Now return to the network tab of the inspector window and save the HAR file as described above.
42 |
43 | ## Running an extraction
44 |
45 | We have a CLI script called [export-from-har.js](/bin/export-from-har.js) which can be executed like this:
46 |
47 | ```bash
48 | node bin/export-from-har.js [options]
49 | ```
50 |
51 | You can see the available exporters in the help screen:
52 |
53 | ```bash
54 | node bin/export-from-har.js -h
55 | ```
56 |
57 | The script will run the same code that is run in the browser extension but uses the requests and responses stored in the HAR file to fulfill the `fetch()` requests in the code. It writes the WXR to the specified WXR filename.
58 |
59 | This allows you to test your code changes to the extraction process with different HAR files quickly. It also means that you can do development offline, without having to use a browser, given you have the required HTTP requests covered.
60 |
61 | The package [fetch-from-har](/packages/fetch-from-har/README.md) is used in the background to use the HAR file as a source. It is invoked with a predefined callbacks to offer reasonable fallbacks per extractor.
62 |
--------------------------------------------------------------------------------
/docs/contributors/design.md:
--------------------------------------------------------------------------------
1 | # Design Contributions
2 |
3 | A guide on how to get started contributing design to the *Free (as in Speech)* project.
4 |
5 | ## Discussions
6 |
7 | The [Make WordPress Design blog](https://make.wordpress.org/design/) is the primary spot for the latest information around WordPress Design Team: including announcements, product goals, meeting notes, meeting agendas, and more.
8 |
9 | Real-time discussions for design take place in the `#design` channel in [Make WordPress Slack](https://make.wordpress.org/chat) (registration required). Weekly meetings for the Design team are on Wednesdays at 19:00UTC.
10 |
11 | ## How Designers Can Contribute
12 |
13 | The *Free (as in Speech)* project uses GitHub for managing code and tracking issues. The main repository is at: [https://github.com/pento/free-as-in-speech/](https://github.com/pento/free-as-in-speech/).
14 |
15 | If you'd like to contribute to the design of this project, feel free to contribute to tickets labeled [Needs Design](https://github.com/pento/free-as-in-speech/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs+Design%22) or [Needs Design Feedback](https://github.com/pento/free-as-in-speech/issues?q=is%3Aissue+is%3Aopen+label%3A"Needs+Design+Feedback%22). We could use your thoughtful replies, mockups, animatics, sketches, doodles. Proposed changes are best done as minimal and specific iterations on the work that precedes it so we can compare.
16 |
17 | The [WordPress Design team](http://make.wordpress.org/design/) uses [Figma](https://www.figma.com/) to collaborate and share work. If you'd like to contribute, join the [#design channel](http://wordpress.slack.com/messages/design/) in [Slack](https://make.wordpress.org/chat/) and ask the team to set you up with a free Figma account. This will give you access to a helpful [library of components](https://www.figma.com/file/ZtN5xslEVYgzU7Dd5CxgGZwq/WordPress-Components?node-id=0%3A1) used in WordPress.
18 |
--------------------------------------------------------------------------------
/docs/contributors/develop.md:
--------------------------------------------------------------------------------
1 | # Code Contributions
2 |
3 | A guide on how to get started contributing code to the *Free (as in Speech)* project.
4 |
5 | ## Discussions
6 |
7 | Real-time discussions for development takes place in `#core-importers` channel in [Make WordPress Slack](https://make.wordpress.org/chat) (registration required).
8 |
9 | ## Development Hub
10 |
11 | The *Free (as in Speech)* project uses GitHub for managing code and tracking issues. The main repository is at: [https://github.com/pento/free-as-in-speech](https://github.com/pento/free-as-in-speech).
12 |
13 | Browse [the issues list](https://github.com/pento/free-as-in-speech/issues) to find issues to work on. The [good first issue](https://github.com/pento/free-as-in-speech/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) and [good first review](https://github.com/pento/free-as-in-speech/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) labels are good starting points.
14 |
15 | ## Contributor Resources
16 |
17 | - [Getting Started](/docs/contributors/getting-started.md) documents getting your development environment setup, this includes your test site and developer tools suggestions.
18 | - [Working with the command-line](/docs/contributors/command-line.md) explains how you can develop offline
19 |
--------------------------------------------------------------------------------
/docs/contributors/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | The following guide is for setting up your local environment to contribute to the *Free (as in Speech)* project.
4 |
5 | ## Development Tools (Node)
6 |
7 | *Free (as in Speech)* is a JavaScript project and requires [Node.js](https://nodejs.org/). The project is built using the latest active LTS release of node, and the latest version of NPM. See the [LTS release schedule](https://github.com/nodejs/Release#release-schedule) for details.
8 |
9 | We recommend using the [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm) since it is the easiest way to install and manage node for macOS, Linux, and Windows 10 using WSL2.
10 |
11 | After installing Node, you can build *Free (as in Speech)* by running the following from within the cloned repository:
12 |
13 | ```bash
14 | npm ci
15 | npm run build
16 | ```
17 |
18 | Once built, *Free (as in Speech)* can be loaded in your browser as a development extension!
19 |
20 | `npm run build` creates a single build of the project once. While developing, you probably will want to use `npm run watch` to run continuous builds automatically as source files change. The dev build also includes additional warnings and errors to help troubleshoot while developing.
21 |
22 | ### Browser Environment
23 |
24 | Whilst you can run the extension within your normal browser, it's usually better to use a clean browser profile for development. You'll need to have [Chrome](https://www.google.com/chrome/) or [Firefox](https://www.mozilla.org/firefox/new/) installed, you can then start a fresh copy of either of them by running `npm run start:chrome` or `npm run start:firefox`, respectively.
25 |
26 | ### Command-line environment
27 |
28 | If you have a HAR file available, you can also develop from the command-line only. You can directly execute the `bin/export-from-har.js` using node like this:
29 |
30 | ```bash
31 | node bin/export-from-har.js [options]
32 | ```
33 |
34 | This will use the HAR file as a source to fulfill HTTP requests. The script is not capable of making HTTP requests directly. Refer to [Working with the command-line](/docs/contributors/command-line.md).
35 |
36 | ## Developer Tools
37 |
38 | We recommend configuring your editor to automatically check for syntax and lint errors. This will help you save time as you develop by automatically fixing minor formatting issues.
39 |
40 | ### EditorConfig
41 |
42 | [EditorConfig](https://editorconfig.org/) defines a standard configuration for setting up your editor, for example using tabs instead of spaces. Your editor might need an extension to support this. Usually it will automatically configure your editor to according to the rules defined in [.editorconfig](https://github.com/pento/free-as-in-speech/blob/HEAD/.editorconfig).
43 |
44 | ### ESLint
45 |
46 | [ESLint](https://eslint.org/) statically analyzes the code to find problems. The lint rules are integrated in the continuous integration process and must pass to be able to commit.
47 |
48 | ## Debugging
49 |
50 | ### Using debug
51 |
52 | Site Parser uses the [debug](https://github.com/visionmedia/debug) module to handle debug messaging. To view debug messages for all modules, prefix your `npm` call with the `DEBUG` environment variable:
53 |
54 | ```bash
55 | DEBUG=siteParser:* node bin/export-from-har.js [options]
56 | ```
57 |
58 | Add the `DEBUG_COLORS` variable for better rendering:
59 |
60 | ```bash
61 | DEBUG=siteParser:* DEBUG_COLORS=true node bin/export-from-har.js [options]
62 | ```
63 |
64 | You can filter for certain modules by changing the value of `DEBUG`. `DEBUG=siteParser:wix:*` will enable debug logging for the Wix importer only.
65 |
66 | Example debug strings:
67 |
68 | ```text
69 | siteParser:wix:*
70 | siteParser:wix:apps:staticPages:*
71 | siteParser:wix:apps:staticPages:ImageVector
72 | siteParser:wix:apps:staticPages:LinkableButton
73 | siteParser:wix:apps:staticPages:SoundCloud
74 | siteParser:wix:apps:staticPages:MusicPlayerData
75 | ```
76 |
77 | ### Browser development
78 |
79 | In the browser development tools you can turn the `DEBUG` on by setting the localStorage:
80 |
81 | ```text
82 | localStorage.debug = 'siteParser:image*'
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/contributors/readme.md:
--------------------------------------------------------------------------------
1 | # Contributor Guide
2 |
3 | Welcome to the *Free (as in Speech)* Project Contributor Guide. This guide is here to help you get setup and start contributing to the project. If you have any questions, you'll find us in the `#core-importers` channel in the WordPress Core Slack, [free to join](https://make.wordpress.org/chat/).
4 |
5 | *Free (as in Speech)* is a sub-project of Core WordPress. Please see the [Core Contributor Handbook](https://make.wordpress.org/core/handbook/) for additional information.
6 |
7 | ## Sections
8 |
9 | Find the section below based on what you are looking to contribute:
10 |
11 | - **Code?** See the [developer section](/docs/contributors/develop.md).
12 |
13 | - **Design?** See the [design section](/docs/contributors/design.md).
14 |
15 | ## Guidelines
16 |
17 | See the [Contributing Guidelines](/CONTRIBUTING.md) for the rules around contributing: This includes the code of conduct and licensing information.
18 |
--------------------------------------------------------------------------------
/docs/contributors/testing-overview.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pento/free-as-in-speech/91db70bbd0cdcabe33f18d19ac2f99cd427b7a3d/docs/contributors/testing-overview.md
--------------------------------------------------------------------------------
/docs/roadmap.md:
--------------------------------------------------------------------------------
1 | # Upcoming Projects & Roadmap
2 |
3 | This document outlines some of the features currently in development or being considered for the project. It should not be confused with the product roadmap for WordPress itself, even if some areas naturally overlap. The main purpose of it is to give visibility to some of the key problems remaining to be solved and as an invitation for those wanting to collaborate on some of the more complex issues to learn about what needs help.
4 |
5 | ## Projects
6 |
7 | - **WXR Library** — Create a standard library for handling WXR files. ([See overview](https://github.com/pento/free-as-in-speech/issues/3).)
8 | - **Wix Exporting** — Our primary focus is currently to produce an MVP of a Wix exporter. ([See overview](https://github.com/pento/free-as-in-speech/issues/6).)
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const glob = require( 'glob' ).sync;
5 |
6 | // Finds all packages which are transpiled with Babel to force Jest to use their source code.
7 | const transpiledPackageNames = glob( 'packages/*/src/index.js' ).map(
8 | ( fileName ) => fileName.split( '/' )[ 1 ]
9 | );
10 |
11 | module.exports = {
12 | rootDir: '.',
13 | moduleNameMapper: {
14 | [ `@wordpress\\/(${ transpiledPackageNames.join(
15 | '|'
16 | ) })$` ]: 'packages/$1/src',
17 | 'site-parsers': 'packages/site-parsers/src',
18 | 'gutenberg-for-node': 'packages/gutenberg-for-node/src',
19 | '@wordpress/block(s|-library)': 'node_modules/$0/build',
20 | },
21 | preset: '@wordpress/jest-preset-default',
22 | setupFiles: [ 'fake-indexeddb/auto', './test/setup-env.js' ],
23 | testURL: 'http://localhost',
24 | testPathIgnorePatterns: [
25 | '/.git/',
26 | '/node_modules/',
27 | '/.*/build/',
28 | '/.*/build-module/',
29 | '/test/setup-env.js',
30 | ],
31 | watchPlugins: [
32 | 'jest-watch-typeahead/filename',
33 | 'jest-watch-typeahead/testname',
34 | ],
35 | };
36 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@wordpress/*": ["./*", "./packages/*/src"]
6 | }
7 | },
8 | "exclude": [
9 | "build",
10 | "build-module",
11 | "node_modules",
12 | "packages/e2e-tests/plugins",
13 | "vendor"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/languages/free-as-in-speech.pot:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "Last-Translator: FULL NAME \n"
6 | "Language-Team: LANGUAGE \n"
7 | "MIME-Version: 1.0\n"
8 | "Content-Type: text/plain; charset=UTF-8\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 | "POT-Creation-Date: 2021-03-16T03:56:04+00:00\n"
11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 | "X-Generator: WP-CLI 2.4.0\n"
13 |
14 | #: action.js:88
15 | msgid "You can export your site now! The following Wix apps will be exported:"
16 | msgstr ""
17 |
18 | #: action.js:111
19 | msgid "If you're ready to export this site, click the export button."
20 | msgstr ""
21 |
22 | #: action.js:117
23 | msgid "Export finished."
24 | msgstr ""
25 |
26 | #: action.js:124
27 | msgid "Export"
28 | msgstr ""
29 |
30 | #: action.js:135
31 | msgid "We were unable to retrieve a list of the installed Wix apps."
32 | msgstr ""
33 |
34 | #: action.js:140
35 | msgid "Please refresh the page to try again."
36 | msgstr ""
37 |
38 | #: action.js:149
39 | msgid "Welcome to WordPress' Free (as in Speech) extension!"
40 | msgstr ""
41 |
42 | #: action.js:156
43 | msgid "This extension helps you get control of your content back from services that otherwise prevent you from choosing to export the content that you own."
44 | msgstr ""
45 |
46 | #: action.js:162
47 | msgid "Currently, Wix is supported."
48 | msgstr ""
49 |
50 | #: action.js:169
51 | msgid "To get started, login to your account on Wix.com, then click this button again!"
52 | msgstr ""
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "free-as-in-speech",
3 | "version": "1.0.0",
4 | "description": "A browser extension for freeing your data from proprietary CMSes.",
5 | "author": "The WordPress Contributors",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "wordpress",
9 | "export"
10 | ],
11 | "homepage": "https://github.com/pento/free-as-in-speech/",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/pento/free-as-in-speech.git",
15 | "directory": "packages/wxr"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/pento/free-as-in-speech/issues"
19 | },
20 | "engines": {
21 | "node": ">= 14.0.0",
22 | "npm": ">= 7.0.0"
23 | },
24 | "dependencies": {
25 | "@wordpress/block-library": "^4.0.0",
26 | "@wordpress/blocks": "^10.0.0",
27 | "@wordpress/components": "13.0.2",
28 | "@wordpress/element": "2.20.2",
29 | "@wordpress/format-library": "^2.2.0",
30 | "@wordpress/i18n": "3.19.2",
31 | "@wordpress/rich-text": "3.25.2",
32 | "@wordpress/wxr": "file:packages/wxr",
33 | "cheerio": "1.0.0-rc.6",
34 | "dayjs": "1.10.4",
35 | "debug": "4.3.2",
36 | "gutenberg-for-node": "file:packages/gutenberg-for-node",
37 | "node-fetch": "2.6.1",
38 | "os-browserify": "0.3.0",
39 | "react": "16.14.0",
40 | "react-dom": "16.14.0",
41 | "site-parsers": "file:packages/site-parsers",
42 | "slugify": "1.5.0",
43 | "uuid": "8.3.2",
44 | "web-streams-polyfill": "3.0.3",
45 | "webextension-polyfill": "0.8.0"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "7.13.16",
49 | "@babel/preset-env": "7.13.15",
50 | "@wordpress/babel-preset-default": "5.2.1",
51 | "@wordpress/env": "4.0.2",
52 | "@wordpress/eslint-plugin": "9.0.3",
53 | "@wordpress/npm-package-json-lint-config": "4.0.3",
54 | "@wordpress/prettier-config": "1.0.3",
55 | "@wordpress/scripts": "14.1.1",
56 | "@wordpress/stylelint-config": "19.0.3",
57 | "babel-loader": "8.2.2",
58 | "chrome-webstore-upload-cli": "1.2.1",
59 | "commander": "7.2.0",
60 | "copy-webpack-plugin": "8.1.1",
61 | "daily-version": "2.0.0",
62 | "dot-json": "1.2.2",
63 | "fake-indexeddb": "3.1.2",
64 | "fetch-from-har": "file:packages/fetch-from-har",
65 | "fetch-mock-jest": "1.5.1",
66 | "glob": "7.1.6",
67 | "husky": "6.0.0",
68 | "is-ci": "3.0.0",
69 | "jest-watch-typeahead": "0.6.3",
70 | "jsdom": "16.5.3",
71 | "lint-staged": "10.5.4",
72 | "node-watch": "0.7.1",
73 | "npm-run-all": "4.1.5",
74 | "po2json": "0.4.5",
75 | "prettier": "npm:wp-prettier@2.2.1-beta-1",
76 | "terser-webpack-plugin": "5.1.1",
77 | "web-ext": "6.1.0",
78 | "web-ext-submit": "6.1.0",
79 | "webpack": "5.36.0",
80 | "webpack-cli": "4.6.0"
81 | },
82 | "scripts": {
83 | "build": "run-p build:*",
84 | "build:language-pack": "node ./bin/build-language-pack.js",
85 | "build:packages": "node ./bin/packages/build.js",
86 | "build:source": "wp-scripts build",
87 | "lint": "run-p lint:*",
88 | "lint-fix": "run-p 'lint:* -- --fix'",
89 | "lint:css": "wp-scripts lint-style **/*.scss **/*.css",
90 | "lint:js": "wp-scripts lint-js",
91 | "lint:md-js": "wp-scripts lint-md-js --config .eslintrc-md.js",
92 | "lint:md-docs": "wp-scripts lint-md-docs",
93 | "lint:pkg-json": "wp-scripts lint-pkg-json . 'packages/*/package.json'",
94 | "prepare": "is-ci || husky install",
95 | "release:chrome": "cd distribution && webstore upload --auto-publish",
96 | "release:firefox": "cd distribution && web-ext-submit",
97 | "start:chrome": "web-ext run --target=chromium",
98 | "start:firefox": "web-ext run",
99 | "test": "npm run test-unit",
100 | "test-unit": "wp-scripts test-unit-js --config jest.config.js",
101 | "test-unit:watch": "npm run test-unit -- --watch",
102 | "watch:packages": "node ./bin/packages/watch.js",
103 | "watch:source": "wp-scripts start",
104 | "watch": "npm-run-all build:packages build:language-pack --parallel watch:*",
105 | "wp-env": "wp-env"
106 | },
107 | "webExt": {
108 | "sourceDir": "distribution",
109 | "run": {
110 | "keepProfileChanges": true,
111 | "firefoxProfile": "./test/web-ext-profile",
112 | "chromiumProfile": "./test/web-ext-profile",
113 | "startUrl": [
114 | "https://www.wix.com"
115 | ]
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/packages/fetch-from-har/README.md:
--------------------------------------------------------------------------------
1 | # Fetch from HAR
2 |
3 | This is a package for using a HAR file to fulfill HTTP requests without making a real request on the network.
4 |
5 | ## Basic usage
6 |
7 | The package only exposes one method that acts as a generator of a function that can be used as a replacement for [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API):
8 |
9 | ```javascript
10 | // @param har object The JSON representing the HAR file (you need to run JSON.parse() on the string)
11 | window.fetch = fetchFromHAR( har, {
12 | // When multiple matching requests (for method + hostname + pathname)
13 | // exist, we'll compare the GET parameters using this callback
14 | // to select the desired one.
15 | queryComparison: ( requestValue, harValue, key, url ) => {
16 | if ( '/index' === url.pathname ) {
17 | if ( 'status' === key && requestValue !== harValue ) {
18 | // The values for the GET parameter "status" must be equal,
19 | // so this mismatch means the request shouldn't match.
20 | return false;
21 | }
22 | }
23 |
24 | // A mismatch of the GET parameter values don't need to match.
25 | return true;
26 | },
27 |
28 | // When no matching request can be found in the HAR, this callback will
29 | // be invoked.
30 | fallback: ( url, entry ) => {
31 | // Can be used for logging missing URLs:
32 | debug( 'Missing URL in HAR:', url );
33 |
34 | // But also to provide fallback responses:
35 | const u = new URL( url );
36 | if ( u.pathname === '/tags' ) {
37 | entry.response.status = 200;
38 | entry.response.statusText = 'OK';
39 | entry.response.content.text = '{"tags":[]}';
40 | }
41 |
42 | return entry;
43 | },
44 | } );
45 |
46 | // Example invocation:
47 | window
48 | .fetch( 'http://example.com/tags' )
49 | .then( ( result ) => result.json() )
50 | .then( ( text ) => debug );
51 | ```
52 |
53 | ## `queryComparison` Callback
54 |
55 | ```javascript
56 | const queryComparison = ( requestValue, harValue, key, url ) => {};
57 | ```
58 |
59 | This will be called for each GET parameter of a `fetch()` request, if more than one request in the HAR file matches the requested `method`, `hostname`, and `pathname`.
60 |
61 | For a request to be considered, each of the requested GET parameters must receive a truthy return value from the callback.
62 |
63 | If the callback is omitted any combination of parameter values is considered acceptable.
64 |
65 | ### `queryComparison` Parameters
66 |
67 | - `key` the name of the GET parameter.
68 | - `requestValue` the value that has been requested in the `fetch()` call.
69 | - `harValue` a request in the HAR file is available with this value (it might match, or not).
70 | - `url` a `URL` object of the whole URL that was requested via `fetch()`
71 |
72 | ### `queryComparison` Return value
73 |
74 | `true` or `false`: whether the entry in the HAR file qualifies as a desired response.
75 |
76 | ## `fallback` Callback
77 |
78 | ```javascript
79 | const fallback = ( url, entry ) => {};
80 | ```
81 |
82 | This will be called if the request cannot be fulfilled with the requests available in the specified HAR object. This acts as a filter to allow the modification of the incoming `entry` value.
83 |
84 | If the callback is omitted, then a 404 response will be generated.
85 |
86 | ### `fallback` Parameters
87 |
88 | - `url` the string value of the URL that was requested via `fetch()`
89 | - `entry` a generated 404 entry that would be returned.
90 |
91 | ### `fallback` Return value
92 |
93 | The HAR entry to be used for a response. The incoming `entry` parameter can be used as a template and only override the desired values.
94 |
--------------------------------------------------------------------------------
/packages/fetch-from-har/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fetch-from-har",
3 | "version": "1.0.0",
4 | "description": "A package for using a HAR file as cache for fetch.",
5 | "author": "The WordPress Contributors",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "fetch",
9 | "har"
10 | ],
11 | "homepage": "https://github.com/pento/free-as-in-speech/",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/pento/free-as-in-speech.git",
15 | "directory": "packages/fetch-from-har"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/pento/free-as-in-speech/issues"
19 | },
20 | "main": "src/index.js",
21 | "dependencies": {
22 | "dayjs": "^1.10.4",
23 | "uuid": "^8.3.2"
24 | },
25 | "publishConfig": {
26 | "access": "public"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/fetch-from-har/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * find the request in the HAR.
3 | *
4 | * @param {Object} HAR
5 | * @param {string} requestUrl
6 | * @param {Object} options
7 | */
8 | function findRequestInHar( HAR, requestUrl, options ) {
9 | const req = new URL( requestUrl );
10 | // lets do a first pass on it.
11 | const entries = HAR.log.entries.filter( ( e ) => {
12 | const u = e.request.url;
13 | if ( u.host !== req.host ) {
14 | return false;
15 | }
16 | if ( u.pathname !== req.pathname ) {
17 | return false;
18 | }
19 | if (
20 | options &&
21 | options.method &&
22 | e.request.method !== ( options.method || 'GET' )
23 | ) {
24 | return false;
25 | }
26 | return true;
27 | } );
28 |
29 | let results = entries;
30 | if ( results.length > 1 ) {
31 | // first lets filter on query params
32 | const withSameQueryString = results.filter( ( e ) => {
33 | for ( const [ name, value ] of req.searchParams ) {
34 | if ( value !== e.request.url.searchParams.get( name ) ) {
35 | return false;
36 | }
37 | }
38 | return true;
39 | } );
40 |
41 | if ( withSameQueryString.length > 0 ) {
42 | results = withSameQueryString;
43 | } else if ( options && 'function' === typeof options.queryComparison ) {
44 | const withAllowedDeviatingQueryString = results.filter( ( e ) => {
45 | for ( const [ name, value ] of req.searchParams ) {
46 | if (
47 | ! options.queryComparison(
48 | e.request.url.searchParams.get( name ),
49 | value,
50 | name,
51 | e.request.url
52 | )
53 | ) {
54 | return false;
55 | }
56 | }
57 | return true;
58 | } );
59 | if ( withAllowedDeviatingQueryString.length > 0 ) {
60 | results = withAllowedDeviatingQueryString;
61 | }
62 | }
63 |
64 | if ( results.length > 1 && req.body ) {
65 | // then try to filter on body
66 | const withTheSameBody = results.filter( ( e ) => {
67 | const data = e.request.postData;
68 | if ( data ) {
69 | return req.body === data.text;
70 | }
71 | return false;
72 | } );
73 | if ( withTheSameBody.length > 0 ) {
74 | results = withTheSameBody;
75 | }
76 | }
77 |
78 | if ( results.length > 1 ) {
79 | }
80 | }
81 | if ( results.length > 0 ) {
82 | const response = results[ 0 ].response || results[ 0 ].request;
83 | if (
84 | ! response.status ||
85 | 302 === response.status ||
86 | 301 === response.status
87 | ) {
88 | if ( response.redirectURL ) {
89 | options.redirects = options.redirects
90 | ? options.redirects + 1
91 | : 1;
92 | if ( options.redirects < 10 ) {
93 | return findRequestInHar(
94 | HAR,
95 | response.redirectURL,
96 | options
97 | );
98 | }
99 | }
100 | }
101 | }
102 |
103 | return results;
104 | }
105 |
106 | function fetchFromHAR( HAR, mockOptions ) {
107 | if (
108 | typeof HAR.log === 'undefined' ||
109 | typeof HAR.log.entries === 'undefined'
110 | ) {
111 | HAR.log = { entries: [] };
112 | }
113 |
114 | HAR.log.entries.forEach( ( e ) => {
115 | e.request.url = new URL( e.request.url );
116 | } );
117 |
118 | return function ( url, options ) {
119 | return new Promise( ( resolve ) => {
120 | const entries = findRequestInHar(
121 | HAR,
122 | url,
123 | Object.assign( options || {}, mockOptions || {} )
124 | );
125 | let entry = {
126 | request: {
127 | url,
128 | },
129 | response: {
130 | status: 404,
131 | statusText: 'Not Found',
132 | content: {
133 | text: '',
134 | },
135 | },
136 | };
137 | if ( ! entries.length ) {
138 | if (
139 | mockOptions &&
140 | 'function' === typeof mockOptions.fallback
141 | ) {
142 | entry = mockOptions.fallback( url, entry );
143 | }
144 | } else {
145 | entry = entries[ 0 ];
146 | }
147 |
148 | const request = entry.request;
149 | const response = entry.response;
150 | resolve( {
151 | ok: ( response.status / 100 || 0 ) === 2, // 200-299
152 | statusText: response.statusText,
153 | status: response.status,
154 | url: response.redirectURL || request.url,
155 | text: () => Promise.resolve( response.content.text ),
156 | json: () =>
157 | Promise.resolve( response.content.text ).then( JSON.parse ),
158 | blob: () =>
159 | Promise.resolve( new Blob( [ response.content.text ] ) ),
160 | clone: response,
161 | headers: {
162 | keys: () =>
163 | response.headers.map( ( header ) => header.name ),
164 | entries: () =>
165 | response.headers.map( ( header ) => [
166 | header.name,
167 | header.value,
168 | ] ),
169 | get: ( n ) => response.headers[ n.toLowerCase() ],
170 | has: ( n ) => n.toLowerCase() in response.headers,
171 | },
172 | } );
173 | } );
174 | };
175 | }
176 |
177 | module.exports = fetchFromHAR;
178 |
--------------------------------------------------------------------------------
/packages/fetch-from-har/src/test/empty-json.har:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/packages/fetch-from-har/src/test/index.js:
--------------------------------------------------------------------------------
1 | const fs = require( 'fs' );
2 | const path = require( 'path' );
3 | const fetchFromHAR = require( '../index' );
4 |
5 | test.each( [
6 | [ 'test.har', 'https://localhost/', { hello: 'world' } ],
7 | [ 'test.har', 'https://localhost/?hello=earth', { hello: 'earth' } ],
8 | [ 'test.har', 'https://not-in-har/', undefined ],
9 | [ 'empty-json.har', 'https://not-in-har/', undefined ],
10 | ] )( '%s -> %s', async ( harFile, testUrl, expected ) => {
11 | const har = JSON.parse(
12 | fs.readFileSync( path.join( __dirname, harFile ) )
13 | );
14 | return fetchFromHAR( har )( testUrl )
15 | .then( ( result ) => result.json() )
16 | .then(
17 | ( result ) => {
18 | expect( result ).toEqual( expected );
19 | },
20 | () => expect( undefined ).toEqual( expected )
21 | );
22 | } );
23 |
24 | test.each( [
25 | [
26 | 'https://localhost/test',
27 | { 'https://localhost/test': '{"hello":"test"}' },
28 | { hello: 'test' },
29 | ],
30 | ] )( 'Fallback test: %s', async ( testUrl, fallback, expected ) => {
31 | const har = JSON.parse(
32 | fs.readFileSync( path.join( __dirname, 'test.har' ) )
33 | );
34 |
35 | return fetchFromHAR( har, {
36 | fallback: ( url, entry ) => {
37 | if ( fallback[ url ] !== undefined ) {
38 | entry.response.status = 200;
39 | entry.response.statusText = 'OK';
40 | entry.response.content.text = fallback[ url ];
41 | }
42 |
43 | return entry;
44 | },
45 | } )( testUrl )
46 | .then( ( result ) => result.json() )
47 | .then( ( result ) => {
48 | expect( result ).toEqual( expected );
49 | } );
50 | } );
51 |
52 | test.each( [
53 | [
54 | 'https://localhost/?hello=something',
55 | {}, // mismatching query strings are ignored.
56 | { hello: 'world' },
57 | ],
58 | [
59 | 'https://localhost/?hello=something',
60 |
61 | {
62 | localhost: {
63 | '/': {
64 | hello: false, // parameter values don't need to match.
65 | },
66 | },
67 | },
68 | { hello: 'world' },
69 | ],
70 | [
71 | 'https://localhost/?hello=earth',
72 | {
73 | localhost: {
74 | '/': {
75 | hello: true, // parameter values must match.
76 | },
77 | },
78 | },
79 | { hello: 'earth' },
80 | ],
81 | ] )( 'Query Comparison: %s', async ( testUrl, mockMapping, expected ) => {
82 | const har = JSON.parse(
83 | fs.readFileSync( path.join( __dirname, 'test.har' ) )
84 | );
85 |
86 | return fetchFromHAR( har, {
87 | queryComparison: ( requestValue, harValue, key, url ) => {
88 | if (
89 | mockMapping &&
90 | mockMapping[ url.host ] &&
91 | mockMapping[ url.host ][ url.pathname ][ key ]
92 | ) {
93 | if (
94 | mockMapping[ url.host ][ url.pathname ][ key ] &&
95 | requestValue !== harValue
96 | ) {
97 | // The values must match, so this is not ok.
98 | return false;
99 | }
100 | }
101 |
102 | return true;
103 | },
104 | } )( testUrl )
105 | .then( ( result ) => result.json() )
106 | .then( ( result ) => {
107 | expect( result ).toEqual( expected );
108 | } );
109 | } );
110 |
--------------------------------------------------------------------------------
/packages/gutenberg-for-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gutenberg-for-node",
3 | "version": "0.0.1",
4 | "description": "A package for supporting Gutenberg library for node.",
5 | "author": "The WordPress Contributors",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "wordpress",
9 | "parser",
10 | "export"
11 | ],
12 | "homepage": "https://github.com/pento/free-as-in-speech/",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/pento/free-as-in-speech.git",
16 | "directory": "packages/gutenberg-for-node"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/pento/free-as-in-speech/issues"
20 | },
21 | "main": "src/index.js",
22 | "module": "build-module/index.js",
23 | "dependencies": {
24 | "jsdom": "16.5.3",
25 | "node-fetch": "2.6.1"
26 | },
27 | "devDependencies": {},
28 | "publishConfig": {
29 | "access": "public"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/gutenberg-for-node/src/index.js:
--------------------------------------------------------------------------------
1 | const { JSDOM, ResourceLoader } = require( 'jsdom' );
2 | const fetchNode = require( 'node-fetch' ).default;
3 | const noop = function () {};
4 |
5 | class Window {
6 | constructor( jsdomConfig = {} ) {
7 | const { proxy, strictSSL, userAgent } = jsdomConfig;
8 | const resources = new ResourceLoader( {
9 | proxy,
10 | strictSSL,
11 | userAgent,
12 | } );
13 | return new JSDOM(
14 | '',
15 | Object.assign( jsdomConfig, {
16 | resources,
17 | } )
18 | ).window;
19 | }
20 | }
21 |
22 | global.window = new Window( { url: 'http://localhost' } );
23 | global.document = window.document;
24 | global.requestAnimationFrame = global.cancelAnimationFrame = noop;
25 | global.navigator = window.navigator;
26 | global.Mousetrap = {
27 | init: noop,
28 | prototype: { stopCallback: noop },
29 | };
30 | window.matchMedia = global.matchMedia = () => ( { addListener: noop } );
31 | global.Node = window.Node;
32 | window.fetch = window.fetch || fetchNode;
33 | global.CSS = {
34 | supports: noop,
35 | escape: noop,
36 | };
37 |
--------------------------------------------------------------------------------
/packages/site-parsers/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const eslintConfig = {
2 | root: true,
3 | extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ],
4 | globals: {
5 | browser: 'readonly',
6 | Blob: 'readonly',
7 | i18n: 'readonly',
8 | WritableStreamDefaultWriter: 'readonly',
9 | },
10 | rules: {
11 | '@wordpress/no-global-event-listener': 'off',
12 | },
13 | };
14 |
15 | module.exports = eslintConfig;
16 |
--------------------------------------------------------------------------------
/packages/site-parsers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "site-parsers",
3 | "version": "0.0.1",
4 | "description": "A package for parsing pages from different providers.",
5 | "author": "The WordPress Contributors",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "wordpress",
9 | "parser",
10 | "export"
11 | ],
12 | "homepage": "https://github.com/pento/free-as-in-speech/",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/pento/free-as-in-speech.git",
16 | "directory": "packages/site-parsers"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/pento/free-as-in-speech/issues"
20 | },
21 | "main": "src/index.js",
22 | "module": "build-module/index.js",
23 | "dependencies": {
24 | "@wordpress/block-library": "^4.0.0",
25 | "@wordpress/blocks": "^10.0.0",
26 | "@wordpress/format-library": "^2.2.0",
27 | "cheerio": "^1.0.0-rc.10",
28 | "debug": "4.3.2",
29 | "node-fetch": "^2.6.1",
30 | "slugify": "^1.6.0"
31 | },
32 | "devDependencies": {},
33 | "publishConfig": {
34 | "access": "public"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/index.js:
--------------------------------------------------------------------------------
1 | const utils = require( './utils' );
2 | const { staticPagesParser } = require( './parsers/wix' );
3 |
4 | module.exports = {
5 | utils,
6 | staticPagesParserWix: staticPagesParser,
7 | };
8 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/anchor.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'Anchor',
6 | parseComponent: ( component ) => {
7 | Logger( 'wix' ).log( 'Anchor' );
8 | const dataQuery = component.dataQuery;
9 |
10 | if ( dataQuery && dataQuery.type === 'Anchor' && dataQuery.name ) {
11 | return createBlock( 'core/paragraph', {
12 | id: dataQuery.name,
13 | } );
14 | }
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/audio.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | /**
5 | * MusicPlayerData covers:
6 | * - Wix music
7 | * - Audio player
8 | */
9 | module.exports = {
10 | type: 'MusicPlayerData',
11 | parseComponent: ( component, { metaData } ) => {
12 | Logger( 'wix' ).log( 'MusicPlayerData' );
13 | if ( ! component.dataQuery.audioRef ) {
14 | return null;
15 | }
16 |
17 | const uri = component.dataQuery.audioRef.uri;
18 | const prefix = metaData.serviceTopology.staticAudioUrl;
19 |
20 | const attrs = {
21 | src: prefix + '/' + uri,
22 | };
23 |
24 | if (
25 | component.propertyQuery &&
26 | component.propertyQuery.type === 'MusicPlayerProperties'
27 | ) {
28 | Object.assign( attrs, {
29 | loop: component.propertyQuery.loop,
30 | autoplay: component.propertyQuery.autoplay,
31 | } );
32 | }
33 |
34 | return createBlock( 'core/audio', attrs );
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/button-stylable.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'StylableButton',
6 | parseComponent: ( component ) => {
7 | Logger( 'wix' ).log( 'StylableButton' );
8 | const link = component.dataQuery.link || {};
9 |
10 | return createBlock( 'core/button', {
11 | url: link.url,
12 | linkTarget: link.target,
13 | text: component.dataQuery.label,
14 | } );
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/button.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { parseWixLink, getHtmlLinkAttributes } = require( '../links.js' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | module.exports = {
6 | type: 'LinkableButton',
7 | parseComponent: ( component, { metaData, page } ) => {
8 | Logger( 'wix' ).log( 'LinkableButton' );
9 |
10 | const link = parseWixLink( component.dataQuery.link, metaData );
11 | const attrs = getHtmlLinkAttributes( link, page.pageId );
12 | attrs.url = attrs.href;
13 |
14 | return createBlock( 'core/buttons', {}, [
15 | createBlock( 'core/button', {
16 | ...attrs,
17 | text: component.dataQuery.label,
18 | } ),
19 | ] );
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/document-media.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | /**
5 | * The parent component handler is ./image.js
6 | * componentType: DocumentMedia
7 | */
8 | module.exports = {
9 | parseComponent: ( component, { metaData, addMediaAttachment } ) => {
10 | Logger( 'wix' ).log( 'DocumentMedia' );
11 |
12 | if ( ! component.dataQuery.link ) {
13 | return null;
14 | }
15 |
16 | const attachment = addMediaAttachment(
17 | metaData.serviceTopology.staticHTMLComponentUrl,
18 | {
19 | uri: component.dataQuery.link.docId,
20 | name: component.dataQuery.link.name,
21 | alt: component.dataQuery.link.name,
22 | }
23 | );
24 |
25 | return createBlock( 'core/file', {
26 | id: attachment.id,
27 | href: attachment.link,
28 | fileName: attachment.name,
29 | textLinkHref: attachment.title,
30 | downloadButtonText: attachment.name,
31 | } );
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/google-map.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'GeoMap',
6 | parseComponent: ( component, { addObject } ) => {
7 | Logger( 'wix' ).log( 'GeoMap' );
8 |
9 | return createBlock( 'core-import/plugin-placeholder', {
10 | id: addObject( 'map', {
11 | height: component.layout.height, // int (px)
12 | width: component.layout.width, // int (px)
13 |
14 | zoom: component.propertyQuery.zoom, // int
15 | showZoom: component.propertyQuery.showZoom, // boolean
16 | showPosition: component.propertyQuery.showPosition, // boolean
17 | showStreetView: component.propertyQuery.showStreetView, // boolean
18 | showDirectionsLink: component.propertyQuery.showDirectionsLink, // boolean
19 | mapDragging: component.propertyQuery.mapDragging, // boolean
20 | mapType: component.propertyQuery.mapType, // enum: ROADMAP | SATELLITE | TERRAIN
21 | showMapType: component.propertyQuery.showMapType, // boolean
22 |
23 | locations: component.dataQuery.locations.map( ( item ) => ( {
24 | latitude: item.latitude,
25 | longitude: item.longitude,
26 | address: item.address,
27 | } ) ),
28 |
29 | pinColor:
30 | component.dataQuery.locations[ 0 ] &&
31 | component.dataQuery.locations[ 0 ].pinColor,
32 | } ),
33 | } );
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/html.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | const SUPPORTED_SOURCE = [ 'htmlEmbedded' ];
5 |
6 | module.exports = {
7 | type: 'HtmlComponent',
8 | parseComponent: async ( component, { metaData } ) => {
9 | Logger( 'wix' ).log( 'HtmlComponent' );
10 | if ( SUPPORTED_SOURCE.indexOf( component.dataQuery.sourceType ) === -1 )
11 | return null;
12 |
13 | const htmlContentUrl =
14 | metaData.serviceTopology.staticHTMLComponentUrl +
15 | component.dataQuery.url;
16 |
17 | return await Promise.resolve()
18 | .then( () => window.fetch( htmlContentUrl ) )
19 | .then( ( response ) => response.text() )
20 | .then( ( content ) => createBlock( 'core/html', { content } ) );
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/image-list.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { parseComponent: linkBarParseComponent } = require( './link-bar' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | const parseImages = ( images, { metaData, addMediaAttachment } ) => {
6 | return images.map( ( img ) => {
7 | const attachment = addMediaAttachment(
8 | metaData.serviceTopology.staticMediaUrl,
9 | img
10 | );
11 |
12 | const attrs = {
13 | id: attachment.id,
14 | alt: img.alt,
15 | title: img.title,
16 | caption: img.description,
17 | url: attachment.attachment_url,
18 | fulUrl: attachment.attachment_url,
19 | };
20 |
21 | if ( img.link ) {
22 | attrs.link = img.link.url;
23 | }
24 |
25 | return attrs;
26 | } );
27 | };
28 |
29 | module.exports = {
30 | type: 'ImageList',
31 | parseComponent: ( component, meta ) => {
32 | Logger( 'wix' ).log( 'ImageList' );
33 |
34 | const images = parseImages( component.dataQuery.items, meta );
35 | const attrs = {
36 | images,
37 | ids: images.map( ( img ) => img.id ),
38 | };
39 |
40 | switch ( component.propertyQuery.type ) {
41 | // Gallery: 3DCarousel, 3DCarousel, Slider Galleries
42 | case 'FreestyleProperties':
43 | case 'SlideShowGalleryProperties':
44 | return createBlock( 'core/gallery', attrs );
45 |
46 | // Gallery: Grid
47 | case 'MasonryProperties':
48 | case 'HoneycombProperties':
49 | case 'MatrixGalleryProperties':
50 | case 'PaginatedGridGalleryProperties':
51 | attrs.columns = component.propertyQuery.numCols || 3;
52 | return createBlock( 'core/gallery', attrs );
53 |
54 | // Gallery: Currently unsupported
55 | case 'ImpressProperties':
56 | break;
57 |
58 | case 'LinkBarProperties':
59 | return linkBarParseComponent( component );
60 | }
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/image-vector.js:
--------------------------------------------------------------------------------
1 | const cheerio = require( 'cheerio' );
2 | const { createBlock } = require( '@wordpress/blocks' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | module.exports = {
6 | type: 'VectorImage',
7 | parseComponent: async ( component, { metaData } ) => {
8 | Logger( 'wix' ).log( 'VectorImage' );
9 | const alt = component.dataQuery.alt;
10 | const title = component.dataQuery.title;
11 | const svgContentUrl =
12 | metaData.serviceTopology.staticServerUrl +
13 | 'shapes/' +
14 | component.dataQuery.svgId;
15 |
16 | return await Promise.resolve()
17 | .then( () => window.fetch( svgContentUrl ) )
18 | .then( ( response ) => response.text() )
19 | .then( ( svgData ) => {
20 | const $ = cheerio.load( svgData );
21 |
22 | return $( 'svg' )
23 | .attr( {
24 | role: 'img',
25 | ...( title && { 'aria-label': title } ),
26 | } )
27 | .prepend(
28 | ( title ? `${ escape( title ) } ` : '' ) +
29 | ( alt ? `${ escape( alt ) } ` : '' )
30 | )
31 | .toString();
32 | } )
33 | .then( ( content ) => createBlock( 'core/html', { content } ) );
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/image.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { parseComponent: parseDocumentMedia } = require( './document-media' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | const parseImage = ( component, { metaData, addMediaAttachment } ) => {
6 | Logger( 'wix' ).log( 'Image' );
7 |
8 | if ( ! component.dataQuery || ! component.dataQuery.uri ) {
9 | return null;
10 | }
11 |
12 | const attachment = addMediaAttachment(
13 | metaData.serviceTopology.staticMediaUrl,
14 | component.dataQuery
15 | );
16 |
17 | return createBlock( 'core/image', {
18 | url: attachment.guid,
19 | alt: component.dataQuery.alt,
20 | width: component.dataQuery.width,
21 | height: component.dataQuery.height,
22 | } );
23 | };
24 |
25 | module.exports = {
26 | type: 'Image',
27 | // eslint-disable-next-line
28 | parseComponent: function ( component ) {
29 | switch ( component.componentType ) {
30 | case 'wysiwyg.viewer.components.documentmedia.DocumentMedia':
31 | return parseDocumentMedia( ...arguments );
32 |
33 | default:
34 | return parseImage( ...arguments );
35 | }
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/link-bar.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | const getServiceNameFromUrl = ( url ) => {
5 | return new URL( url ).hostname.replace( 'www.', '' ).split( '.' )[ 0 ];
6 | };
7 |
8 | module.exports = {
9 | parseComponent: ( component ) => {
10 | Logger( 'wix' ).log( 'LinkBar' );
11 |
12 | const socialLinkAttrs = component.dataQuery.items.map( ( item ) => {
13 | const url = item.link && item.link.url;
14 |
15 | return {
16 | url,
17 | label: item.title,
18 | service: url && getServiceNameFromUrl( url ),
19 | };
20 | } );
21 |
22 | return createBlock(
23 | 'core/social-links',
24 | { openInNewTab: true },
25 | socialLinkAttrs.map( ( attrs ) =>
26 | createBlock( 'core/social-link', attrs )
27 | )
28 | );
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/menu.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { parseWixLink } = require( '../links.js' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | const parseMenuRecursively = ( menuItem, resolver ) => {
6 | menuItem = resolver( menuItem );
7 | if ( ! menuItem.isVisible ) {
8 | return null;
9 | }
10 | const attributes = {
11 | label: menuItem.name || menuItem.label,
12 | };
13 | let innerBlocks = [];
14 |
15 | if ( menuItem.link ) {
16 | const parsedLink = parseWixLink( menuItem.link );
17 | // We're not using this data for now since the post ids not being rewritten,
18 | // resulting in the menu items to be hidden because these seemingly arbitrary
19 | // post ids lead to posts that are not published.
20 | //
21 | // if ( 'post_type' === parsedLink.type ) {
22 | // attributes.type = parsedLink.object;
23 | // attributes.id = parsedLink.objectId;
24 | // }
25 |
26 | attributes.url = parsedLink.url;
27 | }
28 |
29 | if ( menuItem.items && menuItem.items.length > 0 ) {
30 | innerBlocks = menuItem.items
31 | .map( ( menuData ) => parseMenuRecursively( menuData, resolver ) )
32 | .filter( Boolean );
33 | }
34 |
35 | return createBlock( 'core/navigation-link', attributes, innerBlocks );
36 | };
37 |
38 | module.exports = {
39 | type: 'CustomMenuDataRef',
40 | parseComponent: ( component, { resolver } ) => {
41 | Logger( 'wix' ).log( 'CustomMenuDataRef' );
42 |
43 | try {
44 | let menuItems = [];
45 | if (
46 | component.dataQuery.menuRef &&
47 | component.dataQuery.menuRef.items
48 | ) {
49 | menuItems = component.dataQuery.menuRef.items
50 | .map( ( menuData ) =>
51 | parseMenuRecursively( menuData, resolver )
52 | )
53 | .filter( Boolean );
54 | }
55 | return createBlock(
56 | 'core/navigation',
57 | {
58 | orientation: 'horizontal',
59 | },
60 | menuItems
61 | );
62 | } catch ( e ) {
63 | return null;
64 | }
65 | },
66 | };
67 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/separator.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'FiveGridLine',
6 | parseComponent: () => {
7 | Logger( 'wix' ).log( 'FiveGridLine' );
8 |
9 | return createBlock( 'core/separator' );
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/sound-cloud.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | parseComponent: ( component, { getThemeDataRef } ) => {
6 | Logger( 'wix' ).log( 'SoundCloud' );
7 |
8 | const themeData = getThemeDataRef( component.styleId );
9 |
10 | if (
11 | themeData &&
12 | themeData.style &&
13 | themeData.style.properties &&
14 | themeData.style.properties.param_font_resolveUrl
15 | ) {
16 | const url = themeData.style.properties.param_font_resolveUrl.replace(
17 | /^"|"$/g,
18 | ''
19 | );
20 |
21 | return createBlock( 'core/embed', { url } );
22 | }
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/spotify.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | const getSpotifyUrlFromUri = ( uri ) => {
5 | const uriRgx = /^spotify:(?track|user|artist|album|playlist):(?[a-zA-Z0-9]+)$/;
6 | if ( ! uriRgx.test( uri ) ) return uri;
7 |
8 | const uriMatch = uri.match( uriRgx );
9 | return `https://open.spotify.com/${ uriMatch.groups.type }/${ uriMatch.groups.id }`;
10 | };
11 |
12 | module.exports = {
13 | parseComponent: ( component ) => {
14 | Logger( 'wix' ).log( 'Spotify' );
15 |
16 | const tpaData = component.dataQuery.tpaData;
17 |
18 | if ( typeof tpaData === 'object' && tpaData !== null ) {
19 | const content = JSON.parse( tpaData.content );
20 |
21 | return createBlock( 'core/embed', {
22 | url: getSpotifyUrlFromUri( content.spotifyURI ),
23 | } );
24 | }
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/tpa-widget.js:
--------------------------------------------------------------------------------
1 | const { parseComponent: parseSpotify } = require( './spotify' );
2 | const { parseComponent: parseSoundCloud } = require( './sound-cloud' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | const APP_ID = {
6 | SPOTIFY: '2575',
7 | SPOTIFY2: '4909',
8 | WIX_MUSIC: '1662',
9 | SOUND_CLOUD: '3195',
10 | };
11 |
12 | module.exports = {
13 | type: 'TPAWidget',
14 | // eslint-disable-next-line
15 | parseComponent: function ( component ) {
16 | Logger( 'wix' ).log( 'TPAWidget' );
17 |
18 | switch ( component.dataQuery.applicationId ) {
19 | case APP_ID.SPOTIFY:
20 | case APP_ID.SPOTIFY2:
21 | return parseSpotify( ...arguments );
22 | case APP_ID.SOUND_CLOUD:
23 | return parseSoundCloud( ...arguments );
24 | case APP_ID.WIX_MUSIC:
25 | default:
26 | break;
27 | }
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/twitter-follow.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'TwitterFollow',
6 |
7 | parseComponent: ( component ) => {
8 | Logger( 'wix' ).log( 'TwitterFollow' );
9 |
10 | return createBlock( 'core/social-links', { openInNewTab: true }, [
11 | createBlock( 'core/social-link', {
12 | url: `//twitter.com/${ component.dataQuery.accountToFollow }`,
13 | service: 'twitter',
14 | } ),
15 | ] );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/components/video.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | type: 'VideoPlayer',
6 | parseComponent: ( component ) => {
7 | Logger( 'wix' ).log( 'VideoPlayer' );
8 |
9 | if ( ! component.dataQuery.videoUrl ) {
10 | return null;
11 | }
12 |
13 | switch ( component.dataQuery.videoType.toUpperCase() ) {
14 | case 'VIMEO':
15 | case 'YOUTUBE':
16 | case 'FACEBOOK':
17 | case 'DAILYMOTION':
18 | return createBlock( 'core/embed', {
19 | url: component.dataQuery.videoUrl,
20 | providerNameSlug: component.dataQuery.videoType,
21 | } );
22 |
23 | default:
24 | return null;
25 | }
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/containers/column.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { asyncComponentsParser } = require( '../data' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | module.exports = {
6 | componentType: 'wysiwyg.viewer.components.Column',
7 | parseComponent: async ( component, recursiveComponentParser ) => {
8 | Logger( 'wix' ).log( 'ColumnContainer' );
9 |
10 | const innerComponents = await asyncComponentsParser(
11 | component.components,
12 | recursiveComponentParser
13 | );
14 |
15 | return createBlock( 'core/column', {}, innerComponents );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/containers/columns.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { asyncComponentsParser } = require( '../data' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | module.exports = {
6 | componentType: 'wysiwyg.viewer.components.StripColumnsContainer',
7 | parseComponent: async ( component, recursiveComponentParser ) => {
8 | Logger( 'wix' ).log( 'StripColumnsContainer' );
9 |
10 | let innerBlocks = await asyncComponentsParser(
11 | component.components,
12 | recursiveComponentParser
13 | );
14 |
15 | if ( ! innerBlocks.length ) {
16 | return null;
17 | }
18 |
19 | let coverBlock = null;
20 |
21 | if (
22 | 'core/column' === innerBlocks[ 0 ].name &&
23 | 'core/cover' === innerBlocks[ 0 ].innerBlocks.name
24 | ) {
25 | // The column has a cover, we need to inject the column here:
26 | coverBlock = innerBlocks[ 0 ];
27 | innerBlocks = innerBlocks[ 0 ].innerBlocks;
28 | }
29 |
30 | if ( 1 === innerBlocks.length ) {
31 | innerBlocks = innerBlocks[ 0 ];
32 | }
33 |
34 | if ( 'core/column' === innerBlocks.name ) {
35 | // Just a single column, let's unwrap it.
36 | innerBlocks = innerBlocks.innerBlocks;
37 |
38 | if ( null !== coverBlock ) {
39 | coverBlock.innerBlocks = innerBlocks;
40 |
41 | return coverBlock;
42 | }
43 |
44 | return innerBlocks;
45 | }
46 |
47 | if ( 'core/cover' === innerBlocks.name ) {
48 | // Just a single cover. Don't wrap with column.
49 | return innerBlocks;
50 | }
51 |
52 | if ( innerBlocks.length > 1 ) {
53 | // Real columns == more than 1, we need to wrap it with a columns block.
54 | const columnsBlock = createBlock( 'core/columns', {}, innerBlocks );
55 |
56 | if ( null !== coverBlock ) {
57 | coverBlock.innerBlocks = [ columnsBlock ];
58 | return coverBlock;
59 | }
60 |
61 | return columnsBlock;
62 | }
63 |
64 | const innerComponents = await asyncComponentsParser(
65 | component.components,
66 | recursiveComponentParser
67 | );
68 |
69 | return createBlock( 'core/column', {}, innerComponents );
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/containers/cover.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | maybeAddCoverBlock: ( component, { metaData, addMediaAttachment } ) => {
6 | if ( ! component || ! component.innerBlocks ) {
7 | return component;
8 | }
9 | let innerBlocks = component.innerBlocks;
10 |
11 | if ( innerBlocks.name === 'core/cover' ) {
12 | return innerBlocks;
13 | }
14 |
15 | if (
16 | component.designQuery &&
17 | component.designQuery.background &&
18 | component.designQuery.background.mediaRef
19 | ) {
20 | Logger( 'wix' ).log( 'CoverContainer' );
21 |
22 | // If a background is defined, let's make this a cover block.
23 | const attachment = addMediaAttachment(
24 | metaData.serviceTopology.staticMediaUrl,
25 | component.designQuery.background.mediaRef
26 | );
27 |
28 | if (
29 | innerBlocks.length === 1 &&
30 | 'core/column' === innerBlocks[ 0 ].name
31 | ) {
32 | innerBlocks = innerBlocks[ 0 ].innerBlocks;
33 | }
34 |
35 | return createBlock(
36 | 'core/cover',
37 | {
38 | url: attachment.guid,
39 | id: attachment.id,
40 | align:
41 | component.designQuery.background.fittingType === 'fill'
42 | ? 'full'
43 | : 'center',
44 | },
45 | innerBlocks
46 | );
47 | }
48 | return component;
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/containers/form.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { Logger } = require( '../../../utils' );
3 |
4 | module.exports = {
5 | componentType: 'wysiwyg.viewer.components.FormContainer',
6 | parseComponent: (
7 | formComponent,
8 | _recursiveComponentParser,
9 | resolver,
10 | { addObject }
11 | ) => {
12 | const parseFormFields = ( component ) => {
13 | Logger( 'wix' ).log( 'FormContainer' );
14 | component = resolver( component );
15 |
16 | if ( component.components ) {
17 | return component.components
18 | .map( parseFormFields )
19 | .filter( Boolean );
20 | }
21 |
22 | switch ( component.dataQuery.type ) {
23 | case 'DatePicker':
24 | return {
25 | type: 'text',
26 | format: 'date',
27 | label: component.dataQuery.label,
28 | placeholder: component.dataQuery.placeholder,
29 | required: component.propertyQuery.required
30 | ? 'true'
31 | : 'false',
32 | dateFormat: component.propertyQuery.dateFormat,
33 | disabledDates: component.dataQuery.disabledDates,
34 | disabledDaysOfWeek:
35 | component.dataQuery.disabledDaysOfWeek,
36 | };
37 | case 'TextAreaInput':
38 | return {
39 | type: 'textarea',
40 | label: component.dataQuery.label,
41 | placeholder: component.dataQuery.placeholder,
42 | required: component.propertyQuery.required
43 | ? 'true'
44 | : 'false',
45 | };
46 | case 'TextInput':
47 | return {
48 | type: 'text',
49 | label: component.dataQuery.label,
50 | placeholder: component.dataQuery.placeholder,
51 | required: component.propertyQuery.required
52 | ? 'true'
53 | : 'false',
54 | };
55 | case 'SelectableList':
56 | return {
57 | type: 'select',
58 | label: component.dataQuery.label,
59 | options: component.dataQuery.options.map(
60 | ( option ) => {
61 | return {
62 | text: option.text,
63 | value: option.value,
64 | description: option.description,
65 | };
66 | }
67 | ),
68 | required: component.propertyQuery.required
69 | ? 'true'
70 | : 'false',
71 | };
72 | case 'LinkableButton':
73 | return {
74 | type:
75 | component.dataQuery.link &&
76 | component.dataQuery.link.type ===
77 | 'FormSubmitButtonLink'
78 | ? 'submit'
79 | : 'button',
80 | label: component.dataQuery.label,
81 | };
82 | }
83 | };
84 |
85 | const fields = formComponent.components
86 | .map( parseFormFields )
87 | .filter( Boolean );
88 |
89 | const email = formComponent.connectionQuery.items
90 | .map( ( item ) => {
91 | if ( 'ConnectionItem' !== item.type ) return false;
92 | if ( ! item.config ) return false;
93 | if ( ! item.isPrimary ) return false;
94 | const config = JSON.parse( item.config );
95 | if ( ! config.email ) return false;
96 | return config.email;
97 | } )
98 | .filter( Boolean )[ 0 ];
99 |
100 | const form = {
101 | fields,
102 | };
103 |
104 | if ( email ) {
105 | form.email = email;
106 | }
107 |
108 | return createBlock( 'core-import/plugin-placeholder', {
109 | id: addObject( 'contact-form', form ),
110 | } );
111 | },
112 | };
113 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/containers/mobile.js:
--------------------------------------------------------------------------------
1 | const { createBlock } = require( '@wordpress/blocks' );
2 | const { asyncComponentsParser } = require( '../data' );
3 | const { Logger } = require( '../../../utils' );
4 |
5 | module.exports = {
6 | componentType: 'mobile.core.components.Container',
7 | parseComponent: async ( component, recursiveComponentParser ) => {
8 | Logger( 'wix' ).log( 'MobileContainer' );
9 |
10 | const innerComponents = await asyncComponentsParser(
11 | component.components,
12 | recursiveComponentParser
13 | );
14 |
15 | return createBlock( 'core/group', {}, innerComponents );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const slug = require( 'slugify' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { asyncForEach, IdFactory } = require( '../../utils' );
10 |
11 | const getProperTreeLocation = ( fieldName ) => {
12 | switch ( fieldName ) {
13 | case 'designQuery':
14 | case 'background':
15 | case 'mediaRef':
16 | return 'design_data';
17 |
18 | case 'propertyQuery':
19 | return 'component_properties';
20 |
21 | case 'connectionQuery':
22 | return 'connections_data';
23 |
24 | default:
25 | return 'document_data';
26 | }
27 | };
28 |
29 | const isDocumentRefValid = ( refStr ) => {
30 | const cssColorHexReg = new RegExp( /#([a-fA-F0-9]{3}){1,2}\b/ );
31 |
32 | return ! cssColorHexReg.test( refStr );
33 | };
34 |
35 | const resolveQueries = ( input, data, masterPage ) => {
36 | // skip resolving for non-objects
37 | if ( ! input || typeof input !== 'object' ) {
38 | return input;
39 | }
40 |
41 | // walk over all object keys and resolve known queries
42 | Object.entries( input ).forEach( ( entry ) => {
43 | const key = entry[ 0 ];
44 | const val = entry[ 1 ];
45 | const location = getProperTreeLocation( key );
46 |
47 | const mapItem = ( item ) => {
48 | if ( ! item || typeof item.valueOf() !== 'string' ) return item;
49 | if ( item.substr( 0, 1 ) !== '#' || location !== 'document_data' )
50 | return item;
51 |
52 | const query = item.replace( /^#/, '' );
53 | return isDocumentRefValid( item )
54 | ? resolveQueries(
55 | data[ location ][ query ] ||
56 | masterPage[ location ][ query ],
57 | data,
58 | masterPage
59 | )
60 | : item;
61 | };
62 |
63 | if ( Array.isArray( val ) ) {
64 | // Some values can be an array of things that need to get resolved
65 | // Example: `input.linkList = [ '#foo', '#baz' ]`
66 | input[ key ] = val.map( mapItem );
67 | } else if (
68 | val &&
69 | typeof val.valueOf() === 'string' &&
70 | ( val.substr( 0, 1 ) === '#' || location !== 'document_data' )
71 | ) {
72 | // Others are just a string
73 | // Example: `input.link = '#baz'`
74 | const query = val.replace( /^#/, '' );
75 | input[ key ] = isDocumentRefValid( val )
76 | ? resolveQueries(
77 | data[ location ][ query ] ||
78 | ( masterPage && masterPage[ location ][ query ] ),
79 | data,
80 | masterPage
81 | )
82 | : val;
83 | } else if ( typeof val === 'object' ) {
84 | input[ key ] = resolveQueries( val, data, masterPage );
85 | }
86 | } );
87 |
88 | return input;
89 | };
90 |
91 | const addMediaAttachment = ( data, mediaUrl, component ) => {
92 | const key = 'attachment' + ( component.name || component.uri );
93 | const existingId = IdFactory.exists( key );
94 | if ( existingId ) {
95 | return data.attachments[ existingId ];
96 | }
97 |
98 | mediaUrl = mediaUrl.replace( /\/$/, '' );
99 | component.src = mediaUrl + '/' + component.uri;
100 |
101 | const attachment = {
102 | id: IdFactory.get( key ),
103 | title: component.alt,
104 | excerpt: component.description || '',
105 | content: component.description || '',
106 | link: component.src,
107 | guid: component.src,
108 | commentStatus: 'closed',
109 | name: slug( component.name || component.uri || key ),
110 | type: 'attachment',
111 | attachment_url: component.src,
112 | meta: [
113 | {
114 | key: '_wp_attachment_attachment_alt',
115 | value: component.alt || null,
116 | },
117 | ],
118 | };
119 |
120 | data.attachments[ attachment.id ] = attachment;
121 | return attachment;
122 | };
123 |
124 | const addObject = ( data, objType, objData ) => {
125 | const id = 1 + data.objects.length;
126 | data.objects.push( {
127 | type: objType,
128 | data: objData,
129 | } );
130 | return id;
131 | };
132 |
133 | const getThemeDataRef = ( page, id ) => {
134 | return (
135 | page &&
136 | page.config &&
137 | page.config.data &&
138 | page.config.data.theme_data &&
139 | page.config.data.theme_data[ id ]
140 | );
141 | };
142 |
143 | const asyncComponentsParser = async ( components, parser ) => {
144 | const parserComponents = [];
145 | await asyncForEach( components, async ( comp ) => {
146 | parserComponents.push( await parser( comp ) );
147 | } );
148 |
149 | return parserComponents.flat().filter( Boolean );
150 | };
151 |
152 | module.exports = {
153 | resolveQueries,
154 | addMediaAttachment,
155 | addObject,
156 | getThemeDataRef,
157 | asyncComponentsParser,
158 | };
159 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal dependencies
3 | */
4 | const { addHeaderPage, addFooterPage, parsePages } = require( './pages' );
5 | const { convertMenu } = require( './menu' );
6 | const { Logger } = require( '../../utils' );
7 |
8 | const defaultConfig = {
9 | debug: true,
10 | };
11 |
12 | const setupLoggerInstance = ( debug ) => {
13 | Logger( 'wix', debug );
14 | };
15 |
16 | const staticPagesParser = async (
17 | metaData,
18 | masterPage,
19 | pages = [],
20 | config = {}
21 | ) => {
22 | const data = {
23 | pages,
24 | menus: [],
25 | attachments: {},
26 | objects: [],
27 | };
28 | config = Object.assign( defaultConfig, config );
29 |
30 | setupLoggerInstance( config.debug );
31 |
32 | // ↓ methods mutate data object
33 | addHeaderPage( data, masterPage );
34 | addFooterPage( data, masterPage );
35 | await parsePages( data, metaData, masterPage, config );
36 | convertMenu( data, masterPage );
37 |
38 | return data;
39 | };
40 |
41 | module.exports = {
42 | staticPagesParser,
43 | };
44 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/links.js:
--------------------------------------------------------------------------------
1 | const IdFactory = require( '../../utils/idfactory' );
2 |
3 | const parseWixLink = ( link, metaData ) => {
4 | if ( ! link ) {
5 | return { url: '' };
6 | }
7 |
8 | switch ( link.type ) {
9 | case 'DocumentLink':
10 | const prefix = metaData.serviceTopology.staticDocsUrl;
11 | const filename = ( link.docId || '' ).replace( /^ugd\//g, '' );
12 | const url = prefix + '/' + filename;
13 | return {
14 | url,
15 | attachment: {
16 | src: url,
17 | alt: link.name || filename,
18 | },
19 | };
20 | case 'EmailLink':
21 | return {
22 | url:
23 | `mailto:${ link.recipient }` +
24 | ( link.subject ? `?subject=${ link.subject }` : '' ),
25 | };
26 | case 'PhoneLink':
27 | return { url: 'tel:' + link.phoneNumber };
28 | case 'ExternalLink':
29 | return {
30 | url: link.url || '#',
31 | target: link.target || null,
32 | };
33 | case 'AnchorLink':
34 | case 'PageLink':
35 | const page = link.pageId;
36 | if ( ! page || page.skipExport ) {
37 | return { url: '' };
38 | }
39 | return {
40 | type: 'post_type',
41 | object: 'page',
42 | objectId: IdFactory.get( page.id ),
43 | originalId: page.id,
44 | anchor:
45 | ( link.anchorDataId && link.anchorDataId.compId ) || null,
46 | url: '/' + page.pageUriSEO,
47 | title: link.anchorName || null,
48 | target: link.target || null,
49 | };
50 | }
51 | return { url: '' };
52 | };
53 |
54 | const getUrlForLink = ( link, currentPage = null ) => {
55 | if ( link.type === 'post_type' ) {
56 | const hash = link.anchor ? `#${ link.anchor }` : '';
57 | if ( currentPage === link.originalId ) {
58 | // linking to anchor on the same page
59 | return hash;
60 | }
61 | if ( link.url ) {
62 | return link.url + hash;
63 | }
64 | // full link
65 | return `?p=${ link.objectId }` + hash;
66 | }
67 | return link.url;
68 | };
69 |
70 | const getHtmlLinkAttributes = ( link, currentPage = null ) => ( {
71 | href: getUrlForLink( link, currentPage ),
72 | target: ! link.target || link.target === '_self' ? null : link.target,
73 | rel: ! link.target || link.target === '_self' ? null : 'noopener',
74 | title: link.title,
75 | } );
76 |
77 | module.exports = { parseWixLink, getUrlForLink, getHtmlLinkAttributes };
78 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/mappers.js:
--------------------------------------------------------------------------------
1 | const { pasteHandler } = require( '@wordpress/blocks' );
2 | const { asyncComponentsParser } = require( './data' );
3 | const { Logger } = require( '../../utils' );
4 |
5 | const handlerMapper = ( key ) => ( accumulator, currentValue ) => {
6 | accumulator[ currentValue[ key ] ] = currentValue;
7 | return accumulator;
8 | };
9 |
10 | const containerHandlers = [
11 | require( './containers/form.js' ),
12 | require( './containers/column.js' ),
13 | require( './containers/columns.js' ),
14 | require( './containers/mobile.js' ),
15 | ].reduce( handlerMapper( 'componentType' ), {} );
16 |
17 | const componentHandlers = [
18 | require( './components/html.js' ),
19 | require( './components/menu.js' ),
20 | require( './components/image.js' ),
21 | require( './components/image-list.js' ),
22 | require( './components/image-vector.js' ),
23 | require( './components/button.js' ),
24 | require( './components/button-stylable.js' ),
25 | require( './components/google-map.js' ),
26 | require( './components/separator.js' ),
27 | require( './components/anchor.js' ),
28 | require( './components/tpa-widget.js' ),
29 | require( './components/twitter-follow.js' ),
30 | require( './components/audio.js' ),
31 | require( './components/video.js' ),
32 | ].reduce( handlerMapper( 'type' ), {} );
33 |
34 | const wrapResult = ( block, component ) => {
35 | if ( block ) {
36 | block.designQuery = component.designQuery;
37 | }
38 | return block;
39 | };
40 |
41 | /**
42 | * @param {string} componentType (ex. wysiwyg.viewer.components.FiveGridLine)
43 | * @return {string} (ex. FiveGridLine)
44 | */
45 | const getTypeFromComponentPath = ( componentType ) => {
46 | const type = componentType.split( '.' );
47 |
48 | return type[ type.length - 1 ];
49 | };
50 |
51 | module.exports = {
52 | containerMapper: async (
53 | component,
54 | recursiveComponentParser,
55 | resolver,
56 | addMediaAttachment,
57 | addObject
58 | ) => {
59 | if ( component.componentType in containerHandlers ) {
60 | return wrapResult(
61 | await containerHandlers[
62 | component.componentType
63 | ].parseComponent(
64 | component,
65 | recursiveComponentParser,
66 | resolver,
67 | addMediaAttachment,
68 | addObject
69 | ),
70 | component
71 | );
72 | }
73 |
74 | return await asyncComponentsParser(
75 | component.components,
76 | recursiveComponentParser
77 | );
78 | },
79 |
80 | componentMapper: ( component, meta ) => {
81 | const type =
82 | ( component.dataQuery && component.dataQuery.type ) ||
83 | getTypeFromComponentPath( component.componentType );
84 |
85 | if ( type in componentHandlers ) {
86 | return wrapResult(
87 | componentHandlers[ type ].parseComponent( component, meta ),
88 | component
89 | );
90 | }
91 |
92 | if ( component.dataQuery && component.dataQuery.text ) {
93 | Logger( 'wix' ).log( 'Text' );
94 | return pasteHandler( { HTML: component.dataQuery.text } );
95 | }
96 |
97 | return null;
98 | },
99 | };
100 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/menu.js:
--------------------------------------------------------------------------------
1 | const slug = require( 'slugify' );
2 |
3 | const IdFactory = require( '../../utils/idfactory' );
4 | const { resolveQueries } = require( './data' );
5 |
6 | const handleMenuItemsRecursively = ( menu, items, parent = 0 ) => {
7 | const results = [];
8 | items.forEach( ( item ) => {
9 | const id = IdFactory.get( item.title );
10 |
11 | if ( ! item.type ) {
12 | item.type = 'custom';
13 | item.object = 'custom';
14 | item.objectId = IdFactory.get( item.title );
15 | } else if ( 'PageLink' === item.type ) {
16 | item.type = 'post_type';
17 | item.objectId = IdFactory.get( item.pageId.id );
18 | item.status = item.pageId.hidePage ? 'pending' : 'publish';
19 | item.object = 'page';
20 | }
21 |
22 | results.push( {
23 | postId: id,
24 | title: item.title || '',
25 | date: Date.now(),
26 | name: item.title ? slug( item.title || id, { lower: true } ) : '',
27 | type: 'nav_menu_item',
28 | parent,
29 | status: item.status || 'publish',
30 | menuOrder: menu.counter++,
31 | meta: {
32 | _menu_item_type: item.type,
33 | _menu_item_menu_item_parent: String( parent ? parent : '' ),
34 | _menu_item_object_id: item.objectId,
35 | _menu_item_object: item.object,
36 | _menu_item_target: item.target || '',
37 | _menu_item_classes: item.classes || '',
38 | _menu_item_url: item.url || '',
39 | _menu_item_xfn: '',
40 | },
41 | terms: [ { type: 'nav_menu', ...menu } ],
42 | } );
43 |
44 | if ( Array.isArray( item.items ) ) {
45 | results.push(
46 | ...handleMenuItemsRecursively( menu, item.items, id )
47 | );
48 | }
49 | } );
50 | return results;
51 | };
52 |
53 | const convertMenu = ( data, masterPage ) => {
54 | const menus = [];
55 | const pages = [];
56 |
57 | const menuItem = masterPage.data.document_data.CUSTOM_MAIN_MENU;
58 | if ( menuItem ) {
59 | menus.push( {
60 | role: 'main',
61 | ...parseMenu( menuItem, masterPage ),
62 | } );
63 | }
64 |
65 | menus.forEach( ( menu ) => {
66 | const term = {
67 | id: IdFactory.get( menu.title ),
68 | name: menu.title,
69 | slug: slug( `${ menu.title }-1`, { lower: true } ),
70 | counter: 0,
71 | meta: {
72 | menu_role: menu.role || null,
73 | parsing_session_id: 1,
74 | },
75 | };
76 |
77 | pages.push( ...handleMenuItemsRecursively( term, menu.items ) );
78 | } );
79 |
80 | data.menus = pages;
81 | };
82 |
83 | const parseMenu = ( menuItem, masterPage ) => {
84 | menuItem = resolveQueries( menuItem, masterPage.data );
85 | let menu = {
86 | title: menuItem.name || menuItem.label,
87 | };
88 |
89 | if ( menuItem.link ) {
90 | const parsedLink = menuItem.link;
91 | menu = {
92 | ...parsedLink,
93 | ...menu,
94 | };
95 |
96 | if ( parsedLink.attachment ) {
97 | // handleAttachment( parsedLink.attachment );
98 | }
99 |
100 | if ( parsedLink.target !== '_blank' ) {
101 | delete menu.target;
102 | }
103 | }
104 |
105 | if ( menuItem.items && menuItem.items.length > 0 ) {
106 | menu.items = menuItem.items.map( ( menuData ) =>
107 | parseMenu( menuData, masterPage )
108 | );
109 | }
110 |
111 | return menu;
112 | };
113 |
114 | module.exports = { handleMenuItemsRecursively, convertMenu, parseMenu };
115 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/parsers/wix/pages.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const { serialize } = require( '@wordpress/blocks' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { asyncForEach, IdFactory } = require( '../../utils' );
10 | const { maybeAddCoverBlock } = require( './containers/cover.js' );
11 | const { containerMapper, componentMapper } = require( './mappers.js' );
12 | const {
13 | resolveQueries,
14 | addMediaAttachment,
15 | addObject,
16 | getThemeDataRef,
17 | asyncComponentsParser,
18 | } = require( './data' );
19 |
20 | const addHeaderPage = ( data, masterPage ) => {
21 | data.pages.push( {
22 | config: {
23 | structure: masterPage.structure.children.filter(
24 | ( component ) => 'SITE_HEADER' === component.id
25 | )[ 0 ],
26 | data: masterPage.data,
27 | },
28 | pageId: 'header',
29 | title: 'header',
30 | postId: IdFactory.get( 'header' ),
31 | postType: 'wp_template_part',
32 | terms: [
33 | {
34 | type: 'wp_template_part_area',
35 | name: 'header',
36 | slug: 'header',
37 | id: IdFactory.get( 'term-header' ),
38 | },
39 | {
40 | type: 'wp_theme',
41 | slug: 'tt1-blocks',
42 | name: 'tt1-blocks',
43 | id: IdFactory.get( 'term-tt1-blocks' ),
44 | },
45 | ],
46 | } );
47 | };
48 |
49 | const addFooterPage = ( data, masterPage ) => {
50 | data.pages.push( {
51 | config: {
52 | structure: masterPage.structure.children.filter(
53 | ( component ) => 'SITE_FOOTER' === component.id
54 | )[ 0 ],
55 | data: masterPage.data,
56 | },
57 | pageId: 'footer',
58 | title: 'footer',
59 | postId: IdFactory.get( 'footer' ),
60 | postType: 'wp_template_part',
61 | terms: [
62 | {
63 | type: 'wp_template_part_area',
64 | name: 'footer',
65 | slug: 'footer',
66 | id: IdFactory.get( 'term-footer' ),
67 | },
68 | {
69 | type: 'wp_theme',
70 | slug: 'wp_theme',
71 | name: 'tt1-blocks',
72 | id: IdFactory.get( 'term-tt1-blocks' ),
73 | },
74 | ],
75 | } );
76 | };
77 |
78 | const parsePages = async ( data, metaData, masterPage, config ) => {
79 | await asyncForEach( data.pages, async ( page ) => {
80 | const resolver = ( component ) =>
81 | resolveQueries( component, page.config.data, masterPage.data );
82 | const meta = {
83 | resolver,
84 | metaData,
85 | page,
86 | fetch: config.fetch,
87 | debug: config.debug,
88 | addObject: addObject.bind( null, data ),
89 | addMediaAttachment: addMediaAttachment.bind( null, data ),
90 | getThemeDataRef: getThemeDataRef.bind( null, page ),
91 | };
92 |
93 | const recursiveComponentParser = async ( component ) => {
94 | component = resolver( component );
95 |
96 | if ( component.components ) {
97 | return maybeAddCoverBlock(
98 | await containerMapper(
99 | component,
100 | recursiveComponentParser,
101 | resolver,
102 | meta
103 | ),
104 | meta
105 | );
106 | }
107 |
108 | return componentMapper( component, meta );
109 | };
110 |
111 | if ( typeof page.config.structure === 'undefined' ) {
112 | return;
113 | }
114 |
115 | const parsedComponents = await asyncComponentsParser(
116 | page.config.structure.components,
117 | recursiveComponentParser
118 | );
119 |
120 | page.content = serialize( parsedComponents );
121 | } );
122 | };
123 |
124 | module.exports = {
125 | addHeaderPage,
126 | addFooterPage,
127 | parsePages,
128 | };
129 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/utils/idfactory.js:
--------------------------------------------------------------------------------
1 | const ids = {};
2 | let counter = 0;
3 |
4 | module.exports = {
5 | exists: ( idKey ) => {
6 | const key = String( idKey ).replace( /^#/, '' );
7 | if ( undefined === ids[ key ] ) {
8 | return false;
9 | }
10 | return ids[ key ];
11 | },
12 | get: ( idKey ) => {
13 | const key = String( idKey ).replace( /^#/, '' );
14 | if ( undefined === ids[ key ] ) {
15 | ids[ key ] = ++counter;
16 | }
17 | return ids[ key ];
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/utils/index.js:
--------------------------------------------------------------------------------
1 | const asyncForEach = async ( array, callback ) => {
2 | for ( let index = 0; index < array.length; index++ ) {
3 | await callback( array[ index ], index, array );
4 | }
5 | };
6 |
7 | module.exports = {
8 | asyncForEach,
9 | Logger: require( './logger' ),
10 | IdFactory: require( './idfactory' ),
11 | ...require( './register-blocks' ),
12 | };
13 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/utils/logger.js:
--------------------------------------------------------------------------------
1 | const debugLog = require( 'debug' );
2 |
3 | function Logger( provider ) {
4 | const path = `siteParser:${ provider }:apps:staticPages`;
5 |
6 | return {
7 | log( component ) {
8 | debugLog( `${ path }:${ component }` )(
9 | arguments[ 1 ] ? arguments[ 1 ] : '',
10 | arguments[ 2 ] ? arguments[ 2 ] : '',
11 | arguments[ 3 ] ? arguments[ 3 ] : ''
12 | );
13 | },
14 | };
15 | }
16 |
17 | // Logger singleton per provider
18 | module.exports = ( () => {
19 | const instance = {};
20 |
21 | return ( provider ) => {
22 | if ( ! instance[ provider ] ) {
23 | instance[ provider ] = Logger( provider );
24 | }
25 | return instance[ provider ];
26 | };
27 | } )();
28 |
--------------------------------------------------------------------------------
/packages/site-parsers/src/utils/register-blocks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | if ( global.process ) {
5 | global.process.env = {
6 | ...global.process.env,
7 | GUTENBERG_PHASE: 2, // This needs to be a integer, setting it directly would make it a string.
8 | };
9 | }
10 |
11 | if ( typeof global.CSS === 'undefined' ) {
12 | global.CSS = {
13 | supports() {},
14 | escape() {},
15 | };
16 | }
17 |
18 | const {
19 | registerCoreBlocks,
20 | __experimentalRegisterExperimentalCoreBlocks,
21 | } = require( '@wordpress/block-library' );
22 | const { registerBlockType } = require( '@wordpress/blocks' );
23 | require( '@wordpress/format-library' );
24 | let registered = false;
25 |
26 | module.exports = {
27 | registerBlocks: () => {
28 | if ( registered ) {
29 | return;
30 | }
31 | registered = true;
32 | registerCoreBlocks();
33 |
34 | registerBlockType( 'core-import/plugin-placeholder', {
35 | title: 'Plugin Placeholder',
36 | attributes: {
37 | id: {
38 | type: 'int',
39 | default: null,
40 | },
41 | },
42 | } );
43 |
44 | if (
45 | typeof __experimentalRegisterExperimentalCoreBlocks === 'function'
46 | ) {
47 | __experimentalRegisterExperimentalCoreBlocks();
48 | }
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/packages/wxr/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/packages/wxr/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # WXR Package Changelog
2 |
3 |
4 |
5 | ## Unreleased
6 |
7 | - Initial release.
8 |
--------------------------------------------------------------------------------
/packages/wxr/README.md:
--------------------------------------------------------------------------------
1 | # WXR
2 |
3 | This is a package for generating a WordPress Export (WXR) file.
4 |
5 | ## Installation
6 |
7 | Install the module
8 |
9 | ```bash
10 | npm install @wordpress/wxr --save-dev
11 | ```
12 |
13 | ## Usage
14 |
15 | ```js
16 | /**
17 | * WordPress dependencies
18 | */
19 | import { getWXRDriver } from '@wordpress/wxr';
20 |
21 | const myExporter = async () => {
22 | const wxr = await getWXRDriver( '1.2' );
23 | };
24 | ```
25 |
26 | ## API
27 |
28 | ### `getWXRDriver( wxrVersion )`
29 |
30 | Retrieves a driver for generating WXR of the passed version. Currently supports version 1.2.
31 |
32 | ## WXR Driver API
33 |
34 | ### `wxr.clear()`
35 |
36 | Clears the intermediary data store used when generating a WXR file. Should usually be called once,
37 | immediately before starting to add data to the export.
38 |
39 | ### `wxr.export()`
40 |
41 | Returns the full WXR file as a string.
42 |
43 | ### `wxr.stream( writableStream )`
44 |
45 | When generating large WXR files, it may be preferable to limit memory usage by streaming the WXR content,
46 | instead. The WXR will be written to the passed `WritableStream` object.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/packages/wxr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wordpress/wxr",
3 | "version": "1.0.0",
4 | "description": "A package for generating WordPress Export (WXR) files.",
5 | "author": "The WordPress Contributors",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "wordpress",
9 | "wxr",
10 | "export"
11 | ],
12 | "homepage": "https://github.com/pento/free-as-in-speech/",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/pento/free-as-in-speech.git",
16 | "directory": "packages/wxr"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/pento/free-as-in-speech/issues"
20 | },
21 | "main": "src/index.js",
22 | "module": "build-module/index.js",
23 | "dependencies": {
24 | "ajv": "8.2.0",
25 | "dayjs": "^1.10.4",
26 | "idb": "6.0.0",
27 | "jstoxml": "2.0.5",
28 | "web-streams-polyfill": "3.0.2",
29 | "xml-sanitizer": "1.1.6"
30 | },
31 | "devDependencies": {
32 | "fake-indexeddb": "3.1.2"
33 | },
34 | "publishConfig": {
35 | "access": "public"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/__snapshots__/authors.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Authors All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 | 1
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | "
41 | `;
42 |
43 | exports[`Authors Multiple authors are handled 1`] = `
44 | "
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | "
82 | `;
83 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/__snapshots__/basic.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Basic behaviour Creates an empty file 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 | "
33 | `;
34 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/__snapshots__/comments.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Comments All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 | -
32 |
33 |
34 |
35 | 0
36 |
37 | 1234
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 5
48 |
49 | who?
50 | what?
51 |
52 |
53 |
54 |
55 | "
56 | `;
57 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/__snapshots__/sitemeta.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Site Meta All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 1.2
37 |
38 |
39 |
40 | "
41 | `;
42 |
43 | exports[`Site Meta siteUrl and BlogUrl copy from link 1`] = `
44 | "
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 | 1.2
74 |
75 |
76 |
77 | "
78 | `;
79 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/authors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Authors', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addAuthor( {
21 | id: 1,
22 | login: 'pento',
23 | email: 'notmyrealemail@wordpress.org',
24 | display_name: 'pento',
25 | first_name: 'Gary',
26 | last_name: 'Pendergast',
27 | } );
28 |
29 | const xml = await wxr.export();
30 |
31 | expect( xml ).toMatchSnapshot();
32 | } );
33 |
34 | test( 'Multiple authors are handled', async () => {
35 | const wxr = new WXRDriver();
36 | await wxr.connect();
37 |
38 | wxr.addAuthor( {
39 | login: 'pento',
40 | } );
41 |
42 | wxr.addAuthor( {
43 | login: 'not-pento',
44 | } );
45 |
46 | wxr.addAuthor( {
47 | login: 'someone-else',
48 | } );
49 |
50 | const xml = await wxr.export();
51 |
52 | expect( xml ).toMatchSnapshot();
53 | } );
54 | } );
55 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/basic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXR_VERSION, WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Basic behaviour', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'Correct WXR version', () => {
17 | expect( WXR_VERSION ).toEqual( '1.2' );
18 | } );
19 |
20 | test( 'WXRDriver class is defined', () => {
21 | expect( typeof WXRDriver ).toBe( 'function' );
22 | } );
23 |
24 | test( 'Creates an empty file', async () => {
25 | const wxr = new WXRDriver();
26 | await wxr.connect();
27 |
28 | const xml = await wxr.export();
29 |
30 | expect( xml ).toMatchSnapshot();
31 | } );
32 | } );
33 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/comments.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Comments', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | const postId = await wxr.addPost( {
21 | title: 'A Post Title',
22 | } );
23 |
24 | wxr.addComment( postId, {
25 | id: 1234,
26 | post_id: 5678, // This will not be written.
27 | author: 'pento',
28 | author_email: 'notme@wordpress.org',
29 | author_url: 'https://pento.net',
30 | author_IP: '127.0.0.1',
31 | date: '1970-01-01T00:00:10.000Z',
32 | date_gmt: '1970-01-01T00:00:11.000Z',
33 | content: 'This is good content. Plz subscribe me.',
34 | approved: 'trash',
35 | type: 'pingback',
36 | user_id: 5,
37 | meta: [ { key: 'who?', value: 'what?' } ],
38 | } );
39 |
40 | const xml = await wxr.export();
41 |
42 | expect( xml ).toMatchSnapshot();
43 | } );
44 | } );
45 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/posts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Posts', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addCategory( {
21 | slug: 'some-cat',
22 | name: 'Some Category',
23 | } );
24 |
25 | wxr.addTag( {
26 | slug: 'some-tag',
27 | name: 'Some Tag',
28 | } );
29 |
30 | wxr.addTerm( {
31 | taxonomy: 'my-tax',
32 | slug: 'some-term',
33 | name: 'Some Term',
34 | } );
35 |
36 | wxr.addPost( {
37 | id: 1,
38 | title: 'A Post Title',
39 | link: 'https://wordpress.org/',
40 | date: '1970-01-01T00:00:01.000Z',
41 | author: 'pento',
42 | guid: 'should-be-a-proper-guid',
43 | description: 'This will not be written', // Non-writeable field.
44 | content: 'The post content',
45 | excerpt: 'A brief excerpt from the content',
46 | postDate: '1970-01-01T00:00:02.000Z',
47 | date_gmt: '1970-01-01T00:00:03.000Z',
48 | modified: '1970-01-01T00:00:04.000Z',
49 | modified_gmt: '1970-01-01T00:00:05.000Z',
50 | comment_status: 'closed',
51 | ping_status: 'open',
52 | status: 'draft',
53 | menu_order: 1,
54 | type: 'post',
55 | password: 'a-strong-password',
56 | sticky: true,
57 | attachment_url: 'https://make.wordpress.org', // Won't be written for this post type.
58 | terms: [
59 | {
60 | type: 'category',
61 | slug: 'some-cat',
62 | name: 'Some Category',
63 | },
64 | {
65 | type: 'tag',
66 | slug: 'some-tag',
67 | name: 'Some Tag',
68 | },
69 | {
70 | type: 'my-tax',
71 | slug: 'some-term',
72 | name: 'Some Term',
73 | },
74 | ],
75 | meta: [ { key: 'talk about', value: 'pop music' } ],
76 | } );
77 |
78 | const xml = await wxr.export();
79 |
80 | expect( xml ).toMatchSnapshot();
81 | } );
82 |
83 | test( 'attachment_url is only included for the attachment post type', async () => {
84 | const wxr = new WXRDriver();
85 | await wxr.connect();
86 |
87 | wxr.addPost( {
88 | title: 'A Page Title',
89 | type: 'page',
90 | attachment_url: 'https://make.wordpress.org', // Won't be written for this post type.
91 | } );
92 |
93 | wxr.addPost( {
94 | title: 'An Attachment Title',
95 | type: 'attachment',
96 | attachment_url: 'https://make.wordpress.org',
97 | } );
98 |
99 | const xml = await wxr.export();
100 |
101 | expect( xml ).toMatchSnapshot();
102 | } );
103 | } );
104 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/sitemeta.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Site Meta', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.setSiteMeta( {
21 | title: 'A Title',
22 | link: 'https://wordpress.org/',
23 | description: 'A description',
24 | language: 'art-x-emoji',
25 | wxrVersion: '1.2.3.4.5', // Should not be writable, test with a fake value.
26 | siteUrl: 'https://make.wordpress.org/',
27 | blogUrl: 'https://wordpress.org/news/',
28 | } );
29 |
30 | const xml = await wxr.export();
31 |
32 | const cleanedXml = xml.replace(
33 | /.*?<\/pubDate>/,
34 | ' '
35 | );
36 |
37 | expect( cleanedXml ).toMatchSnapshot();
38 | } );
39 |
40 | test( 'siteUrl and BlogUrl copy from link', async () => {
41 | const wxr = new WXRDriver();
42 | await wxr.connect();
43 |
44 | wxr.setSiteMeta( {
45 | link: 'https://wordpress.org/',
46 | } );
47 |
48 | const xml = await wxr.export();
49 |
50 | const cleanedXml = xml.replace(
51 | /.*?<\/pubDate>/,
52 | ' '
53 | );
54 |
55 | expect( cleanedXml ).toMatchSnapshot();
56 | } );
57 | } );
58 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.2/test/terms.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Terms and Taxonomies', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All Category fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addCategory( {
21 | id: 1,
22 | slug: 'some-cat',
23 | name: 'Some Category',
24 | description: 'A category of things.',
25 | meta: [
26 | { key: 'a-key', value: 'a-value' },
27 | { key: 'another-key', value: 'another-value' },
28 | ],
29 | } );
30 |
31 | wxr.addCategory( {
32 | id: 2,
33 | slug: 'some-other-cat',
34 | parent: 'some-cat',
35 | name: 'Some Other Category',
36 | description: 'A different category of things.',
37 | } );
38 |
39 | const xml = await wxr.export();
40 |
41 | expect( xml ).toMatchSnapshot();
42 | } );
43 |
44 | test( 'All Tag fields are written as expected', async () => {
45 | const wxr = new WXRDriver();
46 | await wxr.connect();
47 |
48 | wxr.addTag( {
49 | id: 1,
50 | slug: 'some-tag',
51 | name: 'Some Tag',
52 | description: 'A tag for things.',
53 | meta: [
54 | { key: 'a-key', value: 'a-value' },
55 | { key: 'another-key', value: 'another-value' },
56 | ],
57 | } );
58 |
59 | wxr.addTag( {
60 | id: 2,
61 | slug: 'some-other-tag',
62 | name: 'Some Other Tag',
63 | description: 'A different tag for things.',
64 | } );
65 |
66 | const xml = await wxr.export();
67 |
68 | expect( xml ).toMatchSnapshot();
69 | } );
70 |
71 | test( 'All custom taxonomy fields are written as expected', async () => {
72 | const wxr = new WXRDriver();
73 | await wxr.connect();
74 |
75 | wxr.addTerm( {
76 | id: 1,
77 | taxonomy: 'my-tax',
78 | slug: 'some-term',
79 | name: 'Some Term',
80 | description: 'A Term in a Taxonomy of things.',
81 | meta: [
82 | { key: 'a-key', value: 'a-value' },
83 | { key: 'another-key', value: 'another-value' },
84 | ],
85 | } );
86 |
87 | wxr.addTerm( {
88 | id: 2,
89 | taxonomy: 'my-tax',
90 | slug: 'some-other-term',
91 | name: 'Some Other Term',
92 | description: 'A different Term in the same Taxonomy of things.',
93 | } );
94 |
95 | wxr.addTerm( {
96 | id: 3,
97 | taxonomy: 'my-other-tax',
98 | slug: 'some-term',
99 | name: 'Some Term',
100 | description: 'The same Term in a different Taxonomy of things.',
101 | } );
102 |
103 | const xml = await wxr.export();
104 |
105 | expect( xml ).toMatchSnapshot();
106 | } );
107 | } );
108 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/objects/contact-form.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "title": "Contact Form",
4 | "type": "object",
5 | "properties": {
6 | "email": { "type": "string" },
7 | "fields": {
8 | "type": "array",
9 | "items": {
10 | "type": "object",
11 | "properties": {
12 | "type": {
13 | "type": "string",
14 | "enum": [ "text", "select", "textarea", "submit" ]
15 | },
16 | "label": { "type": "string" },
17 | "placeholder": { "type": "string" },
18 | "required": { "type": "boolean" }
19 | },
20 | "required": [ "type" ],
21 | "allOf": [
22 | {
23 | "if": {
24 | "properties": { "type": { "const": "text" } }
25 | },
26 | "then": {
27 | "properties": {
28 | "format": {
29 | "type": "string",
30 | "enum": [ "date", "email", "phone-number" ]
31 | }
32 | }
33 | }
34 | },
35 | {
36 | "if": {
37 | "properties": {
38 | "type": { "const": "text" },
39 | "format": { "const": "date" }
40 | }
41 | },
42 | "then": {
43 | "properties": { "dateFormat": { "type": "string" } }
44 | }
45 | },
46 | {
47 | "if": {
48 | "properties": { "type": { "const": "select" } }
49 | },
50 | "then": {
51 | "properties": {
52 | "options": {
53 | "type": "array",
54 | "items": {
55 | "type": "object",
56 | "properties": {
57 | "text": { "type": "string" },
58 | "value": { "type": "string" }
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | ]
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/objects/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'contact-form': require( './contact-form.json' ),
3 | map: require( './map.json' ),
4 | };
5 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/objects/test/__snapshots__/contact-form.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Contact Form Object All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 | gary@pento.net
34 |
35 |
36 |
37 |
38 |
39 | 11:30 AM
40 | 12:00 PM
41 | 12:30 PM
42 | 1:00 PM
43 | 1:30 PM
44 | 2:00 PM
45 | 7:00 PM
46 | 7:30 PM
47 | 8:00 PM
48 | 8:30 PM
49 | 9:00 PM
50 | 9:30 PM
51 |
52 |
53 |
54 |
55 |
56 | "
57 | `;
58 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/objects/test/contact-form.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../../index.js' );
10 |
11 | describe( 'Contact Form Object', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addObject( 'contact-form', {
21 | email: 'gary@pento.net',
22 | fields: [
23 | {
24 | type: 'text',
25 | label: 'Name',
26 | required: true,
27 | },
28 | {
29 | type: 'text',
30 | label: 'Email',
31 | format: 'email',
32 | required: true,
33 | },
34 | {
35 | type: 'text',
36 | label: 'Phone',
37 | format: 'phone-number',
38 | },
39 | {
40 | type: 'text',
41 | label: 'Date',
42 | format: 'date',
43 | dateFormat: 'MM/DD/YYYY',
44 | required: true,
45 | },
46 | {
47 | type: 'select',
48 | label: 'Time',
49 | options: [
50 | { text: '11:30 AM', value: '11:30 AM' },
51 | { text: '12:00 PM', value: '12:00 PM' },
52 | { text: '12:30 PM', value: '12:30 PM' },
53 | { text: '1:00 PM', value: '1:00 PM' },
54 | { text: '1:30 PM', value: '1:30 PM' },
55 | { text: '2:00 PM', value: '2:00 PM' },
56 | { text: '7:00 PM', value: '7:00 PM' },
57 | { text: '7:30 PM', value: '7:30 PM' },
58 | { text: '8:00 PM', value: '8:00 PM' },
59 | { text: '8:30 PM', value: '8:30 PM' },
60 | { text: '9:00 PM', value: '9:00 PM' },
61 | { text: '9:30 PM', value: '9:30 PM' },
62 | ],
63 | required: true,
64 | },
65 | {
66 | type: 'textarea',
67 | label: 'Special Request',
68 | placeholder: '',
69 | },
70 | { type: 'submit', label: 'Submit' },
71 | ],
72 | } );
73 |
74 | const xml = await wxr.export();
75 |
76 | expect( xml ).toMatchSnapshot();
77 | } );
78 | } );
79 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/__snapshots__/authors.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Authors All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 | 1
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | "
41 | `;
42 |
43 | exports[`Authors Multiple authors are handled 1`] = `
44 | "
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | "
82 | `;
83 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/__snapshots__/basic.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Basic behaviour Creates an empty file 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 | "
33 | `;
34 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/__snapshots__/comments.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Comments All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 | -
32 |
33 |
34 |
35 | 0
36 |
37 | 1234
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 5
48 |
49 | who?
50 | what?
51 |
52 |
53 |
54 |
55 | "
56 | `;
57 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/__snapshots__/objects.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Objects Object ID increments 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 | gary@pento.net
34 |
35 |
36 |
37 | foo@bar.com
38 |
39 |
40 | "
41 | `;
42 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/__snapshots__/sitemeta.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Site Meta All fields are written as expected 1`] = `
4 | "
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 1.3
37 |
38 |
39 |
40 | "
41 | `;
42 |
43 | exports[`Site Meta siteUrl and BlogUrl copy from link 1`] = `
44 | "
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 | 1.3
74 |
75 |
76 |
77 | "
78 | `;
79 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/authors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Authors', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addAuthor( {
21 | id: 1,
22 | login: 'pento',
23 | email: 'notmyrealemail@wordpress.org',
24 | display_name: 'pento',
25 | first_name: 'Gary',
26 | last_name: 'Pendergast',
27 | } );
28 |
29 | const xml = await wxr.export();
30 |
31 | expect( xml ).toMatchSnapshot();
32 | } );
33 |
34 | test( 'Multiple authors are handled', async () => {
35 | const wxr = new WXRDriver();
36 | await wxr.connect();
37 |
38 | wxr.addAuthor( {
39 | login: 'pento',
40 | } );
41 |
42 | wxr.addAuthor( {
43 | login: 'not-pento',
44 | } );
45 |
46 | wxr.addAuthor( {
47 | login: 'someone-else',
48 | } );
49 |
50 | const xml = await wxr.export();
51 |
52 | expect( xml ).toMatchSnapshot();
53 | } );
54 | } );
55 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/basic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXR_VERSION, WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Basic behaviour', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'Correct WXR version', () => {
17 | expect( WXR_VERSION ).toEqual( '1.3' );
18 | } );
19 |
20 | test( 'WXRDriver class is defined', () => {
21 | expect( typeof WXRDriver ).toBe( 'function' );
22 | } );
23 |
24 | test( 'Creates an empty file', async () => {
25 | const wxr = new WXRDriver();
26 | await wxr.connect();
27 |
28 | const xml = await wxr.export();
29 |
30 | expect( xml ).toMatchSnapshot();
31 | } );
32 | } );
33 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/comments.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Comments', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | const postId = await wxr.addPost( {
21 | title: 'A Post Title',
22 | } );
23 |
24 | wxr.addComment( postId, {
25 | id: 1234,
26 | post_id: 5678, // This will not be written.
27 | author: 'pento',
28 | author_email: 'notme@wordpress.org',
29 | author_url: 'https://pento.net',
30 | author_IP: '127.0.0.1',
31 | date: '1970-01-01T00:00:10.000Z',
32 | date_gmt: '1970-01-01T00:00:11.000Z',
33 | content: 'This is good content. Plz subscribe me.',
34 | approved: 'trash',
35 | type: 'pingback',
36 | user_id: 5,
37 | meta: [ { key: 'who?', value: 'what?' } ],
38 | } );
39 |
40 | const xml = await wxr.export();
41 |
42 | expect( xml ).toMatchSnapshot();
43 | } );
44 | } );
45 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/objects.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Objects', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'Object ID increments', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | const first = await wxr.addObject( 'contact-form', {
21 | email: 'gary@pento.net',
22 | fields: [],
23 | } );
24 |
25 | const second = await wxr.addObject( 'contact-form', {
26 | email: 'foo@bar.com',
27 | fields: [],
28 | } );
29 |
30 | expect( first ).toEqual( 1 );
31 | expect( second ).toEqual( 2 );
32 |
33 | const xml = await wxr.export();
34 |
35 | expect( xml ).toMatchSnapshot();
36 | } );
37 | } );
38 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/posts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Posts', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addCategory( {
21 | slug: 'some-cat',
22 | name: 'Some Category',
23 | } );
24 |
25 | wxr.addTag( {
26 | slug: 'some-tag',
27 | name: 'Some Tag',
28 | } );
29 |
30 | wxr.addTerm( {
31 | taxonomy: 'my-tax',
32 | slug: 'some-term',
33 | name: 'Some Term',
34 | } );
35 |
36 | wxr.addPost( {
37 | id: 1,
38 | title: 'A Post Title',
39 | link: 'https://wordpress.org/',
40 | date: '1970-01-01T00:00:01.000Z',
41 | author: 'pento',
42 | guid: 'should-be-a-proper-guid',
43 | description: 'This will not be written', // Non-writeable field.
44 | content: 'The post content',
45 | excerpt: 'A brief excerpt from the content',
46 | postDate: '1970-01-01T00:00:02.000Z',
47 | date_gmt: '1970-01-01T00:00:03.000Z',
48 | modified: '1970-01-01T00:00:04.000Z',
49 | modified_gmt: '1970-01-01T00:00:05.000Z',
50 | comment_status: 'closed',
51 | ping_status: 'open',
52 | status: 'draft',
53 | menu_order: 1,
54 | type: 'post',
55 | password: 'a-strong-password',
56 | sticky: true,
57 | attachment_url: 'https://make.wordpress.org', // Won't be written for this post type.
58 | terms: [
59 | {
60 | type: 'category',
61 | slug: 'some-cat',
62 | name: 'Some Category',
63 | },
64 | {
65 | type: 'tag',
66 | slug: 'some-tag',
67 | name: 'Some Tag',
68 | },
69 | {
70 | type: 'my-tax',
71 | slug: 'some-term',
72 | name: 'Some Term',
73 | },
74 | ],
75 | meta: [ { key: 'talk about', value: 'pop music' } ],
76 | } );
77 |
78 | const xml = await wxr.export();
79 |
80 | expect( xml ).toMatchSnapshot();
81 | } );
82 |
83 | test( 'attachment_url is only included for the attachment post type', async () => {
84 | const wxr = new WXRDriver();
85 | await wxr.connect();
86 |
87 | wxr.addPost( {
88 | title: 'A Page Title',
89 | type: 'page',
90 | attachment_url: 'https://make.wordpress.org', // Won't be written for this post type.
91 | } );
92 |
93 | wxr.addPost( {
94 | title: 'An Attachment Title',
95 | type: 'attachment',
96 | attachment_url: 'https://make.wordpress.org',
97 | } );
98 |
99 | const xml = await wxr.export();
100 |
101 | expect( xml ).toMatchSnapshot();
102 | } );
103 | } );
104 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/sitemeta.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Site Meta', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.setSiteMeta( {
21 | title: 'A Title',
22 | link: 'https://wordpress.org/',
23 | description: 'A description',
24 | language: 'art-x-emoji',
25 | wxrVersion: '1.2.3.4.5', // Should not be writable, test with a fake value.
26 | siteUrl: 'https://make.wordpress.org/',
27 | blogUrl: 'https://wordpress.org/news/',
28 | } );
29 |
30 | const xml = await wxr.export();
31 |
32 | const cleanedXml = xml.replace(
33 | /.*?<\/pubDate>/,
34 | ' '
35 | );
36 |
37 | expect( cleanedXml ).toMatchSnapshot();
38 | } );
39 |
40 | test( 'siteUrl and BlogUrl copy from link', async () => {
41 | const wxr = new WXRDriver();
42 | await wxr.connect();
43 |
44 | wxr.setSiteMeta( {
45 | link: 'https://wordpress.org/',
46 | } );
47 |
48 | const xml = await wxr.export();
49 |
50 | const cleanedXml = xml.replace(
51 | /.*?<\/pubDate>/,
52 | ' '
53 | );
54 |
55 | expect( cleanedXml ).toMatchSnapshot();
56 | } );
57 | } );
58 |
--------------------------------------------------------------------------------
/packages/wxr/src/1.3/test/terms.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const { WXRDriver } = require( '../index.js' );
10 |
11 | describe( 'Terms and Taxonomies', () => {
12 | beforeEach( () => {
13 | window.indexedDB = new FDBFactory();
14 | } );
15 |
16 | test( 'All Category fields are written as expected', async () => {
17 | const wxr = new WXRDriver();
18 | await wxr.connect();
19 |
20 | wxr.addCategory( {
21 | id: 1,
22 | slug: 'some-cat',
23 | name: 'Some Category',
24 | description: 'A category of things.',
25 | meta: [
26 | { key: 'a-key', value: 'a-value' },
27 | { key: 'another-key', value: 'another-value' },
28 | ],
29 | } );
30 |
31 | wxr.addCategory( {
32 | id: 2,
33 | slug: 'some-other-cat',
34 | parent: 'some-cat',
35 | name: 'Some Other Category',
36 | description: 'A different category of things.',
37 | } );
38 |
39 | const xml = await wxr.export();
40 |
41 | expect( xml ).toMatchSnapshot();
42 | } );
43 |
44 | test( 'All Tag fields are written as expected', async () => {
45 | const wxr = new WXRDriver();
46 | await wxr.connect();
47 |
48 | wxr.addTag( {
49 | id: 1,
50 | slug: 'some-tag',
51 | name: 'Some Tag',
52 | description: 'A tag for things.',
53 | meta: [
54 | { key: 'a-key', value: 'a-value' },
55 | { key: 'another-key', value: 'another-value' },
56 | ],
57 | } );
58 |
59 | wxr.addTag( {
60 | id: 2,
61 | slug: 'some-other-tag',
62 | name: 'Some Other Tag',
63 | description: 'A different tag for things.',
64 | } );
65 |
66 | const xml = await wxr.export();
67 |
68 | expect( xml ).toMatchSnapshot();
69 | } );
70 |
71 | test( 'All custom taxonomy fields are written as expected', async () => {
72 | const wxr = new WXRDriver();
73 | await wxr.connect();
74 |
75 | wxr.addTerm( {
76 | id: 1,
77 | taxonomy: 'my-tax',
78 | slug: 'some-term',
79 | name: 'Some Term',
80 | description: 'A Term in a Taxonomy of things.',
81 | meta: [
82 | { key: 'a-key', value: 'a-value' },
83 | { key: 'another-key', value: 'another-value' },
84 | ],
85 | } );
86 |
87 | wxr.addTerm( {
88 | id: 2,
89 | taxonomy: 'my-tax',
90 | slug: 'some-other-term',
91 | name: 'Some Other Term',
92 | description: 'A different Term in the same Taxonomy of things.',
93 | } );
94 |
95 | wxr.addTerm( {
96 | id: 3,
97 | taxonomy: 'my-other-tax',
98 | slug: 'some-term',
99 | name: 'Some Term',
100 | description: 'The same Term in a different Taxonomy of things.',
101 | } );
102 |
103 | const xml = await wxr.export();
104 |
105 | expect( xml ).toMatchSnapshot();
106 | } );
107 | } );
108 |
--------------------------------------------------------------------------------
/packages/wxr/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal dependencies
3 | */
4 | const { WXRDriver: WXR12Driver } = require( './1.2' );
5 | const { WXRDriver: WXR13Driver } = require( './1.3' );
6 |
7 | const SUPPORTED_VERSIONS = [ '1.2', '1.3' ];
8 |
9 | const getWXRDriver = async ( wxrVersion, reset = false ) => {
10 | if ( ! SUPPORTED_VERSIONS.includes( wxrVersion ) ) {
11 | return;
12 | }
13 |
14 | let driver;
15 |
16 | switch ( wxrVersion ) {
17 | case '1.2':
18 | driver = new WXR12Driver();
19 | break;
20 | case '1.3':
21 | driver = new WXR13Driver();
22 | break;
23 | }
24 |
25 | await driver.connect( { reset } );
26 |
27 | return driver;
28 | };
29 |
30 | module.exports = {
31 | getWXRDriver,
32 | SUPPORTED_VERSIONS,
33 | };
34 |
--------------------------------------------------------------------------------
/packages/wxr/src/test/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal dependencies
3 | */
4 | const { getWXRDriver, SUPPORTED_VERSIONS } = require( '../index.js' );
5 |
6 | describe( 'getWXRDriver', () => {
7 | test( 'function is defined', () => {
8 | expect( typeof getWXRDriver ).toBe( 'function' );
9 | } );
10 |
11 | test( 'SUPPORTED_VERSIONS is an array', () => {
12 | expect( Array.isArray( SUPPORTED_VERSIONS ) ).toBeTruthy();
13 | } );
14 |
15 | test( 'returns something for all SUPPORTED_VERSIONS', async () => {
16 | for ( const version of SUPPORTED_VERSIONS ) {
17 | const wxr = await getWXRDriver( version );
18 | expect( typeof wxr ).toBe( 'object' );
19 | }
20 | } );
21 |
22 | test( 'returns nothing for invalid version', async () => {
23 | const fakeVersion = '1.2.3.4';
24 |
25 | expect( SUPPORTED_VERSIONS ).not.toContain( fakeVersion );
26 |
27 | const wxr = await getWXRDriver( fakeVersion );
28 |
29 | expect( wxr ).toBeUndefined();
30 | } );
31 | } );
32 |
--------------------------------------------------------------------------------
/source/action.js:
--------------------------------------------------------------------------------
1 | const ReactDOM = require( 'react-dom' );
2 | const { Component, createInterpolateElement } = require( '@wordpress/element' );
3 | const { Spinner } = require( '@wordpress/components' );
4 | const { __, setLocaleData } = require( '@wordpress/i18n' );
5 |
6 | const uiLanguage = browser.i18n.getUILanguage();
7 | if ( i18n[ uiLanguage ] ) {
8 | setLocaleData( i18n[ uiLanguage ], 'default' );
9 | }
10 |
11 | class App extends Component {
12 | constructor() {
13 | super( ...arguments );
14 |
15 | this.startWixExport = this.startWixExport.bind( this );
16 |
17 | this.state = {
18 | page: '',
19 | exportStatus: {},
20 | };
21 | }
22 |
23 | componentDidMount() {
24 | browser.tabs
25 | .query( { active: true, currentWindow: true } )
26 | .then( async ( tabs ) => {
27 | const currentTabUrl = tabs[ 0 ].url;
28 |
29 | if (
30 | currentTabUrl.startsWith(
31 | 'https://manage.wix.com/dashboard/'
32 | )
33 | ) {
34 | // Get the config that's stored in the background.js process.
35 | const apps = await browser.runtime.sendMessage( {
36 | type: 'get_wix_apps',
37 | } );
38 |
39 | if ( apps && apps.length > 0 ) {
40 | this.setState( {
41 | page: 'wix-site',
42 | apps,
43 | exportStatus: apps.reduce( ( status, app ) => {
44 | status[ app.id ] = 'not-started';
45 | return status;
46 | }, {} ),
47 | } );
48 | } else {
49 | this.setState( { page: 'wix-site-no-apps' } );
50 | }
51 | }
52 | } );
53 |
54 | browser.runtime.onMessage.addListener( ( message ) => {
55 | switch ( message.type ) {
56 | case 'export_progress_update':
57 | const newExportStatus = { ...this.state.exportStatus };
58 | newExportStatus[ message.data.id ] = message.data.state;
59 | this.setState( { exportStatus: newExportStatus } );
60 | break;
61 | }
62 | } );
63 | }
64 |
65 | startWixExport() {
66 | browser.runtime.sendMessage( {
67 | type: 'start_wix_export',
68 | } );
69 | }
70 |
71 | render() {
72 | const { page, exportStatus, apps } = this.state;
73 |
74 | const exportInProgress = Object.values( exportStatus ).includes(
75 | 'in-progress'
76 | );
77 |
78 | const exportFinished = Object.values( exportStatus ).reduce(
79 | ( finished, status ) => finished && status === 'done',
80 | true
81 | );
82 |
83 | switch ( page ) {
84 | case 'wix-site':
85 | return (
86 |
87 |
88 | { __(
89 | 'You can export your site now! The following Wix apps will be exported:'
90 | ) }
91 |
92 |
93 | { apps.map( ( app ) => {
94 | return (
95 |
96 | { app.name }{ ' ' }
97 | { exportStatus[ app.id ] ===
98 | 'in-progress' ? (
99 |
100 | ) : (
101 | ''
102 | ) }
103 | { exportStatus[ app.id ] === 'done'
104 | ? '✅'
105 | : '' }
106 |
107 | );
108 | } ) }
109 |
110 |
111 | { __(
112 | "If you're ready to export this site, click the export button."
113 | ) }
114 |
115 |
116 | { exportFinished ? (
117 | __( 'Export finished.' )
118 | ) : (
119 |
124 | { __( 'Export' ) }
125 |
126 | ) }
127 |
128 |
129 | );
130 |
131 | case 'wix-site-no-apps':
132 | return (
133 |
134 |
135 | { __(
136 | 'We were unable to retrieve a list of the installed Wix apps.'
137 | ) }
138 |
139 |
140 |
{ __( 'Please refresh the page to try again.' ) }
141 |
142 | );
143 |
144 | default:
145 | return (
146 |
147 |
148 | { createInterpolateElement(
149 | __(
150 | "Welcome to WordPress' Free (as in Speech) extension!"
151 | ),
152 | { em: }
153 | ) }
154 |
155 |
156 | { __(
157 | 'This extension helps you get control of your content back from services that otherwise prevent you from choosing to export the content that you own.'
158 | ) }
159 |
160 |
161 | { createInterpolateElement(
162 | __(
163 | 'Currently, Wix is supported.'
164 | ),
165 | { strong: }
166 | ) }
167 |
168 |
169 | { __(
170 | 'To get started, login to your account on Wix.com, then click this button again!'
171 | ) }
172 |
173 |
174 | );
175 | }
176 | }
177 | }
178 |
179 | ReactDOM.render( , document.getElementById( 'root' ) );
180 |
--------------------------------------------------------------------------------
/source/background.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal dependencies
3 | */
4 | const { getInstalledApps, startExport } = require( './services' );
5 | const { registerBlocks } = require( 'site-parsers' ).utils;
6 |
7 | /**
8 | * Store the wix config in memory, so that it's available whenever it's needed,
9 | * but will be lost when the background process dies.
10 | */
11 | let wixConfig;
12 |
13 | const downloadObjects = new Map();
14 |
15 | /**
16 | * Listen for messages coming from other parts of the extension.
17 | */
18 | browser.runtime.onMessage.addListener( async ( message ) => {
19 | switch ( message.type ) {
20 | case 'save_wix_config':
21 | // Save the config data sent from content.js.
22 | wixConfig = message.data;
23 | break;
24 | case 'get_wix_config':
25 | // Return the config data requested by other parts of the extension.
26 | return new Promise( ( resolve ) => resolve( wixConfig ) );
27 | case 'get_wix_apps':
28 | return new Promise( ( resolve ) => {
29 | if ( wixConfig ) {
30 | resolve( getInstalledApps( 'wix', wixConfig ) );
31 | } else {
32 | resolve( false );
33 | }
34 | } );
35 | case 'start_wix_export':
36 | // Start the export.
37 | const wxr = await startExport( 'wix', wixConfig, ( status ) =>
38 | browser.runtime.sendMessage( {
39 | type: 'export_progress_update',
40 | data: status,
41 | } )
42 | );
43 |
44 | // Present the export as a download.
45 | const url = URL.createObjectURL(
46 | new Blob( [ await wxr.export() ], { type: 'text/xml+wxr' } )
47 | );
48 |
49 | const id = await browser.downloads.download( {
50 | url,
51 | filename: 'wix-export.wxr',
52 | } );
53 |
54 | downloadObjects.set( id, url );
55 |
56 | break;
57 | }
58 | } );
59 |
60 | browser.downloads.onChanged.addListener( ( delta ) => {
61 | if ( delta.state && delta.state.current === 'complete' ) {
62 | if ( downloadObjects.has( delta.id ) ) {
63 | URL.revokeObjectURL( downloadObjects.get( delta.id ) );
64 | downloadObjects.delete( delta.id );
65 | }
66 | }
67 | } );
68 |
69 | // Register the blocks we support for output.
70 | registerBlocks();
71 |
--------------------------------------------------------------------------------
/source/content.js:
--------------------------------------------------------------------------------
1 | // Listen for the message to save the config.
2 | window.addEventListener( 'message', ( e ) => {
3 | switch ( e.data.type ) {
4 | case 'save_wix_config':
5 | browser.runtime.sendMessage( e.data );
6 | break;
7 | }
8 | } );
9 |
10 | const code = `
11 | ( () => {
12 | let tries = 0;
13 | let realPostMessage;
14 |
15 | // window.__INITIAL_STATE__ is set when the page is loaded, but is subsequently deleted once
16 | // it's been loaded into Wix's redux store. In order to intercept it, we load this script as early
17 | // as we possibly can.
18 | //
19 | // As this script is loaded extremely early, it may be run before the window.__INITIAL_STATE__
20 | // object has been set. To allow for this, we can check every 5 ms, so we get it as soon as it's
21 | // defined.
22 | const intervalId = setInterval( () => {
23 | // If the state didn't become available in the first 500ms, it probably isn't there.
24 | // Kill this timer, since it would otherwise be a bit of a performance drag.
25 | if ( tries > 100 ) {
26 | clearInterval( intervalId );
27 | return;
28 | }
29 |
30 | if ( ! window ) {
31 | return;
32 | }
33 |
34 | // Grab a copy of window.postMessage as soon as it's available, to ensure we're using
35 | // an original version.
36 | if ( ! realPostMessage && window.postMessage ) {
37 | realPostMessage = window.postMessage;
38 | }
39 |
40 | tries++;
41 |
42 | if ( window.__INITIAL_STATE__ && window.__MEDIA_TOKEN__ ) {
43 | // To communicate back to content.js, use window.postMessage().
44 | realPostMessage( {
45 | type: 'save_wix_config',
46 | data: {
47 | initialState: window.__INITIAL_STATE__,
48 | mediaToken: window.__MEDIA_TOKEN__,
49 | },
50 | }, '*' );
51 |
52 | clearInterval( intervalId )
53 | }
54 | }, 5 );
55 | } )();
56 | `;
57 |
58 | // Add this script to the DOM, so it's executed in the context of the real page.
59 | const script = document.createElement( 'script' );
60 | script.textContent = code;
61 | document.documentElement.appendChild( script );
62 |
63 | // Remove the script from the DOM once we're done with it.
64 | setTimeout( () => document.documentElement.removeChild( script ), 500 );
65 |
--------------------------------------------------------------------------------
/source/services/index.js:
--------------------------------------------------------------------------------
1 | const {
2 | startExport: startWixExport,
3 | getInstalledApps: getInstalledWixApps,
4 | } = require( './wix' );
5 |
6 | /**
7 | * Start the export for the given service.
8 | *
9 | * @param {string} service The service name. 'wix' is the only valid value at this time.
10 | * @param {Object} config Any config data that needs to be passed to the exporter.
11 | * @param {Function} statusReport A callback to show a message in the popup.
12 | */
13 | const startExport = ( service, config, statusReport ) => {
14 | if ( service === 'wix' ) {
15 | return startWixExport( config, statusReport );
16 | }
17 | };
18 |
19 | /**
20 | * Get a list of the installed apps/plugins/etc for the given service.
21 | *
22 | * @param {string} service The service name. 'wix' is the only valid value at this time.
23 | * @param {Object} config Any config data that needs to be passed to the exporter.
24 | * @return {Array} The instapped apps.
25 | */
26 | const getInstalledApps = ( service, config ) => {
27 | if ( service === 'wix' ) {
28 | return getInstalledWixApps( config );
29 | }
30 | };
31 |
32 | module.exports = {
33 | startExport,
34 | getInstalledApps,
35 | };
36 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/easy-blog-app/index.js:
--------------------------------------------------------------------------------
1 | const cheerio = require( 'cheerio' );
2 | const dayjs = require( 'dayjs' );
3 | const utc = require( 'dayjs/plugin/utc' );
4 | const { pasteHandler, serialize } = require( '@wordpress/blocks' );
5 |
6 | dayjs.extend( utc );
7 |
8 | const parseDateString = ( str ) => {
9 | const lowerCaseStr = str.toLowerCase().trim();
10 |
11 | const today = dayjs.utc().startOf( 'day' );
12 |
13 | switch ( lowerCaseStr ) {
14 | case 'today':
15 | return today.format();
16 | case 'yesterday':
17 | return today.subtract( 1, 'day' ).format();
18 | }
19 |
20 | const parsedRelativeDate = lowerCaseStr.match( /^last ([a-z]+)$/ );
21 | if ( parsedRelativeDate ) {
22 | const thisDay = today.day();
23 | const parsedDay = dayjs.en.weekdays.findIndex(
24 | ( weekday ) => weekday.toLowerCase() === parsedRelativeDate[ 1 ]
25 | );
26 | const isLastWeek = parsedDay >= thisDay;
27 | return today.day( parsedDay - ( isLastWeek ? 7 : 0 ) ).format();
28 | }
29 |
30 | const parsedProperDate = dayjs.utc( lowerCaseStr + ' UTC' );
31 | if ( parsedProperDate.isValid() ) {
32 | return parsedProperDate.format();
33 | }
34 |
35 | return 0;
36 | };
37 |
38 | module.exports = {
39 | /**
40 | * A name for the app, displayed to the user.
41 | */
42 | appName: 'EasyBlog',
43 |
44 | /**
45 | * The Wix application definition ID.
46 | */
47 | appDefinitionId: '13d7e48f-0739-6252-8018-457a75beae4b',
48 |
49 | /**
50 | * This function will be called once the extraction process has started.
51 | *
52 | * @param {Object} config The app-specific config extracted from the Wix page.
53 | */
54 | extract: async ( config ) => {
55 | const url = new URL(
56 | 'https://easy-blog-production.myeasyappsserver.com/'
57 | );
58 | url.searchParams.set( 'instance', config.instance );
59 | // We are here switching to a premium theme that supports tags.
60 | url.searchParams.set( 'theme', 'Photogram' );
61 | return await window
62 | .fetch( url )
63 | .then( ( result ) => result.text() )
64 | .then( ( html ) => {
65 | const $ = cheerio.load( html );
66 | return $( '.blog-post' )
67 | .map( ( i, post ) => {
68 | const $post = $( post );
69 | return {
70 | title:
71 | $post.find( '.post-title' ).text().trim() || '',
72 | content:
73 | $post.find( '.post-text' ).html().trim() || '',
74 | images:
75 | $post
76 | .find( '.post-photo' )
77 | .map( ( index, el ) =>
78 | $( el ).attr( 'src' )
79 | )
80 | .get() || [],
81 | video:
82 | $post
83 | .find( 'iframe.embed-responsive-item' )
84 | .attr( 'src' ) || null,
85 | tags: $post
86 | .find( '.tags .tag' )
87 | .map( ( index, el ) =>
88 | $( el ).text().trim().replace( /^\#/, '' )
89 | )
90 | .get(),
91 | date: parseDateString(
92 | $post
93 | .find( '.post-meta li:first-child' )
94 | .text()
95 | .trim()
96 | ),
97 | };
98 | } )
99 | .get();
100 | } );
101 | },
102 |
103 | /**
104 | * This function is called once we're ready to start generating the WXR file.
105 | *
106 | * @param {Object} data The data blob returned by the extract() function.
107 | * @param {Object} wxr The WXR encoder.
108 | */
109 | save: async ( data, wxr ) => {
110 | data.forEach( ( post ) => {
111 | const postTags = post.tags.map( ( postTag ) => ( {
112 | type: 'tag',
113 | slug: postTag,
114 | name: postTag,
115 | } ) );
116 |
117 | wxr.addPost( {
118 | date: post.date,
119 | title: post.title,
120 | content: pasteHandler( { HTML: post.content, mode: 'BLOCKS' } )
121 | .filter( ( blockContent ) => blockContent !== false )
122 | .map( ( wpBlock ) => serialize( wpBlock ) )
123 | .join( '\n\n' ),
124 | status: 'publish',
125 | sticky: 0,
126 | type: 'post',
127 | comment_status: 'closed',
128 | terms: [ ...postTags ],
129 | } );
130 | } );
131 | },
132 | };
133 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/index.js:
--------------------------------------------------------------------------------
1 | const communitiesBlogApp = require( './communities-blog-app' );
2 | const easyBlogApp = require( './easy-blog-app' );
3 | const mediaManagerSettings = require( './media-manager' );
4 | const staticPages = require( './static-pages' );
5 |
6 | /**
7 | * An array of the defined extractors.
8 | */
9 | module.exports = [
10 | communitiesBlogApp,
11 | mediaManagerSettings,
12 | easyBlogApp,
13 | staticPages,
14 | ];
15 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/media-manager/index.js:
--------------------------------------------------------------------------------
1 | const IdFactory = require( 'site-parsers' ).utils.IdFactory;
2 |
3 | module.exports = {
4 | /**
5 | * A name for the app, displayed to the user.
6 | */
7 | appName: 'Media',
8 |
9 | /**
10 | * The media manager doesn't have an app definition ID, we can use a fake one here.
11 | */
12 | appDefinitionId: 'media-manager',
13 |
14 | /**
15 | * This function will be called once the extraction process has started.
16 | *
17 | * @param {string} mediaToken The site media token.
18 | */
19 | extract: async ( mediaToken ) => {
20 | const checkFolders = [ 'media-root' ];
21 | const knownFolders = [];
22 |
23 | const fileListPromises = [];
24 |
25 | while ( checkFolders.length > 0 ) {
26 | const currentFolder = checkFolders.pop();
27 | knownFolders.push( currentFolder );
28 |
29 | fileListPromises.push(
30 | window
31 | .fetch(
32 | `https://files.wix.com/go/site/media/files/list?site_token=${ mediaToken }&page_size=50&parent_folder_id=${ currentFolder }&media_type=picture,video,music,document,shape,site_icon,archive,swf`,
33 | {
34 | credentials: 'include',
35 | }
36 | )
37 | .then( ( result ) => result.json() )
38 | .catch( () => {
39 | return { files: [] };
40 | } )
41 | .then( ( fileData ) => fileData.files )
42 | );
43 |
44 | const folders = await window
45 | .fetch(
46 | `https://files.wix.com/go/site/media/folders/list?site_token=${ mediaToken }&page_size=50&parent_folder_id=${ currentFolder }`,
47 | {
48 | credentials: 'include',
49 | }
50 | )
51 | .then( ( result ) => result.json() )
52 | .catch( () => {
53 | return { folders: [] };
54 | } );
55 |
56 | if ( Array.isArray( folders.folders ) ) {
57 | folders.folders.forEach( ( folder ) => {
58 | checkFolders.push( folder.folder_id );
59 | } );
60 | }
61 | }
62 |
63 | const fileList = await Promise.all( fileListPromises );
64 |
65 | // If a folder is empty, it will result in a 'null' file in the list. Remove them.
66 | return fileList.flat().filter( ( file ) => file !== null );
67 | },
68 |
69 | /**
70 | * This function is called once we're ready to start generating the WXR file.
71 | *
72 | * @param {Object} files The file list returned by the extract() function.
73 | * @param {Object} wxr The WXR encoder.
74 | */
75 | save: async ( files, wxr ) => {
76 | files.forEach( ( file ) => {
77 | if ( file.media_type === 'video' ) {
78 | wxr.addPost( {
79 | id: IdFactory.get( file.file_output.video[ 0 ].url ),
80 | guid: `https://video.wixstatic.com/${ file.file_output.video[ 0 ].url }`,
81 | date: file.created_ts * 1000,
82 | title: file.original_file_name,
83 | type: 'attachment',
84 | attachment_url: `https://video.wixstatic.com/${ file.file_output.video[ 0 ].url }`,
85 | } );
86 | } else {
87 | const filename = file.original_file_name || file.file_url;
88 | if ( IdFactory.exists( filename ) ) {
89 | return;
90 | }
91 |
92 | wxr.addPost( {
93 | id: IdFactory.get( filename ),
94 | guid: `https://static.wixstatic.com/${ file.file_url }`,
95 | date: file.created_ts * 1000,
96 | title: file.original_file_name,
97 | type: 'attachment',
98 | attachment_url: `https://static.wixstatic.com/${ file.file_url }`,
99 | } );
100 | }
101 | } );
102 | },
103 | };
104 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/site-meta-app/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * A name for the app, displayed to the user.
4 | */
5 | appName: 'Site Meta',
6 |
7 | /**
8 | * The Wix application definition ID.
9 | */
10 | appDefinitionId: '22bef345-3c5b-4c18-b782-74d4085112ff',
11 |
12 | /**
13 | * This function will be called once the extraction process has started.
14 | *
15 | * @param {Object} config The app-specific config extracted from the Wix page.
16 | */
17 | extract: async ( config ) => {
18 | return await window
19 | .fetch(
20 | 'https://www.wix.com/_serverless/dashboard-site-details/widget-data',
21 | {
22 | credentials: 'include',
23 | headers: { Authorization: config.instance },
24 | }
25 | )
26 | .then( ( result ) => result.json() )
27 | .catch( () => {
28 | return { quickActionsData: {} };
29 | } );
30 | },
31 |
32 | /**
33 | * This function is called once we're ready to start generating the WXR file.
34 | *
35 | * @param {Object} data The data blob returned by the extract() function.
36 | * @param {Object} wxr The WXR encoder.
37 | */
38 | save: async ( data, wxr ) => {
39 | wxr.setSiteMeta( {
40 | title: data.quickActionsData.displayName,
41 | link: data.quickActionsData.viewUrl,
42 | } );
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/static-pages/data.js:
--------------------------------------------------------------------------------
1 | const cheerio = require( 'cheerio' );
2 | const IdFactory = require( 'site-parsers' ).utils.IdFactory;
3 |
4 | const extractConfigData = ( html ) => {
5 | const configData = {};
6 |
7 | // The configuration data is embedded in script tags in the HTML.
8 | const $ = cheerio.load( html );
9 | $( 'script' ).each( ( idx, scriptTag ) => {
10 | const currentTag = $( scriptTag );
11 |
12 | if ( currentTag.attr( 'src' ) !== undefined ) {
13 | return;
14 | }
15 |
16 | const vars = currentTag.html().split( /\s*var\s/ );
17 | const metaConfigurationRegExp = /^(siteHeader|editorModel|serviceTopology)\s*=\s*(.*)$/;
18 |
19 | for ( let i = 0; i < vars.length; i++ ) {
20 | let match;
21 | if ( ( match = metaConfigurationRegExp.exec( vars[ i ] ) ) ) {
22 | const metaName = match[ 1 ];
23 | const metaConfigRawJSON = match[ 2 ].replace( /[;\s]+$/, '' );
24 | configData[ metaName ] = JSON.parse( metaConfigRawJSON );
25 | }
26 | }
27 | } );
28 |
29 | return configData;
30 | };
31 |
32 | const fetchPageJson = ( topology, editorUrl ) => ( page ) => {
33 | return window
34 | .fetch( topology.replace( '{filename}', page.pageJsonFileName ), {
35 | referrer: editorUrl,
36 | mode: 'same-origin',
37 | headers: {
38 | Accept:
39 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
40 | 'Upgrade-Insecure-Requests': 1,
41 | },
42 | } )
43 | .then( ( result ) => result.json() )
44 | .catch( () => {
45 | return {
46 | data: { document_data: {} },
47 | structure: { components: [] },
48 | };
49 | } )
50 | .then( ( json ) => {
51 | page.postId = IdFactory.get( page.pageId );
52 | page.config = json;
53 | return page;
54 | } )
55 | .catch( () => {
56 | return {
57 | data: { document_data: {} },
58 | structure: { components: [] },
59 | };
60 | } );
61 | };
62 |
63 | module.exports = { extractConfigData, fetchPageJson };
64 |
--------------------------------------------------------------------------------
/source/services/wix/extractors/static-pages/index.js:
--------------------------------------------------------------------------------
1 | const slug = require( 'slugify' );
2 | const { v4: uuidv4 } = require( 'uuid' );
3 | const { extractConfigData, fetchPageJson } = require( './data.js' );
4 | const { staticPagesParserWix } = require( 'site-parsers' );
5 |
6 | module.exports = {
7 | /**
8 | * A name for the app, displayed to the user.
9 | */
10 | appName: 'Static Pages',
11 |
12 | /**
13 | * The Wix application definition ID.
14 | */
15 | appDefinitionId: 'static-pages',
16 |
17 | /**
18 | * This function will be called once the extraction process has started.
19 | *
20 | * @param {Object} config The app-specific config extracted from the Wix page.
21 | */
22 | extract: async ( config ) => {
23 | const data = {
24 | pages: [],
25 | menus: [],
26 | attachments: {},
27 | objects: [],
28 | };
29 |
30 | const url = new URL(
31 | 'https://manage.wix.com/editor/' + config.metaSiteId
32 | );
33 | url.searchParams.set( 'editorSessionId', uuidv4() );
34 | url.searchParams.set( 'referralInfo', 'my-account' );
35 |
36 | let editorUrl;
37 | const metaData = await window
38 | .fetch( url, {
39 | credentials: 'include',
40 | referrer:
41 | 'https://manage.wix.com/dashboard/' +
42 | config.metaSiteId +
43 | '/home?referralInfo=my-sites',
44 | mode: 'same-origin',
45 | headers: {
46 | Accept:
47 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
48 | 'Upgrade-Insecure-Requests': 1,
49 | },
50 | } )
51 | .then( ( result ) => {
52 | editorUrl = result.url;
53 | return result.text();
54 | } )
55 | .then( extractConfigData );
56 |
57 | if (
58 | undefined === metaData.siteHeader ||
59 | undefined === metaData.siteHeader.pageIdList
60 | ) {
61 | return data;
62 | }
63 |
64 | // This is used to construct a URL from the filename, see fetchPageJson().
65 | const topology = metaData.siteHeader.pageIdList.topology[ 0 ];
66 |
67 | // The masterPage contains the id to object mapping as well as metadata like the site menu.
68 | const masterPage = await window
69 | .fetch(
70 | topology.replace(
71 | '{filename}',
72 | metaData.siteHeader.pageIdList.masterPageJsonFileName
73 | ),
74 | {
75 | credentials: 'include',
76 | referrer: editorUrl,
77 | mode: 'same-origin',
78 | headers: {
79 | Accept:
80 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
81 | 'Upgrade-Insecure-Requests': 1,
82 | },
83 | }
84 | )
85 | .then( ( result ) => result.json() )
86 | .catch( () => {} );
87 |
88 | // Fetch the pages as Json.
89 | const pages = await Promise.all(
90 | metaData.siteHeader.pageIdList.pages.map(
91 | fetchPageJson( topology, editorUrl )
92 | )
93 | );
94 |
95 | return staticPagesParserWix( metaData, masterPage, pages, {
96 | fetch: window.fetch,
97 | } );
98 | },
99 |
100 | /**
101 | * This function is called once we're ready to start generating the WXR file.
102 | *
103 | * @param {Object} data The data blob returned by the extract() function.
104 | * @param {Object} wxr The WXR encoder.
105 | */
106 | save: async ( data, wxr ) => {
107 | data.objects.forEach( ( obj ) => {
108 | wxr.addObject( obj.type, obj.data );
109 | } );
110 | data.pages.forEach( ( post ) => {
111 | const terms = post.terms || [];
112 |
113 | terms.forEach( ( term ) => {
114 | term.taxonomy = term.type;
115 | wxr.addTerm( term );
116 | } );
117 |
118 | wxr.addPost( {
119 | id: post.postId,
120 | title: post.title,
121 | name: slug( post.title ),
122 | content: post.content,
123 | status: post.hidePage ? 'private' : 'publish',
124 | sticky: 0,
125 | type: post.postType || 'page',
126 | comment_status: 'closed',
127 | meta: Object.entries( post.meta || {} ).map( ( meta ) => ( {
128 | key: meta[ 0 ],
129 | value:
130 | typeof meta[ 1 ] === 'object'
131 | ? JSON.stringify( meta[ 1 ] )
132 | : meta[ 1 ],
133 | } ) ),
134 | terms,
135 | } );
136 | } );
137 | data.menus.forEach( ( post ) => {
138 | if ( 'pending' === post.status ) {
139 | // Skip hidden menu entries.
140 | return;
141 | }
142 | post.terms.forEach( ( term ) => {
143 | term.taxonomy = term.type;
144 | wxr.addTerm( term );
145 | } );
146 | wxr.addPost( {
147 | id: post.postId,
148 | title: post.title,
149 | type: post.type,
150 | terms: post.terms,
151 | parent: post.parent,
152 | status: post.status,
153 | menu_order: post.menuOrder,
154 | meta: Object.entries( post.meta ).map( ( meta ) => ( {
155 | key: meta[ 0 ],
156 | value: meta[ 1 ],
157 | } ) ),
158 | } );
159 | } );
160 | Object.values( data.attachments ).forEach( ( post ) => {
161 | wxr.addPost( post );
162 | } );
163 | },
164 | };
165 |
--------------------------------------------------------------------------------
/source/services/wix/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | const { getWXRDriver } = require( '@wordpress/wxr' );
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | const extractors = require( './extractors' );
10 | const siteMetaSettings = require( './extractors/site-meta-app' );
11 |
12 | /**
13 | * Returns an array of the installed apps.
14 | *
15 | * @param {Object} config The Wix config data.
16 | * @return {Array} A list of the installed apps.
17 | */
18 | const getInstalledApps = ( config ) => {
19 | const installed = [];
20 |
21 | [ siteMetaSettings ].concat( extractors ).forEach( ( extractor ) => {
22 | if ( getExtractorConfig( config, extractor.appDefinitionId ) ) {
23 | installed.push( {
24 | id: extractor.appDefinitionId,
25 | name: extractor.appName,
26 | } );
27 | }
28 | } );
29 |
30 | return installed;
31 | };
32 |
33 | /**
34 | * Return the config data for a given Wix app definition ID.
35 | *
36 | * @param {Object} config The Wix config data.
37 | * @param {string} appDefinitionId The Wix app definition ID.
38 | * @return {*} The config data for the given app, or false if no config data can be found.
39 | */
40 | const getExtractorConfig = ( config, appDefinitionId ) => {
41 | if ( appDefinitionId === 'media-manager' ) {
42 | return config.mediaToken;
43 | }
44 |
45 | if ( appDefinitionId === 'static-pages' ) {
46 | return {
47 | metaSiteId: config.initialState.siteMetaData.metaSiteId,
48 | };
49 | }
50 |
51 | return Object.values(
52 | ( config.initialState && config.initialState.embeddedServices ) || {}
53 | ).reduce( ( found, appConfig ) => {
54 | if ( found ) {
55 | return found;
56 | }
57 |
58 | if ( appConfig.appDefinitionId === appDefinitionId ) {
59 | return appConfig;
60 | }
61 |
62 | return false;
63 | }, false );
64 | };
65 |
66 | /**
67 | * Loop through all of the defined extractors, and run them over the content.
68 | *
69 | * @param {Object} config The Wix config data.
70 | * @param {Function} statusReport A callback to show a message in the popup.
71 | */
72 | const startExport = async ( config, statusReport ) => {
73 | const wxr = await getWXRDriver( '1.3', true );
74 |
75 | const extractData = ( fallbackConfig ) => async ( extractor ) => {
76 | // Grab the config data for this extractor.
77 | let extractorConfig = getExtractorConfig(
78 | config,
79 | extractor.appDefinitionId
80 | );
81 |
82 | // If we couldn't find any app config for this extractor, the app isn't enabled.
83 | if ( ! extractorConfig ) {
84 | extractorConfig = fallbackConfig;
85 | if ( ! extractorConfig ) {
86 | return;
87 | }
88 | }
89 | statusReport( {
90 | id: extractor.appDefinitionId,
91 | state: 'in-progress',
92 | } );
93 |
94 | // Run the extractor.
95 | const extractedData = await extractor.extract( extractorConfig );
96 |
97 | // Convert the extracted data to WXR.
98 | await extractor.save( extractedData, wxr );
99 | statusReport( {
100 | id: extractor.appDefinitionId,
101 | state: 'done',
102 | } );
103 |
104 | return extractedData;
105 | };
106 |
107 | // We need to extract the site meta first.
108 | const siteMeta = await extractData( { instance: null } )(
109 | siteMetaSettings
110 | );
111 | if ( siteMeta && siteMeta.quickActionsData ) {
112 | // Backfill the metadata when none available (e.g. in CLI).
113 | if ( ! config.initialState ) {
114 | config.initialState = {};
115 | }
116 | if ( ! config.initialState.siteMetaData ) {
117 | config.initialState.siteMetaData = {};
118 | }
119 | if ( ! config.initialState.siteMetaData.metaSiteId ) {
120 | config.initialState.siteMetaData.metaSiteId =
121 | siteMeta.quickActionsData.metaSiteId;
122 | }
123 | }
124 |
125 | await Promise.all(
126 | extractors.map( extractData( config.extractAll ? {} : null ) )
127 | );
128 |
129 | return wxr;
130 | };
131 |
132 | module.exports = {
133 | getInstalledApps,
134 | startExport,
135 | };
136 |
--------------------------------------------------------------------------------
/test/integration/wix/fetch-from-wix-har.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | const FDBFactory = require( 'fake-indexeddb/lib/FDBFactory' );
5 | const { registerBlocks } = require( 'site-parsers' ).utils;
6 | /**
7 | * Internal dependencies
8 | */
9 | const fs = require( 'fs' );
10 | const path = require( 'path' );
11 | const fetchFromHAR = require( 'fetch-from-har' );
12 | const getWXRFromWixHAR = require( '../../../bin/lib/get-wxr-from-wix-har' );
13 | const { startExport } = require( '../../../source/services/wix' );
14 |
15 | beforeEach( () => {
16 | window.indexedDB = new FDBFactory();
17 | require( '../../../packages/gutenberg-for-node/src/index' );
18 | registerBlocks();
19 | } );
20 |
21 | test.each( [
22 | [
23 | 'empty.har',
24 | {
25 | initialState: {
26 | embeddedServices: {},
27 | },
28 | extractAll: true,
29 | },
30 | false,
31 | false,
32 | ],
33 | [
34 | 'wix-basic.har',
35 | {
36 | initialState: {
37 | embeddedServices: [
38 | '14bcded7-0066-7c35-14d7-466cb3f09103',
39 | 'media-manager',
40 | '22bef345-3c5b-4c18-b782-74d4085112ff',
41 | ].map( ( appDefinitionId ) => {
42 | return { appDefinitionId };
43 | } ),
44 | },
45 | },
46 | false,
47 | false,
48 | ],
49 | [
50 | 'easyblog.har',
51 | {
52 | initialState: {
53 | siteMetaData: {
54 | metaSiteId: 'ae5cf16f-06cb-499f-9fcd-6b08ba634e04',
55 | },
56 | embeddedServices: [
57 | '13d7e48f-0739-6252-8018-457a75beae4b',
58 | ].map( ( appDefinitionId ) => {
59 | return { appDefinitionId };
60 | } ),
61 | },
62 | },
63 | false,
64 | true,
65 | ],
66 | [
67 | 'rockfield.har',
68 | {
69 | initialState: {
70 | embeddedServices: [ 'static-pages' ].map(
71 | ( appDefinitionId ) => {
72 | return { appDefinitionId };
73 | }
74 | ),
75 | },
76 | },
77 | false,
78 | true,
79 | ],
80 | [
81 | 'personal-website.har',
82 | {
83 | initialState: {
84 | embeddedServices: [ 'static-pages' ].map(
85 | ( appDefinitionId ) => {
86 | return { appDefinitionId };
87 | }
88 | ),
89 | },
90 | },
91 | false,
92 | true,
93 | ],
94 | ] )( 'wix: %s', async ( har, config, errorsExpected, logExpected ) => {
95 | const input = fs.readFileSync( path.join( __dirname, 'fixtures', har ) );
96 |
97 | function stripFirstPubDate( xml ) {
98 | return xml
99 | .toString()
100 | .replace( /.*?<\/pubDate>/, ' ' )
101 | .trim();
102 | }
103 |
104 | return getWXRFromWixHAR(
105 | fetchFromHAR,
106 | JSON.parse( input ),
107 | config,
108 | startExport,
109 | ( url, entry ) => entry
110 | )
111 | .then( ( wxrDriver ) => wxrDriver.export() )
112 | .then( ( xml ) => {
113 | expect( stripFirstPubDate( xml ) ).toMatchSnapshot();
114 |
115 | if ( logExpected ) {
116 | expect( console ).toHaveLogged();
117 | }
118 | if ( errorsExpected ) {
119 | expect( console ).toHaveErrored();
120 | }
121 | } );
122 | } );
123 |
--------------------------------------------------------------------------------
/test/integration/wix/fixtures/empty.har:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/test/setup-env.js:
--------------------------------------------------------------------------------
1 | global.TextEncoder = require( 'util' ).TextEncoder;
2 | global.TextDecoder = require( 'util' ).TextDecoder;
3 | require( 'gutenberg-for-node' );
4 |
--------------------------------------------------------------------------------
/test/web-ext-profile/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pento/free-as-in-speech/91db70bbd0cdcabe33f18d19ac2f99cd427b7a3d/test/web-ext-profile/.gitkeep
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | const path = require( 'path' );
5 | const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
6 | const { DefinePlugin } = require( 'webpack' );
7 | const TerserPlugin = require( 'terser-webpack-plugin' );
8 |
9 | const cliConfig = {
10 | stats: 'errors-only',
11 | entry: {
12 | 'fetch-from-har': './packages/fetch-from-har',
13 | 'site-parsers': './packages/site-parsers',
14 | 'gutenberg-for-node': './packages/gutenberg-for-node',
15 | },
16 | output: {
17 | path: path.join( __dirname, 'build' ),
18 | filename: '[name].js',
19 | },
20 | target: 'node',
21 | mode: 'development',
22 | externals: {
23 | canvas: 'util', // https://github.com/jsdom/jsdom/issues/2508
24 | },
25 | };
26 |
27 | const extensionConfig = {
28 | devtool: 'source-map',
29 | stats: 'errors-only',
30 | entry: {
31 | background: './source/background',
32 | content: './source/content',
33 | action: './source/action',
34 | '@wordpress/wxr': './packages/wxr',
35 | 'site-parsers': './packages/site-parsers',
36 | },
37 | output: {
38 | path: path.join( __dirname, 'distribution/build' ),
39 | filename: '[name].js',
40 | },
41 | module: {
42 | rules: [
43 | {
44 | test: /\.js$/,
45 | exclude: /node_modules/,
46 | loader: 'babel-loader',
47 | },
48 | ],
49 | },
50 | resolve: {
51 | fallback: {
52 | path: require.resolve( 'path-browserify' ),
53 | },
54 | },
55 | plugins: [
56 | new DefinePlugin( {
57 | 'process.env.GUTENBERG_PHASE': JSON.stringify( 1 ),
58 | 'process.env.COMPONENT_SYSTEM_PHASE': JSON.stringify( 0 ),
59 | 'process.env.FORCE_REDUCED_MOTION': JSON.stringify(
60 | !! process.env.FORCE_REDUCED_MOTION || false
61 | ),
62 | } ),
63 | new CopyWebpackPlugin( {
64 | patterns: [
65 | {
66 | from: '**/*',
67 | context: 'source',
68 | globOptions: {
69 | ignore: [ '*.js' ],
70 | },
71 | },
72 | {
73 | from:
74 | 'node_modules/webextension-polyfill/dist/browser-polyfill.js',
75 | to: 'polyfills/webextension.js',
76 | },
77 | {
78 | from: 'node_modules/web-streams-polyfill/dist/ponyfill.js',
79 | to: 'polyfills/web-streams.js',
80 | },
81 | ],
82 | } ),
83 | ],
84 | optimization: {
85 | minimizer: [
86 | new TerserPlugin( {
87 | terserOptions: {
88 | mangle: false,
89 | compress: false,
90 | output: {
91 | beautify: true,
92 | indent_level: 2, // eslint-disable-line camelcase
93 | },
94 | },
95 | } ),
96 | ],
97 | },
98 | };
99 |
100 | module.exports = [ extensionConfig, cliConfig ];
101 |
--------------------------------------------------------------------------------